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

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

Issue 2746363013: [MD Bookmarks] Add a drag and drop indicator to bookmarks. (Closed)
Patch Set: rebase 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 /**
6 * Enumeration of valid drop locations relative to an element. These are
7 * bit masks to allow combining multiple locations in a single value.
8 * @enum {number}
9 * @const
10 */
11 var DropPosition = {
12 NONE: 0,
13 ABOVE: 1,
14 ON: 2,
15 BELOW: 4,
16 };
17
18 /** @typedef {{element: BookmarkElement, position: DropPosition}} */
19 var DropDestination;
20
21 cr.define('bookmarks', function() {
22 /**
23 * @param {BookmarkElement} element
24 * @return {boolean}
25 */
26 function isBookmarkItem(element) {
27 return element.tagName == 'BOOKMARKS-ITEM';
28 }
29
30 /**
31 * @param {BookmarkElement} element
32 * @return {boolean}
33 */
34 function isBookmarkFolderNode(element) {
35 return element.tagName == 'BOOKMARKS-FOLDER-NODE';
36 }
37
38 /**
39 * @param {Array<!Element>|undefined} path
40 * @return {BookmarkElement}
41 */
42 function getBookmarkElement(path) {
43 if (!path)
44 return null;
45
46 for (var i = 0; i < path.length; i++) {
47 if (isBookmarkItem(path[i]) || isBookmarkFolderNode(path[i]))
48 return path[i];
49 }
50 return null;
51 }
52
53 /**
54 * @param {BookmarkElement} bookmarkElement
55 * @return {BookmarkNode}
56 */
57 function getBookmarkNode(bookmarkElement) {
58 return bookmarks.Store.getInstance().data.nodes[bookmarkElement.itemId];
59 }
60
61 /**
62 * Contains and provides utility methods for drag data sent by the
63 * bookmarkManagerPrivate API.
64 * @constructor
65 */
66 function DragInfo() {
67 /** @type {DragData} */
68 this.dragData = null;
69 }
70
71 DragInfo.prototype = {
72 /** @param {DragData} newDragData */
73 handleChromeDragEnter: function(newDragData) {
74 this.dragData = newDragData;
75 },
76
77 clearDragData: function() {
78 this.dragData = null;
79 },
80
81 /** @return {boolean} */
82 isDragValid: function() {
83 return !!this.dragData;
84 },
85
86 /** @return {boolean} */
87 isSameProfile: function() {
88 return !!this.dragData && this.dragData.sameProfile;
89 },
90
91 /** @return {boolean} */
92 isDraggingFolders: function() {
93 return !!this.dragData && this.dragData.elements.some(function(node) {
94 return !node.url;
95 });
96 },
97
98 /** @return {boolean} */
99 isDraggingBookmark: function(bookmarkId) {
100 return !!this.dragData && this.isSameProfile() &&
101 this.dragData.elements.some(function(node) {
102 return node.id == bookmarkId;
103 });
104 },
105
106 /** @return {boolean} */
107 isDraggingChildBookmark: function(folderId) {
108 return !!this.dragData && this.isSameProfile() &&
109 this.dragData.elements.some(function(node) {
110 return node.parentId == folderId;
111 });
112 },
113
114 /** @return {boolean} */
115 isDraggingFolderToDescendant: function(itemId, nodes) {
116 if (!this.isSameProfile())
117 return false;
118
119 var parentId = nodes[itemId].parentId;
120 var parents = {};
121 while (parentId) {
122 parents[parentId] = true;
123 parentId = nodes[parentId].parentId;
124 }
125
126 return !!this.dragData && this.dragData.elements.some(function(node) {
127 return parents[node.id];
128 });
129 },
130 };
131
132 /**
133 * Encapsulates the behavior of the drag and drop indicator which puts a line
134 * between items or highlights folders which are valid drop targets.
135 * @constructor
136 */
137 function DropIndicator() {
138 /**
139 * @type {number|null} Timer id used to help minimize flicker.
140 */
141 this.removeDropIndicatorTimer = null;
142
143 /**
144 * The element that had a style applied it to indicate the drop location.
145 * This is used to easily remove the style when necessary.
146 * @type {BookmarkElement|null}
147 */
148 this.lastIndicatorElement = null;
149
150 /**
151 * The style that was applied to indicate the drop location.
152 * @type {?string|null}
153 */
154 this.lastIndicatorClassName = null;
155
156 /**
157 * Used to instantly remove the indicator style in tests.
158 * @type {function((Function|null|string), number)}
tsergeant 2017/03/27 06:49:24 Nit: @private
calamity 2017/03/28 04:15:03 Done. Also privated the rest of the things here.
159 */
160 this.setTimeout_ = window.setTimeout.bind(window);
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 {HTMLElement} 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
tsergeant 2017/03/27 06:49:24 nit: this sentence doesn't quite grammar maybe ju
calamity 2017/03/28 04:15:03 Uhhhhh. Done.
183 * so the drop indicator is no longer for that element.
184 */
185 removeDropIndicatorStyle: function() {
186 if (!this.lastIndicatorElement || !this.lastIndicatorClassName)
187 return;
188
189 this.lastIndicatorElement.classList.remove(this.lastIndicatorClassName);
190 this.lastIndicatorElement = null;
191 this.lastIndicatorClassName = null;
192 },
193
194 /**
195 * Displays the drop indicator on the current drop target to give the
196 * user feedback on where the drop will occur.
197 * @param {DropDestination} dropDest
198 */
199 update: function(dropDest) {
200 window.clearTimeout(this.removeDropIndicatorTimer);
201
202 var indicatorElement = dropDest.element.getDropTarget();
203 var position = dropDest.position;
204
205 this.removeDropIndicatorStyle();
206 this.addDropIndicatorStyle(indicatorElement, position);
207 },
208
209 /**
210 * Stop displaying the drop indicator.
211 */
212 finish: function() {
213 // The use of a timeout is in order to reduce flickering as we move
214 // between valid drop targets.
215 window.clearTimeout(this.removeDropIndicatorTimer);
216 this.removeDropIndicatorTimer = this.setTimeout_(function() {
217 this.removeDropIndicatorStyle();
218 }.bind(this), 100);
219 },
220
221 disableTimeoutForTesting: function() {
222 this.setTimeout_ = function(fn, timeout) {
223 fn();
224 };
225 },
226 };
227
228 /**
229 * Manages drag and drop events for the bookmarks-app.
230 * @constructor
231 */
232 function DNDManager() {
233 /** @private {bookmarks.DragInfo} */
234 this.dragInfo_ = null;
235
236 /** @private {?DropDestination} */
237 this.dropDestination_ = null;
238
239 /** @private {bookmarks.DropIndicator} */
240 this.dropIndicator_ = null;
241
242 /** @private {Object<string, function(!Event)>} */
243 this.documentListeners_ = null;
244 }
245
246 DNDManager.prototype = {
247 init: function() {
248 this.dragInfo_ = new DragInfo();
249 this.dropIndicator_ = new DropIndicator();
250
251 this.documentListeners_ = {
252 'dragstart': this.onDragStart_.bind(this),
253 'dragenter': this.onDragEnter_.bind(this),
254 'dragover': this.onDragOver_.bind(this),
255 'dragleave': this.onDragLeave_.bind(this),
256 'drop': this.onDrop_.bind(this),
257 'dragend': this.clearDragData_.bind(this),
258 'mouseup': this.clearDragData_.bind(this),
259 // TODO(calamity): Add touch support.
260 };
261 for (var event in this.documentListeners_)
262 document.addEventListener(event, this.documentListeners_[event]);
263
264 chrome.bookmarkManagerPrivate.onDragEnter.addListener(
265 this.dragInfo_.handleChromeDragEnter.bind(this.dragInfo_));
266 chrome.bookmarkManagerPrivate.onDragLeave.addListener(
267 this.clearDragData_.bind(this));
268 chrome.bookmarkManagerPrivate.onDrop.addListener(
269 this.clearDragData_.bind(this));
270 },
271
272 destroy: function() {
273 for (var event in this.documentListeners_)
274 document.removeEventListener(event, this.documentListeners_[event]);
275 },
276
277 /** @private */
278 onDragLeave_: function() {
279 this.dropIndicator_.finish();
280 },
281
282 /**
283 * @private
284 * @param {!Event} e
285 */
286 onDrop_: function(e) {
287 if (this.dropDestination_) {
tsergeant 2017/03/27 06:49:23 Nit: {}
calamity 2017/03/28 04:15:03 Done.
288 e.preventDefault();
289 }
290 this.dropDestination_ = null;
291 this.dropIndicator_.finish();
292 },
293
294 /** @private */
295 clearDragData_: function() {
296 this.dragInfo_.clearDragData();
297 this.dropDestination_ = null;
298 this.dropIndicator_.finish();
299 },
300
301 /**
302 * @private
303 * @param {Event} e
304 */
305 onDragStart_: function(e) {
306 var dragElement = getBookmarkElement(e.path);
307 if (!dragElement)
308 return;
309
310 // Determine the selected bookmarks.
311 var state = bookmarks.Store.getInstance().data;
312 var draggedNodes = Object.keys(state.selection.items);
tsergeant 2017/03/27 06:49:24 Hrmm, at the moment we don't really guarantee from
calamity 2017/03/28 04:15:03 Done. Remind me why we didn't use a Set again?
tsergeant 2017/03/28 23:52:22 Yeah, we could probably give it a go with a Set.
313
314 if (isBookmarkFolderNode(dragElement) ||
315 draggedNodes.indexOf(dragElement.itemId) == -1) {
316 // TODO(calamity): clear current selection.
317 draggedNodes = [dragElement.itemId];
318 }
319
320 e.preventDefault();
321
322 // If we are dragging a single link, we can do the *Link* effect.
323 // Otherwise, we only allow copy and move.
324 if (e.dataTransfer) {
325 e.dataTransfer.effectAllowed =
326 draggedNodes.length == 1 && state.nodes[draggedNodes[0]].url ?
327 'copyLink' :
328 'copyMove';
329 }
330
331 // TODO(calamity): account for touch.
332 chrome.bookmarkManagerPrivate.startDrag(draggedNodes, false);
333 },
334
335 /**
336 * @private
337 * @param {Event} e
338 */
339 onDragEnter_: function(e) {
340 e.preventDefault();
341 },
342
343 /**
344 * @private
345 * @param {Event} e
346 */
347 onDragOver_: function(e) {
348 this.dropDestination_ = null;
349
350 // This is necessary to actually trigger the 'none' effect, even though
351 // the event will have this set to 'none' already.
tsergeant 2017/03/27 06:49:24 😕
calamity 2017/03/28 04:15:03 Mate, you have no idea.
352 if (e.dataTransfer)
353 e.dataTransfer.dropEffect = 'none';
354
355 // Allow normal DND on text inputs.
356 if (e.path[0].tagName == 'INPUT')
357 return;
358
359 // The default operation is to allow dropping links etc to do
360 // navigation. We never want to do that for the bookmark manager.
361 e.preventDefault();
362
363 if (!this.dragInfo_.isDragValid())
364 return;
365
366 var overElement = getBookmarkElement(e.path);
367 if (!overElement)
368 return;
369
370 // TODO(calamity): open folders on hover.
371
372 // Now we know that we can drop. Determine if we will drop above, on or
373 // below based on mouse position etc.
374 this.dropDestination_ =
375 this.calculateDropDestination_(e.clientY, overElement);
376 if (!this.dropDestination_)
377 return;
378
379 if (e.dataTransfer) {
380 e.dataTransfer.dropEffect =
381 this.dragInfo_.isSameProfile() ? 'move' : 'copy';
382 }
383
384 this.dropIndicator_.update(this.dropDestination_);
385 },
386
387 /**
388 * This function determines where the drop will occur.
389 * @private
390 * @param {number} elementClientY
391 * @param {!BookmarkElement} overElement
392 * @return {?DropDestination} If no valid drop position is found, null,
393 * otherwise:
394 * element - The target element that will receive the drop.
395 * position - A |DropPosition| relative to the |element|.
396 */
397 calculateDropDestination_: function(elementClientY, overElement) {
398 var validDropPositions = this.calculateValidDropPositions_(overElement);
399 if (validDropPositions == DropPosition.NONE)
400 return null;
401
402 var above = validDropPositions & DropPosition.ABOVE;
403 var below = validDropPositions & DropPosition.BELOW;
404 var on = validDropPositions & DropPosition.ON;
405 var rect = overElement.getDropTarget().getBoundingClientRect();
406 var yRatio = (elementClientY - rect.top) / rect.height;
407
408 if (above && (yRatio <= .25 || yRatio <= .5 && (!below || !on)))
409 return {element: overElement, position: DropPosition.ABOVE};
410
411 if (below && (yRatio > .75 || yRatio > .5 && (!above || !on)))
412 return {element: overElement, position: DropPosition.BELOW};
413
414 if (on)
415 return {element: overElement, position: DropPosition.ON};
416
417 return null;
418 },
419
420 /**
421 * Determines the valid drop positions for the given target element.
422 * @private
423 * @param {!BookmarkElement} overElement The element that we are currently
424 * dragging over.
425 * @return {DropPosition} An bit field enumeration of valid drop locations.
426 */
427 calculateValidDropPositions_: function(overElement) {
428 var dragInfo = this.dragInfo_;
429 var state = bookmarks.Store.getInstance().data;
430
431 // Drags aren't allowed onto the search result list.
432 if (isBookmarkItem(overElement) &&
433 bookmarks.util.isShowingSearch(state)) {
434 return DropPosition.NONE;
435 }
436
437 // Drags of a bookmark onto itself or of a folder into its children aren't
438 // allowed.
439 if (dragInfo.isDraggingBookmark(overElement.itemId) ||
440 dragInfo.isDraggingFolderToDescendant(
441 overElement.itemId, state.nodes)) {
442 return DropPosition.NONE;
443 }
444
445 var validDropPositions = this.calculateDropAboveBelow_(overElement);
446 if (this.canDropOn_(overElement))
447 validDropPositions |= DropPosition.ON;
448
449 return validDropPositions;
450 },
451
452 /**
453 * @private
454 * @param {BookmarkElement} overElement
455 * @return {DropPosition}
456 */
457 calculateDropAboveBelow_: function(overElement) {
458 var dragInfo = this.dragInfo_;
459 var state = bookmarks.Store.getInstance().data;
460
461 // We cannot drop between Bookmarks bar and Other bookmarks.
462 if (getBookmarkNode(overElement).parentId == bookmarks.util.ROOT_NODE_ID)
463 return DropPosition.NONE;
464
465 var isOverFolderNode = isBookmarkFolderNode(overElement);
466
467 // We can only drop between items in the tree if we have any folders.
468 if (isOverFolderNode && !dragInfo.isDraggingFolders())
469 return DropPosition.NONE;
470
471 var validDropPositions = DropPosition.NONE;
472
473 // Cannot drop above if the item above is already in the drag source.
474 var previousElem = overElement.previousElementSibling;
475 if (!previousElem || !dragInfo.isDraggingBookmark(previousElem.itemId)) {
tsergeant 2017/03/27 06:49:24 Nit: {}
calamity 2017/03/28 04:15:03 Done.
476 validDropPositions |= DropPosition.ABOVE;
477 }
478
479 // Don't allow dropping below an expanded sidebar folder item since it is
480 // confusing to the user anyway.
481 if (isOverFolderNode && !state.closedFolders[overElement.itemId] &&
482 bookmarks.util.hasChildFolders(overElement.itemId, state.nodes)) {
483 return validDropPositions;
484 }
485
486 // Cannot drop below if the item below is already in the drag source.
tsergeant 2017/03/27 06:49:23 Does this comment belong on the if statement on li
calamity 2017/03/28 04:15:03 Ugh, I never know what to do with comments like th
487 var nextElement = overElement.nextElementSibling;
488
489 // The template element sits past the end of the last bookmark element.
490 if (!isBookmarkItem(nextElement) && !isBookmarkFolderNode(nextElement))
491 nextElement = null;
492
493 if (!nextElement || !dragInfo.isDraggingBookmark(nextElement.itemId)) {
tsergeant 2017/03/27 06:49:23 {} here too
calamity 2017/03/28 04:15:03 Done.
494 validDropPositions |= DropPosition.BELOW;
495 }
496
497 return validDropPositions;
498 },
499
500 /**
501 * Determine whether we can drop the dragged items on the drop target.
502 * @private
503 * @param {!BookmarkElement} overElement The element that we are currently
504 * dragging over.
505 * @return {boolean} Whether we can drop the dragged items on the drop
506 * target.
507 */
508 canDropOn_: function(overElement) {
509 // We can only drop on a folder.
510 if (getBookmarkNode(overElement).url)
511 return false;
512
513 return !this.dragInfo_.isDraggingChildBookmark(overElement.itemId)
514 },
515 };
516
517 return {
518 DNDManager: DNDManager,
519 DragInfo: DragInfo,
520 DropIndicator: DropIndicator,
521 };
522 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698