1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

Mostly generalize Maniphest's drag-and-drop list

Summary:
I want to use draggable lists in at least three other interfaces:

  - (Today) Reorganizing named search queries.
  - (Today) Reorganizing custom fields.
  - (Future) Dragging tasks around on boards.

This mostly generalizes the drag-and-drop code in Maniphest's task list. It isn't a total generalization and will need some more tweaking (for example, Maniphest's list is unusual in that the user can't drag items to the top of the list), but it substantially separates the Maniphest-specific behaviors from the general dragging behaviors.

This diff causes no functional changes.

Test Plan: Dragged and dropped tasks in Maniphest.

Reviewers: chad

Reviewed By: chad

CC: aran

Differential Revision: https://secure.phabricator.com/D6124
This commit is contained in:
epriestley 2013-06-04 15:28:31 -07:00
parent 5d1f94ac8a
commit 7fbfeca802
3 changed files with 343 additions and 198 deletions

View file

@ -1797,16 +1797,16 @@ celerity_register_resource_map(array(
),
'javelin-behavior-maniphest-subpriority-editor' =>
array(
'uri' => '/res/21b73c2a/rsrc/js/application/maniphest/behavior-subpriorityeditor.js',
'uri' => '/res/994f0a9d/rsrc/js/application/maniphest/behavior-subpriorityeditor.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-magical-init',
2 => 'javelin-dom',
3 => 'javelin-vector',
4 => 'javelin-stratcom',
5 => 'javelin-workflow',
1 => 'javelin-dom',
2 => 'javelin-vector',
3 => 'javelin-stratcom',
4 => 'javelin-workflow',
5 => 'phabricator-draggable-list',
),
'disk' => '/rsrc/js/application/maniphest/behavior-subpriorityeditor.js',
),
@ -2989,6 +2989,21 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/core/DragAndDropFileUpload.js',
),
'phabricator-draggable-list' =>
array(
'uri' => '/res/e72b9768/rsrc/js/core/DraggableList.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-util',
4 => 'javelin-vector',
5 => 'javelin-magical-init',
),
'disk' => '/rsrc/js/core/DraggableList.js',
),
'phabricator-dropdown-menu' =>
array(
'uri' => '/res/a248b7f4/rsrc/js/core/DropdownMenu.js',
@ -4212,7 +4227,7 @@ celerity_register_resource_map(array(
'uri' => '/res/pkg/03ab92cf/maniphest.pkg.css',
'type' => 'css',
),
'1621e522' =>
'c0c9bc0b' =>
array(
'name' => 'maniphest.pkg.js',
'symbols' =>
@ -4223,7 +4238,7 @@ celerity_register_resource_map(array(
3 => 'javelin-behavior-maniphest-transaction-expand',
4 => 'javelin-behavior-maniphest-subpriority-editor',
),
'uri' => '/res/pkg/1621e522/maniphest.pkg.js',
'uri' => '/res/pkg/c0c9bc0b/maniphest.pkg.js',
'type' => 'js',
),
),
@ -4286,11 +4301,11 @@ celerity_register_resource_map(array(
'javelin-behavior-konami' => '98f60e3f',
'javelin-behavior-lightbox-attachments' => '98f60e3f',
'javelin-behavior-load-blame' => '9488bb69',
'javelin-behavior-maniphest-batch-selector' => '1621e522',
'javelin-behavior-maniphest-subpriority-editor' => '1621e522',
'javelin-behavior-maniphest-transaction-controls' => '1621e522',
'javelin-behavior-maniphest-transaction-expand' => '1621e522',
'javelin-behavior-maniphest-transaction-preview' => '1621e522',
'javelin-behavior-maniphest-batch-selector' => 'c0c9bc0b',
'javelin-behavior-maniphest-subpriority-editor' => 'c0c9bc0b',
'javelin-behavior-maniphest-transaction-controls' => 'c0c9bc0b',
'javelin-behavior-maniphest-transaction-expand' => 'c0c9bc0b',
'javelin-behavior-maniphest-transaction-preview' => 'c0c9bc0b',
'javelin-behavior-phabricator-active-nav' => '98f60e3f',
'javelin-behavior-phabricator-autofocus' => '98f60e3f',
'javelin-behavior-phabricator-gesture' => '98f60e3f',

View file

@ -1,222 +1,74 @@
/**
* @provides javelin-behavior-maniphest-subpriority-editor
* @requires javelin-behavior
* javelin-magical-init
* javelin-dom
* javelin-vector
* javelin-stratcom
* javelin-workflow
* phabricator-draggable-list
*/
JX.behavior('maniphest-subpriority-editor', function(config) {
var dragging = null;
var sending = null;
var origin = null;
var targets = null;
var target = null;
var droptarget = JX.$N('li', {className: 'maniphest-subpriority-target'});
var ondrag = function(e) {
if (dragging || sending) {
return;
}
if (!e.isNormalMouseEvent()) {
return;
}
// Can't grab onto slippery nodes.
if (e.getNode('slippery')) {
return;
}
dragging = e.getNode('maniphest-task');
origin = JX.$V(e);
var tasks = JX.DOM.scry(document.body, 'li', 'maniphest-task');
var heads = JX.DOM.scry(document.body, 'h1', 'task-group');
var nodes = tasks.concat(heads);
targets = [];
for (var ii = 0; ii < nodes.length; ii++) {
targets.push({
node: nodes[ii],
y: JX.$V(nodes[ii]).y + (JX.Vector.getDim(nodes[ii]).y / 2)
});
}
targets.sort(function(u, v) { return v.y - u.y; });
JX.DOM.alterClass(dragging, 'maniphest-task-dragging', true);
droptarget.style.height = JX.Vector.getDim(dragging).y + 'px';
e.kill();
};
var onmove = function(e) {
if (!dragging) {
return;
}
var p = JX.$V(e);
// Compute the size and position of the drop target indicator, because we
// need to update our static position computations to account for it.
var adjust_h = JX.Vector.getDim(droptarget).y;
var adjust_y = JX.$V(droptarget).y;
// Find the node we're dragging the task underneath. This is the first
// node in the list that's above the cursor. If that node is the node
// we're dragging or its predecessor, don't select a target, because the
// operation would be a no-op.
var cur_target = null;
for (var ii = 0; ii < targets.length; ii++) {
// If the drop target indicator is above the target, we need to adjust
// the target's trigger height down accordingly. This makes dragging
// items down the list smoother, because the target doesn't jump to the
// next item while the cursor is over it.
var trigger = targets[ii].y;
if (adjust_y <= trigger) {
trigger += adjust_h;
}
// If the cursor is above this target, we aren't dropping underneath it.
if (trigger >= p.y) {
continue;
}
// Don't choose the dragged row or its predecessor as targets.
cur_target = targets[ii].node;
if (cur_target == dragging) {
cur_target = null;
}
if (targets[ii - 1] && targets[ii - 1].node == dragging) {
cur_target = null;
}
break;
}
// If we've selected a new target, update the UI to show where we're
// going to drop the row.
if (cur_target != target) {
if (target) {
JX.DOM.remove(droptarget);
}
if (cur_target) {
if (cur_target.nextSibling) {
if (JX.DOM.isType(cur_target, 'h1')) {
// Dropping at the beginning of a priority list.
cur_target.nextSibling.insertBefore(
droptarget,
cur_target.nextSibling.firstChild);
} else {
// Dropping in the middle of a priority list.
cur_target.parentNode.insertBefore(
droptarget,
cur_target.nextSibling);
}
var draggable = new JX.DraggableList('maniphest-task')
.setFindItemsHandler(function() {
var tasks = JX.DOM.scry(document.body, 'li', 'maniphest-task');
var heads = JX.DOM.scry(document.body, 'h1', 'task-group');
return tasks.concat(heads);
})
.setGhostNode(JX.$N('li', {className: 'maniphest-subpriority-target'}))
.setGhostHandler(function(ghost, target) {
if (target.nextSibling) {
if (JX.DOM.isType(target, 'h1')) {
target.nextSibling.insertBefore(ghost, target.nextSibling.firstChild);
} else {
// Dropping at the end of a priority list.
cur_target.parentNode.appendChild(droptarget);
target.parentNode.insertBefore(ghost, target.nextSibling);
}
} else {
target.parentNode.appendChild(ghost);
}
});
target = cur_target;
if (target) {
// If we've changed where the droptarget is, update the adjustments
// so we accurately reflect document state when we tweak things below.
// This avoids a flash of bad state as the mouse is dragged upward
// across the document.
adjust_h = JX.Vector.getDim(droptarget).y;
adjust_y = JX.$V(droptarget).y;
}
draggable.listen('shouldBeginDrag', function(e) {
if (e.getNode('slippery')) {
JX.Stratcom.context().kill();
}
});
// If the drop target indicator is above the cursor in the document, adjust
// the cursor position for the change in node document position. Do this
// before choosing a new target to avoid a flash of nonsense.
draggable.listen('didBeginDrag', function(node) {
draggable.getGhostNode().style.height = JX.Vector.getDim(node).y + 'px';
JX.DOM.alterClass(node, 'maniphest-task-dragging', true);
});
if (target) {
if (adjust_y <= origin.y) {
p.y -= adjust_h;
}
}
p.x = 0;
p.y -= origin.y;
p.setPos(dragging);
e.kill();
};
var ondrop = function(e) {
if (!dragging) {
return;
}
JX.DOM.alterClass(dragging, 'maniphest-task-dragging', false);
JX.$V(0, 0).setPos(dragging);
if (!target) {
dragging = null;
return;
}
draggable.listen('didEndDrag', function(node) {
JX.DOM.alterClass(node, 'maniphest-task-dragging', false);
});
draggable.listen('didDrop', function(node, after) {
var data = {
task: JX.Stratcom.getData(dragging).taskID
task: JX.Stratcom.getData(node).taskID
};
if (JX.DOM.isType(target, 'h1')) {
data.priority = JX.Stratcom.getData(target).priority;
if (JX.DOM.isType(after, 'h1')) {
data.priority = JX.Stratcom.getData(after).priority;
} else {
data.after = JX.Stratcom.getData(target).taskID;
data.after = JX.Stratcom.getData(after).taskID;
}
target = null;
JX.DOM.remove(dragging);
JX.DOM.replace(droptarget, dragging);
sending = dragging;
dragging = null;
JX.DOM.alterClass(sending, 'maniphest-task-loading', true);
draggable.lock();
JX.DOM.alterClass(node, 'maniphest-task-loading', true);
var onresponse = function(r) {
var nodes = JX.$H(r.tasks).getFragment().firstChild;
var task = JX.DOM.find(nodes, 'li', 'maniphest-task');
JX.DOM.replace(sending, task);
JX.DOM.replace(node, task);
sending = null;
draggable.unlock();
};
new JX.Workflow(config.uri, data)
.setHandler(onresponse)
.start();
e.kill();
};
// NOTE: Javelin does not dispatch mousemove by default.
JX.enableDispatch(document.body, 'mousemove');
JX.Stratcom.listen('mousedown', 'maniphest-task', ondrag);
JX.Stratcom.listen('mousemove', null, onmove);
JX.Stratcom.listen('mouseup', null, ondrop);
});
});

View file

@ -0,0 +1,278 @@
/**
* @provides phabricator-draggable-list
* @requires javelin-install
* javelin-dom
* javelin-stratcom
* javelin-util
* javelin-vector
* javelin-magical-init
* @javelin
*/
JX.install('DraggableList', {
construct : function(sigil, root) {
this._sigil = sigil;
this._root = root || document.body;
// NOTE: Javelin does not dispatch mousemove by default.
JX.enableDispatch(document.body, 'mousemove');
JX.DOM.listen(this._root, 'mousedown', sigil, JX.bind(this, this._ondrag));
JX.Stratcom.listen('mousemove', null, JX.bind(this, this._onmove));
JX.Stratcom.listen('mouseup', null, JX.bind(this, this._ondrop));
},
events : [
'didLock',
'didUnlock',
'shouldBeginDrag',
'didBeginDrag',
'didCancelDrag',
'didEndDrag',
'didDrop'],
properties : {
findItemsHandler : null,
ghostNode: null
},
members : {
_root : null,
_dragging : null,
_locked : 0,
_origin : null,
_target : null,
_targets : null,
_dimensions : null,
_ghostHandler : null,
setGhostHandler : function(handler) {
this._ghostHandler = handler;
return this;
},
getGhostHandler : function() {
return this._ghostHandler || JX.bind(this, this._defaultGhostHandler);
},
_defaultGhostHandler : function(ghost, target) {
var parent = this._dragging.parentNode;
if (target && target.nextSibling) {
parent.insertBefore(ghost, target.nextSibling);
} else if (!target && parent.firstChild) {
parent.insertBefore(ghost, parent.firstChild);
} else {
parent.appendChild(ghost);
}
},
findItems : function() {
var handler = this.getFindItemsHandler();
if (__DEV__) {
if (!handler) {
JX.$E('JX.Draggable.findItems(): No findItemsHandler set!');
}
}
return handler();
},
_ondrag : function(e) {
if (__DEV__) {
var ghost = this.getGhostNode();
if (!ghost) {
JX.$E('JX.Draggable._ondrag(): No ghostNode set!');
}
}
if (this._dragging) {
// Don't start dragging if we're already dragging something.
return;
}
if (this._locked) {
// Don't start drag operations while locked.
return;
}
if (!e.isNormalMouseEvent()) {
// Don't start dragging for shift click, right click, etc.
return;
}
if (this.invoke('shouldBeginDrag', e).getPrevented()) {
return;
}
e.kill();
this._dragging = e.getNode(this._sigil);
this._origin = JX.$V(e);
this._dimensions = JX.$V(this._dragging);
var targets = [];
var items = this.findItems();
for (var ii = 0; ii < items.length; ii++) {
targets.push({
item: items[ii],
y: JX.$V(items[ii]).y + (JX.Vector.getDim(items[ii]).y / 2)
});
}
targets.sort(function(u, v) { return v.y - u.y; });
this._targets = targets;
this._target = null;
this.invoke('didBeginDrag', this._dragging);
},
_onmove : function(e) {
if (!this._dragging) {
return;
}
var ghost = this.getGhostNode();
var target = this._target;
var targets = this._targets;
var dragging = this._dragging;
var origin = this._origin;
var p = JX.$V(e);
// Compute the size and position of the drop target indicator, because we
// need to update our static position computations to account for it.
var adjust_h = JX.Vector.getDim(ghost).y;
var adjust_y = JX.$V(ghost).y;
// Find the node we're dragging the object underneath. This is the first
// node in the list that's above the cursor. If that node is the node
// we're dragging or its predecessor, don't select a target, because the
// operation would be a no-op.
var cur_target = null;
var trigger;
for (var ii = 0; ii < targets.length; ii++) {
// If the drop target indicator is above the target, we need to adjust
// the target's trigger height down accordingly. This makes dragging
// items down the list smoother, because the target doesn't jump to the
// next item while the cursor is over it.
trigger = targets[ii].y;
if (adjust_y <= trigger) {
trigger += adjust_h;
}
// If the cursor is above this target, we aren't dropping underneath it.
if (trigger >= p.y) {
continue;
}
// Don't choose the dragged row or its predecessor as targets.
cur_target = targets[ii].item;
if (cur_target == dragging) {
cur_target = null;
}
if (targets[ii - 1] && targets[ii - 1].item == dragging) {
cur_target = null;
}
break;
}
// If we've selected a new target, update the UI to show where we're
// going to drop the row.
if (cur_target != target) {
if (target) {
JX.DOM.remove(ghost);
}
if (cur_target) {
this.getGhostHandler()(ghost, cur_target);
}
target = cur_target;
if (target) {
// If we've changed where the ghost node is, update the adjustments
// so we accurately reflect document state when we tweak things below.
// This avoids a flash of bad state as the mouse is dragged upward
// across the document.
adjust_h = JX.Vector.getDim(ghost).y;
adjust_y = JX.$V(ghost).y;
}
}
// If the drop target indicator is above the cursor in the document,
// adjust the cursor position for the change in node document position.
// Do this before choosing a new target to avoid a flash of nonsense.
if (target) {
if (adjust_y <= origin.y) {
p.y -= adjust_h;
}
}
p.x = 0;
p.y -= origin.y;
p.setPos(dragging);
this._target = target;
e.kill();
},
_ondrop : function(e) {
if (!this._dragging) {
return;
}
var target = this._target;
var dragging = this._dragging;
var ghost = this.getGhostNode();
this._dragging = null;
JX.$V(0, 0).setPos(dragging);
if (target) {
JX.DOM.remove(dragging);
JX.DOM.replace(ghost, dragging);
this.invoke('didDrop', dragging, target);
} else {
this.invoke('didCancelDrag', dragging);
}
this.invoke('didEndDrag', dragging);
e.kill();
},
lock : function() {
this._locked++;
if (this._locked === 1) {
this.invoke('didLock');
}
return this;
},
unlock : function() {
if (__DEV__) {
if (!this._locked) {
JX.$E("JX.Draggable.unlock(): Draggable is not locked!");
}
}
this._locked--;
if (!this._locked) {
this.invoke('didUnlock');
}
return this;
}
}
});