Changeset View
Changeset View
Standalone View
Standalone View
webroot/rsrc/js/core/DraggableList.js
/** | /** | ||||
* @provides phabricator-draggable-list | * @provides phabricator-draggable-list | ||||
* @requires javelin-install | * @requires javelin-install | ||||
* javelin-dom | * javelin-dom | ||||
* javelin-stratcom | * javelin-stratcom | ||||
* javelin-util | * javelin-util | ||||
* javelin-vector | * javelin-vector | ||||
* javelin-magical-init | * javelin-magical-init | ||||
* @javelin | * @javelin | ||||
*/ | */ | ||||
JX.install('DraggableList', { | JX.install('DraggableList', { | ||||
construct : function(sigil, root) { | construct : function(sigil, root) { | ||||
this._sigil = sigil; | this._sigil = sigil; | ||||
this._root = root || document.body; | this._root = root || document.body; | ||||
this._group = [this]; | |||||
// NOTE: Javelin does not dispatch mousemove by default. | // NOTE: Javelin does not dispatch mousemove by default. | ||||
JX.enableDispatch(document.body, 'mousemove'); | JX.enableDispatch(document.body, 'mousemove'); | ||||
JX.DOM.listen(this._root, 'mousedown', sigil, JX.bind(this, this._ondrag)); | 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('mousemove', null, JX.bind(this, this._onmove)); | ||||
JX.Stratcom.listen('mouseup', null, JX.bind(this, this._ondrop)); | JX.Stratcom.listen('mouseup', null, JX.bind(this, this._ondrop)); | ||||
}, | }, | ||||
Show All 16 Lines | members : { | ||||
_dragging : null, | _dragging : null, | ||||
_locked : 0, | _locked : 0, | ||||
_origin : null, | _origin : null, | ||||
_target : null, | _target : null, | ||||
_targets : null, | _targets : null, | ||||
_dimensions : null, | _dimensions : null, | ||||
_ghostHandler : null, | _ghostHandler : null, | ||||
_ghostNode : null, | _ghostNode : null, | ||||
_group : null, | |||||
getRootNode : function() { | |||||
return this._root; | |||||
}, | |||||
setGhostHandler : function(handler) { | setGhostHandler : function(handler) { | ||||
this._ghostHandler = handler; | this._ghostHandler = handler; | ||||
return this; | return this; | ||||
}, | }, | ||||
getGhostHandler : function() { | getGhostHandler : function() { | ||||
return this._ghostHandler || JX.bind(this, this._defaultGhostHandler); | return this._ghostHandler || JX.bind(this, this._defaultGhostHandler); | ||||
}, | }, | ||||
getGhostNode : function() { | getGhostNode : function() { | ||||
if (!this._ghostNode) { | if (!this._ghostNode) { | ||||
this._ghostNode = JX.$N('li', {className: 'drag-ghost'}); | this._ghostNode = JX.$N('li', {className: 'drag-ghost'}); | ||||
} | } | ||||
return this._ghostNode; | return this._ghostNode; | ||||
}, | }, | ||||
setGhostNode : function(node) { | setGhostNode : function(node) { | ||||
this._ghostNode = node; | this._ghostNode = node; | ||||
return this; | return this; | ||||
}, | }, | ||||
setGroup : function(lists) { | |||||
var result = []; | |||||
var need_self = true; | |||||
for (var ii = 0; ii < lists.length; ii++) { | |||||
if (lists[ii] == this) { | |||||
need_self = false; | |||||
} | |||||
result.push(lists[ii]); | |||||
} | |||||
if (need_self) { | |||||
result.push(this); | |||||
} | |||||
this._group = result; | |||||
return this; | |||||
}, | |||||
_canDragX : function() { | |||||
return this._hasGroup(); | |||||
}, | |||||
_hasGroup : function() { | |||||
return (this._group.length > 1); | |||||
}, | |||||
_defaultGhostHandler : function(ghost, target) { | _defaultGhostHandler : function(ghost, target) { | ||||
var parent = this._dragging.parentNode; | var parent; | ||||
if (!this._hasGroup()) { | |||||
parent = this._dragging.parentNode; | |||||
} else { | |||||
parent = this.getRootNode(); | |||||
} | |||||
if (target && target.nextSibling) { | if (target && target.nextSibling) { | ||||
parent.insertBefore(ghost, target.nextSibling); | parent.insertBefore(ghost, target.nextSibling); | ||||
} else if (!target && parent.firstChild) { | } else if (!target && parent.firstChild) { | ||||
parent.insertBefore(ghost, parent.firstChild); | parent.insertBefore(ghost, parent.firstChild); | ||||
} else { | } else { | ||||
parent.appendChild(ghost); | parent.appendChild(ghost); | ||||
} | } | ||||
}, | }, | ||||
Show All 30 Lines | _ondrag : function(e) { | ||||
} | } | ||||
e.kill(); | e.kill(); | ||||
this._dragging = e.getNode(this._sigil); | this._dragging = e.getNode(this._sigil); | ||||
this._origin = JX.$V(e); | this._origin = JX.$V(e); | ||||
this._dimensions = JX.$V(this._dragging); | this._dimensions = JX.$V(this._dragging); | ||||
for (var ii = 0; ii < this._group.length; ii++) { | |||||
this._group[ii]._clearTarget(); | |||||
this._group[ii]._generateTargets(); | |||||
} | |||||
if (!this.invoke('didBeginDrag', this._dragging).getPrevented()) { | |||||
// Set the height of all the ghosts in the group. In the normal case, | |||||
// this just sets this list's ghost height. | |||||
for (var jj = 0; jj < this._group.length; jj++) { | |||||
var ghost = this._group[jj].getGhostNode(); | |||||
ghost.style.height = JX.Vector.getDim(this._dragging).y + 'px'; | |||||
} | |||||
JX.DOM.alterClass(this._dragging, 'drag-dragging', true); | |||||
} | |||||
}, | |||||
_generateTargets : function() { | |||||
var targets = []; | var targets = []; | ||||
var items = this.findItems(); | var items = this.findItems(); | ||||
for (var ii = 0; ii < items.length; ii++) { | for (var ii = 0; ii < items.length; ii++) { | ||||
targets.push({ | targets.push({ | ||||
item: items[ii], | item: items[ii], | ||||
y: JX.$V(items[ii]).y + (JX.Vector.getDim(items[ii]).y / 2) | y: JX.$V(items[ii]).y + (JX.Vector.getDim(items[ii]).y / 2) | ||||
}); | }); | ||||
} | } | ||||
targets.sort(function(u, v) { return v.y - u.y; }); | targets.sort(function(u, v) { return v.y - u.y; }); | ||||
this._targets = targets; | this._targets = targets; | ||||
this._target = false; | |||||
if (!this.invoke('didBeginDrag', this._dragging).getPrevented()) { | return this; | ||||
}, | |||||
_getTargetList : function(p) { | |||||
var target_list; | |||||
if (this._hasGroup()) { | |||||
var group = this._group; | |||||
for (var ii = 0; ii < group.length; ii++) { | |||||
var root = group[ii].getRootNode(); | |||||
var rp = JX.$V(root); | |||||
var rd = JX.Vector.getDim(root); | |||||
var is_target = false; | |||||
if (p.x >= rp.x && p.y >= rp.y) { | |||||
if (p.x <= (rp.x + rd.x) && p.y <= (rp.y + rd.y)) { | |||||
is_target = true; | |||||
target_list = group[ii]; | |||||
} | |||||
} | |||||
JX.DOM.alterClass(root, 'drag-target-list', is_target); | |||||
} | |||||
} else { | |||||
target_list = this; | |||||
} | |||||
return target_list; | |||||
}, | |||||
_setTarget : function(cur_target) { | |||||
var ghost = this.getGhostNode(); | var ghost = this.getGhostNode(); | ||||
ghost.style.height = JX.Vector.getDim(this._dragging).y + 'px'; | var target = this._target; | ||||
JX.DOM.alterClass(this._dragging, 'drag-dragging', true); | |||||
if (cur_target !== target) { | |||||
this._clearTarget(); | |||||
if (cur_target !== false) { | |||||
var ok = this.getGhostHandler()(ghost, cur_target); | |||||
// If the handler returns explicit `false`, prevent the drag. | |||||
if (ok === false) { | |||||
cur_target = false; | |||||
} | } | ||||
} | |||||
this._target = cur_target; | |||||
} | |||||
return this; | |||||
}, | }, | ||||
_onmove : function(e) { | _clearTarget : function() { | ||||
if (!this._dragging) { | var target = this._target; | ||||
return; | var ghost = this.getGhostNode(); | ||||
if (target !== false) { | |||||
JX.DOM.remove(ghost); | |||||
} | } | ||||
this._target = false; | |||||
return this; | |||||
}, | |||||
_getCurrentTarget : function(p) { | |||||
var ghost = this.getGhostNode(); | var ghost = this.getGhostNode(); | ||||
var target = this._target; | var target = this._target; | ||||
var targets = this._targets; | var targets = this._targets; | ||||
var dragging = this._dragging; | 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_h = JX.Vector.getDim(ghost).y; | ||||
var adjust_y = JX.$V(ghost).y; | var adjust_y = JX.$V(ghost).y; | ||||
// Find the node we're dragging the object underneath. This is the first | // 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 | // 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 | // we're dragging or its predecessor, don't select a target, because the | ||||
// operation would be a no-op. | // operation would be a no-op. | ||||
Show All 21 Lines | _getCurrentTarget : function(p) { | ||||
if (trigger >= p.y) { | if (trigger >= p.y) { | ||||
continue; | continue; | ||||
} | } | ||||
// Don't choose the dragged row or its predecessor as targets. | // Don't choose the dragged row or its predecessor as targets. | ||||
cur_target = targets[ii].item; | cur_target = targets[ii].item; | ||||
if (!dragging) { | |||||
// If the item on the cursor isn't from this list, it can't be | |||||
// dropped onto itself or its predecessor in this list. | |||||
} else { | |||||
if (cur_target == dragging) { | if (cur_target == dragging) { | ||||
cur_target = false; | cur_target = false; | ||||
} | } | ||||
if (targets[ii - 1] && targets[ii - 1].item == dragging) { | if (targets[ii - 1] && targets[ii - 1].item == dragging) { | ||||
cur_target = false; | cur_target = false; | ||||
} | } | ||||
} | |||||
break; | break; | ||||
} | } | ||||
// If the dragged row is the first row, don't allow it to be dragged | // If the dragged row is the first row, don't allow it to be dragged | ||||
// into the first position, since this operation doesn't make sense. | // into the first position, since this operation doesn't make sense. | ||||
if (cur_target === null) { | if (dragging && cur_target === null) { | ||||
var first_item = targets[targets.length - 1].item; | var first_item = targets[targets.length - 1].item; | ||||
if (dragging === first_item) { | if (dragging === first_item) { | ||||
cur_target = false; | cur_target = false; | ||||
} | } | ||||
} | } | ||||
// If we've selected a new target, update the UI to show where we're | return cur_target; | ||||
// going to drop the row. | }, | ||||
if (cur_target !== target) { | |||||
if (target !== false) { | _onmove : function(e) { | ||||
JX.DOM.remove(ghost); | if (!this._dragging) { | ||||
return; | |||||
} | } | ||||
if (cur_target !== false) { | var p = JX.$V(e); | ||||
var ok = this.getGhostHandler()(ghost, cur_target); | |||||
// If the handler returns explicit `false`, prevent the drag. | |||||
if (ok === false) { | |||||
cur_target = false; | |||||
} | |||||
} | |||||
target = cur_target; | var group = this._group; | ||||
var target_list = this._getTargetList(p); | |||||
if (target !== false) { | // Compute the size and position of the drop target indicator, because we | ||||
// need to update our static position computations to account for it. | |||||
// If we've changed where the ghost node is, update the adjustments | var cur_target = false; | ||||
// so we accurately reflect document state when we tweak things below. | if (target_list) { | ||||
// This avoids a flash of bad state as the mouse is dragged upward | cur_target = target_list._getCurrentTarget(p); | ||||
// across the document. | } | ||||
adjust_h = JX.Vector.getDim(ghost).y; | // If we've selected a new target, update the UI to show where we're | ||||
adjust_y = JX.$V(ghost).y; | // going to drop the row. | ||||
for (var ii = 0; ii < group.length; ii++) { | |||||
if (group[ii] == target_list) { | |||||
group[ii]._setTarget(cur_target); | |||||
} else { | |||||
group[ii]._clearTarget(); | |||||
} | } | ||||
} | } | ||||
// If the drop target indicator is above the cursor in the document, | // If the drop target indicator is above the cursor in the document, | ||||
// adjust the cursor position for the change in node document position. | // adjust the cursor position for the change in node document position. | ||||
// Do this before choosing a new target to avoid a flash of nonsense. | // Do this before choosing a new target to avoid a flash of nonsense. | ||||
if (target !== false) { | var origin = this._origin; | ||||
var adjust_h = 0; | |||||
var adjust_y = 0; | |||||
if (this._target !== false) { | |||||
var ghost = this.getGhostNode(); | |||||
adjust_h = JX.Vector.getDim(ghost).y; | |||||
adjust_y = JX.$V(ghost).y; | |||||
if (adjust_y <= origin.y) { | if (adjust_y <= origin.y) { | ||||
p.y -= adjust_h; | p.y -= adjust_h; | ||||
} | } | ||||
} | } | ||||
if (this._canDragX()) { | |||||
p.x -= origin.x; | |||||
} else { | |||||
p.x = 0; | p.x = 0; | ||||
} | |||||
p.y -= origin.y; | p.y -= origin.y; | ||||
p.setPos(dragging); | p.setPos(this._dragging); | ||||
this._target = target; | |||||
e.kill(); | e.kill(); | ||||
}, | }, | ||||
_ondrop : function(e) { | _ondrop : function(e) { | ||||
if (!this._dragging) { | if (!this._dragging) { | ||||
return; | return; | ||||
} | } | ||||
Show All 9 Lines | _ondrop : function(e) { | ||||
if (target !== false) { | if (target !== false) { | ||||
JX.DOM.remove(dragging); | JX.DOM.remove(dragging); | ||||
JX.DOM.replace(ghost, dragging); | JX.DOM.replace(ghost, dragging); | ||||
this.invoke('didDrop', dragging, target); | this.invoke('didDrop', dragging, target); | ||||
} else { | } else { | ||||
this.invoke('didCancelDrag', dragging); | this.invoke('didCancelDrag', dragging); | ||||
} | } | ||||
var group = this._group; | |||||
for (var ii = 0; ii < group.length; ii++) { | |||||
JX.DOM.alterClass(group[ii].getRootNode(), 'drag-target-list', false); | |||||
group[ii]._clearTarget(); | |||||
} | |||||
if (!this.invoke('didEndDrag', dragging).getPrevented()) { | if (!this.invoke('didEndDrag', dragging).getPrevented()) { | ||||
JX.DOM.alterClass(dragging, 'drag-dragging', false); | JX.DOM.alterClass(dragging, 'drag-dragging', false); | ||||
} | } | ||||
e.kill(); | e.kill(); | ||||
}, | }, | ||||
lock : function() { | lock : function() { | ||||
Show All 22 Lines |