Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / resources / enhanced_bookmark_manager / js / dnd.js
blobb4859a6310aaaa38723c993580a05ebc6a181fca
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 that had a style applied it to indicate the drop location.
37     * This is used to easily remove the style when necessary.
38     * @type {Element}
39     */
40   var lastIndicatorElement;
42   /**
43     * The style that was applied to indicate the drop location.
44     * @type {string}
45     */
46   var lastIndicatorClassName;
48   var dropIndicator = {
49     /**
50      * Applies the drop indicator style on the target element and stores that
51      * information to easily remove the style in the future.
52      */
53     addDropIndicatorStyle: function(indicatorElement, position) {
54       var indicatorStyleName = position == DropPosition.ABOVE ? 'drag-above' :
55                                position == DropPosition.BELOW ? 'drag-below' :
56                                'drag-on';
58       lastIndicatorElement = indicatorElement;
59       lastIndicatorClassName = indicatorStyleName;
61       indicatorElement.classList.add(indicatorStyleName);
62     },
64     /**
65      * Clears the drop indicator style from the last element was the drop target
66      * so the drop indicator is no longer for that element.
67      */
68     removeDropIndicatorStyle: function() {
69       if (!lastIndicatorElement || !lastIndicatorClassName)
70         return;
71       lastIndicatorElement.classList.remove(lastIndicatorClassName);
72       lastIndicatorElement = null;
73       lastIndicatorClassName = null;
74     },
76     /**
77       * Displays the drop indicator on the current drop target to give the
78       * user feedback on where the drop will occur.
79       */
80     update: function(dropDest) {
81       window.clearTimeout(removeDropIndicatorTimer);
83       var indicatorElement = dropDest.element;
84       var position = dropDest.position;
85       if (dropDest.element instanceof BookmarkList) {
86         // For an empty bookmark list use 'drop-above' style.
87         position = DropPosition.ABOVE;
88       } else if (dropDest.element instanceof TreeItem) {
89         indicatorElement = indicatorElement.querySelector('.tree-row');
90       }
91       dropIndicator.removeDropIndicatorStyle();
92       dropIndicator.addDropIndicatorStyle(indicatorElement, position);
93     },
95     /**
96      * Stop displaying the drop indicator.
97      */
98     finish: function() {
99       // The use of a timeout is in order to reduce flickering as we move
100       // between valid drop targets.
101       window.clearTimeout(removeDropIndicatorTimer);
102       removeDropIndicatorTimer = window.setTimeout(function() {
103         dropIndicator.removeDropIndicatorStyle();
104       }, 100);
105     }
106   };
108   /**
109     * Delay for expanding folder when pointer hovers on folder in tree view in
110     * milliseconds.
111     * @type {number}
112     * @const
113     */
114   // TODO(yosin): EXPAND_FOLDER_DELAY should follow system settings. 400ms is
115   // taken from Windows default settings.
116   var EXPAND_FOLDER_DELAY = 400;
118   /**
119     * The timestamp when the mouse was over a folder during a drag operation.
120     * Used to open the hovered folder after a certain time.
121     * @type {number}
122     */
123   var lastHoverOnFolderTimeStamp = 0;
125   /**
126     * Expand a folder if the user has hovered for longer than the specified
127     * time during a drag action.
128     */
129   function updateAutoExpander(eventTimeStamp, overElement) {
130     // Expands a folder in tree view when pointer hovers on it longer than
131     // EXPAND_FOLDER_DELAY.
132     var hoverOnFolderTimeStamp = lastHoverOnFolderTimeStamp;
133     lastHoverOnFolderTimeStamp = 0;
134     if (hoverOnFolderTimeStamp) {
135       if (eventTimeStamp - hoverOnFolderTimeStamp >= EXPAND_FOLDER_DELAY)
136         overElement.expanded = true;
137       else
138         lastHoverOnFolderTimeStamp = hoverOnFolderTimeStamp;
139     } else if (overElement instanceof TreeItem &&
140                 bmm.isFolder(overElement.bookmarkNode) &&
141                 overElement.hasChildren &&
142                 !overElement.expanded) {
143       lastHoverOnFolderTimeStamp = eventTimeStamp;
144     }
145   }
147   /**
148     * Stores the information abou the bookmark and folders being dragged.
149     * @type {Object}
150     */
151   var dragData = null;
152   var dragInfo = {
153     handleChromeDragEnter: function(newDragData) {
154       dragData = newDragData;
155     },
156     clearDragData: function() {
157       dragData = null;
158     },
159     isDragValid: function() {
160       return !!dragData;
161     },
162     isSameProfile: function() {
163       return dragData && dragData.sameProfile;
164     },
165     isDraggingFolders: function() {
166       return dragData && dragData.elements.some(function(node) {
167         return !node.url;
168       });
169     },
170     isDraggingBookmark: function(bookmarkId) {
171       return dragData && dragData.elements.some(function(node) {
172         return node.id == bookmarkId;
173       });
174     },
175     isDraggingChildBookmark: function(folderId) {
176       return dragData && dragData.elements.some(function(node) {
177         return node.parentId == folderId;
178       });
179     },
180     isDraggingFolderToDescendant: function(bookmarkNode) {
181       return dragData && dragData.elements.some(function(node) {
182         var dragFolder = bmm.treeLookup[node.id];
183         var dragFolderNode = dragFolder && dragFolder.bookmarkNode;
184         return dragFolderNode && bmm.contains(dragFolderNode, bookmarkNode);
185       });
186     }
187   };
189   /**
190    * External function to select folders or bookmarks after a drop action.
191    * @type {function}
192    */
193   var selectItemsAfterUserAction = null;
195   function getBookmarkElement(el) {
196     while (el && !el.bookmarkNode) {
197       el = el.parentNode;
198     }
199     return el;
200   }
202   // If we are over the list and the list is showing search result, we cannot
203   // drop.
204   function isOverSearch(overElement) {
205     return list.isSearch() && list.contains(overElement);
206   }
208   /**
209    * Determines the valid drop positions for the given target element.
210    * @param {!HTMLElement} overElement The element that we are currently
211    *     dragging over.
212    * @return {DropPosition} An bit field enumeration of valid drop locations.
213    */
214   function calculateValidDropTargets(overElement) {
215     if (!dragInfo.isDragValid() || isOverSearch(overElement))
216       return DropPosition.NONE;
218     if (dragInfo.isSameProfile() &&
219         (dragInfo.isDraggingBookmark(overElement.bookmarkNode.id) ||
220          dragInfo.isDraggingFolderToDescendant(overElement.bookmarkNode))) {
221       return DropPosition.NONE;
222     }
224     var canDropInfo = calculateDropAboveBelow(overElement);
225     if (canDropOn(overElement))
226       canDropInfo |= DropPosition.ON;
228     return canDropInfo;
229   }
231   function calculateDropAboveBelow(overElement) {
232     if (overElement instanceof BookmarkList)
233       return DropPosition.NONE;
235     // We cannot drop between Bookmarks bar and Other bookmarks.
236     if (overElement.bookmarkNode.parentId == bmm.ROOT_ID)
237       return DropPosition.NONE;
239     var isOverTreeItem = overElement instanceof TreeItem;
240     var isOverExpandedTree = isOverTreeItem && overElement.expanded;
241     var isDraggingFolders = dragInfo.isDraggingFolders();
243     // We can only drop between items in the tree if we have any folders.
244     if (isOverTreeItem && !isDraggingFolders)
245       return DropPosition.NONE;
247     // When dragging from a different profile we do not need to consider
248     // conflicts between the dragged items and the drop target.
249     if (!dragInfo.isSameProfile()) {
250       // Don't allow dropping below an expanded tree item since it is confusing
251       // to the user anyway.
252       return isOverExpandedTree ? DropPosition.ABOVE :
253                                   (DropPosition.ABOVE | DropPosition.BELOW);
254     }
256     var resultPositions = DropPosition.NONE;
258     // Cannot drop above if the item above is already in the drag source.
259     var previousElem = overElement.previousElementSibling;
260     if (!previousElem || !dragInfo.isDraggingBookmark(previousElem.bookmarkId))
261       resultPositions |= DropPosition.ABOVE;
263     // Don't allow dropping below an expanded tree item since it is confusing
264     // to the user anyway.
265     if (isOverExpandedTree)
266       return resultPositions;
268     // Cannot drop below if the item below is already in the drag source.
269     var nextElement = overElement.nextElementSibling;
270     if (!nextElement || !dragInfo.isDraggingBookmark(nextElement.bookmarkId))
271       resultPositions |= DropPosition.BELOW;
273     return resultPositions;
274   }
276   /**
277    * Determine whether we can drop the dragged items on the drop target.
278    * @param {!HTMLElement} overElement The element that we are currently
279    *     dragging over.
280    * @return {boolean} Whether we can drop the dragged items on the drop
281    *     target.
282    */
283   function canDropOn(overElement) {
284     // We can only drop on a folder.
285     if (!bmm.isFolder(overElement.bookmarkNode))
286       return false;
288     if (!dragInfo.isSameProfile())
289       return true;
291     if (overElement instanceof BookmarkList) {
292       // We are trying to drop an item past the last item. This is
293       // only allowed if dragged item is different from the last item
294       // in the list.
295       var listItems = list.items;
296       var len = listItems.length;
297       if (!len || !dragInfo.isDraggingBookmark(listItems[len - 1].bookmarkId))
298         return true;
299     }
301     return !dragInfo.isDraggingChildBookmark(overElement.bookmarkNode.id);
302   }
304   /**
305    * Callback for the dragstart event.
306    * @param {Event} e The dragstart event.
307    */
308   function handleDragStart(e) {
309     // Determine the selected bookmarks.
310     var target = e.target;
311     var draggedNodes = [];
312     if (target instanceof ListItem) {
313       // Use selected items.
314       draggedNodes = target.parentNode.selectedItems;
315     } else if (target instanceof TreeItem) {
316       draggedNodes.push(target.bookmarkNode);
317     }
319     // We manage starting the drag by using the extension API.
320     e.preventDefault();
322     if (draggedNodes.length) {
323       // If we are dragging a single link, we can do the *Link* effect.
324       // Otherwise, we only allow copy and move.
325       e.dataTransfer.effectAllowed = draggedNodes.length == 1 &&
326           !bmm.isFolder(draggedNodes[0]) ? 'copyMoveLink' : 'copyMove';
328       chrome.bookmarkManagerPrivate.startDrag(draggedNodes.map(function(node) {
329         return node.id;
330       }));
331     }
332   }
334   function handleDragEnter(e) {
335     e.preventDefault();
336   }
338   /**
339    * Calback for the dragover event.
340    * @param {Event} e The dragover event.
341    */
342   function handleDragOver(e) {
343     // Allow DND on text inputs.
344     if (e.target.tagName != 'INPUT') {
345       // The default operation is to allow dropping links etc to do navigation.
346       // We never want to do that for the bookmark manager.
347       e.preventDefault();
349       // Set to none. This will get set to something if we can do the drop.
350       e.dataTransfer.dropEffect = 'none';
351     }
353     if (!dragInfo.isDragValid())
354       return;
356     var overElement = getBookmarkElement(e.target) ||
357                       (e.target == list ? list : null);
358     if (!overElement)
359       return;
361     updateAutoExpander(e.timeStamp, overElement);
363     var canDropInfo = calculateValidDropTargets(overElement);
364     if (canDropInfo == DropPosition.NONE)
365       return;
367     // Now we know that we can drop. Determine if we will drop above, on or
368     // below based on mouse position etc.
370     dropDestination = calcDropPosition(e.clientY, overElement, canDropInfo);
371     if (!dropDestination) {
372       e.dataTransfer.dropEffect = 'none';
373       return;
374     }
376     e.dataTransfer.dropEffect = dragInfo.isSameProfile() ? 'move' : 'copy';
377     dropIndicator.update(dropDestination);
378   }
380   /**
381    * This function determines where the drop will occur relative to the element.
382    * @return {?Object} If no valid drop position is found, null, otherwise
383    *     an object containing the following parameters:
384    *       element - The target element that will receive the drop.
385    *       position - A |DropPosition| relative to the |element|.
386    */
387   function calcDropPosition(elementClientY, overElement, canDropInfo) {
388     if (overElement instanceof BookmarkList) {
389       // Dropping on the BookmarkList either means dropping below the last
390       // bookmark element or on the list itself if it is empty.
391       var length = overElement.items.length;
392       if (length)
393         return {
394           element: overElement.getListItemByIndex(length - 1),
395           position: DropPosition.BELOW
396         };
397       return {element: overElement, position: DropPosition.ON};
398     }
400     var above = canDropInfo & DropPosition.ABOVE;
401     var below = canDropInfo & DropPosition.BELOW;
402     var on = canDropInfo & DropPosition.ON;
403     var rect = overElement.getBoundingClientRect();
404     var yRatio = (elementClientY - rect.top) / rect.height;
406     if (above && (yRatio <= .25 || yRatio <= .5 && (!below || !on)))
407       return {element: overElement, position: DropPosition.ABOVE};
408     if (below && (yRatio > .75 || yRatio > .5 && (!above || !on)))
409       return {element: overElement, position: DropPosition.BELOW};
410     if (on)
411       return {element: overElement, position: DropPosition.ON};
412     return null;
413   }
415   function calculateDropInfo(eventTarget, dropDestination) {
416     if (!dropDestination || !dragInfo.isDragValid())
417       return null;
419     var dropPos = dropDestination.position;
420     var relatedNode = dropDestination.element.bookmarkNode;
421     var dropInfoResult = {
422         selectTarget: null,
423         selectedTreeId: -1,
424         parentId: dropPos == DropPosition.ON ? relatedNode.id :
425                                                relatedNode.parentId,
426         index: -1,
427         relatedIndex: -1
428       };
430     // Try to find the index in the dataModel so we don't have to always keep
431     // the index for the list items up to date.
432     var overElement = getBookmarkElement(eventTarget);
433     if (overElement instanceof ListItem) {
434       dropInfoResult.relatedIndex =
435           overElement.parentNode.dataModel.indexOf(relatedNode);
436       dropInfoResult.selectTarget = list;
437     } else if (overElement instanceof BookmarkList) {
438       dropInfoResult.relatedIndex = overElement.dataModel.length - 1;
439       dropInfoResult.selectTarget = list;
440     } else {
441       // Tree
442       dropInfoResult.relatedIndex = relatedNode.index;
443       dropInfoResult.selectTarget = tree;
444       dropInfoResult.selectedTreeId =
445           tree.selectedItem ? tree.selectedItem.bookmarkId : null;
446     }
448     if (dropPos == DropPosition.ABOVE)
449       dropInfoResult.index = dropInfoResult.relatedIndex;
450     else if (dropPos == DropPosition.BELOW)
451       dropInfoResult.index = dropInfoResult.relatedIndex + 1;
453     return dropInfoResult;
454   }
456   function handleDragLeave(e) {
457     dropIndicator.finish();
458   }
460   function handleDrop(e) {
461     var dropInfo = calculateDropInfo(e.target, dropDestination);
462     if (dropInfo) {
463       selectItemsAfterUserAction(dropInfo.selectTarget,
464                                  dropInfo.selectedTreeId);
465       if (dropInfo.index != -1)
466         chrome.bookmarkManagerPrivate.drop(dropInfo.parentId, dropInfo.index);
467       else
468         chrome.bookmarkManagerPrivate.drop(dropInfo.parentId);
470       e.preventDefault();
471     }
472     dropDestination = null;
473     dropIndicator.finish();
474   }
476   function clearDragData() {
477     dragInfo.clearDragData();
478     dropDestination = null;
479   }
481   function init(selectItemsAfterUserActionFunction) {
482     function deferredClearData() {
483       setTimeout(clearDragData);
484     }
486     selectItemsAfterUserAction = selectItemsAfterUserActionFunction;
488     document.addEventListener('dragstart', handleDragStart);
489     document.addEventListener('dragenter', handleDragEnter);
490     document.addEventListener('dragover', handleDragOver);
491     document.addEventListener('dragleave', handleDragLeave);
492     document.addEventListener('drop', handleDrop);
493     document.addEventListener('dragend', deferredClearData);
494     document.addEventListener('mouseup', deferredClearData);
496     chrome.bookmarkManagerPrivate.onDragEnter.addListener(
497         dragInfo.handleChromeDragEnter);
498     chrome.bookmarkManagerPrivate.onDragLeave.addListener(deferredClearData);
499     chrome.bookmarkManagerPrivate.onDrop.addListener(deferredClearData);
500   }
501   return {init: init};