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

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: address comments, add test 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.isSameProfile() &&
94 this.dragData.elements.some(function(node) {
95 return !node.url;
96 });
97 },
98
99 /** @return {boolean} */
100 isDraggingBookmark: function(bookmarkId) {
101 return !!this.dragData && this.isSameProfile() &&
102 this.dragData.elements.some(function(node) {
103 return node.id == bookmarkId;
104 });
105 },
106
107 /** @return {boolean} */
108 isDraggingChildBookmark: function(folderId) {
109 return !!this.dragData && this.isSameProfile() &&
110 this.dragData.elements.some(function(node) {
111 return node.parentId == folderId;
112 });
113 },
114
115 /** @return {boolean} */
116 isDraggingFolderToDescendant: function(itemId, nodes) {
117 if (!this.isSameProfile())
118 return false;
119
120 var parentId = nodes[itemId].parentId;
121 var parents = {};
122 while (parentId) {
123 parents[parentId] = true;
124 parentId = nodes[parentId].parentId;
125 }
126
127 return !!this.dragData && this.dragData.elements.some(function(node) {
128 return parents[node.id];
129 });
130 },
131 };
132
133 /**
134 * Encapsulates the behavior of the drag and drop indicator which puts a line
135 * between items or highlights folders which are valid drop targets.
136 * @constructor
137 */
138 function DropIndicator() {
139 /**
140 * @type {number|null} Timer id used to help minimize flicker.
141 */
142 this.removeDropIndicatorTimer = null;
143
144 /**
145 * The element that had a style applied it to indicate the drop location.
146 * This is used to easily remove the style when necessary.
147 * @type {BookmarkElement|null}
148 */
149 this.lastIndicatorElement = null;
150
151 /**
152 * The style that was applied to indicate the drop location.
153 * @type {?string|null}
154 */
155 this.lastIndicatorClassName = null;
156
157 /**
158 * Used to instantly remove the indicator style in tests.
159 * @type {boolean}
160 */
161 this.synchronous_ = false;
tsergeant 2017/03/23 00:28:23 Hmmm, an alternative way to do this would be this
calamity 2017/03/23 06:12:33 Done.
162 }
163
164 DropIndicator.prototype = {
165 /**
166 * Applies the drop indicator style on the target element and stores that
167 * information to easily remove the style in the future.
168 * @param {HTMLElement} indicatorElement
169 * @param {DropPosition} position
170 */
171 addDropIndicatorStyle: function(indicatorElement, position) {
172 var indicatorStyleName = position == DropPosition.ABOVE ?
173 'drag-above' :
174 position == DropPosition.BELOW ? 'drag-below' : 'drag-on';
175
176 this.lastIndicatorElement = indicatorElement;
177 this.lastIndicatorClassName = indicatorStyleName;
178
179 indicatorElement.classList.add(indicatorStyleName);
180 },
181
182 /**
183 * Clears the drop indicator style from the last element was the drop target
184 * so the drop indicator is no longer for that element.
185 */
186 removeDropIndicatorStyle: function() {
187 if (!this.lastIndicatorElement || !this.lastIndicatorClassName)
188 return;
189
190 this.lastIndicatorElement.classList.remove(this.lastIndicatorClassName);
191 this.lastIndicatorElement = null;
192 this.lastIndicatorClassName = null;
193 },
194
195 /**
196 * Displays the drop indicator on the current drop target to give the
197 * user feedback on where the drop will occur.
198 * @param {DropDestination} dropDest
199 */
200 update: function(dropDest) {
201 window.clearTimeout(this.removeDropIndicatorTimer);
202
203 var indicatorElement = dropDest.element.getDropTarget();
204 var position = dropDest.position;
205
206 this.removeDropIndicatorStyle();
207 this.addDropIndicatorStyle(indicatorElement, position);
208 },
209
210 /**
211 * Stop displaying the drop indicator.
212 */
213 finish: function() {
214 if (this.synchronous_) {
215 this.removeDropIndicatorStyle();
216 return;
217 }
218
219 // The use of a timeout is in order to reduce flickering as we move
220 // between valid drop targets.
221 window.clearTimeout(this.removeDropIndicatorTimer);
222 this.removeDropIndicatorTimer = window.setTimeout(function() {
223 this.removeDropIndicatorStyle();
224 }.bind(this), 100);
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_) {
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);
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 // Allow normal DND on text inputs.
349 if (e.path[0].tagName == 'INPUT')
350 return;
351
352 // The default operation is to allow dropping links etc to do
353 // navigation. We never want to do that for the bookmark manager.
354 e.preventDefault();
355
356 if (!this.dragInfo_.isDragValid())
357 return;
358
359 var overElement = getBookmarkElement(e.path);
360 if (!overElement)
361 return;
362
363 // TODO(calamity): open folders on hover.
364
365 // Now we know that we can drop. Determine if we will drop above, on or
366 // below based on mouse position etc.
367 this.dropDestination_ =
368 this.calculateDropDestination_(e.clientY, overElement);
369 if (!this.dropDestination_)
370 return;
371
372 if (e.dataTransfer) {
373 e.dataTransfer.dropEffect =
374 this.dragInfo_.isSameProfile() ? 'move' : 'copy';
375 }
376 this.dropIndicator_.update(this.dropDestination_);
377 },
378
379 /**
380 * This function determines where the drop will occur.
381 * @param {number} elementClientY
382 * @param {!BookmarkElement} overElement
383 * @return {?DropDestination} If no valid drop position is found, null,
384 * otherwise:
385 * element - The target element that will receive the drop.
386 * position - A |DropPosition| relative to the |element|.
tsergeant 2017/03/23 00:28:23 This and the methods below are all missing @privat
calamity 2017/03/23 06:12:33 Done.
387 */
388 calculateDropDestination_: function(elementClientY, overElement) {
389 var validDropPositions = this.calculateValidDropPositions_(overElement);
390 if (validDropPositions == DropPosition.NONE)
391 return null;
392
393 var above = validDropPositions & DropPosition.ABOVE;
394 var below = validDropPositions & DropPosition.BELOW;
395 var on = validDropPositions & DropPosition.ON;
396 var rect = overElement.getDropTarget().getBoundingClientRect();
397 var yRatio = (elementClientY - rect.top) / rect.height;
398
399 if (above && (yRatio <= .25 || yRatio <= .5 && (!below || !on)))
400 return {element: overElement, position: DropPosition.ABOVE};
401
402 if (below && (yRatio > .75 || yRatio > .5 && (!above || !on)))
403 return {element: overElement, position: DropPosition.BELOW};
404
405 if (on)
406 return {element: overElement, position: DropPosition.ON};
407
408 return null;
409 },
410
411 /**
412 * Determines the valid drop positions for the given target element.
413 * @param {!BookmarkElement} overElement The element that we are currently
414 * dragging over.
415 * @return {DropPosition} An bit field enumeration of valid drop locations.
416 */
417 calculateValidDropPositions_: function(overElement) {
418 var dragInfo = this.dragInfo_;
419 var state = bookmarks.Store.getInstance().data;
420
421 // Drags aren't allowed onto the search result list.
422 if (isBookmarkItem(overElement) &&
423 bookmarks.util.isShowingSearch(state)) {
424 return DropPosition.NONE;
425 }
426
427 // Drags of a bookmark onto itself or of a folder into its children aren't
428 // allowed.
429 if (dragInfo.isDraggingBookmark(overElement.itemId) ||
430 dragInfo.isDraggingFolderToDescendant(
431 overElement.itemId, state.nodes)) {
432 return DropPosition.NONE;
433 }
434
435 var validDropPositions = this.calculateDropAboveBelow_(overElement);
436 if (this.canDropOn_(overElement))
437 validDropPositions |= DropPosition.ON;
438
439 return validDropPositions;
440 },
441
442 /**
443 * @param {BookmarkElement} overElement
444 * @return {DropPosition}
445 */
446 calculateDropAboveBelow_: function(overElement) {
447 var dragInfo = this.dragInfo_;
448 var state = bookmarks.Store.getInstance().data;
449
450 // We cannot drop between Bookmarks bar and Other bookmarks.
451 if (getBookmarkNode(overElement).parentId == bookmarks.util.ROOT_NODE_ID)
452 return DropPosition.NONE;
453
454 var isOverFolderNode = isBookmarkFolderNode(overElement);
455
456 // We can only drop between items in the tree if we have any folders.
457 if (isOverFolderNode && !dragInfo.isDraggingFolders())
458 return DropPosition.NONE;
459
460 var validDropPositions = DropPosition.NONE;
461
462 // Cannot drop above if the item above is already in the drag source.
463 var previousElem = overElement.previousElementSibling;
464 if (!previousElem || !dragInfo.isDraggingBookmark(previousElem.itemId)) {
465 validDropPositions |= DropPosition.ABOVE;
466 }
467
468 // Don't allow dropping below an expanded sidebar folder item since it is
469 // confusing to the user anyway.
470 if (isOverFolderNode && !state.closedFolders[overElement.itemId])
471 return validDropPositions;
472
473 // Cannot drop below if the item below is already in the drag source.
474 var nextElement = overElement.nextElementSibling;
475
476 // The template element sits past the end of the last bookmark element.
477 if (!isBookmarkItem(nextElement) && !isBookmarkFolderNode(nextElement))
478 nextElement = null;
479
480 if (!nextElement || !dragInfo.isDraggingBookmark(nextElement.itemId)) {
481 validDropPositions |= DropPosition.BELOW;
482 }
483
484 return validDropPositions;
485 },
486
487 /**
488 * Determine whether we can drop the dragged items on the drop target.
489 * @param {!BookmarkElement} overElement The element that we are currently
490 * dragging over.
491 * @return {boolean} Whether we can drop the dragged items on the drop
492 * target.
493 */
494 canDropOn_: function(overElement) {
495 // We can only drop on a folder.
496 if (getBookmarkNode(overElement).url)
497 return false;
498
499 return !this.dragInfo_.isDraggingChildBookmark(overElement.itemId)
500 },
501 };
502
503 return {
504 DNDManager: DNDManager,
505 DragInfo: DragInfo,
506 DropIndicator: DropIndicator,
507 };
508 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698