@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+343 -198
+28 -13
src/__celerity_resource_map__.php
··· 1797 1797 ), 1798 1798 'javelin-behavior-maniphest-subpriority-editor' => 1799 1799 array( 1800 - 'uri' => '/res/21b73c2a/rsrc/js/application/maniphest/behavior-subpriorityeditor.js', 1800 + 'uri' => '/res/994f0a9d/rsrc/js/application/maniphest/behavior-subpriorityeditor.js', 1801 1801 'type' => 'js', 1802 1802 'requires' => 1803 1803 array( 1804 1804 0 => 'javelin-behavior', 1805 - 1 => 'javelin-magical-init', 1806 - 2 => 'javelin-dom', 1807 - 3 => 'javelin-vector', 1808 - 4 => 'javelin-stratcom', 1809 - 5 => 'javelin-workflow', 1805 + 1 => 'javelin-dom', 1806 + 2 => 'javelin-vector', 1807 + 3 => 'javelin-stratcom', 1808 + 4 => 'javelin-workflow', 1809 + 5 => 'phabricator-draggable-list', 1810 1810 ), 1811 1811 'disk' => '/rsrc/js/application/maniphest/behavior-subpriorityeditor.js', 1812 1812 ), ··· 2989 2989 ), 2990 2990 'disk' => '/rsrc/js/core/DragAndDropFileUpload.js', 2991 2991 ), 2992 + 'phabricator-draggable-list' => 2993 + array( 2994 + 'uri' => '/res/e72b9768/rsrc/js/core/DraggableList.js', 2995 + 'type' => 'js', 2996 + 'requires' => 2997 + array( 2998 + 0 => 'javelin-install', 2999 + 1 => 'javelin-dom', 3000 + 2 => 'javelin-stratcom', 3001 + 3 => 'javelin-util', 3002 + 4 => 'javelin-vector', 3003 + 5 => 'javelin-magical-init', 3004 + ), 3005 + 'disk' => '/rsrc/js/core/DraggableList.js', 3006 + ), 2992 3007 'phabricator-dropdown-menu' => 2993 3008 array( 2994 3009 'uri' => '/res/a248b7f4/rsrc/js/core/DropdownMenu.js', ··· 4212 4227 'uri' => '/res/pkg/03ab92cf/maniphest.pkg.css', 4213 4228 'type' => 'css', 4214 4229 ), 4215 - '1621e522' => 4230 + 'c0c9bc0b' => 4216 4231 array( 4217 4232 'name' => 'maniphest.pkg.js', 4218 4233 'symbols' => ··· 4223 4238 3 => 'javelin-behavior-maniphest-transaction-expand', 4224 4239 4 => 'javelin-behavior-maniphest-subpriority-editor', 4225 4240 ), 4226 - 'uri' => '/res/pkg/1621e522/maniphest.pkg.js', 4241 + 'uri' => '/res/pkg/c0c9bc0b/maniphest.pkg.js', 4227 4242 'type' => 'js', 4228 4243 ), 4229 4244 ), ··· 4286 4301 'javelin-behavior-konami' => '98f60e3f', 4287 4302 'javelin-behavior-lightbox-attachments' => '98f60e3f', 4288 4303 'javelin-behavior-load-blame' => '9488bb69', 4289 - 'javelin-behavior-maniphest-batch-selector' => '1621e522', 4290 - 'javelin-behavior-maniphest-subpriority-editor' => '1621e522', 4291 - 'javelin-behavior-maniphest-transaction-controls' => '1621e522', 4292 - 'javelin-behavior-maniphest-transaction-expand' => '1621e522', 4293 - 'javelin-behavior-maniphest-transaction-preview' => '1621e522', 4304 + 'javelin-behavior-maniphest-batch-selector' => 'c0c9bc0b', 4305 + 'javelin-behavior-maniphest-subpriority-editor' => 'c0c9bc0b', 4306 + 'javelin-behavior-maniphest-transaction-controls' => 'c0c9bc0b', 4307 + 'javelin-behavior-maniphest-transaction-expand' => 'c0c9bc0b', 4308 + 'javelin-behavior-maniphest-transaction-preview' => 'c0c9bc0b', 4294 4309 'javelin-behavior-phabricator-active-nav' => '98f60e3f', 4295 4310 'javelin-behavior-phabricator-autofocus' => '98f60e3f', 4296 4311 'javelin-behavior-phabricator-gesture' => '98f60e3f',
+37 -185
webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js
··· 1 1 /** 2 2 * @provides javelin-behavior-maniphest-subpriority-editor 3 3 * @requires javelin-behavior 4 - * javelin-magical-init 5 4 * javelin-dom 6 5 * javelin-vector 7 6 * javelin-stratcom 8 7 * javelin-workflow 8 + * phabricator-draggable-list 9 9 */ 10 10 11 11 JX.behavior('maniphest-subpriority-editor', function(config) { 12 12 13 - var dragging = null; 14 - var sending = null; 15 - var origin = null; 16 - var targets = null; 17 - var target = null; 18 - var droptarget = JX.$N('li', {className: 'maniphest-subpriority-target'}); 19 - 20 - var ondrag = function(e) { 21 - if (dragging || sending) { 22 - return; 23 - } 24 - 25 - if (!e.isNormalMouseEvent()) { 26 - return; 27 - } 28 - 29 - // Can't grab onto slippery nodes. 30 - if (e.getNode('slippery')) { 31 - return; 32 - } 33 - 34 - dragging = e.getNode('maniphest-task'); 35 - origin = JX.$V(e); 36 - 37 - var tasks = JX.DOM.scry(document.body, 'li', 'maniphest-task'); 38 - var heads = JX.DOM.scry(document.body, 'h1', 'task-group'); 39 - 40 - var nodes = tasks.concat(heads); 41 - 42 - targets = []; 43 - for (var ii = 0; ii < nodes.length; ii++) { 44 - targets.push({ 45 - node: nodes[ii], 46 - y: JX.$V(nodes[ii]).y + (JX.Vector.getDim(nodes[ii]).y / 2) 47 - }); 48 - } 49 - targets.sort(function(u, v) { return v.y - u.y; }); 50 - 51 - JX.DOM.alterClass(dragging, 'maniphest-task-dragging', true); 52 - 53 - droptarget.style.height = JX.Vector.getDim(dragging).y + 'px'; 54 - 55 - e.kill(); 56 - }; 57 - 58 - var onmove = function(e) { 59 - if (!dragging) { 60 - return; 61 - } 62 - 63 - var p = JX.$V(e); 64 - 65 - // Compute the size and position of the drop target indicator, because we 66 - // need to update our static position computations to account for it. 67 - 68 - var adjust_h = JX.Vector.getDim(droptarget).y; 69 - var adjust_y = JX.$V(droptarget).y; 70 - 71 - // Find the node we're dragging the task underneath. This is the first 72 - // node in the list that's above the cursor. If that node is the node 73 - // we're dragging or its predecessor, don't select a target, because the 74 - // operation would be a no-op. 75 - 76 - var cur_target = null; 77 - for (var ii = 0; ii < targets.length; ii++) { 78 - 79 - // If the drop target indicator is above the target, we need to adjust 80 - // the target's trigger height down accordingly. This makes dragging 81 - // items down the list smoother, because the target doesn't jump to the 82 - // next item while the cursor is over it. 83 - 84 - var trigger = targets[ii].y; 85 - if (adjust_y <= trigger) { 86 - trigger += adjust_h; 87 - } 88 - 89 - // If the cursor is above this target, we aren't dropping underneath it. 90 - 91 - if (trigger >= p.y) { 92 - continue; 93 - } 94 - 95 - // Don't choose the dragged row or its predecessor as targets. 96 - 97 - cur_target = targets[ii].node; 98 - if (cur_target == dragging) { 99 - cur_target = null; 100 - } 101 - if (targets[ii - 1] && targets[ii - 1].node == dragging) { 102 - cur_target = null; 103 - } 104 - 105 - break; 106 - } 107 - 108 - // If we've selected a new target, update the UI to show where we're 109 - // going to drop the row. 110 - 111 - if (cur_target != target) { 112 - 113 - if (target) { 114 - JX.DOM.remove(droptarget); 115 - } 116 - 117 - if (cur_target) { 118 - if (cur_target.nextSibling) { 119 - if (JX.DOM.isType(cur_target, 'h1')) { 120 - // Dropping at the beginning of a priority list. 121 - cur_target.nextSibling.insertBefore( 122 - droptarget, 123 - cur_target.nextSibling.firstChild); 124 - } else { 125 - // Dropping in the middle of a priority list. 126 - cur_target.parentNode.insertBefore( 127 - droptarget, 128 - cur_target.nextSibling); 129 - } 13 + var draggable = new JX.DraggableList('maniphest-task') 14 + .setFindItemsHandler(function() { 15 + var tasks = JX.DOM.scry(document.body, 'li', 'maniphest-task'); 16 + var heads = JX.DOM.scry(document.body, 'h1', 'task-group'); 17 + return tasks.concat(heads); 18 + }) 19 + .setGhostNode(JX.$N('li', {className: 'maniphest-subpriority-target'})) 20 + .setGhostHandler(function(ghost, target) { 21 + if (target.nextSibling) { 22 + if (JX.DOM.isType(target, 'h1')) { 23 + target.nextSibling.insertBefore(ghost, target.nextSibling.firstChild); 130 24 } else { 131 - // Dropping at the end of a priority list. 132 - cur_target.parentNode.appendChild(droptarget); 25 + target.parentNode.insertBefore(ghost, target.nextSibling); 133 26 } 27 + } else { 28 + target.parentNode.appendChild(ghost); 134 29 } 135 - 136 - target = cur_target; 137 - 138 - if (target) { 30 + }); 139 31 140 - // If we've changed where the droptarget is, update the adjustments 141 - // so we accurately reflect document state when we tweak things below. 142 - // This avoids a flash of bad state as the mouse is dragged upward 143 - // across the document. 144 - 145 - adjust_h = JX.Vector.getDim(droptarget).y; 146 - adjust_y = JX.$V(droptarget).y; 147 - } 32 + draggable.listen('shouldBeginDrag', function(e) { 33 + if (e.getNode('slippery')) { 34 + JX.Stratcom.context().kill(); 148 35 } 36 + }); 149 37 150 - // If the drop target indicator is above the cursor in the document, adjust 151 - // the cursor position for the change in node document position. Do this 152 - // before choosing a new target to avoid a flash of nonsense. 38 + draggable.listen('didBeginDrag', function(node) { 39 + draggable.getGhostNode().style.height = JX.Vector.getDim(node).y + 'px'; 40 + JX.DOM.alterClass(node, 'maniphest-task-dragging', true); 41 + }); 153 42 154 - if (target) { 155 - if (adjust_y <= origin.y) { 156 - p.y -= adjust_h; 157 - } 158 - } 43 + draggable.listen('didEndDrag', function(node) { 44 + JX.DOM.alterClass(node, 'maniphest-task-dragging', false); 45 + }); 159 46 160 - p.x = 0; 161 - p.y -= origin.y; 162 - p.setPos(dragging); 163 - 164 - e.kill(); 165 - }; 166 - 167 - var ondrop = function(e) { 168 - if (!dragging) { 169 - return; 170 - } 171 - 172 - JX.DOM.alterClass(dragging, 'maniphest-task-dragging', false); 173 - JX.$V(0, 0).setPos(dragging); 174 - 175 - if (!target) { 176 - dragging = null; 177 - return; 178 - } 179 - 47 + draggable.listen('didDrop', function(node, after) { 180 48 var data = { 181 - task: JX.Stratcom.getData(dragging).taskID 49 + task: JX.Stratcom.getData(node).taskID 182 50 }; 183 51 184 - if (JX.DOM.isType(target, 'h1')) { 185 - data.priority = JX.Stratcom.getData(target).priority; 52 + if (JX.DOM.isType(after, 'h1')) { 53 + data.priority = JX.Stratcom.getData(after).priority; 186 54 } else { 187 - data.after = JX.Stratcom.getData(target).taskID; 55 + data.after = JX.Stratcom.getData(after).taskID; 188 56 } 189 57 190 - target = null; 191 - 192 - JX.DOM.remove(dragging); 193 - JX.DOM.replace(droptarget, dragging); 194 - 195 - sending = dragging; 196 - dragging = null; 197 - 198 - JX.DOM.alterClass(sending, 'maniphest-task-loading', true); 58 + draggable.lock(); 59 + JX.DOM.alterClass(node, 'maniphest-task-loading', true); 199 60 200 61 var onresponse = function(r) { 201 62 var nodes = JX.$H(r.tasks).getFragment().firstChild; 202 63 var task = JX.DOM.find(nodes, 'li', 'maniphest-task'); 203 - JX.DOM.replace(sending, task); 64 + JX.DOM.replace(node, task); 204 65 205 - sending = null; 66 + draggable.unlock(); 206 67 }; 207 68 208 69 new JX.Workflow(config.uri, data) 209 70 .setHandler(onresponse) 210 71 .start(); 211 - 212 - e.kill(); 213 - }; 214 - 215 - // NOTE: Javelin does not dispatch mousemove by default. 216 - JX.enableDispatch(document.body, 'mousemove'); 217 - 218 - JX.Stratcom.listen('mousedown', 'maniphest-task', ondrag); 219 - JX.Stratcom.listen('mousemove', null, onmove); 220 - JX.Stratcom.listen('mouseup', null, ondrop); 72 + }); 221 73 222 74 });
+278
webroot/rsrc/js/core/DraggableList.js
··· 1 + /** 2 + * @provides phabricator-draggable-list 3 + * @requires javelin-install 4 + * javelin-dom 5 + * javelin-stratcom 6 + * javelin-util 7 + * javelin-vector 8 + * javelin-magical-init 9 + * @javelin 10 + */ 11 + 12 + JX.install('DraggableList', { 13 + 14 + construct : function(sigil, root) { 15 + this._sigil = sigil; 16 + this._root = root || document.body; 17 + 18 + // NOTE: Javelin does not dispatch mousemove by default. 19 + JX.enableDispatch(document.body, 'mousemove'); 20 + 21 + JX.DOM.listen(this._root, 'mousedown', sigil, JX.bind(this, this._ondrag)); 22 + JX.Stratcom.listen('mousemove', null, JX.bind(this, this._onmove)); 23 + JX.Stratcom.listen('mouseup', null, JX.bind(this, this._ondrop)); 24 + }, 25 + 26 + events : [ 27 + 'didLock', 28 + 'didUnlock', 29 + 'shouldBeginDrag', 30 + 'didBeginDrag', 31 + 'didCancelDrag', 32 + 'didEndDrag', 33 + 'didDrop'], 34 + 35 + properties : { 36 + findItemsHandler : null, 37 + ghostNode: null 38 + }, 39 + 40 + members : { 41 + _root : null, 42 + _dragging : null, 43 + _locked : 0, 44 + _origin : null, 45 + _target : null, 46 + _targets : null, 47 + _dimensions : null, 48 + _ghostHandler : null, 49 + 50 + setGhostHandler : function(handler) { 51 + this._ghostHandler = handler; 52 + return this; 53 + }, 54 + 55 + getGhostHandler : function() { 56 + return this._ghostHandler || JX.bind(this, this._defaultGhostHandler); 57 + }, 58 + 59 + _defaultGhostHandler : function(ghost, target) { 60 + var parent = this._dragging.parentNode; 61 + if (target && target.nextSibling) { 62 + parent.insertBefore(ghost, target.nextSibling); 63 + } else if (!target && parent.firstChild) { 64 + parent.insertBefore(ghost, parent.firstChild); 65 + } else { 66 + parent.appendChild(ghost); 67 + } 68 + }, 69 + 70 + findItems : function() { 71 + var handler = this.getFindItemsHandler(); 72 + if (__DEV__) { 73 + if (!handler) { 74 + JX.$E('JX.Draggable.findItems(): No findItemsHandler set!'); 75 + } 76 + } 77 + 78 + return handler(); 79 + }, 80 + 81 + _ondrag : function(e) { 82 + if (__DEV__) { 83 + var ghost = this.getGhostNode(); 84 + if (!ghost) { 85 + JX.$E('JX.Draggable._ondrag(): No ghostNode set!'); 86 + } 87 + } 88 + 89 + if (this._dragging) { 90 + // Don't start dragging if we're already dragging something. 91 + return; 92 + } 93 + 94 + if (this._locked) { 95 + // Don't start drag operations while locked. 96 + return; 97 + } 98 + 99 + if (!e.isNormalMouseEvent()) { 100 + // Don't start dragging for shift click, right click, etc. 101 + return; 102 + } 103 + 104 + if (this.invoke('shouldBeginDrag', e).getPrevented()) { 105 + return; 106 + } 107 + 108 + e.kill(); 109 + 110 + this._dragging = e.getNode(this._sigil); 111 + this._origin = JX.$V(e); 112 + this._dimensions = JX.$V(this._dragging); 113 + 114 + var targets = []; 115 + var items = this.findItems(); 116 + for (var ii = 0; ii < items.length; ii++) { 117 + targets.push({ 118 + item: items[ii], 119 + y: JX.$V(items[ii]).y + (JX.Vector.getDim(items[ii]).y / 2) 120 + }); 121 + } 122 + targets.sort(function(u, v) { return v.y - u.y; }); 123 + this._targets = targets; 124 + this._target = null; 125 + 126 + this.invoke('didBeginDrag', this._dragging); 127 + }, 128 + 129 + _onmove : function(e) { 130 + if (!this._dragging) { 131 + return; 132 + } 133 + 134 + var ghost = this.getGhostNode(); 135 + var target = this._target; 136 + var targets = this._targets; 137 + var dragging = this._dragging; 138 + var origin = this._origin; 139 + 140 + var p = JX.$V(e); 141 + 142 + // Compute the size and position of the drop target indicator, because we 143 + // need to update our static position computations to account for it. 144 + 145 + var adjust_h = JX.Vector.getDim(ghost).y; 146 + var adjust_y = JX.$V(ghost).y; 147 + 148 + // Find the node we're dragging the object underneath. This is the first 149 + // node in the list that's above the cursor. If that node is the node 150 + // we're dragging or its predecessor, don't select a target, because the 151 + // operation would be a no-op. 152 + 153 + var cur_target = null; 154 + var trigger; 155 + for (var ii = 0; ii < targets.length; ii++) { 156 + 157 + // If the drop target indicator is above the target, we need to adjust 158 + // the target's trigger height down accordingly. This makes dragging 159 + // items down the list smoother, because the target doesn't jump to the 160 + // next item while the cursor is over it. 161 + 162 + trigger = targets[ii].y; 163 + if (adjust_y <= trigger) { 164 + trigger += adjust_h; 165 + } 166 + 167 + // If the cursor is above this target, we aren't dropping underneath it. 168 + 169 + if (trigger >= p.y) { 170 + continue; 171 + } 172 + 173 + // Don't choose the dragged row or its predecessor as targets. 174 + 175 + cur_target = targets[ii].item; 176 + if (cur_target == dragging) { 177 + cur_target = null; 178 + } 179 + if (targets[ii - 1] && targets[ii - 1].item == dragging) { 180 + cur_target = null; 181 + } 182 + 183 + break; 184 + } 185 + 186 + // If we've selected a new target, update the UI to show where we're 187 + // going to drop the row. 188 + 189 + if (cur_target != target) { 190 + 191 + if (target) { 192 + JX.DOM.remove(ghost); 193 + } 194 + 195 + if (cur_target) { 196 + this.getGhostHandler()(ghost, cur_target); 197 + } 198 + 199 + target = cur_target; 200 + 201 + if (target) { 202 + 203 + // If we've changed where the ghost node is, update the adjustments 204 + // so we accurately reflect document state when we tweak things below. 205 + // This avoids a flash of bad state as the mouse is dragged upward 206 + // across the document. 207 + 208 + adjust_h = JX.Vector.getDim(ghost).y; 209 + adjust_y = JX.$V(ghost).y; 210 + } 211 + } 212 + 213 + // If the drop target indicator is above the cursor in the document, 214 + // adjust the cursor position for the change in node document position. 215 + // Do this before choosing a new target to avoid a flash of nonsense. 216 + 217 + if (target) { 218 + if (adjust_y <= origin.y) { 219 + p.y -= adjust_h; 220 + } 221 + } 222 + 223 + p.x = 0; 224 + p.y -= origin.y; 225 + p.setPos(dragging); 226 + this._target = target; 227 + 228 + e.kill(); 229 + }, 230 + 231 + _ondrop : function(e) { 232 + if (!this._dragging) { 233 + return; 234 + } 235 + 236 + var target = this._target; 237 + var dragging = this._dragging; 238 + var ghost = this.getGhostNode(); 239 + 240 + this._dragging = null; 241 + 242 + JX.$V(0, 0).setPos(dragging); 243 + 244 + if (target) { 245 + JX.DOM.remove(dragging); 246 + JX.DOM.replace(ghost, dragging); 247 + this.invoke('didDrop', dragging, target); 248 + } else { 249 + this.invoke('didCancelDrag', dragging); 250 + } 251 + 252 + this.invoke('didEndDrag', dragging); 253 + e.kill(); 254 + }, 255 + 256 + lock : function() { 257 + this._locked++; 258 + if (this._locked === 1) { 259 + this.invoke('didLock'); 260 + } 261 + return this; 262 + }, 263 + 264 + unlock : function() { 265 + if (__DEV__) { 266 + if (!this._locked) { 267 + JX.$E("JX.Draggable.unlock(): Draggable is not locked!"); 268 + } 269 + } 270 + this._locked--; 271 + if (!this._locked) { 272 + this.invoke('didUnlock'); 273 + } 274 + return this; 275 + } 276 + } 277 + 278 + });