bugzilla-4intranet/js/scrumcards.js

450 lines
13 KiB
JavaScript
Raw Normal View History

/**
2014-08-18 16:54:23 +04:00
* Code for drag&drop and/or editing of SCRUM-like cards from Bugzilla
* License: Dual-license GPL 3.0+ or MPL 1.1+
* Author: (c) Vitaliy Filippov 2010-2011
*/
var pressedButton;
var pasteMode = false;
var ctrl = false;
var highlitCard;
var selectedcards = {};
var cuttedcards = [];
var cuttedids = [];
// Добавить карточки для багов с ID'шниками из поля addbugs
function addNewCards()
{
for (var i = idlist.length-1; i >= 0; i--)
{
if (idlist[i] === '')
idlist.pop();
else
break;
}
var val = document.getElementById('addbugs').value;
var re = /(\d+)/g;
var m;
while (m = re.exec(val))
idlist.push(m[1]);
document.getElementById('idlist_value').value = idlist.join(',');
document.getElementById('scrumform').submit();
}
// Добавить пустую страницу
function addEmptyPage()
{
for (var i = 0; i < nr*nc; i++)
idlist.push('');
document.getElementById('idlist_value').value = idlist.join(',');
document.getElementById('scrumform').submit();
}
// Обработчик нажатия Enter на поле addbugs
function addNewIfEnter(ev)
{
if (ev.keyCode == 10 || ev.keyCode == 13)
{
addNewCards();
return true;
}
return false;
}
// Удалить все карточки
function deleteAllCards()
{
idlist = [];
document.getElementById('idlist_value').value = idlist.join(',');
document.getElementById('pages').innerHTML = '';
document.getElementById('scrumform').submit();
}
// Сбросить состояние кнопок, выделение карточек, режим вставки
function resetAll()
{
if (pressedButton)
{
buttonHandler(null, pressedButton);
pressedButton = null;
}
deselectAll();
stopPaste();
return true;
}
// Снять выделение
function deselectAll()
{
for (var i in selectedcards)
{
var e = document.getElementById('cardtd_'+i);
e.className = 'cardtd';
}
selectedcards = {};
}
// Обработчик, пытающийся по иерархии найти кнопку (id=btn_*),
// и вызвать на ней buttonHandler
function guessButton(ev)
{
ev = DragMaster.fixEvent(ev);
var t;
if (t = searchButtonTarget(ev))
return buttonHandler(ev, t);
return true;
}
// Обработчик mousedown и mouseup на кнопках (id=btn_*)
function buttonHandler(ev, target)
{
if (!pressedButton)
{
pressedButton = target;
target.style.borderStyle = 'inset';
target.style.padding = '4px 2px 2px 4px';
}
else if (pressedButton == target)
{
target.style.borderStyle = 'outset';
target.style.padding = '3px';
pressedButton = null;
if (target.id == 'btn_cut')
deleteSelectedCards(true);
else if (target.id == 'btn_delete')
deleteSelectedCards(false);
else if (target.id == 'btn_paste')
pasteCards();
else if (target.id == 'btn_paste_beg')
doPasteCards(0);
return true;
}
return false;
}
// Обработчик клика-выделения по карточке
function selectCard(ev, target)
{
var empty = isEmptyHash(selectedcards, target.id.substr(7));
if (pasteMode)
{
doPasteCards(id_to_coord(target.id)+1);
return true;
}
else if (ctrl || !selectedcards[target.id.substr(7)])
{
if (!ctrl)
deselectAll();
var issel = target.className == 'cardtd selected';
target.className = issel ? 'cardtd' : 'cardtd selected';
if (issel)
delete selectedcards[target.id.substr(7)];
else
selectedcards[target.id.substr(7)] = true;
return true;
}
return false;
}
// Подсветка карточки в режиме вставки
function highlightCard(ev)
{
if (pasteMode)
{
ev = DragMaster.fixEvent(ev);
var t = searchCardTarget(ev);
t.className = 'cardtd highlight';
highlitCard = t;
}
}
// Снятие подсветки карточки в режиме вставки
function unlightCard(ev)
{
if (pasteMode)
{
ev = DragMaster.fixEvent(ev);
var t = searchCardTarget(ev);
t.className = 'cardtd';
}
}
// Преобразование номера в координаты [лист, строка, столбец]
function to_coord(i)
{
var to_k = Math.floor(i / nr / nc);
var to_i = Math.floor((i / nc) % nr);
var to_j = Math.floor(i % nc);
return [to_k, to_i, to_j];
}
// Преобразование номера в строку с координатами лист_строка_столбец
function to_coord_id(i)
{
return to_coord(i).join('_');
}
// Преобразование id элемента (.*лист_строка_столбец.*) в номер
function id_to_coord(id)
{
var m = /(\d+)_(\d+)_(\d+)/.exec(id.substr(7));
return (parseInt(m[1])*nr+parseInt(m[2]))*nc+parseInt(m[3]);
}
// Удалить(cut=0)/вырезать(cut=1) выделенные карточки
function deleteSelectedCards(cut)
{
var shift = 0;
var coord = 0;
var n = nr * nc * np;
if (cut)
{
cuttedcards = [];
cuttedids = [];
}
for (var k = 0; k < np; k++)
{
for (var i = 0; i < nr; i++)
{
for (var j = 0; j < nc; j++, coord++)
{
var s = selectedcards[k+'_'+i+'_'+j];
var e = document.getElementById('cardtd_'+k+'_'+i+'_'+j);
2014-09-24 16:53:55 +04:00
if (!e)
continue;
if (s)
{
if (cut)
{
cuttedcards.push(e.innerHTML);
cuttedids.push(idlist[coord]);
}
shift++;
}
else if (shift > 0)
{
var to = to_coord_id(coord-shift);
document.getElementById('cardtd_'+to).innerHTML = e.innerHTML;
idlist[coord-shift] = idlist[coord];
}
else if (coord + shift < n)
continue;
// во всех трёх случаях - если выделена для
// удаления, если перемещена в другую, или
// если находится в конце - очищаем
e.innerHTML = emptycell;
idlist[coord] = '';
}
}
}
if (cut && cuttedids.length)
document.getElementById('cut_status').innerHTML = 'Вырезано '+cuttedids.length+' карточек.';
deselectAll();
document.getElementById('idlist_value').value = idlist.join(',');
}
// Перейти в режим выбора места вставки вырезанных карточек
function pasteCards()
{
if (!pasteMode)
{
if (!cuttedids.length)
{
alert('Сначала выделите и вырежьте какие-нибудь карточки!');
return;
}
alert('Кликните на карточку, после которой нужно вставить вырезанное, либо на кнопку "В начало".');
deselectAll();
pasteMode = true;
document.getElementById('btn_paste_beg').style.display = '';
}
}
// Вставить вырезанные карточки в позицию coord
function doPasteCards(coord)
{
stopPaste();
var nx = cuttedids.length;
if (nx <= 0)
return;
2014-09-24 16:53:55 +04:00
if (!document.getElementById('cardtd_'+to_coord_id(coord)))
{
alert('Некуда вставлять');
return;
}
var n = nr * nc * np;
var from, to;
for (var i = n-nx-1; i >= coord; i--)
{
from = to_coord_id(i);
to = to_coord_id(i+nx);
2014-09-24 16:53:55 +04:00
if (document.getElementById('cardtd_'+from) &&
document.getElementById('cardtd_'+to))
{
document.getElementById('cardtd_'+to).innerHTML =
document.getElementById('cardtd_'+from).innerHTML;
idlist[i+nx] = idlist[i];
}
}
for (var i = 0; i < nx; i++)
{
to = to_coord_id(i+coord);
document.getElementById('cardtd_'+to).innerHTML = cuttedcards[i];
document.getElementById('cardtd_'+to).className = 'cardtd selected';
idlist[i+coord] = cuttedids[i];
selectedcards[to] = true;
}
cuttedids = [];
cuttedcards = [];
document.getElementById('cut_status').innerHTML = '';
document.getElementById('idlist_value').value = idlist.join(',');
}
// Выйти из режима вставки
function stopPaste()
{
if (highlitCard)
highlitCard.className = 'cardtd';
document.getElementById('btn_paste_beg').style.display = 'none';
pasteMode = false;
}
// Обработчик keydown/keyup, записывает состояние ctrl
function ctrlDown(ev)
{
if (ev.keyCode == 17)
{
ctrl = ev.type == 'keydown';
return true;
}
return false;
}
// Попробовать найти по иерархии карточку (id=cardtd_*)
function searchCardTarget(e)
{
var nt = e._target, i;
while (nt && (!nt.attributes ||
!(i = nt.attributes.getNamedItem('id')) ||
i.value.substr(0, 7) != 'cardtd_'))
nt = nt.parentNode;
return nt;
}
// Попробовать найти по иерархии кнопку (id=btn_*)
function searchButtonTarget(e)
{
var nt = e._target, i;
while (nt && (!nt.attributes ||
!(i = nt.attributes.getNamedItem('id')) ||
i.value.substr(0, 4) != 'btn_'))
nt = nt.parentNode;
return nt;
}
// Попробовать найти по иерархии что-нибудь кроме карточек и кнопок,
// по сути это только <input> и <textarea>, клик по чему не сбрасывает всё нахрен
function searchNonResetTarget(e)
{
var nt = e._target, i;
while (nt && (!(i = nt.nodeName.toLowerCase()) ||
!(i == 'input' || i == 'textarea')))
nt = nt.parentNode;
return nt;
}
// Глобальный обработчик события mouseup
function mouseUpHandler(e)
{
e = DragMaster.fixEvent(e);
var t;
if (searchNonResetTarget(e)) {}
else if (t = searchCardTarget(e))
selectCard(e, t);
else if (t = searchButtonTarget(e))
buttonHandler(e, t);
else
resetAll();
return true;
}
// Класс "Перетаскиваемая карточка", унаследованный от "Перетаскиваемого объекта"
// Логика - при начале перетаскивания удаляется из родительского элемента,
// который есть строка таблицы, и вместо себя вставляет пустую ячейку.
// При окончании перетаскивания сначала возвращается на место, а потом дёргает
// обработчики вырезания и вставки карточек.
var CardDragObject = function(e)
{
DragObject.call(this, e);
this.n = id_to_coord(e.id);
};
CardDragObject.prototype = new DragObject();
CardDragObject.prototype.onDragStart = function() {
this.tmp = document.createElement('td');
this.element.style.opacity = 0.5;
this.element.parentNode.insertBefore(this.tmp, this.element);
this.element.parentNode.parentNode.parentNode.appendChild(this.element);
};
CardDragObject.prototype.onDragSuccess = function(target, pos) {
this.tmp.parentNode.insertBefore(this.element, this.tmp);
this.tmp.parentNode.removeChild(this.tmp);
if (!selectedcards[this.element.id.substr(7)])
{
deselectAll();
selectedcards[this.element.id.substr(7)] = true;
}
var to = id_to_coord(target.element.id)+1;
var w = target.element.scrollWidth;
if (pos.x < w/2)
to--;
var decr = 0;
for (var i = 0; i < to; i++)
if (selectedcards[to_coord_id(i)])
decr++;
to -= decr;
deleteSelectedCards(true);
doPasteCards(to);
};
CardDragObject.prototype.onDragFail = function() {
this.tmp.parentNode.insertBefore(this.element, this.tmp);
this.tmp.parentNode.removeChild(this.tmp);
};
// Каждая карточка также является целью перетаскивания
// Тут вся логика - подсветить левый/правый край в зависимости
// от положения мышки
var CardDropTarget = function(e)
{
DropTarget.call(this, e);
this.n = id_to_coord(e.id);
};
CardDropTarget.prototype = new DropTarget();
CardDropTarget.prototype.onLeave = function()
{
this.element.style.border = '1px dashed black';
};
CardDropTarget.prototype.onMove = function(pos)
{
var w = this.element.scrollWidth;
this.element.style.border = '1px dashed black';
if (pos.x < w/2)
this.element.style.borderLeft = '5px solid red';
else
this.element.style.borderRight = '5px solid red';
};
// 2 если hash пуст
// 1 если в нём один ключ key
// 0 если в нём есть ключ, не равный key
function isEmptyHash(hash, key)
{
var empty = 2;
for (var i in hash)
{
if (key !== undefined && i == key)
empty = 1;
else
{
empty = 0;
break;
}
}
return empty;
}
// Установка обработчиков событий:
for (var k = 0; k < np; k++)
{
for (var i = 0; i < nr; i++)
{
for (var j = 0; j < nc; j++)
{
var e = document.getElementById('cardtd_'+k+'_'+i+'_'+j);
2014-09-24 16:53:55 +04:00
if (e)
{
addListener(e, 'mouseover', highlightCard);
addListener(e, 'mouseout', unlightCard);
new CardDragObject(e);
new CardDropTarget(e);
}
}
}
}
addListener(document, "mousedown", guessButton);
addListener(document, "mouseup", mouseUpHandler);
addListener(document, "keydown", ctrlDown);
addListener(document.getElementById('addbugs'), "keypress", addNewIfEnter);
addListener(document, "keyup", ctrlDown);