blob: 330905b542cb69af2a9ac3ecae82fd2e6d68b44e [file] [log] [blame]
// 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.
/** @typedef {?{elements: !Array<BookmarkNode>, sameProfile: boolean}} */
let NormalizedDragData;
cr.define('bookmarks', function() {
/** @const {number} */
const DRAG_THRESHOLD = 15;
/**
* @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 {BookmarkElement} element
* @return {boolean}
*/
function isBookmarkList(element) {
return element.tagName == 'BOOKMARKS-LIST';
}
/**
* @param {BookmarkElement} element
* @return {boolean}
*/
function isClosedBookmarkFolderNode(element) {
return isBookmarkFolderNode(element) &&
!(/** @type {BookmarksFolderNodeElement} */ (element).isOpen);
}
/**
* @param {Array<!Element>|undefined} path
* @return {BookmarkElement}
*/
function getBookmarkElement(path) {
if (!path)
return null;
for (let i = 0; i < path.length; i++) {
if (isBookmarkItem(path[i]) || isBookmarkFolderNode(path[i]) ||
isBookmarkList(path[i])) {
return path[i];
}
}
return null;
}
/**
* @param {Array<!Element>|undefined} path
* @return {BookmarkElement}
*/
function getDragElement(path) {
const dragElement = getBookmarkElement(path);
for (let i = 0; i < path.length; i++) {
if (path[i].tagName == 'BUTTON')
return null;
}
return dragElement && dragElement.getAttribute('draggable') ? dragElement :
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 {NormalizedDragData} */
this.dragData = null;
}
DragInfo.prototype = {
/** @param {DragData} newDragData */
setNativeDragData: function(newDragData) {
this.dragData = {
sameProfile: newDragData.sameProfile,
elements:
newDragData.elements.map((x) => bookmarks.util.normalizeNode(x))
};
},
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;
let parentId = nodes[itemId].parentId;
const parents = {};
while (parentId) {
parents[parentId] = true;
parentId = nodes[parentId].parentId;
}
return !!this.dragData && this.dragData.elements.some(function(node) {
return parents[node.id];
});
},
};
/**
* Manages auto expanding of sidebar folders on hover while dragging.
* @constructor
*/
function AutoExpander() {
/** @const {number} */
this.EXPAND_FOLDER_DELAY = 400;
/** @private {?BookmarkElement} */
this.lastElement_ = null;
/** @type {!bookmarks.Debouncer} */
this.debouncer_ = new bookmarks.Debouncer(() => {
const store = bookmarks.Store.getInstance();
store.dispatch(
bookmarks.actions.changeFolderOpen(this.lastElement_.itemId, true));
this.reset();
});
}
AutoExpander.prototype = {
/**
* @param {Event} e
* @param {?BookmarkElement} overElement
*/
update: function(e, overElement) {
const itemId = overElement ? overElement.itemId : null;
const store = bookmarks.Store.getInstance();
// If dragging over a new closed folder node with children reset the
// expander. Falls through to reset the expander delay.
if (overElement && overElement != this.lastElement_ &&
isClosedBookmarkFolderNode(overElement) &&
bookmarks.util.hasChildFolders(itemId, store.data.nodes)) {
this.reset();
this.lastElement_ = overElement;
}
// If dragging over the same node, reset the expander delay.
if (overElement && overElement == this.lastElement_) {
this.debouncer_.restartTimeout(this.EXPAND_FOLDER_DELAY);
return;
}
// Otherwise, cancel the expander.
this.reset();
},
reset: function() {
this.debouncer_.reset();
this.lastElement_ = null;
},
};
/**
* Manages auto scrolling of elements on hover during internal drags. Native
* drags do this by themselves.
* @constructor
*/
function AutoScroller() {
/** @const {number} */
this.SCROLL_ZONE_LENGTH = 20;
/** @const {number} */
this.SCROLL_DISTANCE = 10;
/** @const {number} */
this.SCROLL_INTERVAL = 100;
/** @private {?number} */
this.intervalId_ = null;
}
AutoScroller.prototype = {
/** @param {!Event} e */
update: function(e) {
this.reset();
const scrollParent = e.path.find((el) => {
return el.nodeType == Node.ELEMENT_NODE &&
window.getComputedStyle(el).overflowY == 'auto';
});
if (!scrollParent)
return;
const rect = scrollParent.getBoundingClientRect();
let yDelta = 0;
if (e.clientY < rect.top + this.SCROLL_ZONE_LENGTH)
yDelta = -this.SCROLL_DISTANCE;
else if (e.clientY > rect.bottom - this.SCROLL_ZONE_LENGTH)
yDelta = this.SCROLL_DISTANCE;
this.intervalId_ = window.setInterval(() => {
scrollParent.scrollTop += yDelta;
}, this.SCROLL_INTERVAL);
},
reset: function() {
if (this.intervalId_ == null)
return;
window.clearInterval(this.intervalId_);
this.intervalId_ = null;
},
};
/**
* 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.removeDropIndicatorTimeoutId_ = 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 {!Object}
*/
this.timerProxy = 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) {
const 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) {
this.timerProxy.clearTimeout(this.removeDropIndicatorTimeoutId_);
this.removeDropIndicatorTimeoutId_ = null;
const indicatorElement = dropDest.element.getDropTarget();
const position = dropDest.position;
this.removeDropIndicatorStyle();
this.addDropIndicatorStyle(indicatorElement, position);
},
/**
* Stop displaying the drop indicator.
*/
finish: function() {
if (this.removeDropIndicatorTimeoutId_)
return;
// The use of a timeout is in order to reduce flickering as we move
// between valid drop targets.
this.removeDropIndicatorTimeoutId_ = this.timerProxy.setTimeout(() => {
this.removeDropIndicatorStyle();
}, 100);
},
};
/**
* Manages drag and drop events for the bookmarks-app.
*
* This class manages an internal drag and drop based on mouse events and then
* delegates to the native drag and drop in chrome.bookmarkManagerPrivate when
* the mouse leaves the web content area. This allows us to render a drag and
* drop chip UI for internal drags, while correctly handling and avoiding
* conflict with native drags.
*
* The event flows look like
*
* mousedown -> mousemove -> mouseup
* |
* v
* dragstart/dragleave (if the drag leaves the browser window)
* |
* v
* external drag -> bookmarkManagerPrivate.onDragEnter -> dragover -> drop
*
* @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;
/** @private {?bookmarks.AutoScroller} */
this.autoScroller_ = null;
/** @private {?bookmarks.AutoExpander} */
this.autoExpander_ = null;
/**
* Used to instantly clearDragData in tests.
* @private {!Object}
*/
this.timerProxy_ = window;
/**
* The bookmark drag and drop indicator chip.
* @private {BookmarksDndChipElement}
*/
this.chip_ = null;
/**
* The element that initiated an internal drag. Not used once native drag
* starts.
* @private {BookmarkElement}
*/
this.internalDragElement_ = null;
/**
* Where the internal drag started.
* @private {?{x: number, y: number}}
*/
this.mouseDownPos_ = null;
}
DNDManager.prototype = {
init: function() {
this.dragInfo_ = new DragInfo();
this.dropIndicator_ = new DropIndicator();
this.autoExpander_ = new AutoExpander();
this.autoScroller_ = new AutoScroller();
this.documentListeners_ = {
'mousedown': this.onMousedown_.bind(this),
'mousemove': this.onMouseMove_.bind(this),
'mouseup': this.onMouseUp_.bind(this),
'mouseleave': this.onMouseLeave_.bind(this),
'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),
// TODO(calamity): Add touch support.
};
for (const event in this.documentListeners_)
document.addEventListener(event, this.documentListeners_[event]);
chrome.bookmarkManagerPrivate.onDragEnter.addListener(
this.handleChromeDragEnter_.bind(this));
chrome.bookmarkManagerPrivate.onDragLeave.addListener(
this.clearDragData_.bind(this));
chrome.bookmarkManagerPrivate.onDrop.addListener(
this.clearDragData_.bind(this));
},
destroy: function() {
if (this.chip_ && this.chip_.parentElement)
document.body.removeChild(this.chip_);
for (const event in this.documentListeners_)
document.removeEventListener(event, this.documentListeners_[event]);
},
////////////////////////////////////////////////////////////////////////////
// MouseEvent handlers:
/**
* @private
* @param {Event} e
*/
onMousedown_: function(e) {
const dragElement = getDragElement(e.path);
if (e.button != 0 || !dragElement)
return;
this.internalDragElement_ = dragElement;
this.mouseDownPos_ = {
x: e.clientX,
y: e.clientY,
};
},
/**
* @private
* @param {Event} e
*/
onMouseMove_: function(e) {
// mousemove events still fire when dragged onto the the bookmarks bar.
// Once we are outside of the web contents, allow the native drag to
// start.
if (!this.internalDragElement_ || e.clientX < 0 ||
e.clientX > window.innerWidth || e.clientY < 0 ||
e.clientY > window.innerHeight) {
return;
}
this.dropDestination_ = null;
// Prevents a native drag from starting.
e.preventDefault();
this.autoScroller_.update(e);
// On the first mousemove after a mousedown, calculate the items to drag.
// This can't be done in mousedown because the user may be shift-clicking
// an item.
if (!this.dragInfo_.isDragValid()) {
// If the mouse hasn't been moved far enough, defer to next mousemove.
if (Math.abs(this.mouseDownPos_.x - e.clientX) < DRAG_THRESHOLD &&
Math.abs(this.mouseDownPos_.y - e.clientY) < DRAG_THRESHOLD) {
return;
}
const dragData = this.calculateDragData_();
if (!dragData) {
this.clearDragData_();
return;
}
this.dragInfo_.dragData = dragData;
}
const state = bookmarks.Store.getInstance().data;
const items = this.dragInfo_.dragData.elements;
this.dndChip.showForItems(
e.clientX, e.clientY, items,
this.internalDragElement_ ?
state.nodes[this.internalDragElement_.itemId] :
items[0]);
this.onDragOverCommon_(e);
},
/**
* This event fires when the mouse leaves the browser window (not the web
* content area).
* @private
*/
onMouseLeave_: function() {
if (!this.internalDragElement_)
return;
this.startNativeDrag_();
},
/**
* @private
*/
onMouseUp_: function() {
if (!this.internalDragElement_)
return;
if (this.dropDestination_) {
// Complete the drag by moving all dragged items to the drop
// destination.
const dropInfo = this.calculateDropInfo_(this.dropDestination_);
const shouldHighlight = this.shouldHighlight_(this.dropDestination_);
const movePromises = this.dragInfo_.dragData.elements.map((item) => {
return new Promise((resolve) => {
chrome.bookmarks.move(
item.id, {
parentId: dropInfo.parentId,
index: dropInfo.index == -1 ? undefined : dropInfo.index
},
resolve);
});
});
if (shouldHighlight) {
bookmarks.ApiListener.trackUpdatedItems();
Promise.all(movePromises)
.then(() => bookmarks.ApiListener.highlightUpdatedItems());
}
}
this.clearDragData_();
},
////////////////////////////////////////////////////////////////////////////
// DragEvent handlers:
/**
* This should only fire when a mousemove goes from the content area to the
* browser chrome.
* @private
* @param {Event} e
*/
onDragStart_: function(e) {
// |e| will be for the originally dragged bookmark item which dragstart
// was disabled for due to mousemove's preventDefault.
const dragElement = getDragElement(e.path);
if (!dragElement)
return;
// Prevent normal drags of all bookmark items.
e.preventDefault();
if (!this.startNativeDrag_())
return;
// If we are dragging a single link, we can do the *Link* effect.
// Otherwise, we only allow copy and move.
if (e.dataTransfer) {
const draggedNodes = this.dragInfo_.dragData.elements;
e.dataTransfer.effectAllowed =
draggedNodes.length == 1 && draggedNodes[0].url ? 'copyLink' :
'copyMove';
}
},
/** @private */
onDragLeave_: function() {
this.dropIndicator_.finish();
},
/**
* @private
* @param {!Event} e
*/
onDrop_: function(e) {
if (this.dropDestination_) {
e.preventDefault();
const dropInfo = this.calculateDropInfo_(this.dropDestination_);
const index = dropInfo.index != -1 ? dropInfo.index : undefined;
const shouldHighlight = this.shouldHighlight_(this.dropDestination_);
if (shouldHighlight)
bookmarks.ApiListener.trackUpdatedItems();
chrome.bookmarkManagerPrivate.drop(
dropInfo.parentId, index,
shouldHighlight ? bookmarks.ApiListener.highlightUpdatedItems :
undefined);
}
this.clearDragData_();
},
/**
* @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;
if (this.onDragOverCommon_(e) && e.dataTransfer) {
e.dataTransfer.dropEffect =
this.dragInfo_.isSameProfile() ? 'move' : 'copy';
}
},
/**
* @private
* @param {DragData} dragData
*/
handleChromeDragEnter_: function(dragData) {
this.dragInfo_.setNativeDragData(dragData);
},
////////////////////////////////////////////////////////////////////////////
// Common drag methods:
/** @private */
clearDragData_: function() {
this.dndChip.hide();
this.autoScroller_.reset();
this.internalDragElement_ = null;
this.mouseDownPos_ = null;
this.autoExpander_.reset();
// Defer the clearing of the data so that the bookmark manager API's drop
// event doesn't clear the drop data before the web drop event has a
// chance to execute (on Mac).
this.timerProxy_.setTimeout(() => {
this.dragInfo_.clearDragData();
this.dropDestination_ = null;
this.dropIndicator_.finish();
}, 0);
},
/**
* Starts a native drag by sending a message to the browser.
* @private
* @return {boolean}
*/
startNativeDrag_: function() {
const state = bookmarks.Store.getInstance().data;
if (!this.dragInfo_.isDragValid())
return false;
const draggedNodes =
this.dragInfo_.dragData.elements.map((item) => item.id);
// Clear the drag data here so that the chip is hidden. The native drag
// will return after the clearing and set up its data.
this.clearDragData_();
// TODO(calamity): account for touch.
chrome.bookmarkManagerPrivate.startDrag(draggedNodes, false);
return true;
},
/**
* @private
* @param {Event} e
* @return {boolean}
*/
onDragOverCommon_: function(e) {
const state = bookmarks.Store.getInstance().data;
const items = this.dragInfo_.dragData.elements;
const overElement = getBookmarkElement(e.path);
this.autoExpander_.update(e, overElement);
if (!overElement) {
this.dropIndicator_.finish();
return false;
}
// 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_) {
this.dropIndicator_.finish();
return false;
}
this.dropIndicator_.update(this.dropDestination_);
return true;
},
////////////////////////////////////////////////////////////////////////////
// Other methods:
/**
* @param {DropDestination} dropDestination
* @return {{parentId: string, index: number}}
*/
calculateDropInfo_: function(dropDestination) {
if (isBookmarkList(dropDestination.element)) {
return {
index: 0,
parentId: bookmarks.Store.getInstance().data.selectedFolder,
};
}
const node = getBookmarkNode(dropDestination.element);
const position = dropDestination.position;
let index = -1;
let parentId = node.id;
if (position != DropPosition.ON) {
const state = bookmarks.Store.getInstance().data;
// Drops between items in the normal list and the sidebar use the drop
// destination node's parent.
parentId = assert(node.parentId);
index = state.nodes[parentId].children.indexOf(node.id);
if (position == DropPosition.BELOW)
index++;
}
return {
index: index,
parentId: parentId,
};
},
/**
* Calculates which items should be dragged based on the initial drag item
* and the current selection. Dragged items will end up selected.
* @private
*/
calculateDragData_: function() {
const dragId = this.internalDragElement_.itemId;
const store = bookmarks.Store.getInstance();
const state = store.data;
// Determine the selected bookmarks.
let draggedNodes = Array.from(state.selection.items);
// Change selection to the dragged node if the node is not part of the
// existing selection.
if (isBookmarkFolderNode(this.internalDragElement_) ||
draggedNodes.indexOf(dragId) == -1) {
store.dispatch(bookmarks.actions.deselectItems());
if (!isBookmarkFolderNode(this.internalDragElement_)) {
store.dispatch(bookmarks.actions.selectItem(dragId, state, {
clear: false,
range: false,
toggle: false,
}));
}
draggedNodes = [dragId];
}
// If any node can't be dragged, end the drag.
const anyUnmodifiable = draggedNodes.some(
(itemId) => !bookmarks.util.canEditNode(state, itemId));
if (anyUnmodifiable)
return null;
return {
elements: draggedNodes.map((id) => state.nodes[id]),
sameProfile: true,
};
},
/**
* 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) {
const validDropPositions = this.calculateValidDropPositions_(overElement);
if (validDropPositions == DropPosition.NONE)
return null;
const above = validDropPositions & DropPosition.ABOVE;
const below = validDropPositions & DropPosition.BELOW;
const on = validDropPositions & DropPosition.ON;
const rect = overElement.getDropTarget().getBoundingClientRect();
const 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) {
const dragInfo = this.dragInfo_;
const state = bookmarks.Store.getInstance().data;
let itemId = overElement.itemId;
// Drags aren't allowed onto the search result list.
if ((isBookmarkList(overElement) || isBookmarkItem(overElement)) &&
bookmarks.util.isShowingSearch(state)) {
return DropPosition.NONE;
}
if (isBookmarkList(overElement))
itemId = state.selectedFolder;
if (!bookmarks.util.canReorderChildren(state, itemId))
return DropPosition.NONE;
// Drags of a bookmark onto itself or of a folder into its children aren't
// allowed.
if (dragInfo.isDraggingBookmark(itemId) ||
dragInfo.isDraggingFolderToDescendant(itemId, state.nodes)) {
return DropPosition.NONE;
}
let validDropPositions = this.calculateDropAboveBelow_(overElement);
if (this.canDropOn_(overElement))
validDropPositions |= DropPosition.ON;
return validDropPositions;
},
/**
* @private
* @param {BookmarkElement} overElement
* @return {DropPosition}
*/
calculateDropAboveBelow_: function(overElement) {
const dragInfo = this.dragInfo_;
const state = bookmarks.Store.getInstance().data;
if (isBookmarkList(overElement))
return DropPosition.NONE;
// We cannot drop between Bookmarks bar and Other bookmarks.
if (getBookmarkNode(overElement).parentId == ROOT_NODE_ID)
return DropPosition.NONE;
const isOverFolderNode = isBookmarkFolderNode(overElement);
// We can only drop between items in the tree if we have any folders.
if (isOverFolderNode && !dragInfo.isDraggingFolders())
return DropPosition.NONE;
let validDropPositions = DropPosition.NONE;
// Cannot drop above if the item above is already in the drag source.
const 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 && !isClosedBookmarkFolderNode(overElement) &&
bookmarks.util.hasChildFolders(overElement.itemId, state.nodes)) {
return validDropPositions;
}
const nextElement = overElement.nextElementSibling;
// 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) {
// Allow dragging onto empty bookmark lists.
if (isBookmarkList(overElement)) {
const state = bookmarks.Store.getInstance().data;
return state.selectedFolder &&
state.nodes[state.selectedFolder].children.length == 0;
}
// We can only drop on a folder.
if (getBookmarkNode(overElement).url)
return false;
return !this.dragInfo_.isDraggingChildBookmark(overElement.itemId);
},
/**
* @param {DropDestination} dropDestination
* @private
*/
shouldHighlight_: function(dropDestination) {
return isBookmarkItem(dropDestination.element) ||
isBookmarkList(dropDestination.element);
},
/** @param {!Object} timerProxy */
setTimerProxyForTesting: function(timerProxy) {
this.timerProxy_ = timerProxy;
this.dropIndicator_.timerProxy = timerProxy;
},
/** @return {BookmarksDndChipElement} */
get dndChip() {
if (!this.chip_) {
this.chip_ =
/** @type {BookmarksDndChipElement} */ (
document.createElement('bookmarks-dnd-chip'));
document.body.appendChild(this.chip_);
}
return this.chip_;
},
};
return {
AutoExpander: AutoExpander,
AutoScroller: AutoScroller,
DNDManager: DNDManager,
DragInfo: DragInfo,
DropIndicator: DropIndicator,
};
});