blob: 69b836bf7425eaf0b0f8a58275803a9caa9e02fe [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);
John Lee9f2ee96852020-03-12 02:17:28300
301 const dragImage = this.element_.getDragImage();
302 const dragImageRect = dragImage.getBoundingClientRect();
303
304 let scaleFactor = 1;
305 let verticalOffset = 0;
306
307 // <if expr="chromeos">
308 // Touch on ChromeOS automatically scales drag images by 1.2 and adds a
309 // vertical offset of 25px. See //ash/drag_drop/drag_drop_controller.cc.
310 scaleFactor = 1.2;
311 verticalOffset = 25;
312 // </if>
313
314 const xDiffFromCenter =
315 event.clientX - draggedItemRect.left - (draggedItemRect.width / 2);
316 const yDiffFromCenter = event.clientY - draggedItemRect.top -
317 verticalOffset - (draggedItemRect.height / 2);
318
John Leeaa89aa72020-02-21 01:53:29319 event.dataTransfer.setDragImage(
John Lee9f2ee96852020-03-12 02:17:28320 dragImage, (dragImageRect.width / 2 + xDiffFromCenter / scaleFactor),
321 (dragImageRect.height / 2 + yDiffFromCenter / scaleFactor));
John Leeaa89aa72020-02-21 01:53:29322
323 if (isTabElement(this.element_)) {
324 event.dataTransfer.setData(
325 getTabIdDataType(), this.element_.tab.id.toString());
John Lee49ccb722020-02-26 03:30:05326
327 if (this.element_.tab.pinned) {
328 event.dataTransfer.setData(
329 'pinned', this.element_.tab.pinned.toString());
330 }
John Leeaa89aa72020-02-21 01:53:29331 } else if (isTabGroupElement(this.element_)) {
332 event.dataTransfer.setData(
333 getGroupIdDataType(), this.element_.dataset.groupId);
334 }
335 }
336
337 /** @param {!DragEvent} event */
338 update(event) {
339 event.dataTransfer.dropEffect = 'move';
340 if (isTabGroupElement(this.element_)) {
341 this.updateForTabGroupElement_(event);
342 } else if (isTabElement(this.element_)) {
343 this.updateForTabElement_(event);
344 }
345 }
346
347 /**
348 * @param {!DragEvent} event
349 * @private
350 */
351 updateForTabGroupElement_(event) {
352 const tabGroupElement =
353 /** @type {!TabGroupElement} */ (this.element_);
354 const composedPath = /** @type {!Array<!Element>} */ (event.composedPath());
355 if (composedPath.includes(assert(this.element_))) {
356 // Dragging over itself or a child of itself.
357 return;
358 }
359
360 const dragOverTabElement =
361 /** @type {!TabElement|undefined} */ (composedPath.find(isTabElement));
362 if (dragOverTabElement && !dragOverTabElement.tab.pinned) {
John Lee7b0b7a62020-03-03 06:00:50363 let dragOverIndex = this.delegate_.getIndexOfTab(dragOverTabElement);
364 dragOverIndex +=
365 this.shouldOffsetIndexForGroup_(dragOverTabElement) ? 1 : 0;
John Leeaa89aa72020-02-21 01:53:29366 this.delegate_.placeTabGroupElement(tabGroupElement, dragOverIndex);
367 return;
368 }
369
John Lee7b0b7a62020-03-03 06:00:50370 const dragOverGroupElement = /** @type {!TabGroupElement|undefined} */ (
371 composedPath.find(isTabGroupElement));
John Leeaa89aa72020-02-21 01:53:29372 if (dragOverGroupElement) {
John Lee7b0b7a62020-03-03 06:00:50373 let dragOverIndex = this.delegate_.getIndexOfTab(
John Leeaa89aa72020-02-21 01:53:29374 /** @type {!TabElement} */ (dragOverGroupElement.firstElementChild));
John Lee7b0b7a62020-03-03 06:00:50375 dragOverIndex +=
376 this.shouldOffsetIndexForGroup_(dragOverGroupElement) ? 1 : 0;
John Leeaa89aa72020-02-21 01:53:29377 this.delegate_.placeTabGroupElement(tabGroupElement, dragOverIndex);
378 }
379 }
380
381 /**
382 * @param {!DragEvent} event
383 * @private
384 */
385 updateForTabElement_(event) {
386 const tabElement = /** @type {!TabElement} */ (this.element_);
387 const composedPath = /** @type {!Array<!Element>} */ (event.composedPath());
388 const dragOverTabElement =
389 /** @type {?TabElement} */ (composedPath.find(isTabElement));
390 if (dragOverTabElement &&
391 dragOverTabElement.tab.pinned !== tabElement.tab.pinned) {
392 // Can only drag between the same pinned states.
393 return;
394 }
395
396 const previousGroupId = (tabElement.parentElement &&
397 isTabGroupElement(tabElement.parentElement)) ?
398 tabElement.parentElement.dataset.groupId :
399 undefined;
400
401 const dragOverTabGroup =
402 /** @type {?TabGroupElement} */ (composedPath.find(isTabGroupElement));
403 if (dragOverTabGroup &&
404 dragOverTabGroup.dataset.groupId !== previousGroupId) {
405 this.delegate_.placeTabElement(
406 tabElement, this.dstIndex, false, dragOverTabGroup.dataset.groupId);
407 return;
408 }
409
410 if (!dragOverTabGroup && previousGroupId) {
411 this.delegate_.placeTabElement(
412 tabElement, this.dstIndex, false, undefined);
413 return;
414 }
415
416 if (!dragOverTabElement) {
417 return;
418 }
419
420 const dragOverIndex = this.delegate_.getIndexOfTab(dragOverTabElement);
421 this.delegate_.placeTabElement(
422 tabElement, dragOverIndex, tabElement.tab.pinned, previousGroupId);
423 }
424}
425
John Leec5bf0f22020-02-05 21:01:13426export class DragManager {
427 /** @param {!DragManagerDelegateElement} delegate */
428 constructor(delegate) {
429 /** @private {!DragManagerDelegateElement} */
430 this.delegate_ = delegate;
431
John Leeaa89aa72020-02-21 01:53:29432 /** @type {?DragSession} */
433 this.dragSession_ = null;
John Leec5bf0f22020-02-05 21:01:13434
John Leec5bf0f22020-02-05 21:01:13435 /** @private {!TabsApiProxy} */
436 this.tabsProxy_ = TabsApiProxy.getInstance();
437 }
438
John Leeaa89aa72020-02-21 01:53:29439 /** @private */
440 onDragLeave_() {
John Lee7b0b7a62020-03-03 06:00:50441 if (this.dragSession_ && !this.dragSession_.isDraggingPlaceholder()) {
442 return;
John Lee77e94e1a2020-02-27 02:16:44443 }
444
John Lee7b0b7a62020-03-03 06:00:50445 this.dragSession_.cancel();
446 this.dragSession_ = null;
John Lee9b3b01f2020-02-19 03:45:04447 }
448
449 /** @param {!DragEvent} event */
John Leeaa89aa72020-02-21 01:53:29450 onDragOver_(event) {
John Leec5bf0f22020-02-05 21:01:13451 event.preventDefault();
John Leeaa89aa72020-02-21 01:53:29452 if (!this.dragSession_) {
John Leec5bf0f22020-02-05 21:01:13453 return;
454 }
455
John Leeaa89aa72020-02-21 01:53:29456 this.dragSession_.update(event);
John Leec5bf0f22020-02-05 21:01:13457 }
458
John Leeaa89aa72020-02-21 01:53:29459 /** @param {!DragEvent} event */
460 onDragStart_(event) {
461 const draggedItem =
462 /** @type {!Array<!Element>} */ (event.composedPath()).find(item => {
463 return isTabElement(item) || isTabGroupElement(item);
464 });
465 if (!draggedItem) {
John Leec5bf0f22020-02-05 21:01:13466 return;
467 }
468
John Leeaa89aa72020-02-21 01:53:29469 this.dragSession_ = DragSession.createFromElement(
470 this.delegate_,
471 /** @type {!TabElement|!TabGroupElement} */ (draggedItem));
472 this.dragSession_.start(event);
John Leec5bf0f22020-02-05 21:01:13473 }
474
John Leeaa89aa72020-02-21 01:53:29475 /** @param {!DragEvent} event */
476 onDragEnd_(event) {
477 if (!this.dragSession_) {
John Leec5bf0f22020-02-05 21:01:13478 return;
479 }
480
John Leeaa89aa72020-02-21 01:53:29481 this.dragSession_.cancel();
482 this.dragSession_ = null;
John Leec5bf0f22020-02-05 21:01:13483 }
484
John Lee49ccb722020-02-26 03:30:05485 /** @param {!DragEvent} event */
486 onDragEnter_(event) {
487 if (this.dragSession_) {
488 return;
489 }
490
491 this.dragSession_ = DragSession.createFromEvent(this.delegate_, event);
492 }
493
John Leec5bf0f22020-02-05 21:01:13494 /**
495 * @param {!DragEvent} event
John Leec5bf0f22020-02-05 21:01:13496 */
John Leeaa89aa72020-02-21 01:53:29497 onDrop_(event) {
John Lee7b0b7a62020-03-03 06:00:50498 if (!this.dragSession_) {
John Leec5bf0f22020-02-05 21:01:13499 return;
500 }
501
John Lee7b0b7a62020-03-03 06:00:50502 this.dragSession_.finish(event);
503 this.dragSession_ = null;
John Leec5bf0f22020-02-05 21:01:13504 }
505
John Leeaa89aa72020-02-21 01:53:29506 startObserving() {
507 this.delegate_.addEventListener(
508 'dragstart', e => this.onDragStart_(/** @type {!DragEvent} */ (e)));
509 this.delegate_.addEventListener(
510 'dragend', e => this.onDragEnd_(/** @type {!DragEvent} */ (e)));
John Lee49ccb722020-02-26 03:30:05511 this.delegate_.addEventListener(
512 'dragenter', (e) => this.onDragEnter_(/** @type {!DragEvent} */ (e)));
John Leeaa89aa72020-02-21 01:53:29513 this.delegate_.addEventListener('dragleave', () => this.onDragLeave_());
514 this.delegate_.addEventListener(
515 'dragover', e => this.onDragOver_(/** @type {!DragEvent} */ (e)));
516 this.delegate_.addEventListener(
517 'drop', e => this.onDrop_(/** @type {!DragEvent} */ (e)));
John Leec5bf0f22020-02-05 21:01:13518 }
519}