1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 cr.define('dnd', function() {
8 /** @const */ var BookmarkList = bmm.BookmarkList;
9 /** @const */ var ListItem = cr.ui.ListItem;
10 /** @const */ var TreeItem = cr.ui.TreeItem;
13 * Enumeration of valid drop locations relative to an element. These are
14 * bit masks to allow combining multiple locations in a single value.
26 * @type {Object} Drop information calculated in |handleDragOver|.
28 var dropDestination = null;
31 * @type {number} Timer id used to help minimize flicker.
33 var removeDropIndicatorTimer;
36 * The element currently targeted by a touch.
39 var currentTouchTarget;
42 * The element that had a style applied it to indicate the drop location.
43 * This is used to easily remove the style when necessary.
46 var lastIndicatorElement;
49 * The style that was applied to indicate the drop location.
52 var lastIndicatorClassName;
56 * Applies the drop indicator style on the target element and stores that
57 * information to easily remove the style in the future.
59 addDropIndicatorStyle: function(indicatorElement, position) {
60 var indicatorStyleName = position == DropPosition.ABOVE ? 'drag-above' :
61 position == DropPosition.BELOW ? 'drag-below' :
64 lastIndicatorElement = indicatorElement;
65 lastIndicatorClassName = indicatorStyleName;
67 indicatorElement.classList.add(indicatorStyleName);
71 * Clears the drop indicator style from the last element was the drop target
72 * so the drop indicator is no longer for that element.
74 removeDropIndicatorStyle: function() {
75 if (!lastIndicatorElement || !lastIndicatorClassName)
77 lastIndicatorElement.classList.remove(lastIndicatorClassName);
78 lastIndicatorElement = null;
79 lastIndicatorClassName = null;
83 * Displays the drop indicator on the current drop target to give the
84 * user feedback on where the drop will occur.
86 update: function(dropDest) {
87 window.clearTimeout(removeDropIndicatorTimer);
89 var indicatorElement = dropDest.element;
90 var position = dropDest.position;
91 if (dropDest.element instanceof BookmarkList) {
92 // For an empty bookmark list use 'drop-above' style.
93 position = DropPosition.ABOVE;
94 } else if (dropDest.element instanceof TreeItem) {
95 indicatorElement = indicatorElement.querySelector('.tree-row');
97 dropIndicator.removeDropIndicatorStyle();
98 dropIndicator.addDropIndicatorStyle(indicatorElement, position);
102 * Stop displaying the drop indicator.
105 // The use of a timeout is in order to reduce flickering as we move
106 // between valid drop targets.
107 window.clearTimeout(removeDropIndicatorTimer);
108 removeDropIndicatorTimer = window.setTimeout(function() {
109 dropIndicator.removeDropIndicatorStyle();
115 * Delay for expanding folder when pointer hovers on folder in tree view in
120 // TODO(yosin): EXPAND_FOLDER_DELAY should follow system settings. 400ms is
121 // taken from Windows default settings.
122 var EXPAND_FOLDER_DELAY = 400;
125 * The timestamp when the mouse was over a folder during a drag operation.
126 * Used to open the hovered folder after a certain time.
129 var lastHoverOnFolderTimeStamp = 0;
132 * Expand a folder if the user has hovered for longer than the specified
133 * time during a drag action.
135 function updateAutoExpander(eventTimeStamp, overElement) {
136 // Expands a folder in tree view when pointer hovers on it longer than
137 // EXPAND_FOLDER_DELAY.
138 var hoverOnFolderTimeStamp = lastHoverOnFolderTimeStamp;
139 lastHoverOnFolderTimeStamp = 0;
140 if (hoverOnFolderTimeStamp) {
141 if (eventTimeStamp - hoverOnFolderTimeStamp >= EXPAND_FOLDER_DELAY)
142 overElement.expanded = true;
144 lastHoverOnFolderTimeStamp = hoverOnFolderTimeStamp;
145 } else if (overElement instanceof TreeItem &&
146 bmm.isFolder(overElement.bookmarkNode) &&
147 overElement.hasChildren &&
148 !overElement.expanded) {
149 lastHoverOnFolderTimeStamp = eventTimeStamp;
154 * Stores the information about the bookmark and folders being dragged.
159 handleChromeDragEnter: function(newDragData) {
160 dragData = newDragData;
162 clearDragData: function() {
165 isDragValid: function() {
168 isSameProfile: function() {
169 return dragData && dragData.sameProfile;
171 isDraggingFolders: function() {
172 return dragData && dragData.elements.some(function(node) {
176 isDraggingBookmark: function(bookmarkId) {
177 return dragData && dragData.elements.some(function(node) {
178 return node.id == bookmarkId;
181 isDraggingChildBookmark: function(folderId) {
182 return dragData && dragData.elements.some(function(node) {
183 return node.parentId == folderId;
186 isDraggingFolderToDescendant: function(bookmarkNode) {
187 return dragData && dragData.elements.some(function(node) {
188 var dragFolder = bmm.treeLookup[node.id];
189 var dragFolderNode = dragFolder && dragFolder.bookmarkNode;
190 return dragFolderNode && bmm.contains(dragFolderNode, bookmarkNode);
196 * External function to select folders or bookmarks after a drop action.
199 var selectItemsAfterUserAction = null;
201 function getBookmarkElement(el) {
202 while (el && !el.bookmarkNode) {
208 // If we are over the list and the list is showing search result, we cannot
210 function isOverSearch(overElement) {
211 return bmm.list.isSearch() && bmm.list.contains(overElement);
215 * Determines the valid drop positions for the given target element.
216 * @param {!HTMLElement} overElement The element that we are currently
218 * @return {DropPosition} An bit field enumeration of valid drop locations.
220 function calculateValidDropTargets(overElement) {
221 // Don't allow dropping if there is an ephemeral item being edited.
222 if (bmm.list.hasEphemeral())
223 return DropPosition.NONE;
225 if (!dragInfo.isDragValid() || isOverSearch(overElement))
226 return DropPosition.NONE;
228 if (dragInfo.isSameProfile() &&
229 (dragInfo.isDraggingBookmark(overElement.bookmarkNode.id) ||
230 dragInfo.isDraggingFolderToDescendant(overElement.bookmarkNode))) {
231 return DropPosition.NONE;
234 var canDropInfo = calculateDropAboveBelow(overElement);
235 if (canDropOn(overElement))
236 canDropInfo |= DropPosition.ON;
241 function calculateDropAboveBelow(overElement) {
242 if (overElement instanceof BookmarkList)
243 return DropPosition.NONE;
245 // We cannot drop between Bookmarks bar and Other bookmarks.
246 if (overElement.bookmarkNode.parentId == bmm.ROOT_ID)
247 return DropPosition.NONE;
249 var isOverTreeItem = overElement instanceof TreeItem;
250 var isOverExpandedTree = isOverTreeItem && overElement.expanded;
251 var isDraggingFolders = dragInfo.isDraggingFolders();
253 // We can only drop between items in the tree if we have any folders.
254 if (isOverTreeItem && !isDraggingFolders)
255 return DropPosition.NONE;
257 // When dragging from a different profile we do not need to consider
258 // conflicts between the dragged items and the drop target.
259 if (!dragInfo.isSameProfile()) {
260 // Don't allow dropping below an expanded tree item since it is confusing
261 // to the user anyway.
262 return isOverExpandedTree ? DropPosition.ABOVE :
263 (DropPosition.ABOVE | DropPosition.BELOW);
266 var resultPositions = DropPosition.NONE;
268 // Cannot drop above if the item above is already in the drag source.
269 var previousElem = overElement.previousElementSibling;
270 if (!previousElem || !dragInfo.isDraggingBookmark(previousElem.bookmarkId))
271 resultPositions |= DropPosition.ABOVE;
273 // Don't allow dropping below an expanded tree item since it is confusing
274 // to the user anyway.
275 if (isOverExpandedTree)
276 return resultPositions;
278 // Cannot drop below if the item below is already in the drag source.
279 var nextElement = overElement.nextElementSibling;
280 if (!nextElement || !dragInfo.isDraggingBookmark(nextElement.bookmarkId))
281 resultPositions |= DropPosition.BELOW;
283 return resultPositions;
287 * Determine whether we can drop the dragged items on the drop target.
288 * @param {!HTMLElement} overElement The element that we are currently
290 * @return {boolean} Whether we can drop the dragged items on the drop
293 function canDropOn(overElement) {
294 // We can only drop on a folder.
295 if (!bmm.isFolder(overElement.bookmarkNode))
298 if (!dragInfo.isSameProfile())
301 if (overElement instanceof BookmarkList) {
302 // We are trying to drop an item past the last item. This is
303 // only allowed if dragged item is different from the last item
305 var listItems = bmm.list.items;
306 var len = listItems.length;
307 if (!len || !dragInfo.isDraggingBookmark(listItems[len - 1].bookmarkId))
311 return !dragInfo.isDraggingChildBookmark(overElement.bookmarkNode.id);
315 * Callback for the dragstart event.
316 * @param {Event} e The dragstart event.
318 function handleDragStart(e) {
319 // Determine the selected bookmarks.
320 var target = e.target;
321 var draggedNodes = [];
322 var isFromTouch = target == currentTouchTarget;
324 if (target instanceof ListItem) {
325 // Use selected items.
326 draggedNodes = target.parentNode.selectedItems;
327 } else if (target instanceof TreeItem) {
328 draggedNodes.push(target.bookmarkNode);
331 // We manage starting the drag by using the extension API.
334 // Do not allow dragging if there is an ephemeral item being edited at the
336 if (bmm.list.hasEphemeral())
339 if (draggedNodes.length) {
340 // If we are dragging a single link, we can do the *Link* effect.
341 // Otherwise, we only allow copy and move.
342 e.dataTransfer.effectAllowed = draggedNodes.length == 1 &&
343 !bmm.isFolder(draggedNodes[0]) ? 'copyMoveLink' : 'copyMove';
345 chrome.bookmarkManagerPrivate.startDrag(draggedNodes.map(function(node) {
351 function handleDragEnter(e) {
356 * Calback for the dragover event.
357 * @param {Event} e The dragover event.
359 function handleDragOver(e) {
360 // Allow DND on text inputs.
361 if (e.target.tagName != 'INPUT') {
362 // The default operation is to allow dropping links etc to do navigation.
363 // We never want to do that for the bookmark manager.
366 // Set to none. This will get set to something if we can do the drop.
367 e.dataTransfer.dropEffect = 'none';
370 if (!dragInfo.isDragValid())
373 var overElement = getBookmarkElement(e.target) ||
374 (e.target == bmm.list ? bmm.list : null);
378 updateAutoExpander(e.timeStamp, overElement);
380 var canDropInfo = calculateValidDropTargets(overElement);
381 if (canDropInfo == DropPosition.NONE)
384 // Now we know that we can drop. Determine if we will drop above, on or
385 // below based on mouse position etc.
387 dropDestination = calcDropPosition(e.clientY, overElement, canDropInfo);
388 if (!dropDestination) {
389 e.dataTransfer.dropEffect = 'none';
393 e.dataTransfer.dropEffect = dragInfo.isSameProfile() ? 'move' : 'copy';
394 dropIndicator.update(dropDestination);
398 * This function determines where the drop will occur relative to the element.
399 * @return {?Object} If no valid drop position is found, null, otherwise
400 * an object containing the following parameters:
401 * element - The target element that will receive the drop.
402 * position - A |DropPosition| relative to the |element|.
404 function calcDropPosition(elementClientY, overElement, canDropInfo) {
405 if (overElement instanceof BookmarkList) {
406 // Dropping on the BookmarkList either means dropping below the last
407 // bookmark element or on the list itself if it is empty.
408 var length = overElement.items.length;
411 element: overElement.getListItemByIndex(length - 1),
412 position: DropPosition.BELOW
414 return {element: overElement, position: DropPosition.ON};
417 var above = canDropInfo & DropPosition.ABOVE;
418 var below = canDropInfo & DropPosition.BELOW;
419 var on = canDropInfo & DropPosition.ON;
420 var rect = overElement.getBoundingClientRect();
421 var yRatio = (elementClientY - rect.top) / rect.height;
423 if (above && (yRatio <= .25 || yRatio <= .5 && (!below || !on)))
424 return {element: overElement, position: DropPosition.ABOVE};
425 if (below && (yRatio > .75 || yRatio > .5 && (!above || !on)))
426 return {element: overElement, position: DropPosition.BELOW};
428 return {element: overElement, position: DropPosition.ON};
432 function calculateDropInfo(eventTarget, dropDestination) {
433 if (!dropDestination || !dragInfo.isDragValid())
436 var dropPos = dropDestination.position;
437 var relatedNode = dropDestination.element.bookmarkNode;
438 var dropInfoResult = {
441 parentId: dropPos == DropPosition.ON ? relatedNode.id :
442 relatedNode.parentId,
447 // Try to find the index in the dataModel so we don't have to always keep
448 // the index for the list items up to date.
449 var overElement = getBookmarkElement(eventTarget);
450 if (overElement instanceof ListItem) {
451 dropInfoResult.relatedIndex =
452 overElement.parentNode.dataModel.indexOf(relatedNode);
453 dropInfoResult.selectTarget = bmm.list;
454 } else if (overElement instanceof BookmarkList) {
455 dropInfoResult.relatedIndex = overElement.dataModel.length - 1;
456 dropInfoResult.selectTarget = bmm.list;
459 dropInfoResult.relatedIndex = relatedNode.index;
460 dropInfoResult.selectTarget = bmm.tree;
461 dropInfoResult.selectedTreeId =
462 bmm.tree.selectedItem ? bmm.tree.selectedItem.bookmarkId : null;
465 if (dropPos == DropPosition.ABOVE)
466 dropInfoResult.index = dropInfoResult.relatedIndex;
467 else if (dropPos == DropPosition.BELOW)
468 dropInfoResult.index = dropInfoResult.relatedIndex + 1;
470 return dropInfoResult;
473 function handleDragLeave(e) {
474 dropIndicator.finish();
477 function handleDrop(e) {
478 var dropInfo = calculateDropInfo(e.target, dropDestination);
480 selectItemsAfterUserAction(dropInfo.selectTarget,
481 dropInfo.selectedTreeId);
482 if (dropInfo.index != -1)
483 chrome.bookmarkManagerPrivate.drop(dropInfo.parentId, dropInfo.index);
485 chrome.bookmarkManagerPrivate.drop(dropInfo.parentId);
489 dropDestination = null;
490 dropIndicator.finish();
493 function setCurrentTouchTarget(e) {
494 // Only set a new target for a single touch point.
495 if (e.touches.length == 1)
496 currentTouchTarget = getBookmarkElement(e.target);
499 function clearCurrentTouchTarget(e) {
500 if (getBookmarkElement(e.target) == currentTouchTarget)
501 currentTouchTarget = null;
504 function clearDragData() {
505 dragInfo.clearDragData();
506 dropDestination = null;
509 function init(selectItemsAfterUserActionFunction) {
510 function deferredClearData() {
511 setTimeout(clearDragData, 0);
514 selectItemsAfterUserAction = selectItemsAfterUserActionFunction;
516 document.addEventListener('dragstart', handleDragStart);
517 document.addEventListener('dragenter', handleDragEnter);
518 document.addEventListener('dragover', handleDragOver);
519 document.addEventListener('dragleave', handleDragLeave);
520 document.addEventListener('drop', handleDrop);
521 document.addEventListener('dragend', deferredClearData);
522 document.addEventListener('mouseup', deferredClearData);
523 document.addEventListener('mousedown', clearCurrentTouchTarget);
524 document.addEventListener('touchcancel', clearCurrentTouchTarget);
525 document.addEventListener('touchend', clearCurrentTouchTarget);
526 document.addEventListener('touchstart', setCurrentTouchTarget);
528 chrome.bookmarkManagerPrivate.onDragEnter.addListener(
529 dragInfo.handleChromeDragEnter);
530 chrome.bookmarkManagerPrivate.onDragLeave.addListener(deferredClearData);
531 chrome.bookmarkManagerPrivate.onDrop.addListener(deferredClearData);