Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(8649)

Unified Diff: chrome/browser/resources/md_bookmarks/dnd_manager.js

Issue 2746363013: [MD Bookmarks] Add a drag and drop indicator to bookmarks. (Closed)
Patch Set: fix nit Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/browser/resources/md_bookmarks/dnd_manager.js
diff --git a/chrome/browser/resources/md_bookmarks/dnd_manager.js b/chrome/browser/resources/md_bookmarks/dnd_manager.js
new file mode 100644
index 0000000000000000000000000000000000000000..5304a097925c4814a01d081614ccf30334d34101
--- /dev/null
+++ b/chrome/browser/resources/md_bookmarks/dnd_manager.js
@@ -0,0 +1,519 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * Enumeration of valid drop locations relative to an element. These are
+ * bit masks to allow combining multiple locations in a single value.
+ * @enum {number}
+ * @const
+ */
+var DropPosition = {
+ NONE: 0,
+ ABOVE: 1,
+ ON: 2,
+ BELOW: 4,
+};
+
+/** @typedef {{element: BookmarkElement, position: DropPosition}} */
+var DropDestination;
+
+cr.define('bookmarks', function() {
+ /**
+ * @param {BookmarkElement} element
+ * @return {boolean}
+ */
+ function isBookmarkItem(element) {
+ return element.tagName == 'BOOKMARKS-ITEM';
+ }
+
+ /**
+ * @param {BookmarkElement} element
+ * @return {boolean}
+ */
+ function isBookmarkFolderNode(element) {
+ return element.tagName == 'BOOKMARKS-FOLDER-NODE';
+ }
+
+ /**
+ * @param {Array<!Element>|undefined} path
+ * @return {BookmarkElement}
+ */
+ function getBookmarkElement(path) {
+ if (!path)
+ return null;
+
+ for (var i = 0; i < path.length; i++) {
+ if (isBookmarkItem(path[i]) || isBookmarkFolderNode(path[i]))
+ return path[i];
+ }
+ return null;
+ }
+
+ /**
+ * @param {BookmarkElement} bookmarkElement
+ * @return {BookmarkNode}
+ */
+ function getBookmarkNode(bookmarkElement) {
+ return bookmarks.Store.getInstance().data.nodes[bookmarkElement.itemId];
+ }
+
+ /**
+ * Contains and provides utility methods for drag data sent by the
+ * bookmarkManagerPrivate API.
+ * @constructor
+ */
+ function DragInfo() {
+ /** @type {DragData} */
+ this.dragData = null;
+ }
+
+ DragInfo.prototype = {
+ /** @param {DragData} newDragData */
+ handleChromeDragEnter: function(newDragData) {
+ this.dragData = newDragData;
+ },
+
+ clearDragData: function() {
+ this.dragData = null;
+ },
+
+ /** @return {boolean} */
+ isDragValid: function() {
+ return !!this.dragData;
+ },
+
+ /** @return {boolean} */
+ isSameProfile: function() {
+ return !!this.dragData && this.dragData.sameProfile;
+ },
+
+ /** @return {boolean} */
+ isDraggingFolders: function() {
+ return !!this.dragData && this.dragData.elements.some(function(node) {
+ return !node.url;
+ });
+ },
+
+ /** @return {boolean} */
+ isDraggingBookmark: function(bookmarkId) {
+ return !!this.dragData && this.isSameProfile() &&
+ this.dragData.elements.some(function(node) {
+ return node.id == bookmarkId;
+ });
+ },
+
+ /** @return {boolean} */
+ isDraggingChildBookmark: function(folderId) {
+ return !!this.dragData && this.isSameProfile() &&
+ this.dragData.elements.some(function(node) {
+ return node.parentId == folderId;
+ });
+ },
+
+ /** @return {boolean} */
+ isDraggingFolderToDescendant: function(itemId, nodes) {
+ if (!this.isSameProfile())
+ return false;
+
+ var parentId = nodes[itemId].parentId;
+ var parents = {};
+ while (parentId) {
+ parents[parentId] = true;
+ parentId = nodes[parentId].parentId;
+ }
+
+ return !!this.dragData && this.dragData.elements.some(function(node) {
+ return parents[node.id];
+ });
+ },
+ };
+
+ /**
+ * Encapsulates the behavior of the drag and drop indicator which puts a line
+ * between items or highlights folders which are valid drop targets.
+ * @constructor
+ */
+ function DropIndicator() {
+ /**
+ * @private {number|null} Timer id used to help minimize flicker.
+ */
+ this.removeDropIndicatorTimer_ = null;
+
+ /**
+ * The element that had a style applied it to indicate the drop location.
+ * This is used to easily remove the style when necessary.
+ * @private {BookmarkElement|null}
+ */
+ this.lastIndicatorElement_ = null;
+
+ /**
+ * The style that was applied to indicate the drop location.
+ * @private {?string|null}
+ */
+ this.lastIndicatorClassName_ = null;
+
+ /**
+ * Used to instantly remove the indicator style in tests.
+ * @private {function((Function|null|string), number)}
+ */
+ this.setTimeout_ = window.setTimeout.bind(window);
+ }
+
+ DropIndicator.prototype = {
+ /**
+ * Applies the drop indicator style on the target element and stores that
+ * information to easily remove the style in the future.
+ * @param {HTMLElement} indicatorElement
+ * @param {DropPosition} position
+ */
+ addDropIndicatorStyle: function(indicatorElement, position) {
+ var indicatorStyleName = position == DropPosition.ABOVE ?
+ 'drag-above' :
+ position == DropPosition.BELOW ? 'drag-below' : 'drag-on';
+
+ this.lastIndicatorElement_ = indicatorElement;
+ this.lastIndicatorClassName_ = indicatorStyleName;
+
+ indicatorElement.classList.add(indicatorStyleName);
+ },
+
+ /**
+ * Clears the drop indicator style from the last drop target.
+ */
+ removeDropIndicatorStyle: function() {
+ if (!this.lastIndicatorElement_ || !this.lastIndicatorClassName_)
+ return;
+
+ this.lastIndicatorElement_.classList.remove(this.lastIndicatorClassName_);
+ this.lastIndicatorElement_ = null;
+ this.lastIndicatorClassName_ = null;
+ },
+
+ /**
+ * Displays the drop indicator on the current drop target to give the
+ * user feedback on where the drop will occur.
+ * @param {DropDestination} dropDest
+ */
+ update: function(dropDest) {
+ window.clearTimeout(this.removeDropIndicatorTimer_);
+
+ var indicatorElement = dropDest.element.getDropTarget();
+ var position = dropDest.position;
+
+ this.removeDropIndicatorStyle();
+ this.addDropIndicatorStyle(indicatorElement, position);
+ },
+
+ /**
+ * Stop displaying the drop indicator.
+ */
+ finish: function() {
+ // The use of a timeout is in order to reduce flickering as we move
+ // between valid drop targets.
+ window.clearTimeout(this.removeDropIndicatorTimer_);
+ this.removeDropIndicatorTimer_ = this.setTimeout_(function() {
+ this.removeDropIndicatorStyle();
+ }.bind(this), 100);
+ },
+
+ disableTimeoutForTesting: function() {
+ this.setTimeout_ = function(fn, timeout) {
+ fn();
+ };
+ },
+ };
+
+ /**
+ * Manages drag and drop events for the bookmarks-app.
+ * @constructor
+ */
+ function DNDManager() {
+ /** @private {bookmarks.DragInfo} */
+ this.dragInfo_ = null;
+
+ /** @private {?DropDestination} */
+ this.dropDestination_ = null;
+
+ /** @private {bookmarks.DropIndicator} */
+ this.dropIndicator_ = null;
+
+ /** @private {Object<string, function(!Event)>} */
+ this.documentListeners_ = null;
+ }
+
+ DNDManager.prototype = {
+ init: function() {
+ this.dragInfo_ = new DragInfo();
+ this.dropIndicator_ = new DropIndicator();
+
+ this.documentListeners_ = {
+ 'dragstart': this.onDragStart_.bind(this),
+ 'dragenter': this.onDragEnter_.bind(this),
+ 'dragover': this.onDragOver_.bind(this),
+ 'dragleave': this.onDragLeave_.bind(this),
+ 'drop': this.onDrop_.bind(this),
+ 'dragend': this.clearDragData_.bind(this),
+ 'mouseup': this.clearDragData_.bind(this),
+ // TODO(calamity): Add touch support.
+ };
+ for (var event in this.documentListeners_)
+ document.addEventListener(event, this.documentListeners_[event]);
+
+ chrome.bookmarkManagerPrivate.onDragEnter.addListener(
+ this.dragInfo_.handleChromeDragEnter.bind(this.dragInfo_));
+ chrome.bookmarkManagerPrivate.onDragLeave.addListener(
+ this.clearDragData_.bind(this));
+ chrome.bookmarkManagerPrivate.onDrop.addListener(
+ this.clearDragData_.bind(this));
+ },
+
+ destroy: function() {
+ for (var event in this.documentListeners_)
+ document.removeEventListener(event, this.documentListeners_[event]);
+ },
+
+ /** @private */
+ onDragLeave_: function() {
+ this.dropIndicator_.finish();
+ },
+
+ /**
+ * @private
+ * @param {!Event} e
+ */
+ onDrop_: function(e) {
+ if (this.dropDestination_)
+ e.preventDefault();
+
+ this.dropDestination_ = null;
+ this.dropIndicator_.finish();
+ },
+
+ /** @private */
+ clearDragData_: function() {
+ this.dragInfo_.clearDragData();
+ this.dropDestination_ = null;
+ this.dropIndicator_.finish();
+ },
+
+ /**
+ * @private
+ * @param {Event} e
+ */
+ onDragStart_: function(e) {
+ var dragElement = getBookmarkElement(e.path);
+ if (!dragElement)
+ return;
+
+ // Determine the selected bookmarks.
+ var state = bookmarks.Store.getInstance().data;
+ var draggedNodes = Object.keys(state.selection.items);
+
+ if (isBookmarkFolderNode(dragElement) ||
+ draggedNodes.indexOf(dragElement.itemId) == -1) {
+ // TODO(calamity): clear current selection.
+ draggedNodes = [dragElement.itemId];
+ }
+
+ e.preventDefault();
+
+ // If we are dragging a single link, we can do the *Link* effect.
+ // Otherwise, we only allow copy and move.
+ if (e.dataTransfer) {
+ e.dataTransfer.effectAllowed =
+ draggedNodes.length == 1 && state.nodes[draggedNodes[0]].url ?
+ 'copyLink' :
+ 'copyMove';
+ }
+
+ // TODO(calamity): account for touch.
+ chrome.bookmarkManagerPrivate.startDrag(draggedNodes, false);
+ },
+
+ /**
+ * @private
+ * @param {Event} e
+ */
+ onDragEnter_: function(e) {
+ e.preventDefault();
+ },
+
+ /**
+ * @private
+ * @param {Event} e
+ */
+ onDragOver_: function(e) {
+ this.dropDestination_ = null;
+
+ // This is necessary to actually trigger the 'none' effect, even though
+ // the event will have this set to 'none' already.
+ if (e.dataTransfer)
+ e.dataTransfer.dropEffect = 'none';
+
+ // Allow normal DND on text inputs.
+ if (e.path[0].tagName == 'INPUT')
+ return;
+
+ // The default operation is to allow dropping links etc to do
+ // navigation. We never want to do that for the bookmark manager.
+ e.preventDefault();
+
+ if (!this.dragInfo_.isDragValid())
+ return;
+
+ var overElement = getBookmarkElement(e.path);
+ if (!overElement)
+ return;
+
+ // TODO(calamity): open folders on hover.
+
+ // Now we know that we can drop. Determine if we will drop above, on or
+ // below based on mouse position etc.
+ this.dropDestination_ =
+ this.calculateDropDestination_(e.clientY, overElement);
+ if (!this.dropDestination_)
+ return;
+
+ if (e.dataTransfer) {
+ e.dataTransfer.dropEffect =
+ this.dragInfo_.isSameProfile() ? 'move' : 'copy';
+ }
+
+ this.dropIndicator_.update(this.dropDestination_);
+ },
+
+ /**
+ * This function determines where the drop will occur.
+ * @private
+ * @param {number} elementClientY
+ * @param {!BookmarkElement} overElement
+ * @return {?DropDestination} If no valid drop position is found, null,
+ * otherwise:
+ * element - The target element that will receive the drop.
+ * position - A |DropPosition| relative to the |element|.
+ */
+ calculateDropDestination_: function(elementClientY, overElement) {
+ var validDropPositions = this.calculateValidDropPositions_(overElement);
+ if (validDropPositions == DropPosition.NONE)
+ return null;
+
+ var above = validDropPositions & DropPosition.ABOVE;
+ var below = validDropPositions & DropPosition.BELOW;
+ var on = validDropPositions & DropPosition.ON;
+ var rect = overElement.getDropTarget().getBoundingClientRect();
+ var yRatio = (elementClientY - rect.top) / rect.height;
+
+ if (above && (yRatio <= .25 || yRatio <= .5 && (!below || !on)))
+ return {element: overElement, position: DropPosition.ABOVE};
+
+ if (below && (yRatio > .75 || yRatio > .5 && (!above || !on)))
+ return {element: overElement, position: DropPosition.BELOW};
+
+ if (on)
+ return {element: overElement, position: DropPosition.ON};
+
+ return null;
+ },
+
+ /**
+ * Determines the valid drop positions for the given target element.
+ * @private
+ * @param {!BookmarkElement} overElement The element that we are currently
+ * dragging over.
+ * @return {DropPosition} An bit field enumeration of valid drop locations.
+ */
+ calculateValidDropPositions_: function(overElement) {
+ var dragInfo = this.dragInfo_;
+ var state = bookmarks.Store.getInstance().data;
+
+ // Drags aren't allowed onto the search result list.
+ if (isBookmarkItem(overElement) &&
+ bookmarks.util.isShowingSearch(state)) {
+ return DropPosition.NONE;
+ }
+
+ // Drags of a bookmark onto itself or of a folder into its children aren't
+ // allowed.
+ if (dragInfo.isDraggingBookmark(overElement.itemId) ||
+ dragInfo.isDraggingFolderToDescendant(
+ overElement.itemId, state.nodes)) {
+ return DropPosition.NONE;
+ }
+
+ var validDropPositions = this.calculateDropAboveBelow_(overElement);
+ if (this.canDropOn_(overElement))
+ validDropPositions |= DropPosition.ON;
+
+ return validDropPositions;
+ },
+
+ /**
+ * @private
+ * @param {BookmarkElement} overElement
+ * @return {DropPosition}
+ */
+ calculateDropAboveBelow_: function(overElement) {
+ var dragInfo = this.dragInfo_;
+ var state = bookmarks.Store.getInstance().data;
+
+ // We cannot drop between Bookmarks bar and Other bookmarks.
+ if (getBookmarkNode(overElement).parentId == bookmarks.util.ROOT_NODE_ID)
+ return DropPosition.NONE;
+
+ var isOverFolderNode = isBookmarkFolderNode(overElement);
+
+ // We can only drop between items in the tree if we have any folders.
+ if (isOverFolderNode && !dragInfo.isDraggingFolders())
+ return DropPosition.NONE;
+
+ var validDropPositions = DropPosition.NONE;
+
+ // Cannot drop above if the item above is already in the drag source.
+ var previousElem = overElement.previousElementSibling;
+ if (!previousElem || !dragInfo.isDraggingBookmark(previousElem.itemId))
+ validDropPositions |= DropPosition.ABOVE;
+
+ // Don't allow dropping below an expanded sidebar folder item since it is
+ // confusing to the user anyway.
+ if (isOverFolderNode && !state.closedFolders[overElement.itemId] &&
+ bookmarks.util.hasChildFolders(overElement.itemId, state.nodes)) {
+ return validDropPositions;
+ }
+
+ var nextElement = overElement.nextElementSibling;
+
+ // The template element sits past the end of the last bookmark element.
+ if (!isBookmarkItem(nextElement) && !isBookmarkFolderNode(nextElement))
+ nextElement = null;
+
+ // Cannot drop below if the item below is already in the drag source.
+ if (!nextElement || !dragInfo.isDraggingBookmark(nextElement.itemId))
+ validDropPositions |= DropPosition.BELOW;
+
+ return validDropPositions;
+ },
+
+ /**
+ * Determine whether we can drop the dragged items on the drop target.
+ * @private
+ * @param {!BookmarkElement} overElement The element that we are currently
+ * dragging over.
+ * @return {boolean} Whether we can drop the dragged items on the drop
+ * target.
+ */
+ canDropOn_: function(overElement) {
+ // We can only drop on a folder.
+ if (getBookmarkNode(overElement).url)
+ return false;
+
+ return !this.dragInfo_.isDraggingChildBookmark(overElement.itemId)
+ },
+ };
+
+ return {
+ DNDManager: DNDManager,
+ DragInfo: DragInfo,
+ DropIndicator: DropIndicator,
+ };
+});
« no previous file with comments | « chrome/browser/resources/md_bookmarks/dnd_manager.html ('k') | chrome/browser/resources/md_bookmarks/folder_node.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698