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

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: fix nit Created 3 years, 8 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 * @private {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 * @private {BookmarkElement|null}
147 */
148 this.lastIndicatorElement_ = null;
149
150 /**
151 * The style that was applied to indicate the drop location.
152 * @private {?string|null}
153 */
154 this.lastIndicatorClassName_ = null;
155
156 /**
157 * Used to instantly remove the indicator style in tests.
158 * @private {function((Function|null|string), number)}
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 drop target.
183 */
184 removeDropIndicatorStyle: function() {
185 if (!this.lastIndicatorElement_ || !this.lastIndicatorClassName_)
186 return;
187
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.getDropTarget();
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_ = this.setTimeout_(function() {
216 this.removeDropIndicatorStyle();
217 }.bind(this), 100);
218 },
219
220 disableTimeoutForTesting: function() {
221 this.setTimeout_ = function(fn, timeout) {
222 fn();
223 };
224 },
225 };
226
227 /**
228 * Manages drag and drop events for the bookmarks-app.
229 * @constructor
230 */
231 function DNDManager() {
232 /** @private {bookmarks.DragInfo} */
233 this.dragInfo_ = null;
234
235 /** @private {?DropDestination} */
236 this.dropDestination_ = null;
237
238 /** @private {bookmarks.DropIndicator} */
239 this.dropIndicator_ = null;
240
241 /** @private {Object<string, function(!Event)>} */
242 this.documentListeners_ = null;
243 }
244
245 DNDManager.prototype = {
246 init: function() {
247 this.dragInfo_ = new DragInfo();
248 this.dropIndicator_ = new DropIndicator();
249
250 this.documentListeners_ = {
251 'dragstart': this.onDragStart_.bind(this),
252 'dragenter': this.onDragEnter_.bind(this),
253 'dragover': this.onDragOver_.bind(this),
254 'dragleave': this.onDragLeave_.bind(this),
255 'drop': this.onDrop_.bind(this),
256 'dragend': this.clearDragData_.bind(this),
257 'mouseup': this.clearDragData_.bind(this),
258 // TODO(calamity): Add touch support.
259 };
260 for (var event in this.documentListeners_)
261 document.addEventListener(event, this.documentListeners_[event]);
262
263 chrome.bookmarkManagerPrivate.onDragEnter.addListener(
264 this.dragInfo_.handleChromeDragEnter.bind(this.dragInfo_));
265 chrome.bookmarkManagerPrivate.onDragLeave.addListener(
266 this.clearDragData_.bind(this));
267 chrome.bookmarkManagerPrivate.onDrop.addListener(
268 this.clearDragData_.bind(this));
269 },
270
271 destroy: function() {
272 for (var event in this.documentListeners_)
273 document.removeEventListener(event, this.documentListeners_[event]);
274 },
275
276 /** @private */
277 onDragLeave_: function() {
278 this.dropIndicator_.finish();
279 },
280
281 /**
282 * @private
283 * @param {!Event} e
284 */
285 onDrop_: function(e) {
286 if (this.dropDestination_)
287 e.preventDefault();
288
289 this.dropDestination_ = null;
290 this.dropIndicator_.finish();
291 },
292
293 /** @private */
294 clearDragData_: function() {
295 this.dragInfo_.clearDragData();
296 this.dropDestination_ = null;
297 this.dropIndicator_.finish();
298 },
299
300 /**
301 * @private
302 * @param {Event} e
303 */
304 onDragStart_: function(e) {
305 var dragElement = getBookmarkElement(e.path);
306 if (!dragElement)
307 return;
308
309 // Determine the selected bookmarks.
310 var state = bookmarks.Store.getInstance().data;
311 var draggedNodes = Object.keys(state.selection.items);
312
313 if (isBookmarkFolderNode(dragElement) ||
314 draggedNodes.indexOf(dragElement.itemId) == -1) {
315 // TODO(calamity): clear current selection.
316 draggedNodes = [dragElement.itemId];
317 }
318
319 e.preventDefault();
320
321 // If we are dragging a single link, we can do the *Link* effect.
322 // Otherwise, we only allow copy and move.
323 if (e.dataTransfer) {
324 e.dataTransfer.effectAllowed =
325 draggedNodes.length == 1 && state.nodes[draggedNodes[0]].url ?
326 'copyLink' :
327 'copyMove';
328 }
329
330 // TODO(calamity): account for touch.
331 chrome.bookmarkManagerPrivate.startDrag(draggedNodes, false);
332 },
333
334 /**
335 * @private
336 * @param {Event} e
337 */
338 onDragEnter_: function(e) {
339 e.preventDefault();
340 },
341
342 /**
343 * @private
344 * @param {Event} e
345 */
346 onDragOver_: function(e) {
347 this.dropDestination_ = null;
348
349 // This is necessary to actually trigger the 'none' effect, even though
350 // the event will have this set to 'none' already.
351 if (e.dataTransfer)
352 e.dataTransfer.dropEffect = 'none';
353
354 // Allow normal DND on text inputs.
355 if (e.path[0].tagName == 'INPUT')
356 return;
357
358 // The default operation is to allow dropping links etc to do
359 // navigation. We never want to do that for the bookmark manager.
360 e.preventDefault();
361
362 if (!this.dragInfo_.isDragValid())
363 return;
364
365 var overElement = getBookmarkElement(e.path);
366 if (!overElement)
367 return;
368
369 // TODO(calamity): open folders on hover.
370
371 // Now we know that we can drop. Determine if we will drop above, on or
372 // below based on mouse position etc.
373 this.dropDestination_ =
374 this.calculateDropDestination_(e.clientY, overElement);
375 if (!this.dropDestination_)
376 return;
377
378 if (e.dataTransfer) {
379 e.dataTransfer.dropEffect =
380 this.dragInfo_.isSameProfile() ? 'move' : 'copy';
381 }
382
383 this.dropIndicator_.update(this.dropDestination_);
384 },
385
386 /**
387 * This function determines where the drop will occur.
388 * @private
389 * @param {number} elementClientY
390 * @param {!BookmarkElement} overElement
391 * @return {?DropDestination} If no valid drop position is found, null,
392 * otherwise:
393 * element - The target element that will receive the drop.
394 * position - A |DropPosition| relative to the |element|.
395 */
396 calculateDropDestination_: function(elementClientY, overElement) {
397 var validDropPositions = this.calculateValidDropPositions_(overElement);
398 if (validDropPositions == DropPosition.NONE)
399 return null;
400
401 var above = validDropPositions & DropPosition.ABOVE;
402 var below = validDropPositions & DropPosition.BELOW;
403 var on = validDropPositions & DropPosition.ON;
404 var rect = overElement.getDropTarget().getBoundingClientRect();
405 var yRatio = (elementClientY - rect.top) / rect.height;
406
407 if (above && (yRatio <= .25 || yRatio <= .5 && (!below || !on)))
408 return {element: overElement, position: DropPosition.ABOVE};
409
410 if (below && (yRatio > .75 || yRatio > .5 && (!above || !on)))
411 return {element: overElement, position: DropPosition.BELOW};
412
413 if (on)
414 return {element: overElement, position: DropPosition.ON};
415
416 return null;
417 },
418
419 /**
420 * Determines the valid drop positions for the given target element.
421 * @private
422 * @param {!BookmarkElement} overElement The element that we are currently
423 * dragging over.
424 * @return {DropPosition} An bit field enumeration of valid drop locations.
425 */
426 calculateValidDropPositions_: function(overElement) {
427 var dragInfo = this.dragInfo_;
428 var state = bookmarks.Store.getInstance().data;
429
430 // Drags aren't allowed onto the search result list.
431 if (isBookmarkItem(overElement) &&
432 bookmarks.util.isShowingSearch(state)) {
433 return DropPosition.NONE;
434 }
435
436 // Drags of a bookmark onto itself or of a folder into its children aren't
437 // allowed.
438 if (dragInfo.isDraggingBookmark(overElement.itemId) ||
439 dragInfo.isDraggingFolderToDescendant(
440 overElement.itemId, state.nodes)) {
441 return DropPosition.NONE;
442 }
443
444 var validDropPositions = this.calculateDropAboveBelow_(overElement);
445 if (this.canDropOn_(overElement))
446 validDropPositions |= DropPosition.ON;
447
448 return validDropPositions;
449 },
450
451 /**
452 * @private
453 * @param {BookmarkElement} overElement
454 * @return {DropPosition}
455 */
456 calculateDropAboveBelow_: function(overElement) {
457 var dragInfo = this.dragInfo_;
458 var state = bookmarks.Store.getInstance().data;
459
460 // We cannot drop between Bookmarks bar and Other bookmarks.
461 if (getBookmarkNode(overElement).parentId == bookmarks.util.ROOT_NODE_ID)
462 return DropPosition.NONE;
463
464 var isOverFolderNode = isBookmarkFolderNode(overElement);
465
466 // We can only drop between items in the tree if we have any folders.
467 if (isOverFolderNode && !dragInfo.isDraggingFolders())
468 return DropPosition.NONE;
469
470 var validDropPositions = DropPosition.NONE;
471
472 // Cannot drop above if the item above is already in the drag source.
473 var previousElem = overElement.previousElementSibling;
474 if (!previousElem || !dragInfo.isDraggingBookmark(previousElem.itemId))
475 validDropPositions |= DropPosition.ABOVE;
476
477 // Don't allow dropping below an expanded sidebar folder item since it is
478 // confusing to the user anyway.
479 if (isOverFolderNode && !state.closedFolders[overElement.itemId] &&
480 bookmarks.util.hasChildFolders(overElement.itemId, state.nodes)) {
481 return validDropPositions;
482 }
483
484 var nextElement = overElement.nextElementSibling;
485
486 // The template element sits past the end of the last bookmark element.
487 if (!isBookmarkItem(nextElement) && !isBookmarkFolderNode(nextElement))
488 nextElement = null;
489
490 // Cannot drop below if the item below is already in the drag source.
491 if (!nextElement || !dragInfo.isDraggingBookmark(nextElement.itemId))
492 validDropPositions |= DropPosition.BELOW;
493
494 return validDropPositions;
495 },
496
497 /**
498 * Determine whether we can drop the dragged items on the drop target.
499 * @private
500 * @param {!BookmarkElement} overElement The element that we are currently
501 * dragging over.
502 * @return {boolean} Whether we can drop the dragged items on the drop
503 * target.
504 */
505 canDropOn_: function(overElement) {
506 // We can only drop on a folder.
507 if (getBookmarkNode(overElement).url)
508 return false;
509
510 return !this.dragInfo_.isDraggingChildBookmark(overElement.itemId)
511 },
512 };
513
514 return {
515 DNDManager: DNDManager,
516 DragInfo: DragInfo,
517 DropIndicator: DropIndicator,
518 };
519 });
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