Index: resources/celerity/map.php =================================================================== --- resources/celerity/map.php +++ resources/celerity/map.php @@ -7,7 +7,7 @@ return array( 'names' => array( - 'core.pkg.css' => '6d59624c', + 'core.pkg.css' => 'ac7deb21', 'core.pkg.js' => 'c907bd96', 'darkconsole.pkg.js' => 'ca8671ce', 'differential.pkg.css' => '827749c1', @@ -137,7 +137,7 @@ 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', 'rsrc/css/phui/phui-list.css' => '2edb76cf', 'rsrc/css/phui/phui-object-box.css' => '4f916b80', - 'rsrc/css/phui/phui-object-item-list-view.css' => '642fe6b9', + 'rsrc/css/phui/phui-object-item-list-view.css' => 'fdd2c06f', 'rsrc/css/phui/phui-pinboard-view.css' => '53c5fca0', 'rsrc/css/phui/phui-property-list-view.css' => '354465ae', 'rsrc/css/phui/phui-remarkup-preview.css' => '19ad512b', @@ -392,6 +392,7 @@ 'rsrc/js/application/policy/behavior-policy-control.js' => 'c01153ea', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '263aeb8c', 'rsrc/js/application/ponder/behavior-votebox.js' => '327dbe61', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'd4cbe3d5', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '9eb2cedb', 'rsrc/js/application/releeph/releeph-request-state-change.js' => 'fe7fc914', @@ -416,7 +417,7 @@ 'rsrc/js/application/uiexample/notification-example.js' => 'c51a6616', 'rsrc/js/core/Busy.js' => '6453c869', 'rsrc/js/core/DragAndDropFileUpload.js' => 'ae6abfba', - 'rsrc/js/core/DraggableList.js' => '6f5a879c', + 'rsrc/js/core/DraggableList.js' => '5fb99faa', 'rsrc/js/core/DropdownMenu.js' => '2f6f80f4', 'rsrc/js/core/DropdownMenuItem.js' => '0f386ef4', 'rsrc/js/core/FileUpload.js' => '96713558', @@ -602,6 +603,7 @@ 'javelin-behavior-policy-control' => 'c01153ea', 'javelin-behavior-policy-rule-editor' => '263aeb8c', 'javelin-behavior-ponder-votebox' => '327dbe61', + 'javelin-behavior-project-boards' => 'd4cbe3d5', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-refresh-csrf' => 'c4b31646', 'javelin-behavior-releeph-preview-branch' => '9eb2cedb', @@ -673,7 +675,7 @@ 'phabricator-countdown-css' => '86b7b0a0', 'phabricator-crumbs-view-css' => '2d9db584', 'phabricator-drag-and-drop-file-upload' => 'ae6abfba', - 'phabricator-draggable-list' => '6f5a879c', + 'phabricator-draggable-list' => '5fb99faa', 'phabricator-dropdown-menu' => '2f6f80f4', 'phabricator-fatal-config-template-css' => '25d446d6', 'phabricator-feed-css' => '4716c86f', @@ -741,7 +743,7 @@ 'phui-info-panel-css' => '27ea50a1', 'phui-list-view-css' => '2edb76cf', 'phui-object-box-css' => '4f916b80', - 'phui-object-item-list-view-css' => '642fe6b9', + 'phui-object-item-list-view-css' => 'fdd2c06f', 'phui-pinboard-view-css' => '53c5fca0', 'phui-property-list-view-css' => '354465ae', 'phui-remarkup-preview-css' => '19ad512b', @@ -1153,6 +1155,15 @@ array( 0 => 'javelin-install', ), + '5fb99faa' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'javelin-util', + 4 => 'javelin-vector', + 5 => 'javelin-magical-init', + ), '61d927ec' => array( 0 => 'javelin-behavior', @@ -1192,15 +1203,6 @@ 1 => 'javelin-dom', 2 => 'javelin-workflow', ), - '6f5a879c' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'javelin-util', - 4 => 'javelin-vector', - 5 => 'javelin-magical-init', - ), '71755c79' => array( 0 => 'javelin-behavior', @@ -1711,6 +1713,13 @@ 1 => 'javelin-dom', 2 => 'javelin-view', ), + 'd4cbe3d5' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'phabricator-draggable-list', + ), 'd6ca6b1c' => array( 0 => 'javelin-install', Index: src/applications/project/controller/PhabricatorProjectBoardController.php =================================================================== --- src/applications/project/controller/PhabricatorProjectBoardController.php +++ src/applications/project/controller/PhabricatorProjectBoardController.php @@ -61,9 +61,18 @@ $task_map[$default_phid][] = $task->getPHID(); } + $board_id = celerity_generate_unique_node_id(); + $board = id(new PHUIWorkboardView()) ->setUser($viewer) - ->setFluidishLayout(true); + ->setFluidishLayout(true) + ->setID($board_id); + + $this->initBehavior( + 'project-boards', + array( + 'boardID' => $board_id, + )); foreach ($columns as $column) { $panel = id(new PHUIWorkpanelView()) @@ -74,7 +83,8 @@ $cards = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setCards(true) - ->setFlush(true); + ->setFlush(true) + ->addSigil('project-column'); $task_phids = idx($task_map, $column->getPHID(), array()); foreach (array_select_keys($tasks, $task_phids) as $task) { $cards->addItem($this->renderTaskCard($task)); @@ -148,6 +158,7 @@ ->setHeader($task->getTitle()) ->setGrippable($can_edit) ->setHref('/T'.$task->getID()) + ->addSigil('project-card') ->addAction( id(new PHUIListItemView()) ->setName(pht('Edit')) Index: webroot/rsrc/css/phui/phui-object-item-list-view.css =================================================================== --- webroot/rsrc/css/phui/phui-object-item-list-view.css +++ webroot/rsrc/css/phui/phui-object-item-list-view.css @@ -572,3 +572,7 @@ padding-top: 0; } +.drag-target-list { + /* TODO: This is a work in progress. */ + background: red; +} Index: webroot/rsrc/js/application/projects/behavior-project-boards.js =================================================================== --- /dev/null +++ webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -0,0 +1,29 @@ +/** + * @provides javelin-behavior-project-boards + * @requires javelin-behavior + * javelin-dom + * javelin-util + * phabricator-draggable-list + */ + +JX.behavior('project-boards', function(config) { + + function finditems(col) { + return JX.DOM.scry(col, 'li', 'project-card'); + } + + var lists = []; + var ii; + var cols = JX.DOM.scry(JX.$(config.boardID), 'ul', 'project-column'); + + for (ii = 0; ii < cols.length; ii++) { + var list = new JX.DraggableList('project-card', cols[ii]) + .setFindItemsHandler(JX.bind(null, finditems, cols[ii])); + lists.push(list); + } + + for (ii = 0; ii < lists.length; ii++) { + lists[ii].setGroup(lists); + } + +}); Index: webroot/rsrc/js/core/DraggableList.js =================================================================== --- webroot/rsrc/js/core/DraggableList.js +++ webroot/rsrc/js/core/DraggableList.js @@ -14,6 +14,7 @@ construct : function(sigil, root) { this._sigil = sigil; this._root = root || document.body; + this._group = [this]; // NOTE: Javelin does not dispatch mousemove by default. JX.enableDispatch(document.body, 'mousemove'); @@ -46,6 +47,11 @@ _dimensions : null, _ghostHandler : null, _ghostNode : null, + _group : null, + + getRootNode : function() { + return this._root; + }, setGhostHandler : function(handler) { this._ghostHandler = handler; @@ -68,8 +74,41 @@ return this; }, + setGroup : function(lists) { + var result = []; + var need_self = true; + for (var ii = 0; ii < lists.length; ii++) { + if (lists[ii] == this) { + need_self = false; + } + result.push(lists[ii]); + } + + if (need_self) { + result.push(this); + } + + this._group = result; + return this; + }, + + _canDragX : function() { + return this._hasGroup(); + }, + + _hasGroup : function() { + return (this._group.length > 1); + }, + _defaultGhostHandler : function(ghost, target) { - var parent = this._dragging.parentNode; + var parent; + + if (!this._hasGroup()) { + parent = this._dragging.parentNode; + } else { + parent = this.getRootNode(); + } + if (target && target.nextSibling) { parent.insertBefore(ghost, target.nextSibling); } else if (!target && parent.firstChild) { @@ -116,6 +155,24 @@ this._origin = JX.$V(e); this._dimensions = JX.$V(this._dragging); + for (var ii = 0; ii < this._group.length; ii++) { + this._group[ii]._clearTarget(); + this._group[ii]._generateTargets(); + } + + 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'; + } + + JX.DOM.alterClass(this._dragging, 'drag-dragging', true); + } + }, + + _generateTargets : function() { var targets = []; var items = this.findItems(); for (var ii = 0; ii < items.length; ii++) { @@ -126,30 +183,73 @@ } targets.sort(function(u, v) { return v.y - u.y; }); this._targets = targets; - this._target = false; - if (!this.invoke('didBeginDrag', this._dragging).getPrevented()) { - var ghost = this.getGhostNode(); - ghost.style.height = JX.Vector.getDim(this._dragging).y + 'px'; - JX.DOM.alterClass(this._dragging, 'drag-dragging', true); + return this; + }, + + _getTargetList : function(p) { + var target_list; + if (this._hasGroup()) { + var group = this._group; + for (var ii = 0; ii < group.length; ii++) { + var root = group[ii].getRootNode(); + var rp = JX.$V(root); + var rd = JX.Vector.getDim(root); + + var is_target = false; + if (p.x >= rp.x && p.y >= rp.y) { + if (p.x <= (rp.x + rd.x) && p.y <= (rp.y + rd.y)) { + is_target = true; + target_list = group[ii]; + } + } + + JX.DOM.alterClass(root, 'drag-target-list', is_target); + } + } else { + target_list = this; } + + return target_list; }, - _onmove : function(e) { - if (!this._dragging) { - return; + _setTarget : function(cur_target) { + var ghost = this.getGhostNode(); + var target = this._target; + + if (cur_target !== target) { + this._clearTarget(); + if (cur_target !== false) { + var ok = this.getGhostHandler()(ghost, cur_target); + // If the handler returns explicit `false`, prevent the drag. + if (ok === false) { + cur_target = false; + } + } + + this._target = cur_target; + } + + return this; + }, + + _clearTarget : function() { + var target = this._target; + var ghost = this.getGhostNode(); + + if (target !== false) { + JX.DOM.remove(ghost); } + this._target = false; + return this; + }, + + _getCurrentTarget : function(p) { 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; @@ -187,11 +287,16 @@ // Don't choose the dragged row or its predecessor as targets. cur_target = targets[ii].item; - if (cur_target == dragging) { - cur_target = false; - } - if (targets[ii - 1] && targets[ii - 1].item == dragging) { - cur_target = false; + if (!dragging) { + // If the item on the cursor isn't from this list, it can't be + // dropped onto itself or its predecessor in this list. + } else { + if (cur_target == dragging) { + cur_target = false; + } + if (targets[ii - 1] && targets[ii - 1].item == dragging) { + cur_target = false; + } } break; @@ -199,41 +304,42 @@ // If the dragged row is the first row, don't allow it to be dragged // into the first position, since this operation doesn't make sense. - if (cur_target === null) { + if (dragging && cur_target === null) { var first_item = targets[targets.length - 1].item; if (dragging === first_item) { cur_target = false; } } - // If we've selected a new target, update the UI to show where we're - // going to drop the row. + return cur_target; + }, - if (cur_target !== target) { + _onmove : function(e) { + if (!this._dragging) { + return; + } - if (target !== false) { - JX.DOM.remove(ghost); - } + var p = JX.$V(e); - if (cur_target !== false) { - var ok = this.getGhostHandler()(ghost, cur_target); - // If the handler returns explicit `false`, prevent the drag. - if (ok === false) { - cur_target = false; - } - } + var group = this._group; + var target_list = this._getTargetList(p); - target = cur_target; + // Compute the size and position of the drop target indicator, because we + // need to update our static position computations to account for it. - if (target !== false) { + var cur_target = false; + if (target_list) { + cur_target = target_list._getCurrentTarget(p); + } - // 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. + // If we've selected a new target, update the UI to show where we're + // going to drop the row. - adjust_h = JX.Vector.getDim(ghost).y; - adjust_y = JX.$V(ghost).y; + for (var ii = 0; ii < group.length; ii++) { + if (group[ii] == target_list) { + group[ii]._setTarget(cur_target); + } else { + group[ii]._clearTarget(); } } @@ -241,16 +347,28 @@ // 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 !== false) { + var origin = this._origin; + + 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.x = 0; + if (this._canDragX()) { + p.x -= origin.x; + } else { + p.x = 0; + } + p.y -= origin.y; - p.setPos(dragging); - this._target = target; + p.setPos(this._dragging); e.kill(); }, @@ -276,6 +394,12 @@ this.invoke('didCancelDrag', dragging); } + var group = this._group; + for (var ii = 0; ii < group.length; ii++) { + JX.DOM.alterClass(group[ii].getRootNode(), 'drag-target-list', false); + group[ii]._clearTarget(); + } + if (!this.invoke('didEndDrag', dragging).getPrevented()) { JX.DOM.alterClass(dragging, 'drag-dragging', false); }