[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/webroot/rsrc/js/core/ -> DraggableList.js (source)

   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  });


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1