diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 3e332da3d2..1d77264e7b 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -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', diff --git a/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js b/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js index 242a8dd684..68abcc6fad 100644 --- a/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js +++ b/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js @@ -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); + }); }); diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js new file mode 100644 index 0000000000..d47fa53f04 --- /dev/null +++ b/webroot/rsrc/js/core/DraggableList.js @@ -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; + } + } + +});