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 that had a style applied it to indicate the drop location.
37 * This is used to easily remove the style when necessary.
40 var lastIndicatorElement
;
43 * The style that was applied to indicate the drop location.
46 var lastIndicatorClassName
;
50 * Applies the drop indicator style on the target element and stores that
51 * information to easily remove the style in the future.
53 addDropIndicatorStyle: function(indicatorElement
, position
) {
54 var indicatorStyleName
= position
== DropPosition
.ABOVE
? 'drag-above' :
55 position
== DropPosition
.BELOW
? 'drag-below' :
58 lastIndicatorElement
= indicatorElement
;
59 lastIndicatorClassName
= indicatorStyleName
;
61 indicatorElement
.classList
.add(indicatorStyleName
);
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.
68 removeDropIndicatorStyle: function() {
69 if (!lastIndicatorElement
|| !lastIndicatorClassName
)
71 lastIndicatorElement
.classList
.remove(lastIndicatorClassName
);
72 lastIndicatorElement
= null;
73 lastIndicatorClassName
= null;
77 * Displays the drop indicator on the current drop target to give the
78 * user feedback on where the drop will occur.
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');
91 dropIndicator
.removeDropIndicatorStyle();
92 dropIndicator
.addDropIndicatorStyle(indicatorElement
, position
);
96 * Stop displaying the drop indicator.
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();
109 * Delay for expanding folder when pointer hovers on folder in tree view in
114 // TODO(yosin): EXPAND_FOLDER_DELAY should follow system settings. 400ms is
115 // taken from Windows default settings.
116 var EXPAND_FOLDER_DELAY
= 400;
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.
123 var lastHoverOnFolderTimeStamp
= 0;
126 * Expand a folder if the user has hovered for longer than the specified
127 * time during a drag action.
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;
138 lastHoverOnFolderTimeStamp
= hoverOnFolderTimeStamp
;
139 } else if (overElement
instanceof TreeItem
&&
140 bmm
.isFolder(overElement
.bookmarkNode
) &&
141 overElement
.hasChildren
&&
142 !overElement
.expanded
) {
143 lastHoverOnFolderTimeStamp
= eventTimeStamp
;
148 * Stores the information about the bookmark and folders being dragged.
153 handleChromeDragEnter: function(newDragData
) {
154 dragData
= newDragData
;
156 clearDragData: function() {
159 isDragValid: function() {
162 isSameProfile: function() {
163 return dragData
&& dragData
.sameProfile
;
165 isDraggingFolders: function() {
166 return dragData
&& dragData
.elements
.some(function(node
) {
170 isDraggingBookmark: function(bookmarkId
) {
171 return dragData
&& dragData
.elements
.some(function(node
) {
172 return node
.id
== bookmarkId
;
175 isDraggingChildBookmark: function(folderId
) {
176 return dragData
&& dragData
.elements
.some(function(node
) {
177 return node
.parentId
== folderId
;
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
);
190 * External function to select folders or bookmarks after a drop action.
193 var selectItemsAfterUserAction
= null;
195 function getBookmarkElement(el
) {
196 while (el
&& !el
.bookmarkNode
) {
202 // If we are over the list and the list is showing search result, we cannot
204 function isOverSearch(overElement
) {
205 return list
.isSearch() && list
.contains(overElement
);
209 * Determines the valid drop positions for the given target element.
210 * @param {!HTMLElement} overElement The element that we are currently
212 * @return {DropPosition} An bit field enumeration of valid drop locations.
214 function calculateValidDropTargets(overElement
) {
215 // Don't allow dropping if there is an ephemeral item being edited.
216 if (list
.hasEphemeral())
217 return DropPosition
.NONE
;
219 if (!dragInfo
.isDragValid() || isOverSearch(overElement
))
220 return DropPosition
.NONE
;
222 if (dragInfo
.isSameProfile() &&
223 (dragInfo
.isDraggingBookmark(overElement
.bookmarkNode
.id
) ||
224 dragInfo
.isDraggingFolderToDescendant(overElement
.bookmarkNode
))) {
225 return DropPosition
.NONE
;
228 var canDropInfo
= calculateDropAboveBelow(overElement
);
229 if (canDropOn(overElement
))
230 canDropInfo
|= DropPosition
.ON
;
235 function calculateDropAboveBelow(overElement
) {
236 if (overElement
instanceof BookmarkList
)
237 return DropPosition
.NONE
;
239 // We cannot drop between Bookmarks bar and Other bookmarks.
240 if (overElement
.bookmarkNode
.parentId
== bmm
.ROOT_ID
)
241 return DropPosition
.NONE
;
243 var isOverTreeItem
= overElement
instanceof TreeItem
;
244 var isOverExpandedTree
= isOverTreeItem
&& overElement
.expanded
;
245 var isDraggingFolders
= dragInfo
.isDraggingFolders();
247 // We can only drop between items in the tree if we have any folders.
248 if (isOverTreeItem
&& !isDraggingFolders
)
249 return DropPosition
.NONE
;
251 // When dragging from a different profile we do not need to consider
252 // conflicts between the dragged items and the drop target.
253 if (!dragInfo
.isSameProfile()) {
254 // Don't allow dropping below an expanded tree item since it is confusing
255 // to the user anyway.
256 return isOverExpandedTree
? DropPosition
.ABOVE
:
257 (DropPosition
.ABOVE
| DropPosition
.BELOW
);
260 var resultPositions
= DropPosition
.NONE
;
262 // Cannot drop above if the item above is already in the drag source.
263 var previousElem
= overElement
.previousElementSibling
;
264 if (!previousElem
|| !dragInfo
.isDraggingBookmark(previousElem
.bookmarkId
))
265 resultPositions
|= DropPosition
.ABOVE
;
267 // Don't allow dropping below an expanded tree item since it is confusing
268 // to the user anyway.
269 if (isOverExpandedTree
)
270 return resultPositions
;
272 // Cannot drop below if the item below is already in the drag source.
273 var nextElement
= overElement
.nextElementSibling
;
274 if (!nextElement
|| !dragInfo
.isDraggingBookmark(nextElement
.bookmarkId
))
275 resultPositions
|= DropPosition
.BELOW
;
277 return resultPositions
;
281 * Determine whether we can drop the dragged items on the drop target.
282 * @param {!HTMLElement} overElement The element that we are currently
284 * @return {boolean} Whether we can drop the dragged items on the drop
287 function canDropOn(overElement
) {
288 // We can only drop on a folder.
289 if (!bmm
.isFolder(overElement
.bookmarkNode
))
292 if (!dragInfo
.isSameProfile())
295 if (overElement
instanceof BookmarkList
) {
296 // We are trying to drop an item past the last item. This is
297 // only allowed if dragged item is different from the last item
299 var listItems
= list
.items
;
300 var len
= listItems
.length
;
301 if (!len
|| !dragInfo
.isDraggingBookmark(listItems
[len
- 1].bookmarkId
))
305 return !dragInfo
.isDraggingChildBookmark(overElement
.bookmarkNode
.id
);
309 * Callback for the dragstart event.
310 * @param {Event} e The dragstart event.
312 function handleDragStart(e
) {
313 // Determine the selected bookmarks.
314 var target
= e
.target
;
315 var draggedNodes
= [];
316 if (target
instanceof ListItem
) {
317 // Use selected items.
318 draggedNodes
= target
.parentNode
.selectedItems
;
319 } else if (target
instanceof TreeItem
) {
320 draggedNodes
.push(target
.bookmarkNode
);
323 // We manage starting the drag by using the extension API.
326 // Do not allow dragging if there is an ephemeral item being edited at the
328 if (list
.hasEphemeral())
331 if (draggedNodes
.length
) {
332 // If we are dragging a single link, we can do the *Link* effect.
333 // Otherwise, we only allow copy and move.
334 e
.dataTransfer
.effectAllowed
= draggedNodes
.length
== 1 &&
335 !bmm
.isFolder(draggedNodes
[0]) ? 'copyMoveLink' : 'copyMove';
337 chrome
.bookmarkManagerPrivate
.startDrag(draggedNodes
.map(function(node
) {
343 function handleDragEnter(e
) {
348 * Calback for the dragover event.
349 * @param {Event} e The dragover event.
351 function handleDragOver(e
) {
352 // Allow DND on text inputs.
353 if (e
.target
.tagName
!= 'INPUT') {
354 // The default operation is to allow dropping links etc to do navigation.
355 // We never want to do that for the bookmark manager.
358 // Set to none. This will get set to something if we can do the drop.
359 e
.dataTransfer
.dropEffect
= 'none';
362 if (!dragInfo
.isDragValid())
365 var overElement
= getBookmarkElement(e
.target
) ||
366 (e
.target
== list
? list
: null);
370 updateAutoExpander(e
.timeStamp
, overElement
);
372 var canDropInfo
= calculateValidDropTargets(overElement
);
373 if (canDropInfo
== DropPosition
.NONE
)
376 // Now we know that we can drop. Determine if we will drop above, on or
377 // below based on mouse position etc.
379 dropDestination
= calcDropPosition(e
.clientY
, overElement
, canDropInfo
);
380 if (!dropDestination
) {
381 e
.dataTransfer
.dropEffect
= 'none';
385 e
.dataTransfer
.dropEffect
= dragInfo
.isSameProfile() ? 'move' : 'copy';
386 dropIndicator
.update(dropDestination
);
390 * This function determines where the drop will occur relative to the element.
391 * @return {?Object} If no valid drop position is found, null, otherwise
392 * an object containing the following parameters:
393 * element - The target element that will receive the drop.
394 * position - A |DropPosition| relative to the |element|.
396 function calcDropPosition(elementClientY
, overElement
, canDropInfo
) {
397 if (overElement
instanceof BookmarkList
) {
398 // Dropping on the BookmarkList either means dropping below the last
399 // bookmark element or on the list itself if it is empty.
400 var length
= overElement
.items
.length
;
403 element
: overElement
.getListItemByIndex(length
- 1),
404 position
: DropPosition
.BELOW
406 return {element
: overElement
, position
: DropPosition
.ON
};
409 var above
= canDropInfo
& DropPosition
.ABOVE
;
410 var below
= canDropInfo
& DropPosition
.BELOW
;
411 var on
= canDropInfo
& DropPosition
.ON
;
412 var rect
= overElement
.getBoundingClientRect();
413 var yRatio
= (elementClientY
- rect
.top
) / rect
.height
;
415 if (above
&& (yRatio
<= .25 || yRatio
<= .5 && (!below
|| !on
)))
416 return {element
: overElement
, position
: DropPosition
.ABOVE
};
417 if (below
&& (yRatio
> .75 || yRatio
> .5 && (!above
|| !on
)))
418 return {element
: overElement
, position
: DropPosition
.BELOW
};
420 return {element
: overElement
, position
: DropPosition
.ON
};
424 function calculateDropInfo(eventTarget
, dropDestination
) {
425 if (!dropDestination
|| !dragInfo
.isDragValid())
428 var dropPos
= dropDestination
.position
;
429 var relatedNode
= dropDestination
.element
.bookmarkNode
;
430 var dropInfoResult
= {
433 parentId
: dropPos
== DropPosition
.ON
? relatedNode
.id
:
434 relatedNode
.parentId
,
439 // Try to find the index in the dataModel so we don't have to always keep
440 // the index for the list items up to date.
441 var overElement
= getBookmarkElement(eventTarget
);
442 if (overElement
instanceof ListItem
) {
443 dropInfoResult
.relatedIndex
=
444 overElement
.parentNode
.dataModel
.indexOf(relatedNode
);
445 dropInfoResult
.selectTarget
= list
;
446 } else if (overElement
instanceof BookmarkList
) {
447 dropInfoResult
.relatedIndex
= overElement
.dataModel
.length
- 1;
448 dropInfoResult
.selectTarget
= list
;
451 dropInfoResult
.relatedIndex
= relatedNode
.index
;
452 dropInfoResult
.selectTarget
= tree
;
453 dropInfoResult
.selectedTreeId
=
454 tree
.selectedItem
? tree
.selectedItem
.bookmarkId
: null;
457 if (dropPos
== DropPosition
.ABOVE
)
458 dropInfoResult
.index
= dropInfoResult
.relatedIndex
;
459 else if (dropPos
== DropPosition
.BELOW
)
460 dropInfoResult
.index
= dropInfoResult
.relatedIndex
+ 1;
462 return dropInfoResult
;
465 function handleDragLeave(e
) {
466 dropIndicator
.finish();
469 function handleDrop(e
) {
470 var dropInfo
= calculateDropInfo(e
.target
, dropDestination
);
472 selectItemsAfterUserAction(dropInfo
.selectTarget
,
473 dropInfo
.selectedTreeId
);
474 if (dropInfo
.index
!= -1)
475 chrome
.bookmarkManagerPrivate
.drop(dropInfo
.parentId
, dropInfo
.index
);
477 chrome
.bookmarkManagerPrivate
.drop(dropInfo
.parentId
);
481 dropDestination
= null;
482 dropIndicator
.finish();
485 function clearDragData() {
486 dragInfo
.clearDragData();
487 dropDestination
= null;
490 function init(selectItemsAfterUserActionFunction
) {
491 function deferredClearData() {
492 setTimeout(clearDragData
);
495 selectItemsAfterUserAction
= selectItemsAfterUserActionFunction
;
497 document
.addEventListener('dragstart', handleDragStart
);
498 document
.addEventListener('dragenter', handleDragEnter
);
499 document
.addEventListener('dragover', handleDragOver
);
500 document
.addEventListener('dragleave', handleDragLeave
);
501 document
.addEventListener('drop', handleDrop
);
502 document
.addEventListener('dragend', deferredClearData
);
503 document
.addEventListener('mouseup', deferredClearData
);
505 chrome
.bookmarkManagerPrivate
.onDragEnter
.addListener(
506 dragInfo
.handleChromeDragEnter
);
507 chrome
.bookmarkManagerPrivate
.onDragLeave
.addListener(deferredClearData
);
508 chrome
.bookmarkManagerPrivate
.onDrop
.addListener(deferredClearData
);