[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 this._group = [this]; 18 19 // NOTE: Javelin does not dispatch mousemove by default. 20 JX.enableDispatch(document.body, 'mousemove'); 21 22 JX.DOM.listen(this._root, 'mousedown', sigil, JX.bind(this, this._ondrag)); 23 JX.Stratcom.listen('mousemove', null, JX.bind(this, this._onmove)); 24 JX.Stratcom.listen('scroll', null, JX.bind(this, this._onmove)); 25 JX.Stratcom.listen('mouseup', null, JX.bind(this, this._ondrop)); 26 }, 27 28 events : [ 29 'didLock', 30 'didUnlock', 31 'shouldBeginDrag', 32 'didBeginDrag', 33 'didCancelDrag', 34 'didEndDrag', 35 'didDrop', 36 'didSend', 37 'didReceive'], 38 39 properties : { 40 findItemsHandler : null 41 }, 42 43 members : { 44 _root : null, 45 _dragging : null, 46 _locked : 0, 47 _origin : null, 48 _originScroll : null, 49 _target : null, 50 _targets : null, 51 _dimensions : null, 52 _ghostHandler : null, 53 _ghostNode : null, 54 _group : null, 55 _lastMousePosition: null, 56 _lastAdjust: null, 57 58 getRootNode : function() { 59 return this._root; 60 }, 61 62 setGhostHandler : function(handler) { 63 this._ghostHandler = handler; 64 return this; 65 }, 66 67 getGhostHandler : function() { 68 return this._ghostHandler || JX.bind(this, this._defaultGhostHandler); 69 }, 70 71 getGhostNode : function() { 72 if (!this._ghostNode) { 73 this._ghostNode = JX.$N('li', {className: 'drag-ghost'}); 74 } 75 return this._ghostNode; 76 }, 77 78 setGhostNode : function(node) { 79 this._ghostNode = node; 80 return this; 81 }, 82 83 setGroup : function(lists) { 84 var result = []; 85 var need_self = true; 86 for (var ii = 0; ii < lists.length; ii++) { 87 if (lists[ii] == this) { 88 need_self = false; 89 } 90 result.push(lists[ii]); 91 } 92 93 if (need_self) { 94 result.push(this); 95 } 96 97 this._group = result; 98 return this; 99 }, 100 101 _canDragX : function() { 102 return this._hasGroup(); 103 }, 104 105 _hasGroup : function() { 106 return (this._group.length > 1); 107 }, 108 109 _defaultGhostHandler : function(ghost, target) { 110 var parent; 111 112 if (!this._hasGroup()) { 113 parent = this._dragging.parentNode; 114 } else { 115 parent = this.getRootNode(); 116 } 117 118 if (target && target.nextSibling) { 119 parent.insertBefore(ghost, target.nextSibling); 120 } else if (!target && parent.firstChild) { 121 parent.insertBefore(ghost, parent.firstChild); 122 } else { 123 parent.appendChild(ghost); 124 } 125 }, 126 127 findItems : function() { 128 var handler = this.getFindItemsHandler(); 129 if (__DEV__) { 130 if (!handler) { 131 JX.$E('JX.Draggable.findItems(): No findItemsHandler set!'); 132 } 133 } 134 135 return handler(); 136 }, 137 138 _ondrag : function(e) { 139 if (this._dragging) { 140 // Don't start dragging if we're already dragging something. 141 return; 142 } 143 144 if (this._locked) { 145 // Don't start drag operations while locked. 146 return; 147 } 148 149 if (!e.isNormalMouseEvent()) { 150 // Don't start dragging for shift click, right click, etc. 151 return; 152 } 153 154 if (this.invoke('shouldBeginDrag', e).getPrevented()) { 155 return; 156 } 157 158 if (e.getNode('tag:a')) { 159 // Never start a drag if we're somewhere inside an <a> tag. This makes 160 // links unclickable in Firefox. 161 return; 162 } 163 164 if (JX.Stratcom.pass()) { 165 // Let other handlers deal with this event before we do. 166 return; 167 } 168 169 e.kill(); 170 171 this._dragging = e.getNode(this._sigil); 172 this._origin = JX.$V(e); 173 this._originScroll = JX.Vector.getAggregateScrollForNode(this._dragging); 174 this._dimensions = JX.$V(this._dragging); 175 176 for (var ii = 0; ii < this._group.length; ii++) { 177 this._group[ii]._clearTarget(); 178 } 179 180 if (!this.invoke('didBeginDrag', this._dragging).getPrevented()) { 181 // Set the height of all the ghosts in the group. In the normal case, 182 // this just sets this list's ghost height. 183 for (var jj = 0; jj < this._group.length; jj++) { 184 var ghost = this._group[jj].getGhostNode(); 185 ghost.style.height = JX.Vector.getDim(this._dragging).y + 'px'; 186 } 187 188 JX.DOM.alterClass(this._dragging, 'drag-dragging', true); 189 } 190 }, 191 192 _getTargets : function() { 193 if (this._targets === null) { 194 var targets = []; 195 var items = this.findItems(); 196 for (var ii = 0; ii < items.length; ii++) { 197 var item = items[ii]; 198 199 var ipos = JX.$V(item); 200 if (item == this._dragging) { 201 // If the item we're measuring is also the item we're dragging, 202 // we need to measure its position as though it was still in the 203 // list, not its current position in the document (which is 204 // under the cursor). To do this, adjust the measured position by 205 // removing the offsets we added to put the item underneath the 206 // cursor. 207 if (this._lastAdjust) { 208 ipos.x -= this._lastAdjust.x; 209 ipos.y -= this._lastAdjust.y; 210 } 211 } 212 213 targets.push({ 214 item: items[ii], 215 y: ipos.y + (JX.Vector.getDim(items[ii]).y / 2) 216 }); 217 } 218 targets.sort(function(u, v) { return v.y - u.y; }); 219 this._targets = targets; 220 } 221 222 return this._targets; 223 }, 224 225 _dirtyTargetCache: function() { 226 if (this._hasGroup()) { 227 var group = this._group; 228 for (var ii = 0; ii < group.length; ii++) { 229 group[ii]._targets = null; 230 } 231 } else { 232 this._targets = null; 233 } 234 235 return this; 236 }, 237 238 _getTargetList : function(p) { 239 var target_list; 240 if (this._hasGroup()) { 241 var group = this._group; 242 for (var ii = 0; ii < group.length; ii++) { 243 var root = group[ii].getRootNode(); 244 var rp = JX.$V(root); 245 var rd = JX.Vector.getDim(root); 246 247 var is_target = false; 248 if (p.x >= rp.x && p.y >= rp.y) { 249 if (p.x <= (rp.x + rd.x) && p.y <= (rp.y + rd.y)) { 250 is_target = true; 251 target_list = group[ii]; 252 } 253 } 254 255 JX.DOM.alterClass(root, 'drag-target-list', is_target); 256 } 257 } else { 258 target_list = this; 259 } 260 261 return target_list; 262 }, 263 264 _setTarget : function(cur_target) { 265 var ghost = this.getGhostNode(); 266 var target = this._target; 267 268 if (cur_target !== target) { 269 this._clearTarget(); 270 if (cur_target !== false) { 271 var ok = this.getGhostHandler()(ghost, cur_target); 272 // If the handler returns explicit `false`, prevent the drag. 273 if (ok === false) { 274 cur_target = false; 275 } 276 } 277 278 this._target = cur_target; 279 } 280 281 return this; 282 }, 283 284 _clearTarget : function() { 285 var target = this._target; 286 var ghost = this.getGhostNode(); 287 288 if (target !== false) { 289 JX.DOM.remove(ghost); 290 } 291 292 this._target = false; 293 return this; 294 }, 295 296 _getCurrentTarget : function(p) { 297 var ghost = this.getGhostNode(); 298 var targets = this._getTargets(); 299 var dragging = this._dragging; 300 301 var adjust_h = JX.Vector.getDim(ghost).y; 302 var adjust_y = JX.$V(ghost).y; 303 304 // Find the node we're dragging the object underneath. This is the first 305 // node in the list that's above the cursor. If that node is the node 306 // we're dragging or its predecessor, don't select a target, because the 307 // operation would be a no-op. 308 309 // NOTE: When we're dragging into the first position in the list, we 310 // use the target `null`. When we don't have a valid target, we use 311 // the target `false`. Spooky! Magic! Anyway, `null` and `false` mean 312 // completely different things. 313 314 var cur_target = null; 315 var trigger; 316 for (var ii = 0; ii < targets.length; ii++) { 317 318 // If the drop target indicator is above the target, we need to adjust 319 // the target's trigger height down accordingly. This makes dragging 320 // items down the list smoother, because the target doesn't jump to the 321 // next item while the cursor is over it. 322 323 trigger = targets[ii].y; 324 if (adjust_y <= trigger) { 325 trigger += adjust_h; 326 } 327 328 // If the cursor is above this target, we aren't dropping underneath it. 329 330 if (trigger >= p.y) { 331 continue; 332 } 333 334 // Don't choose the dragged row or its predecessor as targets. 335 336 cur_target = targets[ii].item; 337 if (!dragging) { 338 // If the item on the cursor isn't from this list, it can't be 339 // dropped onto itself or its predecessor in this list. 340 } else { 341 if (cur_target == dragging) { 342 cur_target = false; 343 } 344 if (targets[ii - 1] && targets[ii - 1].item == dragging) { 345 cur_target = false; 346 } 347 } 348 349 break; 350 } 351 352 // If the dragged row is the first row, don't allow it to be dragged 353 // into the first position, since this operation doesn't make sense. 354 if (dragging && cur_target === null) { 355 var first_item = targets[targets.length - 1].item; 356 if (dragging === first_item) { 357 cur_target = false; 358 } 359 } 360 361 return cur_target; 362 }, 363 364 _onmove : function(e) { 365 // We'll get a callback here for "mousemove" (and can determine the 366 // location of the cursor) and also for "scroll" (and can not). If this 367 // is a move, save the mouse position, so if we get a scroll next we can 368 // reuse the known position. 369 370 if (e.getType() == 'mousemove') { 371 this._lastMousePosition = JX.$V(e); 372 } 373 374 if (!this._dragging) { 375 return; 376 } 377 378 if (!this._lastMousePosition) { 379 return; 380 } 381 382 if (e.getType() == 'scroll') { 383 // If this is a scroll event, the positions of drag targets may have 384 // changed. 385 this._dirtyTargetCache(); 386 } 387 388 var p = JX.$V(this._lastMousePosition.x, this._lastMousePosition.y); 389 390 var group = this._group; 391 var target_list = this._getTargetList(p); 392 393 // Compute the size and position of the drop target indicator, because we 394 // need to update our static position computations to account for it. 395 396 var cur_target = false; 397 if (target_list) { 398 cur_target = target_list._getCurrentTarget(p); 399 } 400 401 // If we've selected a new target, update the UI to show where we're 402 // going to drop the row. 403 404 for (var ii = 0; ii < group.length; ii++) { 405 if (group[ii] == target_list) { 406 group[ii]._setTarget(cur_target); 407 } else { 408 group[ii]._clearTarget(); 409 } 410 } 411 412 // If the drop target indicator is above the cursor in the document, 413 // adjust the cursor position for the change in node document position. 414 // Do this before choosing a new target to avoid a flash of nonsense. 415 416 var scroll = JX.Vector.getAggregateScrollForNode(this._dragging); 417 418 var origin = { 419 x: this._origin.x + (this._originScroll.x - scroll.x), 420 y: this._origin.y + (this._originScroll.y - scroll.y) 421 }; 422 423 var adjust_h = 0; 424 var adjust_y = 0; 425 if (this._target !== false) { 426 var ghost = this.getGhostNode(); 427 adjust_h = JX.Vector.getDim(ghost).y; 428 adjust_y = JX.$V(ghost).y; 429 430 if (adjust_y <= origin.y) { 431 p.y -= adjust_h; 432 } 433 } 434 435 if (this._canDragX()) { 436 p.x -= origin.x; 437 } else { 438 p.x = 0; 439 } 440 441 p.y -= origin.y; 442 this._lastAdjust = new JX.Vector(p.x, p.y); 443 p.setPos(this._dragging); 444 445 e.kill(); 446 }, 447 448 _ondrop : function(e) { 449 if (!this._dragging) { 450 return; 451 } 452 453 var p = JX.$V(e); 454 455 var dragging = this._dragging; 456 this._dragging = null; 457 458 var target = false; 459 var ghost = false; 460 461 var target_list = this._getTargetList(p); 462 if (target_list) { 463 target = target_list._target; 464 ghost = target_list.getGhostNode(); 465 } 466 467 JX.$V(0, 0).setPos(dragging); 468 469 if (target !== false) { 470 JX.DOM.remove(dragging); 471 JX.DOM.replace(ghost, dragging); 472 this.invoke('didSend', dragging, target_list); 473 target_list.invoke('didReceive', dragging, this); 474 target_list.invoke('didDrop', dragging, target, this); 475 } else { 476 this.invoke('didCancelDrag', dragging); 477 } 478 479 var group = this._group; 480 for (var ii = 0; ii < group.length; ii++) { 481 JX.DOM.alterClass(group[ii].getRootNode(), 'drag-target-list', false); 482 group[ii]._clearTarget(); 483 group[ii]._dirtyTargetCache(); 484 group[ii]._lastAdjust = null; 485 } 486 487 if (!this.invoke('didEndDrag', dragging).getPrevented()) { 488 JX.DOM.alterClass(dragging, 'drag-dragging', false); 489 } 490 491 e.kill(); 492 }, 493 494 lock : function() { 495 for (var ii = 0; ii < this._group.length; ii++) { 496 this._group[ii]._lock(); 497 } 498 return this; 499 }, 500 501 _lock : function() { 502 this._locked++; 503 if (this._locked === 1) { 504 this.invoke('didLock'); 505 } 506 return this; 507 }, 508 509 unlock: function() { 510 for (var ii = 0; ii < this._group.length; ii++) { 511 this._group[ii]._unlock(); 512 } 513 return this; 514 }, 515 516 _unlock : function() { 517 if (__DEV__) { 518 if (!this._locked) { 519 JX.$E('JX.Draggable.unlock(): Draggable is not locked!'); 520 } 521 } 522 this._locked--; 523 if (!this._locked) { 524 this.invoke('didUnlock'); 525 } 526 return this; 527 } 528 529 } 530 531 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |