blob: fafb25a77c30ac35c5597ebbb9e2aace7d28798b [file] [log] [blame]
John Leec5bf0f22020-02-05 21:01:131// Copyright 2020 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
5import './strings.m.js';
6
7import {assert} from 'chrome://resources/js/assert.m.js';
8import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
9
10import {isTabElement, TabElement} from './tab.js';
11import {isTabGroupElement, TabGroupElement} from './tab_group.js';
John Lee49ccb722020-02-26 03:30:0512import {TabData, TabNetworkState, TabsApiProxy} from './tabs_api_proxy.js';
13
14/** @const {number} */
15export const PLACEHOLDER_TAB_ID = -1;
16
John Lee7b0b7a62020-03-03 06:00:5017/** @const {string} */
18export const PLACEHOLDER_GROUP_ID = 'placeholder';
19
John Lee49ccb722020-02-26 03:30:0520/**
21 * The data type key for pinned state of a tab. Since drag events only expose
22 * whether or not a data type exists (not the actual value), presence of this
23 * data type means that the tab is pinned.
24 * @const {string}
25 */
26const PINNED_DATA_TYPE = 'pinned';
John Leec5bf0f22020-02-05 21:01:1327
28/**
29 * Gets the data type of tab IDs on DataTransfer objects in drag events. This
30 * is a function so that loadTimeData can get overridden by tests.
31 * @return {string}
32 */
33function getTabIdDataType() {
34 return loadTimeData.getString('tabIdDataType');
35}
36
37/** @return {string} */
38function getGroupIdDataType() {
39 return loadTimeData.getString('tabGroupIdDataType');
40}
41
John Lee49ccb722020-02-26 03:30:0542/** @return {!TabData} */
43function getDefaultTabData() {
44 return {
45 active: false,
46 alertStates: [],
47 blocked: false,
48 crashed: false,
49 id: -1,
50 index: -1,
51 isDefaultFavicon: false,
52 networkState: TabNetworkState.NONE,
53 pinned: false,
54 shouldHideThrobber: false,
55 showIcon: true,
56 title: '',
57 url: '',
58 };
59}
60
John Leec5bf0f22020-02-05 21:01:1361/**
62 * @interface
63 */
64export class DragManagerDelegate {
65 /**
66 * @param {!TabElement} tabElement
67 * @return {number}
68 */
69 getIndexOfTab(tabElement) {}
70
John Leeaa89aa72020-02-21 01:53:2971 /**
72 * @param {!TabElement} element
73 * @param {number} index
74 * @param {boolean} pinned
John Lee49ccb722020-02-26 03:30:0575 * @param {string=} groupId
John Leeaa89aa72020-02-21 01:53:2976 */
77 placeTabElement(element, index, pinned, groupId) {}
78
79 /**
80 * @param {!TabGroupElement} element
81 * @param {number} index
82 */
83 placeTabGroupElement(element, index) {}
John Leec5bf0f22020-02-05 21:01:1384}
85
86/** @typedef {!DragManagerDelegate|!HTMLElement} */
87let DragManagerDelegateElement;
88
John Leeaa89aa72020-02-21 01:53:2989class DragSession {
90 /**
91 * @param {!DragManagerDelegateElement} delegate
92 * @param {!TabElement|!TabGroupElement} element
93 * @param {number} srcIndex
94 * @param {string=} srcGroup
95 */
96 constructor(delegate, element, srcIndex, srcGroup) {
97 /** @const @private {!DragManagerDelegateElement} */
98 this.delegate_ = delegate;
99
100 /** @const {!TabElement|!TabGroupElement} */
101 this.element_ = element;
102
103 /** @const {number} */
104 this.srcIndex = srcIndex;
105
106 /** @const {string|undefined} */
107 this.srcGroup = srcGroup;
108
109 /** @private @const {!TabsApiProxy} */
110 this.tabsProxy_ = TabsApiProxy.getInstance();
111 }
112
113 /**
114 * @param {!DragManagerDelegateElement} delegate
115 * @param {!TabElement|!TabGroupElement} element
116 * @return {!DragSession}
117 */
118 static createFromElement(delegate, element) {
119 if (isTabGroupElement(element)) {
120 return new DragSession(
121 delegate, element,
122 delegate.getIndexOfTab(
123 /** @type {!TabElement} */ (element.firstElementChild)));
124 }
125
126 const srcIndex = delegate.getIndexOfTab(
127 /** @type {!TabElement} */ (element));
128 const srcGroup =
129 (element.parentElement && isTabGroupElement(element.parentElement)) ?
130 element.parentElement.dataset.groupId :
131 undefined;
132 return new DragSession(delegate, element, srcIndex, srcGroup);
133 }
134
John Lee49ccb722020-02-26 03:30:05135 /**
136 * @param {!DragManagerDelegateElement} delegate
137 * @param {!DragEvent} event
138 * @return {?DragSession}
139 */
140 static createFromEvent(delegate, event) {
141 if (event.dataTransfer.types.includes(getTabIdDataType())) {
142 const isPinned = event.dataTransfer.types.includes('pinned');
143 const placeholderTabElement =
144 /** @type {!TabElement} */ (document.createElement('tabstrip-tab'));
145 placeholderTabElement.tab = /** @type {!TabData} */ (Object.assign(
146 getDefaultTabData(), {id: PLACEHOLDER_TAB_ID, pinned: isPinned}));
147 placeholderTabElement.setDragging(true);
148 delegate.placeTabElement(placeholderTabElement, -1, isPinned);
149 return DragSession.createFromElement(delegate, placeholderTabElement);
150 }
151
John Lee7b0b7a62020-03-03 06:00:50152 if (event.dataTransfer.types.includes(getGroupIdDataType())) {
153 const placeholderGroupElement = /** @type {!TabGroupElement} */
154 (document.createElement('tabstrip-tab-group'));
155 placeholderGroupElement.dataset.groupId = PLACEHOLDER_GROUP_ID;
156 placeholderGroupElement.setDragging(true);
157 delegate.placeTabGroupElement(placeholderGroupElement, -1);
158 return DragSession.createFromElement(delegate, placeholderGroupElement);
159 }
160
John Lee49ccb722020-02-26 03:30:05161 return null;
162 }
163
John Leeaa89aa72020-02-21 01:53:29164 /** @return {string|undefined} */
165 get dstGroup() {
166 if (isTabElement(this.element_) && this.element_.parentElement &&
167 isTabGroupElement(this.element_.parentElement)) {
168 return this.element_.parentElement.dataset.groupId;
169 }
170
171 return undefined;
172 }
173
174 /** @return {number} */
175 get dstIndex() {
176 if (isTabElement(this.element_)) {
177 return this.delegate_.getIndexOfTab(
178 /** @type {!TabElement} */ (this.element_));
179 }
180
John Lee7b0b7a62020-03-03 06:00:50181 if (this.element_.children.length === 0) {
182 // If this group element has no children, it was a placeholder element
183 // being dragged. Find out the destination index by finding the index of
184 // the tab closest to it and incrementing it by 1.
185 const previousElement = this.element_.previousElementSibling;
186 if (!previousElement) {
187 return 0;
188 }
189 if (isTabElement(previousElement)) {
190 return this.delegate_.getIndexOfTab(
191 /** @private {!TabElement} */ (previousElement)) +
192 1;
193 }
194
195 assert(isTabGroupElement(previousElement));
196 return this.delegate_.getIndexOfTab(/** @private {!TabElement} */ (
197 previousElement.lastElementChild)) +
198 1;
199 }
200
John Leeaa89aa72020-02-21 01:53:29201 // If a tab group is moving backwards (to the front of the tab strip), the
202 // new index is the index of the first tab in that group. If a tab group is
203 // moving forwards (to the end of the tab strip), the new index is the index
204 // of the last tab in that group.
205 let dstIndex = this.delegate_.getIndexOfTab(
206 /** @type {!TabElement} */ (this.element_.firstElementChild));
207 if (this.srcIndex <= dstIndex) {
208 dstIndex += this.element_.childElementCount - 1;
209 }
210 return dstIndex;
211 }
212
213 cancel() {
John Lee77e94e1a2020-02-27 02:16:44214 if (this.isDraggingPlaceholder()) {
John Lee49ccb722020-02-26 03:30:05215 this.element_.remove();
216 return;
217 }
218
John Leeaa89aa72020-02-21 01:53:29219 if (isTabGroupElement(this.element_)) {
220 this.delegate_.placeTabGroupElement(
221 /** @type {!TabGroupElement} */ (this.element_), this.srcIndex);
222 } else if (isTabElement(this.element_)) {
223 this.delegate_.placeTabElement(
224 /** @type {!TabElement} */ (this.element_), this.srcIndex,
225 this.element_.tab.pinned, this.srcGroup);
226 }
227
228 this.element_.setDragging(false);
229 }
230
John Lee77e94e1a2020-02-27 02:16:44231 /** @return {boolean} */
232 isDraggingPlaceholder() {
John Lee7b0b7a62020-03-03 06:00:50233 return this.isDraggingPlaceholderTab_() ||
234 this.isDraggingPlaceholderGroup_();
235 }
236
237 /**
238 * @return {boolean}
239 * @private
240 */
241 isDraggingPlaceholderTab_() {
John Lee77e94e1a2020-02-27 02:16:44242 return isTabElement(this.element_) &&
John Lee7b0b7a62020-03-03 06:00:50243 this.element_.tab.id === PLACEHOLDER_TAB_ID;
244 }
245
246 /**
247 * @return {boolean}
248 * @private
249 */
250 isDraggingPlaceholderGroup_() {
251 return isTabGroupElement(this.element_) &&
252 this.element_.dataset.groupId === PLACEHOLDER_GROUP_ID;
John Lee77e94e1a2020-02-27 02:16:44253 }
254
John Lee49ccb722020-02-26 03:30:05255 /** @param {!DragEvent} event */
256 finish(event) {
John Lee7b0b7a62020-03-03 06:00:50257 if (this.isDraggingPlaceholderTab_()) {
John Lee49ccb722020-02-26 03:30:05258 const id = Number(event.dataTransfer.getData(getTabIdDataType()));
259 this.element_.tab = Object.assign({}, this.element_.tab, {id});
John Lee7b0b7a62020-03-03 06:00:50260 } else if (this.isDraggingPlaceholderGroup_()) {
261 this.element_.dataset.groupId =
262 event.dataTransfer.getData(getGroupIdDataType());
John Leeaa89aa72020-02-21 01:53:29263 }
264
265 const dstIndex = this.dstIndex;
266 if (isTabElement(this.element_)) {
267 this.tabsProxy_.moveTab(this.element_.tab.id, dstIndex);
268 } else if (isTabGroupElement(this.element_)) {
269 this.tabsProxy_.moveGroup(this.element_.dataset.groupId, dstIndex);
270 }
271
John Lee49ccb722020-02-26 03:30:05272 const dstGroup = this.dstGroup;
273 if (dstGroup && dstGroup !== this.srcGroup) {
274 this.tabsProxy_.groupTab(this.element_.tab.id, dstGroup);
275 } else if (!dstGroup && this.srcGroup) {
276 this.tabsProxy_.ungroupTab(this.element_.tab.id);
277 }
278
John Leeaa89aa72020-02-21 01:53:29279 this.element_.setDragging(false);
280 }
281
John Lee7b0b7a62020-03-03 06:00:50282 /**
283 * @param {!TabElement|!TabGroupElement} dragOverElement
284 * @return {boolean}
285 */
286 shouldOffsetIndexForGroup_(dragOverElement) {
287 // Since TabGroupElements do not have any TabElements, they need to offset
288 // the index for any elements that come after it as if there is at least
289 // one element inside of it.
290 return this.isDraggingPlaceholder() &&
291 !!(dragOverElement.compareDocumentPosition(this.element_) &
292 Node.DOCUMENT_POSITION_PRECEDING);
293 }
294
John Leeaa89aa72020-02-21 01:53:29295 /** @param {!DragEvent} event */
296 start(event) {
297 event.dataTransfer.effectAllowed = 'move';
298 const draggedItemRect = this.element_.getBoundingClientRect();
299 this.element_.setDragging(true);
300 event.dataTransfer.setDragImage(
301 this.element_.getDragImage(), event.clientX - draggedItemRect.left,
302 event.clientY - draggedItemRect.top);
303
304 if (isTabElement(this.element_)) {
305 event.dataTransfer.setData(
306 getTabIdDataType(), this.element_.tab.id.toString());
John Lee49ccb722020-02-26 03:30:05307
308 if (this.element_.tab.pinned) {
309 event.dataTransfer.setData(
310 'pinned', this.element_.tab.pinned.toString());
311 }
John Leeaa89aa72020-02-21 01:53:29312 } else if (isTabGroupElement(this.element_)) {
313 event.dataTransfer.setData(
314 getGroupIdDataType(), this.element_.dataset.groupId);
315 }
316 }
317
318 /** @param {!DragEvent} event */
319 update(event) {
320 event.dataTransfer.dropEffect = 'move';
321 if (isTabGroupElement(this.element_)) {
322 this.updateForTabGroupElement_(event);
323 } else if (isTabElement(this.element_)) {
324 this.updateForTabElement_(event);
325 }
326 }
327
328 /**
329 * @param {!DragEvent} event
330 * @private
331 */
332 updateForTabGroupElement_(event) {
333 const tabGroupElement =
334 /** @type {!TabGroupElement} */ (this.element_);
335 const composedPath = /** @type {!Array<!Element>} */ (event.composedPath());
336 if (composedPath.includes(assert(this.element_))) {
337 // Dragging over itself or a child of itself.
338 return;
339 }
340
341 const dragOverTabElement =
342 /** @type {!TabElement|undefined} */ (composedPath.find(isTabElement));
343 if (dragOverTabElement && !dragOverTabElement.tab.pinned) {
John Lee7b0b7a62020-03-03 06:00:50344 let dragOverIndex = this.delegate_.getIndexOfTab(dragOverTabElement);
345 dragOverIndex +=
346 this.shouldOffsetIndexForGroup_(dragOverTabElement) ? 1 : 0;
John Leeaa89aa72020-02-21 01:53:29347 this.delegate_.placeTabGroupElement(tabGroupElement, dragOverIndex);
348 return;
349 }
350
John Lee7b0b7a62020-03-03 06:00:50351 const dragOverGroupElement = /** @type {!TabGroupElement|undefined} */ (
352 composedPath.find(isTabGroupElement));
John Leeaa89aa72020-02-21 01:53:29353 if (dragOverGroupElement) {
John Lee7b0b7a62020-03-03 06:00:50354 let dragOverIndex = this.delegate_.getIndexOfTab(
John Leeaa89aa72020-02-21 01:53:29355 /** @type {!TabElement} */ (dragOverGroupElement.firstElementChild));
John Lee7b0b7a62020-03-03 06:00:50356 dragOverIndex +=
357 this.shouldOffsetIndexForGroup_(dragOverGroupElement) ? 1 : 0;
John Leeaa89aa72020-02-21 01:53:29358 this.delegate_.placeTabGroupElement(tabGroupElement, dragOverIndex);
359 }
360 }
361
362 /**
363 * @param {!DragEvent} event
364 * @private
365 */
366 updateForTabElement_(event) {
367 const tabElement = /** @type {!TabElement} */ (this.element_);
368 const composedPath = /** @type {!Array<!Element>} */ (event.composedPath());
369 const dragOverTabElement =
370 /** @type {?TabElement} */ (composedPath.find(isTabElement));
371 if (dragOverTabElement &&
372 dragOverTabElement.tab.pinned !== tabElement.tab.pinned) {
373 // Can only drag between the same pinned states.
374 return;
375 }
376
377 const previousGroupId = (tabElement.parentElement &&
378 isTabGroupElement(tabElement.parentElement)) ?
379 tabElement.parentElement.dataset.groupId :
380 undefined;
381
382 const dragOverTabGroup =
383 /** @type {?TabGroupElement} */ (composedPath.find(isTabGroupElement));
384 if (dragOverTabGroup &&
385 dragOverTabGroup.dataset.groupId !== previousGroupId) {
386 this.delegate_.placeTabElement(
387 tabElement, this.dstIndex, false, dragOverTabGroup.dataset.groupId);
388 return;
389 }
390
391 if (!dragOverTabGroup && previousGroupId) {
392 this.delegate_.placeTabElement(
393 tabElement, this.dstIndex, false, undefined);
394 return;
395 }
396
397 if (!dragOverTabElement) {
398 return;
399 }
400
401 const dragOverIndex = this.delegate_.getIndexOfTab(dragOverTabElement);
402 this.delegate_.placeTabElement(
403 tabElement, dragOverIndex, tabElement.tab.pinned, previousGroupId);
404 }
405}
406
John Leec5bf0f22020-02-05 21:01:13407export class DragManager {
408 /** @param {!DragManagerDelegateElement} delegate */
409 constructor(delegate) {
410 /** @private {!DragManagerDelegateElement} */
411 this.delegate_ = delegate;
412
John Leeaa89aa72020-02-21 01:53:29413 /** @type {?DragSession} */
414 this.dragSession_ = null;
John Leec5bf0f22020-02-05 21:01:13415
John Leec5bf0f22020-02-05 21:01:13416 /** @private {!TabsApiProxy} */
417 this.tabsProxy_ = TabsApiProxy.getInstance();
418 }
419
John Leeaa89aa72020-02-21 01:53:29420 /** @private */
421 onDragLeave_() {
John Lee7b0b7a62020-03-03 06:00:50422 if (this.dragSession_ && !this.dragSession_.isDraggingPlaceholder()) {
423 return;
John Lee77e94e1a2020-02-27 02:16:44424 }
425
John Lee7b0b7a62020-03-03 06:00:50426 this.dragSession_.cancel();
427 this.dragSession_ = null;
John Lee9b3b01f2020-02-19 03:45:04428 }
429
430 /** @param {!DragEvent} event */
John Leeaa89aa72020-02-21 01:53:29431 onDragOver_(event) {
John Leec5bf0f22020-02-05 21:01:13432 event.preventDefault();
John Leeaa89aa72020-02-21 01:53:29433 if (!this.dragSession_) {
John Leec5bf0f22020-02-05 21:01:13434 return;
435 }
436
John Leeaa89aa72020-02-21 01:53:29437 this.dragSession_.update(event);
John Leec5bf0f22020-02-05 21:01:13438 }
439
John Leeaa89aa72020-02-21 01:53:29440 /** @param {!DragEvent} event */
441 onDragStart_(event) {
442 const draggedItem =
443 /** @type {!Array<!Element>} */ (event.composedPath()).find(item => {
444 return isTabElement(item) || isTabGroupElement(item);
445 });
446 if (!draggedItem) {
John Leec5bf0f22020-02-05 21:01:13447 return;
448 }
449
John Leeaa89aa72020-02-21 01:53:29450 this.dragSession_ = DragSession.createFromElement(
451 this.delegate_,
452 /** @type {!TabElement|!TabGroupElement} */ (draggedItem));
453 this.dragSession_.start(event);
John Leec5bf0f22020-02-05 21:01:13454 }
455
John Leeaa89aa72020-02-21 01:53:29456 /** @param {!DragEvent} event */
457 onDragEnd_(event) {
458 if (!this.dragSession_) {
John Leec5bf0f22020-02-05 21:01:13459 return;
460 }
461
John Leeaa89aa72020-02-21 01:53:29462 this.dragSession_.cancel();
463 this.dragSession_ = null;
John Leec5bf0f22020-02-05 21:01:13464 }
465
John Lee49ccb722020-02-26 03:30:05466 /** @param {!DragEvent} event */
467 onDragEnter_(event) {
468 if (this.dragSession_) {
469 return;
470 }
471
472 this.dragSession_ = DragSession.createFromEvent(this.delegate_, event);
473 }
474
John Leec5bf0f22020-02-05 21:01:13475 /**
476 * @param {!DragEvent} event
John Leec5bf0f22020-02-05 21:01:13477 */
John Leeaa89aa72020-02-21 01:53:29478 onDrop_(event) {
John Lee7b0b7a62020-03-03 06:00:50479 if (!this.dragSession_) {
John Leec5bf0f22020-02-05 21:01:13480 return;
481 }
482
John Lee7b0b7a62020-03-03 06:00:50483 this.dragSession_.finish(event);
484 this.dragSession_ = null;
John Leec5bf0f22020-02-05 21:01:13485 }
486
John Leeaa89aa72020-02-21 01:53:29487 startObserving() {
488 this.delegate_.addEventListener(
489 'dragstart', e => this.onDragStart_(/** @type {!DragEvent} */ (e)));
490 this.delegate_.addEventListener(
491 'dragend', e => this.onDragEnd_(/** @type {!DragEvent} */ (e)));
John Lee49ccb722020-02-26 03:30:05492 this.delegate_.addEventListener(
493 'dragenter', (e) => this.onDragEnter_(/** @type {!DragEvent} */ (e)));
John Leeaa89aa72020-02-21 01:53:29494 this.delegate_.addEventListener('dragleave', () => this.onDragLeave_());
495 this.delegate_.addEventListener(
496 'dragover', e => this.onDragOver_(/** @type {!DragEvent} */ (e)));
497 this.delegate_.addEventListener(
498 'drop', e => this.onDrop_(/** @type {!DragEvent} */ (e)));
John Leec5bf0f22020-02-05 21:01:13499 }
500}