Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(212)

Side by Side Diff: chrome/browser/resources/md_bookmarks/dnd_manager.js

Issue 2758703002: [WIP] MD Bookmarks Drag and Drop. (Closed)
Patch Set: Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2017 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.
4
5 /** @typedef {{element: BookmarkElement, position: DropPosition}} */
6 var DropDestination;
7
8 cr.define('bookmarks', function() {
9 /** @const */
10 var ROOT_NODE_ID = '0';
11
12 /**
13 * @record
14 */
15 function BookmarkElement() {}
16
17 /** @type {string} */
18 BookmarkElement.itemId;
19
20 /**
21 * Enumeration of valid drop locations relative to an element. These are
22 * bit masks to allow combining multiple locations in a single value.
23 * @enum {number}
24 * @const
25 */
26 var DropPosition = {NONE: 0, ABOVE: 1, ON: 2, BELOW: 4};
27
28 /**
29 * @param {BookmarkElement} element
30 * @return {boolean}
31 */
32 function isBookmarkItem(element) {
33 return element.tagName == 'BOOKMARKS-ITEM';
34 }
35
36 /**
37 * @param {BookmarkElement} element
38 * @return {boolean}
39 */
40 function isBookmarkFolderNode(element) {
41 return element.tagName == 'BOOKMARKS-FOLDER-NODE';
42 }
43
44 /**
45 * @param {Array<!Element>|undefined} path
46 * @return {BookmarkElement}
47 */
48 function getBookmarkElement(path) {
49 if (!path)
50 return null;
51
52 for (var i = 0; i < path.length; i++) {
53 if (isBookmarkItem(path[i]) || isBookmarkFolderNode(path[i]))
54 return path[i];
55 }
56 return null;
57 }
58
59 /**
60 * @param {BookmarkElement} bookmarkElement
61 * @return {BookmarkNode}
62 */
63 function getBookmarkNode(bookmarkElement) {
64 return bookmarks.Store.getInstance().data.nodes[bookmarkElement.itemId];
65 }
66
67 /** @constructor */
68 function DragData() {
69 /** @type {Array<number>} */
70 this.elements = null;
71
72 /** @type {boolean} */
73 this.sameProfile = false;
74 }
75
76 /**
77 * @constructor
78 */
79 function DragInfo() {
80 /** @type {DragData} */
81 this.dragData = null;
82 }
83
84 DragInfo.prototype = {
85 /** @param {DragData} newDragData */
86 handleChromeDragEnter: function(newDragData) {
87 this.dragData = newDragData;
88 },
89
90 clearDragData: function() {
91 this.dragData = null;
92 },
93
94 /** @return {boolean} */
95 isDragValid: function() {
96 return !!this.dragData;
97 },
98
99 /** @return {boolean} */
100 isSameProfile: function() {
101 return !!this.dragData && this.dragData.sameProfile;
102 },
103
104 /** @return {boolean} */
105 isDraggingFolders: function() {
106 return !!this.dragData && this.dragData.elements.some(function(node) {
107 return !node.url;
108 });
109 },
110
111 /** @return {boolean} */
112 isDraggingBookmark: function(bookmarkId) {
113 return !!this.dragData && this.dragData.elements.some(function(node) {
114 return node.id == bookmarkId;
115 });
116 },
117
118 /** @return {boolean} */
119 isDraggingChildBookmark: function(folderId) {
120 return !!this.dragData && this.dragData.elements.some(function(node) {
121 return node.parentId == folderId;
122 });
123 },
124
125 /** @return {boolean} */
126 isDraggingFolderToDescendant: function(itemId, nodes) {
127 var parentId = nodes[itemId].parentId;
128 var parents = {};
129 while (parentId) {
130 parents[parentId] = true;
131 parentId = nodes[parentId].parentId;
132 }
133
134 return !!this.dragData && this.dragData.elements.some(function(node) {
135 return parents[node.id];
136 });
137 },
138 };
139
140 /**
141 * @constructor
142 */
143 function DropIndicator() {
144 /**
145 * @type {number|null} Timer id used to help minimize flicker.
146 */
147 this.removeDropIndicatorTimer = null;
148
149 /**
150 * The element that had a style applied it to indicate the drop location.
151 * This is used to easily remove the style when necessary.
152 * @type {BookmarkElement|null}
153 */
154 this.lastIndicatorElement = null;
155
156 /**
157 * The style that was applied to indicate the drop location.
158 * @type {?string|null}
159 */
160 this.lastIndicatorClassName = null;
161 }
162
163 DropIndicator.prototype = {
164 /**
165 * Applies the drop indicator style on the target element and stores that
166 * information to easily remove the style in the future.
167 * @param {BookmarkElement} indicatorElement
168 * @param {DropPosition} position
169 */
170 addDropIndicatorStyle: function(indicatorElement, position) {
171 var indicatorStyleName = position == DropPosition.ABOVE ?
172 'drag-above' :
173 position == DropPosition.BELOW ? 'drag-below' : 'drag-on';
174
175 this.lastIndicatorElement = indicatorElement;
176 this.lastIndicatorClassName = indicatorStyleName;
177
178 indicatorElement.classList.add(indicatorStyleName);
179 },
180
181 /**
182 * Clears the drop indicator style from the last element was the drop target
183 * so the drop indicator is no longer for that element.
184 */
185 removeDropIndicatorStyle: function() {
186 if (!this.lastIndicatorElement || !this.lastIndicatorClassName)
187 return;
188 this.lastIndicatorElement.classList.remove(this.lastIndicatorClassName);
189 this.lastIndicatorElement = null;
190 this.lastIndicatorClassName = null;
191 },
192
193 /**
194 * Displays the drop indicator on the current drop target to give the
195 * user feedback on where the drop will occur.
196 * @param {DropDestination} dropDest
197 */
198 update: function(dropDest) {
199 window.clearTimeout(this.removeDropIndicatorTimer);
200
201 var indicatorElement = dropDest.element;
202 var position = dropDest.position;
203
204 this.removeDropIndicatorStyle();
205 this.addDropIndicatorStyle(indicatorElement, position);
206 },
207
208 /**
209 * Stop displaying the drop indicator.
210 */
211 finish: function() {
212 // The use of a timeout is in order to reduce flickering as we move
213 // between valid drop targets.
214 window.clearTimeout(this.removeDropIndicatorTimer);
215 this.removeDropIndicatorTimer = window.setTimeout(function() {
216 this.removeDropIndicatorStyle();
217 }.bind(this), 100);
218 }
219 };
220
221 Polymer({
222 is: 'bookmarks-dnd-manager',
223
224 /** @private {bookmarks.DragInfo} */
225 dragInfo_: null,
226
227 /** @private {?DropDestination} */
228 dropDestination_: null,
229
230 /** @private {bookmarks.DropIndicator} */
231 dropIndicator_: null,
232
233 /** @private {Object<string, function(!Event)>} */
234 documentListeners_: null,
235
236 /** @override */
237 attached: function() {
238 this.dragInfo_ = new DragInfo();
239 this.dropIndicator_ = new DropIndicator();
240
241 this.documentListeners_ = {
242 'dragstart': this.onDragStart_.bind(this),
243 'dragenter': this.onDragEnter_.bind(this),
244 'dragover': this.onDragOver_.bind(this),
245 'dragleave': this.onDragLeave_.bind(this),
246 'drop': this.onDrop_.bind(this),
247 'dragend': this.clearDragData_.bind(this),
248 'mouseup': this.clearDragData_.bind(this),
249 // TODO(calamity): Add touch support.
250 };
251 for (var event in this.documentListeners_)
252 document.addEventListener(event, this.documentListeners_[event]);
253
254 chrome.bookmarkManagerPrivate.onDragEnter.addListener(
255 this.dragInfo_.handleChromeDragEnter.bind(this.dragInfo_));
256 chrome.bookmarkManagerPrivate.onDragLeave.addListener(
257 this.clearDragData_.bind(this));
258 chrome.bookmarkManagerPrivate.onDrop.addListener(
259 this.clearDragData_.bind(this));
260 },
261
262 /** @override */
263 detached: function() {
264 for (var event in this.documentListeners_)
265 document.removeEventListener(event, this.documentListeners_[event]);
266 },
267
268 /** @private */
269 onDragLeave_: function() {
270 this.dropIndicator_.finish();
271 },
272
273 /**
274 * @private
275 * @param {!Event} e
276 */
277 onDrop_: function(e) {
278 var dropInfo = this.calculateDropInfo_(e, this.dropDestination_);
279 if (dropInfo) {
280 // TODO(calamity): cache drop action here and reselect items
281 // post-action.
282 if (dropInfo.index != -1)
283 chrome.bookmarkManagerPrivate.drop(dropInfo.parentId, dropInfo.index);
284 else
285 chrome.bookmarkManagerPrivate.drop(dropInfo.parentId);
286
287 e.preventDefault();
288
289 var action;
290 var dragTarget = getBookmarkElement(e.path);
291 if (isBookmarkItem(dragTarget)) {
292 action = 'BookmarkManager_DropToList';
293 if (this.dropDestination_.position == DropPosition.ON)
294 action = 'BookmarkManager_DropToListItem';
295 } else if (isBookmarkFolderNode(dragTarget)) {
296 action = 'BookmarkManager_DropToTree';
297 if (this.dropDestination_.position == DropPosition.ON)
298 action = 'BookmarkManager_DropToTreeItem';
299 }
300 if (action)
301 chrome.metricsPrivate.recordUserAction(action);
302 }
303 this.dropDestination_ = null;
304 this.dropIndicator_.finish();
305 },
306
307 /** @private */
308 clearDragData_: function() {
309 this.dragInfo_.clearDragData();
310 this.dropDestination_ = null;
311 },
312
313 /**
314 * @private
315 * @param {Event} e
316 */
317 onDragStart_: function(e) {
318 // Determine the selected bookmarks.
319 var state = bookmarks.Store.getInstance().data;
320 var selectedItems = Object.keys(state.selection.items);
321 var draggedNodes = selectedItems.length ?
322 selectedItems :
323 [getBookmarkElement(e.path).itemId];
324
325 e.preventDefault();
326
327 if (!draggedNodes.length)
328 return;
329
330 // If we are dragging a single link, we can do the *Link* effect.
331 // Otherwise, we only allow copy and move.
332 e.dataTransfer.effectAllowed =
333 draggedNodes.length == 1 && state.nodes[draggedNodes[0]].url ?
334 'copyMoveLink' :
335 'copyMove';
336
337 // TODO(calamity): account for touch.
338 chrome.bookmarkManagerPrivate.startDrag(draggedNodes, false);
339 var dragTarget = getBookmarkElement(e.path);
340 if (isBookmarkItem(dragTarget)) {
341 chrome.metricsPrivate.recordUserAction(
342 'BookmarkManager_StartDragFromList');
343 } else if (isBookmarkFolderNode(dragTarget)) {
344 chrome.metricsPrivate.recordUserAction(
345 'BookmarkManager_StartDragFromTree');
346 }
347
348 chrome.metricsPrivate.recordSmallCount(
349 'BookmarkManager.NumDragged', draggedNodes.length);
350 },
351
352 /**
353 * @private
354 * @param {Event} e
355 */
356 onDragEnter_: function(e) {
357 e.preventDefault();
358 },
359
360 /**
361 * @private
362 * @param {Event} e
363 */
364 onDragOver_: function(e) {
365 // Allow DND on text inputs.
366 if (e.path[0].tagName != 'INPUT') {
367 // The default operation is to allow dropping links etc to do
368 // navigation. We never want to do that for the bookmark manager.
369 e.preventDefault();
370
371 // Set to none. This will get set to something if we can do the drop.
372 e.dataTransfer.dropEffect = 'none';
373 }
374
375 if (!this.dragInfo_.isDragValid())
376 return;
377
378 var overElement = getBookmarkElement(e.path) if (!overElement) return;
379
380 // TODO(calamity): open folders on hover.
381
382 // Now we know that we can drop. Determine if we will drop above, on or
383 // below based on mouse position etc.
384 this.dropDestination_ =
385 this.calculateDropDestination_(e.clientY, overElement);
386 if (!this.dropDestination_) {
387 e.dataTransfer.dropEffect = 'none';
388 return;
389 }
390
391 e.dataTransfer.dropEffect =
392 this.dragInfo_.isSameProfile() ? 'move' : 'copy';
393 this.dropIndicator_.update(this.dropDestination_);
394 },
395
396 /**
397 * This function determines where the drop will occur.
398 * @param {number} elementClientY
399 * @param {BookmarkElement} overElement
400 * @return {?DropDestination} If no valid drop position is found, null,
401 * otherwise:
402 * element - The target element that will receive the drop.
403 * position - A |DropPosition| relative to the |element|.
404 */
405 calculateDropDestination_: function(elementClientY, overElement) {
406 var validDropPositions = this.calculateValidDropPositions(overElement);
407 if (validDropPositions == DropPosition.NONE)
408 return null;
409
410 var above = validDropPositions & DropPosition.ABOVE;
411 var below = validDropPositions & DropPosition.BELOW;
412 var on = validDropPositions & DropPosition.ON;
413 var rect = overElement.getBoundingClientRect();
414 var yRatio = (elementClientY - rect.top) / rect.height;
415
416 if (above && (yRatio <= .25 || yRatio <= .5 && (!below || !on)))
417 return {element: overElement, position: DropPosition.ABOVE};
418
419 if (below && (yRatio > .75 || yRatio > .5 && (!above || !on)))
420 return {element: overElement, position: DropPosition.BELOW};
421
422 if (on)
423 return {element: overElement, position: DropPosition.ON};
424
425 return null;
426 },
427
428 /**
429 * @param {!Event} e
430 * @param {?DropDestination} dropDestination
431 * @return {?{parentId: string, index: number}}
432 */
433 calculateDropInfo_: function(e, dropDestination) {
434 if (!dropDestination || !this.dragInfo_.isDragValid())
435 return null;
436
437 var dropPos = dropDestination.position;
438 var relatedNode = getBookmarkNode(dropDestination.element);
439 var dropInfoResult = {
440 parentId: '',
441 index: -1,
442 };
443
444 var parentId =
445 dropPos == DropPosition.ON ? relatedNode.id : relatedNode.parentId;
446 if (parentId)
447 dropInfoResult.parentId = parentId;
448
449 var relatedIndex = -1;
450
451 // Try to find the index in the data model so we don't have to always keep
452 // the index for the list items up to date.
453 var state = bookmarks.Store.getInstance().data;
454 var listItems = bookmarks.util.getDisplayedList(state);
455 var overElement = getBookmarkElement(e.path);
456 // TODO(calamity): Handle dropping to an empty bookmark list.
457 if (isBookmarkItem(overElement)) {
458 relatedIndex = listItems.indexOf(relatedNode.id);
459 } else {
460 relatedIndex =
461 state.nodes[relatedNode.parentId].children.indexOf(relatedNode.id);
462 }
463
464 if (dropPos == DropPosition.ABOVE)
465 dropInfoResult.index = relatedIndex;
466 else if (dropPos == DropPosition.BELOW)
467 dropInfoResult.index = relatedIndex + 1;
468
469 return dropInfoResult;
470 },
471
472 /**
473 * Determines the valid drop positions for the given target element.
474 * @param {!BookmarkElement} overElement The element that we are currently
475 * dragging over.
476 * @return {DropPosition} An bit field enumeration of valid drop locations.
477 */
478 calculateValidDropPositions: function(overElement) {
479 var dragInfo = this.dragInfo_;
480 var state = bookmarks.Store.getInstance().data;
481 if (!dragInfo.isDragValid())
482 return DropPosition.NONE;
483
484 // Drags aren't allowed onto the search result list.
485 if (isBookmarkItem(overElement) &&
486 bookmarks.util.isShowingSearch(state)) {
487 return DropPosition.NONE;
488 }
489
490 // Drags of a bookmark onto itself or of a folder into its children aren't
491 // allowed.
492 if (this.dragInfo_.isSameProfile() &&
493 (dragInfo.isDraggingBookmark(overElement.itemId) ||
494 dragInfo.isDraggingFolderToDescendant(
495 overElement.itemId, state.nodes))) {
496 return DropPosition.NONE;
497 }
498
499 var validDropPositions = this.calculateDropAboveBelow(overElement);
500 if (this.canDropOn(overElement))
501 validDropPositions |= DropPosition.ON;
502
503 return validDropPositions;
504 },
505
506 /**
507 * @param {BookmarkElement} overElement
508 * @return {DropPosition}
509 */
510 calculateDropAboveBelow: function(overElement) {
511 var dragInfo = this.dragInfo_;
512 var state = bookmarks.Store.getInstance().data;
513
514 // We cannot drop between Bookmarks bar and Other bookmarks.
515 if (getBookmarkNode(overElement).parentId == ROOT_NODE_ID)
516 return DropPosition.NONE;
517
518 var isOverFolderNode = isBookmarkFolderNode(overElement);
519 var isDraggingFolders = dragInfo.isDraggingFolders();
520
521 // We can only drop between items in the tree if we have any folders.
522 if (isOverFolderNode && !isDraggingFolders)
523 return DropPosition.NONE;
524
525 var resultPositions = DropPosition.NONE;
526
527 // Cannot drop above if the item above is already in the drag source.
528 var previousElem = overElement.previousElementSibling;
529 if (!dragInfo.isSameProfile() || !previousElem ||
530 !dragInfo.isDraggingBookmark(previousElem.itemId)) {
531 resultPositions |= DropPosition.ABOVE;
532 }
533
534 // Don't allow dropping below an expanded sidebar folder item since it is
535 // confusing to the user anyway.
536 if (isOverFolderNode && state.closedFolders[overElement.itemId])
537 return resultPositions;
538
539 // Cannot drop below if the item below is already in the drag source.
540 var nextElement = overElement.nextElementSibling;
541
542 // The template element sits past the end of the last bookmark item.
543 if (nextElement.tagName == 'TEMPLATE')
544 nextElement = null;
545
546 if (!dragInfo.isSameProfile() || !nextElement ||
547 !dragInfo.isDraggingBookmark(nextElement.itemId)) {
548 resultPositions |= DropPosition.BELOW;
549 }
550
551 return resultPositions;
552 },
553
554 /**
555 * Determine whether we can drop the dragged items on the drop target.
556 * @param {!BookmarkElement} overElement The element that we are currently
557 * dragging over.
558 * @return {boolean} Whether we can drop the dragged items on the drop
559 * target.
560 */
561 canDropOn: function(overElement) {
562 // We can only drop on a folder.
563 if (getBookmarkNode(overElement).url)
564 return false;
565
566 if (!this.dragInfo_.isSameProfile())
567 return true;
568
569 return !this.dragInfo_.isDraggingChildBookmark(overElement.itemId)
570 },
571 });
572
573 return {
574 DragInfo: DragInfo,
575 DropIndicator: DropIndicator,
576 };
577 });
OLDNEW
« no previous file with comments | « chrome/browser/resources/md_bookmarks/dnd_manager.html ('k') | chrome/browser/resources/md_bookmarks/folder_node.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698