Remove the old signature of NotificationManager::closePersistent().
[chromium-blink-merge.git] / chrome / browser / resources / bookmark_manager / js / dnd.js
blob0f0bffde29fa865f70c32d283a48ddaa5fcbd099
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() {
6   'use strict';
8   /** @const */ var BookmarkList = bmm.BookmarkList;
9   /** @const */ var ListItem = cr.ui.ListItem;
10   /** @const */ var TreeItem = cr.ui.TreeItem;
12   /**
13    * Enumeration of valid drop locations relative to an element. These are
14    * bit masks to allow combining multiple locations in a single value.
15    * @enum {number}
16    * @const
17    */
18   var DropPosition = {
19     NONE: 0,
20     ABOVE: 1,
21     ON: 2,
22     BELOW: 4
23   };
25   /**
26    * @type {Object} Drop information calculated in |handleDragOver|.
27    */
28   var dropDestination = null;
30   /**
31     * @type {number} Timer id used to help minimize flicker.
32     */
33   var removeDropIndicatorTimer;
35   /**
36     * The element currently targeted by a touch.
37     * @type {Element}
38     */
39   var currentTouchTarget;
41   /**
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.
44     * @type {Element}
45     */
46   var lastIndicatorElement;
48   /**
49     * The style that was applied to indicate the drop location.
50     * @type {?string}
51     */
52   var lastIndicatorClassName;
54   var dropIndicator = {
55     /**
56      * Applies the drop indicator style on the target element and stores that
57      * information to easily remove the style in the future.
58      */
59     addDropIndicatorStyle: function(indicatorElement, position) {
60       var indicatorStyleName = position == DropPosition.ABOVE ? 'drag-above' :
61                                position == DropPosition.BELOW ? 'drag-below' :
62                                'drag-on';
64       lastIndicatorElement = indicatorElement;
65       lastIndicatorClassName = indicatorStyleName;
67       indicatorElement.classList.add(indicatorStyleName);
68     },
70     /**
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.
73      */
74     removeDropIndicatorStyle: function() {
75       if (!lastIndicatorElement || !lastIndicatorClassName)
76         return;
77       lastIndicatorElement.classList.remove(lastIndicatorClassName);
78       lastIndicatorElement = null;
79       lastIndicatorClassName = null;
80     },
82     /**
83       * Displays the drop indicator on the current drop target to give the
84       * user feedback on where the drop will occur.
85       */
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');
96       }
97       dropIndicator.removeDropIndicatorStyle();
98       dropIndicator.addDropIndicatorStyle(indicatorElement, position);
99     },
101     /**
102      * Stop displaying the drop indicator.
103      */
104     finish: function() {
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();
110       }, 100);
111     }
112   };
114   /**
115     * Delay for expanding folder when pointer hovers on folder in tree view in
116     * milliseconds.
117     * @type {number}
118     * @const
119     */
120   // TODO(yosin): EXPAND_FOLDER_DELAY should follow system settings. 400ms is
121   // taken from Windows default settings.
122   var EXPAND_FOLDER_DELAY = 400;
124   /**
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.
127     * @type {number}
128     */
129   var lastHoverOnFolderTimeStamp = 0;
131   /**
132     * Expand a folder if the user has hovered for longer than the specified
133     * time during a drag action.
134     */
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;
143       else
144         lastHoverOnFolderTimeStamp = hoverOnFolderTimeStamp;
145     } else if (overElement instanceof TreeItem &&
146                 bmm.isFolder(overElement.bookmarkNode) &&
147                 overElement.hasChildren &&
148                 !overElement.expanded) {
149       lastHoverOnFolderTimeStamp = eventTimeStamp;
150     }
151   }
153   /**
154     * Stores the information about the bookmark and folders being dragged.
155     * @type {Object}
156     */
157   var dragData = null;
158   var dragInfo = {
159     handleChromeDragEnter: function(newDragData) {
160       dragData = newDragData;
161     },
162     clearDragData: function() {
163       dragData = null;
164     },
165     isDragValid: function() {
166       return !!dragData;
167     },
168     isSameProfile: function() {
169       return dragData && dragData.sameProfile;
170     },
171     isDraggingFolders: function() {
172       return dragData && dragData.elements.some(function(node) {
173         return !node.url;
174       });
175     },
176     isDraggingBookmark: function(bookmarkId) {
177       return dragData && dragData.elements.some(function(node) {
178         return node.id == bookmarkId;
179       });
180     },
181     isDraggingChildBookmark: function(folderId) {
182       return dragData && dragData.elements.some(function(node) {
183         return node.parentId == folderId;
184       });
185     },
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);
191       });
192     }
193   };
195   /**
196    * External function to select folders or bookmarks after a drop action.
197    * @type {?Function}
198    */
199   var selectItemsAfterUserAction = null;
201   function getBookmarkElement(el) {
202     while (el && !el.bookmarkNode) {
203       el = el.parentNode;
204     }
205     return el;
206   }
208   // If we are over the list and the list is showing search result, we cannot
209   // drop.
210   function isOverSearch(overElement) {
211     return bmm.list.isSearch() && bmm.list.contains(overElement);
212   }
214   /**
215    * Determines the valid drop positions for the given target element.
216    * @param {!HTMLElement} overElement The element that we are currently
217    *     dragging over.
218    * @return {DropPosition} An bit field enumeration of valid drop locations.
219    */
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;
232     }
234     var canDropInfo = calculateDropAboveBelow(overElement);
235     if (canDropOn(overElement))
236       canDropInfo |= DropPosition.ON;
238     return canDropInfo;
239   }
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);
264     }
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;
284   }
286   /**
287    * Determine whether we can drop the dragged items on the drop target.
288    * @param {!HTMLElement} overElement The element that we are currently
289    *     dragging over.
290    * @return {boolean} Whether we can drop the dragged items on the drop
291    *     target.
292    */
293   function canDropOn(overElement) {
294     // We can only drop on a folder.
295     if (!bmm.isFolder(overElement.bookmarkNode))
296       return false;
298     if (!dragInfo.isSameProfile())
299       return true;
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
304       // in the list.
305       var listItems = bmm.list.items;
306       var len = listItems.length;
307       if (!len || !dragInfo.isDraggingBookmark(listItems[len - 1].bookmarkId))
308         return true;
309     }
311     return !dragInfo.isDraggingChildBookmark(overElement.bookmarkNode.id);
312   }
314   /**
315    * Callback for the dragstart event.
316    * @param {Event} e The dragstart event.
317    */
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);
329     }
331     // We manage starting the drag by using the extension API.
332     e.preventDefault();
334     // Do not allow dragging if there is an ephemeral item being edited at the
335     // moment.
336     if (bmm.list.hasEphemeral())
337       return;
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) {
346         return node.id;
347       }), isFromTouch);
348     }
349   }
351   function handleDragEnter(e) {
352     e.preventDefault();
353   }
355   /**
356    * Calback for the dragover event.
357    * @param {Event} e The dragover event.
358    */
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.
364       e.preventDefault();
366       // Set to none. This will get set to something if we can do the drop.
367       e.dataTransfer.dropEffect = 'none';
368     }
370     if (!dragInfo.isDragValid())
371       return;
373     var overElement = getBookmarkElement(e.target) ||
374                       (e.target == bmm.list ? bmm.list : null);
375     if (!overElement)
376       return;
378     updateAutoExpander(e.timeStamp, overElement);
380     var canDropInfo = calculateValidDropTargets(overElement);
381     if (canDropInfo == DropPosition.NONE)
382       return;
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';
390       return;
391     }
393     e.dataTransfer.dropEffect = dragInfo.isSameProfile() ? 'move' : 'copy';
394     dropIndicator.update(dropDestination);
395   }
397   /**
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|.
403    */
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;
409       if (length)
410         return {
411           element: overElement.getListItemByIndex(length - 1),
412           position: DropPosition.BELOW
413         };
414       return {element: overElement, position: DropPosition.ON};
415     }
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};
427     if (on)
428       return {element: overElement, position: DropPosition.ON};
429     return null;
430   }
432   function calculateDropInfo(eventTarget, dropDestination) {
433     if (!dropDestination || !dragInfo.isDragValid())
434       return null;
436     var dropPos = dropDestination.position;
437     var relatedNode = dropDestination.element.bookmarkNode;
438     var dropInfoResult = {
439         selectTarget: null,
440         selectedTreeId: -1,
441         parentId: dropPos == DropPosition.ON ? relatedNode.id :
442                                                relatedNode.parentId,
443         index: -1,
444         relatedIndex: -1
445       };
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;
457     } else {
458       // Tree
459       dropInfoResult.relatedIndex = relatedNode.index;
460       dropInfoResult.selectTarget = bmm.tree;
461       dropInfoResult.selectedTreeId =
462           bmm.tree.selectedItem ? bmm.tree.selectedItem.bookmarkId : null;
463     }
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;
471   }
473   function handleDragLeave(e) {
474     dropIndicator.finish();
475   }
477   function handleDrop(e) {
478     var dropInfo = calculateDropInfo(e.target, dropDestination);
479     if (dropInfo) {
480       selectItemsAfterUserAction(dropInfo.selectTarget,
481                                  dropInfo.selectedTreeId);
482       if (dropInfo.index != -1)
483         chrome.bookmarkManagerPrivate.drop(dropInfo.parentId, dropInfo.index);
484       else
485         chrome.bookmarkManagerPrivate.drop(dropInfo.parentId);
487       e.preventDefault();
488     }
489     dropDestination = null;
490     dropIndicator.finish();
491   }
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);
497   }
499   function clearCurrentTouchTarget(e) {
500     if (getBookmarkElement(e.target) == currentTouchTarget)
501       currentTouchTarget = null;
502   }
504   function clearDragData() {
505     dragInfo.clearDragData();
506     dropDestination = null;
507   }
509   function init(selectItemsAfterUserActionFunction) {
510     function deferredClearData() {
511       setTimeout(clearDragData, 0);
512     }
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);
532   }
533   return {init: init};