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
);