diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,8 +7,8 @@ */ return array( 'names' => array( - 'core.pkg.css' => '8b9c004a', - 'core.pkg.js' => 'bf947f93', + 'core.pkg.css' => 'cd66e467', + 'core.pkg.js' => 'c5178ede', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', 'differential.pkg.js' => '5c2ba922', @@ -105,7 +105,7 @@ 'rsrc/css/core/core.css' => '5b3563c8', 'rsrc/css/core/remarkup.css' => 'e1c8b32f', 'rsrc/css/core/syntax.css' => '9fd11da8', - 'rsrc/css/core/z-index.css' => 'a36a45da', + 'rsrc/css/core/z-index.css' => '5c7025bf', 'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa', 'rsrc/css/font/font-aleo.css' => '8bdb2835', 'rsrc/css/font/font-awesome.css' => 'c43323c5', @@ -142,7 +142,7 @@ 'rsrc/css/phui/phui-info-view.css' => '6d7c3509', 'rsrc/css/phui/phui-list.css' => '9da2aa00', 'rsrc/css/phui/phui-object-box.css' => '407eaf5a', - 'rsrc/css/phui/phui-object-item-list-view.css' => 'febf4a79', + 'rsrc/css/phui/phui-object-item-list-view.css' => '699de05e', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 'rsrc/css/phui/phui-profile-menu.css' => 'ab4fcf5f', @@ -446,7 +446,7 @@ 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', 'rsrc/js/core/DragAndDropFileUpload.js' => 'ad10aeac', - 'rsrc/js/core/DraggableList.js' => '255d85da', + 'rsrc/js/core/DraggableList.js' => 'f1844746', 'rsrc/js/core/FileUpload.js' => '477359c8', 'rsrc/js/core/Hovercard.js' => 'c6f720ff', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', @@ -741,7 +741,7 @@ 'phabricator-countdown-css' => 'e7544472', 'phabricator-dashboard-css' => 'eb458607', 'phabricator-drag-and-drop-file-upload' => 'ad10aeac', - 'phabricator-draggable-list' => '255d85da', + 'phabricator-draggable-list' => 'f1844746', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', 'phabricator-file-upload' => '477359c8', @@ -780,7 +780,7 @@ 'phabricator-uiexample-reactor-select' => 'a155550f', 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', - 'phabricator-zindex-css' => 'a36a45da', + 'phabricator-zindex-css' => '5c7025bf', 'phame-css' => '6d5b3682', 'pholio-css' => '95174bdd', 'pholio-edit-css' => '3ad9d1ee', @@ -818,7 +818,7 @@ 'phui-inline-comment-view-css' => '0fdb3667', 'phui-list-view-css' => '9da2aa00', 'phui-object-box-css' => '407eaf5a', - 'phui-object-item-list-view-css' => 'febf4a79', + 'phui-object-item-list-view-css' => '699de05e', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', 'phui-profile-menu-css' => 'ab4fcf5f', @@ -1021,14 +1021,6 @@ 'phabricator-drag-and-drop-file-upload', 'phabricator-draggable-list', ), - '255d85da' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), '2926fff2' => array( 'javelin-behavior', 'javelin-dom', @@ -2019,6 +2011,14 @@ 'javelin-workflow', 'javelin-json', ), + 'f1844746' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), 'f411b6ae' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -383,17 +383,24 @@ ), $this->header); - $header = javelin_tag( + // Wrap the header content in a with the "slippery" sigil. This + // prevents us from beginning a drag if you click the text (like "T123"), + // but not if you click the white space after the header. + $header = phutil_tag( 'div', array( 'class' => 'phui-object-item-name', - 'sigil' => 'slippery', ), - array( - $this->headIcons, - $header_name, - $header_link, - )); + javelin_tag( + 'span', + array( + 'sigil' => 'slippery', + ), + array( + $this->headIcons, + $header_name, + $header_link, + ))); $icons = array(); if ($this->icons) { diff --git a/webroot/rsrc/css/core/z-index.css b/webroot/rsrc/css/core/z-index.css --- a/webroot/rsrc/css/core/z-index.css +++ b/webroot/rsrc/css/core/z-index.css @@ -81,10 +81,6 @@ z-index: 5; } -.drag-dragging { - z-index: 5; -} - .phui-calendar-date-number { z-index: 5; } @@ -114,6 +110,10 @@ z-index: 9; } +.drag-frame { + z-index: 10; +} + .jx-mask { z-index: 10; } diff --git a/webroot/rsrc/css/phui/phui-object-item-list-view.css b/webroot/rsrc/css/phui/phui-object-item-list-view.css --- a/webroot/rsrc/css/phui/phui-object-item-list-view.css +++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css @@ -593,15 +593,36 @@ } .drag-dragging { - position: relative; - background: {$sh-yellowbackground}; - opacity: 0.9; + opacity: 0.25; } .drag-sending { opacity: 0.5; } +.drag-clone, +.drag-frame { + /* This allows mousewheel events to pass through the clone and frame while + they are being dragged. Without this, the mousewheel does not work during + a drag operation. */ + pointer-events: none; +} + +.drag-frame { + position: fixed; + overflow: hidden; + left: 0; + right: 0; + top: 0; + bottom: 0; +} + +.drag-clone { + position: absolute; + list-style: none; +} + + /* - State --------------------------------------------------------------------- Provides a list of object status or states, success or fail, etc diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -44,15 +44,15 @@ _root : null, _dragging : null, _locked : 0, - _origin : null, - _originScroll : null, _target : null, _targets : null, _ghostHandler : null, _ghostNode : null, _group : null, _lastMousePosition: null, - _lastAdjust: null, + _frame: null, + _clone: null, + _offset: null, getRootNode : function() { return this._root; @@ -131,7 +131,17 @@ } } - return handler(); + var items = handler(); + + // Make sure the clone element is never included as a target. + for (var ii = 0; ii < items.length; ii++) { + if (items[ii] === this._clone) { + items.splice(ii, 1); + break; + } + } + + return items; }, _ondrag : function(e) { @@ -167,24 +177,67 @@ e.kill(); - this._dragging = e.getNode(this._sigil); - this._origin = JX.$V(e); - this._originScroll = JX.Vector.getAggregateScrollForNode(this._dragging); + var drag = e.getNode(this._sigil); for (var ii = 0; ii < this._group.length; ii++) { this._group[ii]._clearTarget(); } - 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'; - } + var pos = JX.$V(drag); + var dim = JX.Vector.getDim(drag); - JX.DOM.alterClass(this._dragging, 'drag-dragging', true); + // Create and adjust the ghost nodes. + for (var jj = 0; jj < this._group.length; jj++) { + var ghost = this._group[jj].getGhostNode(); + ghost.style.height = dim.y + 'px'; } + + // Here's what's going on: we're cloning the thing that's being dragged. + // This is the "clone", stored in "this._clone". We're going to leave the + // original where it is in the document, and put the clone at top-level + // so it can be freely dragged around the whole document, even if it's + // inside a container with overflow hidden. + + // Because the clone has been moved up, CSS classes which rely on some + // parent selector won't work. Draggable objects need to pick up all of + // their CSS properties without relying on container classes. This isn't + // great, but leaving them where they are in the document creates a large + // number of positioning problems with scrollable, absolute, relative, + // or overflow hidden containers. + + // Note that we don't actually want to let the user drag it outside the + // document. One problem is that doing so lets the user drag objects + // infinitely far to the right by dragging them to the edge so the + // document extends, scrolling the document, dragging them to the edge + // of the new larger document, scrolling the document, and so on forever. + + // To prevent this, we're putting a "frame" (stored in "this._frame") at + // top level, then putting the clone inside the frame. The frame has the + // same size as the entire viewport, and overflow hidden, so dragging the + // item outside the document just cuts it off. + + // Create the clone for dragging. + var clone = drag.cloneNode(true); + + pos.setPos(clone); + dim.setDim(clone); + + JX.DOM.alterClass(drag, 'drag-dragging', true); + JX.DOM.alterClass(clone, 'drag-clone', true); + + var frame = JX.$N('div', {className: 'drag-frame'}); + frame.appendChild(clone); + + document.body.appendChild(frame); + + this._dragging = drag; + this._clone = clone; + this._frame = frame; + + var cursor = JX.$V(e); + this._offset = new JX.Vector(pos.x - cursor.x, pos.y - cursor.y); + + this.invoke('didBeginDrag', this._dragging); }, _getTargets : function() { @@ -195,18 +248,6 @@ var item = items[ii]; var ipos = JX.$V(item); - if (item == this._dragging) { - // If the item we're measuring is also the item we're dragging, - // we need to measure its position as though it was still in the - // list, not its current position in the document (which is - // under the cursor). To do this, adjust the measured position by - // removing the offsets we added to put the item underneath the - // cursor. - if (this._lastAdjust) { - ipos.x -= this._lastAdjust.x; - ipos.y -= this._lastAdjust.y; - } - } targets.push({ item: items[ii], @@ -398,39 +439,18 @@ } } - // 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. - - var scroll = JX.Vector.getAggregateScrollForNode(this._dragging); - - var origin = { - x: this._origin.x + (this._originScroll.x - scroll.x), - y: this._origin.y + (this._originScroll.y - scroll.y) - }; + var f = JX.$V(this._frame); + p.x -= f.x; + p.y -= f.y; - 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) { - p.y -= adjust_h; - } - } + p.y += this._offset.y; + this._clone.style.top = p.y + 'px'; if (this._canDragX()) { - p.x -= origin.x; - } else { - p.x = 0; + p.x += this._offset.x; + this._clone.style.left = p.x + 'px'; } - p.y -= origin.y; - this._lastAdjust = new JX.Vector(p.x, p.y); - p.setPos(this._dragging); - e.kill(); }, @@ -444,6 +464,10 @@ var dragging = this._dragging; this._dragging = null; + JX.DOM.remove(this._frame); + this._frame = null; + this._clone = null; + var target = false; var ghost = false; @@ -469,7 +493,6 @@ for (var ii = 0; ii < group.length; ii++) { JX.DOM.alterClass(group[ii].getRootNode(), 'drag-target-list', false); group[ii]._clearTarget(); - group[ii]._lastAdjust = null; } if (!this.invoke('didEndDrag', dragging).getPrevented()) {