drag-and-drop.js (96080B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 // Wrap in a block to prevent leaking to window scope. 8 { 9 const isTab = element => gBrowser.isTab(element); 10 const isTabGroupLabel = element => gBrowser.isTabGroupLabel(element); 11 const isSplitViewWrapper = element => gBrowser.isSplitViewWrapper(element); 12 13 /** 14 * The elements in the tab strip from `this.dragAndDropElements` that contain 15 * logical information are: 16 * 17 * - <tab> (.tabbrowser-tab) 18 * - <tab-group> label element (.tab-group-label) 19 * - <tab-split-view-wrapper> 20 * 21 * The elements in the tab strip that contain the space inside of the <tabs> 22 * element are: 23 * 24 * - <tab> (.tabbrowser-tab) 25 * - <tab-group> label element wrapper (.tab-group-label-container) 26 * - <tab-split-view-wrapper> 27 * 28 * When working with tab strip items, if you need logical information, you 29 * can get it directly, e.g. `element.elementIndex` or `element._tPos`. If 30 * you need spatial information like position or dimensions, then you should 31 * call this function. For example, `elementToMove(element).getBoundingClientRect()` 32 * or `elementToMove(element).style.top`. 33 * 34 * @param {MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement} element 35 * @returns {MozTabbrowserTab|vbox} 36 */ 37 const elementToMove = element => { 38 if (isTab(element) || isSplitViewWrapper(element)) { 39 return element; 40 } 41 if (isTabGroupLabel(element)) { 42 return element.closest(".tab-group-label-container"); 43 } 44 throw new Error(`Element "${element.tagName}" is not expected to move`); 45 }; 46 47 window.TabDragAndDrop = class { 48 #dragTime = 0; 49 #pinnedDropIndicatorTimeout = null; 50 51 constructor(tabbrowserTabs) { 52 this._tabbrowserTabs = tabbrowserTabs; 53 } 54 55 init() { 56 this._pinnedDropIndicator = document.getElementById( 57 "pinned-drop-indicator" 58 ); 59 this._dragToPinPromoCard = document.getElementById( 60 "drag-to-pin-promo-card" 61 ); 62 this._tabDropIndicator = this._tabbrowserTabs.querySelector( 63 ".tab-drop-indicator" 64 ); 65 } 66 67 // Event handlers 68 69 handle_dragstart(event) { 70 if (this._tabbrowserTabs._isCustomizing) { 71 return; 72 } 73 74 let tab = this._getDragTarget(event); 75 if (!tab) { 76 return; 77 } 78 if (tab.splitview) { 79 tab = tab.splitview; 80 } 81 82 this._tabbrowserTabs.previewPanel?.deactivate(null, { force: true }); 83 this.startTabDrag(event, tab); 84 } 85 86 handle_dragover(event) { 87 var dropEffect = this.getDropEffectForTabDrag(event); 88 89 var ind = this._tabDropIndicator; 90 if (dropEffect == "" || dropEffect == "none") { 91 ind.hidden = true; 92 return; 93 } 94 event.preventDefault(); 95 event.stopPropagation(); 96 97 var arrowScrollbox = this._tabbrowserTabs.arrowScrollbox; 98 99 // autoscroll the tab strip if we drag over the scroll 100 // buttons, even if we aren't dragging a tab, but then 101 // return to avoid drawing the drop indicator 102 var pixelsToScroll = 0; 103 if (this._tabbrowserTabs.overflowing) { 104 switch (event.originalTarget) { 105 case arrowScrollbox._scrollButtonUp: 106 pixelsToScroll = arrowScrollbox.scrollIncrement * -1; 107 break; 108 case arrowScrollbox._scrollButtonDown: 109 pixelsToScroll = arrowScrollbox.scrollIncrement; 110 break; 111 } 112 if (pixelsToScroll) { 113 arrowScrollbox.scrollByPixels( 114 (this._rtlMode ? -1 : 1) * pixelsToScroll, 115 true 116 ); 117 } 118 } 119 120 let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0); 121 if ( 122 (dropEffect == "move" || dropEffect == "copy") && 123 document == draggedTab.ownerDocument && 124 !draggedTab._dragData.fromTabList 125 ) { 126 ind.hidden = true; 127 if (this.#isAnimatingMoveTogetherSelectedTabs()) { 128 // Wait for moving selected tabs together animation to finish. 129 return; 130 } 131 this.finishMoveTogetherSelectedTabs(draggedTab); 132 this._updateTabStylesOnDrag(draggedTab, dropEffect); 133 134 if (dropEffect == "move") { 135 this.#setMovingTabMode(true); 136 137 // Pinned tabs in expanded vertical mode are on a grid format and require 138 // different logic to drag and drop. 139 if (this._tabbrowserTabs.isContainerVerticalPinnedGrid(draggedTab)) { 140 this._animateExpandedPinnedTabMove(event); 141 return; 142 } 143 this._animateTabMove(event); 144 return; 145 } 146 } 147 148 this.finishAnimateTabMove(); 149 150 if (dropEffect == "link") { 151 let target = this._getDragTarget(event, { 152 ignoreSides: true, 153 }); 154 if (target) { 155 if (!this.#dragTime) { 156 this.#dragTime = Date.now(); 157 } 158 let overGroupLabel = isTabGroupLabel(target); 159 if ( 160 Date.now() >= 161 this.#dragTime + 162 Services.prefs.getIntPref( 163 overGroupLabel 164 ? "browser.tabs.dragDrop.expandGroup.delayMS" 165 : "browser.tabs.dragDrop.selectTab.delayMS" 166 ) 167 ) { 168 if (overGroupLabel) { 169 target.group.collapsed = false; 170 } else { 171 this._tabbrowserTabs.selectedItem = target; 172 } 173 } 174 if (isTab(target)) { 175 // Dropping on the target tab would replace the loaded page rather 176 // than opening a new tab, so hide the drop indicator. 177 ind.hidden = true; 178 return; 179 } 180 } 181 } 182 183 var rect = arrowScrollbox.getBoundingClientRect(); 184 var newMargin; 185 if (pixelsToScroll) { 186 // if we are scrolling, put the drop indicator at the edge 187 // so that it doesn't jump while scrolling 188 let scrollRect = arrowScrollbox.scrollClientRect; 189 let minMargin = this._tabbrowserTabs.verticalMode 190 ? scrollRect.top - rect.top 191 : scrollRect.left - rect.left; 192 let maxMargin = this._tabbrowserTabs.verticalMode 193 ? Math.min(minMargin + scrollRect.height, scrollRect.bottom) 194 : Math.min(minMargin + scrollRect.width, scrollRect.right); 195 if (this._rtlMode) { 196 [minMargin, maxMargin] = [ 197 this._tabbrowserTabs.clientWidth - maxMargin, 198 this._tabbrowserTabs.clientWidth - minMargin, 199 ]; 200 } 201 newMargin = pixelsToScroll > 0 ? maxMargin : minMargin; 202 } else { 203 let newIndex = this._getDropIndex(event); 204 let children = this._tabbrowserTabs.dragAndDropElements; 205 if (newIndex == children.length) { 206 let itemRect = children.at(-1).getBoundingClientRect(); 207 if (this._tabbrowserTabs.verticalMode) { 208 newMargin = itemRect.bottom - rect.top; 209 } else if (this._rtlMode) { 210 newMargin = rect.right - itemRect.left; 211 } else { 212 newMargin = itemRect.right - rect.left; 213 } 214 } else { 215 let itemRect = children[newIndex].getBoundingClientRect(); 216 if (this._tabbrowserTabs.verticalMode) { 217 newMargin = rect.top - itemRect.bottom; 218 } else if (this._rtlMode) { 219 newMargin = rect.right - itemRect.right; 220 } else { 221 newMargin = itemRect.left - rect.left; 222 } 223 } 224 } 225 226 ind.hidden = false; 227 newMargin += this._tabbrowserTabs.verticalMode 228 ? ind.clientHeight 229 : ind.clientWidth / 2; 230 if (this._rtlMode) { 231 newMargin *= -1; 232 } 233 ind.style.transform = this._tabbrowserTabs.verticalMode 234 ? "translateY(" + Math.round(newMargin) + "px)" 235 : "translateX(" + Math.round(newMargin) + "px)"; 236 } 237 238 // eslint-disable-next-line complexity 239 handle_drop(event) { 240 var dt = event.dataTransfer; 241 var dropEffect = dt.dropEffect; 242 var draggedTab; 243 let movingTabs; 244 /** @type {TabMetricsContext} */ 245 const dropMetricsContext = gBrowser.TabMetrics.userTriggeredContext( 246 gBrowser.TabMetrics.METRIC_SOURCE.DRAG_AND_DROP 247 ); 248 if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) { 249 // tab copy or move 250 draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0); 251 // not our drop then 252 if (!draggedTab) { 253 return; 254 } 255 movingTabs = draggedTab._dragData.movingTabs; 256 draggedTab.container.tabDragAndDrop.finishMoveTogetherSelectedTabs( 257 draggedTab 258 ); 259 } 260 261 if (this._rtlMode) { 262 // In `startTabDrag` we reverse the moving tabs order to handle 263 // positioning and animation. For drop, we require the original 264 // order, so reverse back. 265 movingTabs?.reverse(); 266 } 267 268 let overPinnedDropIndicator = 269 this._pinnedDropIndicator.hasAttribute("visible") && 270 this._pinnedDropIndicator.hasAttribute("interactive"); 271 this._resetTabsAfterDrop(draggedTab?.ownerDocument); 272 273 this._tabDropIndicator.hidden = true; 274 event.stopPropagation(); 275 if (draggedTab && dropEffect == "copy") { 276 let duplicatedDraggedTab; 277 let duplicatedTabs = []; 278 let dropTarget = 279 this._tabbrowserTabs.dragAndDropElements[this._getDropIndex(event)]; 280 for (let tab of movingTabs) { 281 let duplicatedTab = gBrowser.duplicateTab(tab); 282 duplicatedTabs.push(duplicatedTab); 283 if (tab == draggedTab) { 284 duplicatedDraggedTab = duplicatedTab; 285 } 286 } 287 gBrowser.moveTabsBefore(duplicatedTabs, dropTarget, dropMetricsContext); 288 if (draggedTab.container != this._tabbrowserTabs || event.shiftKey) { 289 this._tabbrowserTabs.selectedItem = duplicatedDraggedTab; 290 } 291 } else if (draggedTab && draggedTab.container == this._tabbrowserTabs) { 292 let oldTranslateX = Math.round(draggedTab._dragData.translateX); 293 let oldTranslateY = Math.round(draggedTab._dragData.translateY); 294 let tabWidth = Math.round(draggedTab._dragData.tabWidth); 295 let tabHeight = Math.round(draggedTab._dragData.tabHeight); 296 let translateOffsetX = oldTranslateX % tabWidth; 297 let translateOffsetY = oldTranslateY % tabHeight; 298 let newTranslateX = oldTranslateX - translateOffsetX; 299 let newTranslateY = oldTranslateY - translateOffsetY; 300 let isPinned = draggedTab.pinned; 301 let numPinned = gBrowser.pinnedTabCount; 302 303 if (this._tabbrowserTabs.isContainerVerticalPinnedGrid(draggedTab)) { 304 // Update both translate axis for pinned vertical expanded tabs 305 if (oldTranslateX > 0 && translateOffsetX > tabWidth / 2) { 306 newTranslateX += tabWidth; 307 } else if (oldTranslateX < 0 && -translateOffsetX > tabWidth / 2) { 308 newTranslateX -= tabWidth; 309 } 310 if (oldTranslateY > 0 && translateOffsetY > tabHeight / 2) { 311 newTranslateY += tabHeight; 312 } else if (oldTranslateY < 0 && -translateOffsetY > tabHeight / 2) { 313 newTranslateY -= tabHeight; 314 } 315 } else { 316 let tabs = this._tabbrowserTabs.dragAndDropElements.slice( 317 isPinned ? 0 : numPinned, 318 isPinned ? numPinned : undefined 319 ); 320 let size = this._tabbrowserTabs.verticalMode ? "height" : "width"; 321 let screenAxis = this._tabbrowserTabs.verticalMode 322 ? "screenY" 323 : "screenX"; 324 let tabSize = this._tabbrowserTabs.verticalMode 325 ? tabHeight 326 : tabWidth; 327 let firstTab = tabs[0]; 328 let lastTab = tabs.at(-1); 329 let lastMovingTabScreen = movingTabs.at(-1)[screenAxis]; 330 let firstMovingTabScreen = movingTabs[0][screenAxis]; 331 let startBound = firstTab[screenAxis] - firstMovingTabScreen; 332 let endBound = 333 lastTab[screenAxis] + 334 window.windowUtils.getBoundsWithoutFlushing(lastTab)[size] - 335 (lastMovingTabScreen + tabSize); 336 if (this._tabbrowserTabs.verticalMode) { 337 newTranslateY = Math.min( 338 Math.max(oldTranslateY, startBound), 339 endBound 340 ); 341 } else { 342 newTranslateX = RTL_UI 343 ? Math.min(Math.max(oldTranslateX, endBound), startBound) 344 : Math.min(Math.max(oldTranslateX, startBound), endBound); 345 } 346 } 347 348 let { 349 dropElement, 350 dropBefore, 351 shouldCreateGroupOnDrop, 352 shouldDropIntoCollapsedTabGroup, 353 fromTabList, 354 } = draggedTab._dragData; 355 356 let dropIndex; 357 let directionForward = false; 358 if (fromTabList) { 359 dropIndex = this._getDropIndex(event); 360 if (dropIndex && dropIndex > movingTabs[0].elementIndex) { 361 dropIndex--; 362 directionForward = true; 363 } 364 } 365 366 const dragToPinTargets = [ 367 this._tabbrowserTabs.pinnedTabsContainer, 368 this._dragToPinPromoCard, 369 ]; 370 let shouldPin = 371 isTab(draggedTab) && 372 !draggedTab.pinned && 373 (overPinnedDropIndicator || 374 dragToPinTargets.some(el => el.contains(event.target))); 375 let shouldUnpin = 376 isTab(draggedTab) && 377 draggedTab.pinned && 378 this._tabbrowserTabs.arrowScrollbox.contains(event.target); 379 380 let shouldTranslate = 381 !gReduceMotion && 382 !shouldCreateGroupOnDrop && 383 !shouldDropIntoCollapsedTabGroup && 384 !isTabGroupLabel(draggedTab) && 385 !isSplitViewWrapper(draggedTab) && 386 !shouldPin && 387 !shouldUnpin; 388 if (this._tabbrowserTabs.isContainerVerticalPinnedGrid(draggedTab)) { 389 shouldTranslate &&= 390 (oldTranslateX && oldTranslateX != newTranslateX) || 391 (oldTranslateY && oldTranslateY != newTranslateY); 392 } else if (this._tabbrowserTabs.verticalMode) { 393 shouldTranslate &&= oldTranslateY && oldTranslateY != newTranslateY; 394 } else { 395 shouldTranslate &&= oldTranslateX && oldTranslateX != newTranslateX; 396 } 397 398 let moveTabs = () => { 399 if (dropIndex !== undefined) { 400 for (let tab of movingTabs) { 401 gBrowser.moveTabTo( 402 tab, 403 { elementIndex: dropIndex }, 404 dropMetricsContext 405 ); 406 if (!directionForward) { 407 dropIndex++; 408 } 409 } 410 } else if (dropElement && dropBefore) { 411 gBrowser.moveTabsBefore( 412 movingTabs, 413 dropElement, 414 dropMetricsContext 415 ); 416 } else if (dropElement && dropBefore != undefined) { 417 gBrowser.moveTabsAfter(movingTabs, dropElement, dropMetricsContext); 418 } 419 420 if (isTabGroupLabel(draggedTab)) { 421 this._setIsDraggingTabGroup(draggedTab.group, false); 422 this._expandGroupOnDrop(draggedTab); 423 } 424 }; 425 426 if (shouldPin || shouldUnpin) { 427 for (let item of movingTabs) { 428 if (shouldPin) { 429 gBrowser.pinTab(item, { 430 telemetrySource: 431 gBrowser.TabMetrics.METRIC_SOURCE.DRAG_AND_DROP, 432 }); 433 } else if (shouldUnpin) { 434 gBrowser.unpinTab(item); 435 } 436 } 437 } 438 439 if (shouldTranslate) { 440 let translationPromises = []; 441 for (let item of movingTabs) { 442 item = elementToMove(item); 443 let translationPromise = new Promise(resolve => { 444 item.toggleAttribute("tabdrop-samewindow", true); 445 item.style.transform = `translate(${newTranslateX}px, ${newTranslateY}px)`; 446 let postTransitionCleanup = () => { 447 item.removeAttribute("tabdrop-samewindow"); 448 resolve(); 449 }; 450 if (gReduceMotion) { 451 postTransitionCleanup(); 452 } else { 453 let onTransitionEnd = transitionendEvent => { 454 if ( 455 transitionendEvent.propertyName != "transform" || 456 transitionendEvent.originalTarget != item 457 ) { 458 return; 459 } 460 item.removeEventListener("transitionend", onTransitionEnd); 461 462 postTransitionCleanup(); 463 }; 464 item.addEventListener("transitionend", onTransitionEnd); 465 } 466 }); 467 translationPromises.push(translationPromise); 468 } 469 Promise.all(translationPromises).then(() => { 470 this.finishAnimateTabMove(); 471 moveTabs(); 472 }); 473 } else { 474 this.finishAnimateTabMove(); 475 if (shouldCreateGroupOnDrop) { 476 // This makes the tab group contents reflect the visual order of 477 // the tabs right before dropping. 478 let tabsInGroup = dropBefore 479 ? [...movingTabs, dropElement] 480 : [dropElement, ...movingTabs]; 481 gBrowser.addTabGroup(tabsInGroup, { 482 insertBefore: dropElement, 483 isUserTriggered: true, 484 color: draggedTab._dragData.tabGroupCreationColor, 485 telemetryUserCreateSource: "drag", 486 }); 487 } else if ( 488 shouldDropIntoCollapsedTabGroup && 489 isTabGroupLabel(dropElement) && 490 isTab(draggedTab) 491 ) { 492 // If the dragged tab is the active tab in a collapsed tab group 493 // and the user dropped it onto the label of its tab group, leave 494 // the dragged tab where it was. Otherwise, drop it into the target 495 // tab group. 496 if (dropElement.group != draggedTab.group) { 497 dropElement.group.addTabs(movingTabs, dropMetricsContext); 498 } 499 } else { 500 moveTabs(); 501 this._tabbrowserTabs._notifyBackgroundTab(movingTabs.at(-1)); 502 } 503 } 504 } else if (isTabGroupLabel(draggedTab)) { 505 gBrowser.adoptTabGroup(draggedTab.group, { 506 elementIndex: this._getDropIndex(event), 507 }); 508 } else if (isSplitViewWrapper(draggedTab)) { 509 gBrowser.adoptSplitView(draggedTab, { 510 elementIndex: this._getDropIndex(event), 511 }); 512 } else if (draggedTab) { 513 // Move the tabs into this window. To avoid multiple tab-switches in 514 // the original window, the selected tab should be adopted last. 515 const dropIndex = this._getDropIndex(event); 516 let newIndex = dropIndex; 517 let selectedTab; 518 let indexForSelectedTab; 519 for (let i = 0; i < movingTabs.length; ++i) { 520 const tab = movingTabs[i]; 521 if (tab.selected) { 522 selectedTab = tab; 523 indexForSelectedTab = newIndex; 524 } else { 525 const newTab = gBrowser.adoptTab(tab, { 526 elementIndex: newIndex, 527 selectTab: tab == draggedTab, 528 }); 529 if (newTab) { 530 ++newIndex; 531 } 532 } 533 } 534 if (selectedTab) { 535 const newTab = gBrowser.adoptTab(selectedTab, { 536 elementIndex: indexForSelectedTab, 537 selectTab: selectedTab == draggedTab, 538 }); 539 if (newTab) { 540 ++newIndex; 541 } 542 } 543 544 // Restore tab selection 545 gBrowser.addRangeToMultiSelectedTabs( 546 this._tabbrowserTabs.dragAndDropElements[dropIndex], 547 this._tabbrowserTabs.dragAndDropElements[newIndex - 1] 548 ); 549 } else { 550 // Pass true to disallow dropping javascript: or data: urls 551 let links; 552 try { 553 links = Services.droppedLinkHandler.dropLinks(event, true); 554 } catch (ex) {} 555 556 if (!links || links.length === 0) { 557 return; 558 } 559 560 let inBackground = Services.prefs.getBoolPref( 561 "browser.tabs.loadInBackground" 562 ); 563 if (event.shiftKey) { 564 inBackground = !inBackground; 565 } 566 567 let targetTab = this._getDragTarget(event, { ignoreSides: true }); 568 let userContextId = 569 this._tabbrowserTabs.selectedItem.getAttribute("usercontextid"); 570 let replace = isTab(targetTab); 571 let newIndex = this._getDropIndex(event); 572 let urls = links.map(link => link.url); 573 let policyContainer = 574 Services.droppedLinkHandler.getPolicyContainer(event); 575 let triggeringPrincipal = 576 Services.droppedLinkHandler.getTriggeringPrincipal(event); 577 578 (async () => { 579 if ( 580 urls.length >= 581 Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn") 582 ) { 583 // Sync dialog cannot be used inside drop event handler. 584 let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs( 585 urls.length, 586 window 587 ); 588 if (!answer) { 589 return; 590 } 591 } 592 593 let nextItem = this._tabbrowserTabs.dragAndDropElements[newIndex]; 594 let tabGroup = isTab(nextItem) && nextItem.group; 595 gBrowser.loadTabs(urls, { 596 inBackground, 597 replace, 598 allowThirdPartyFixup: true, 599 targetTab, 600 elementIndex: newIndex, 601 tabGroup, 602 userContextId, 603 triggeringPrincipal, 604 policyContainer, 605 }); 606 })(); 607 } 608 609 if (draggedTab) { 610 delete draggedTab._dragData; 611 } 612 } 613 614 handle_dragend(event) { 615 var dt = event.dataTransfer; 616 var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0); 617 618 // Prevent this code from running if a tabdrop animation is 619 // running since calling finishAnimateTabMove would clear 620 // any CSS transition that is running. 621 if (draggedTab.hasAttribute("tabdrop-samewindow")) { 622 return; 623 } 624 625 this.finishMoveTogetherSelectedTabs(draggedTab); 626 this.finishAnimateTabMove(); 627 if (isTabGroupLabel(draggedTab)) { 628 this._setIsDraggingTabGroup(draggedTab.group, false); 629 this._expandGroupOnDrop(draggedTab); 630 } 631 this._resetTabsAfterDrop(draggedTab.ownerDocument); 632 633 if ( 634 dt.mozUserCancelled || 635 dt.dropEffect != "none" || 636 !Services.prefs.getBoolPref("browser.tabs.allowTabDetach") || 637 this._tabbrowserTabs._isCustomizing 638 ) { 639 delete draggedTab._dragData; 640 return; 641 } 642 643 // Disable detach within the browser toolbox 644 let [tabAxisPos, tabAxisStart, tabAxisEnd] = this._tabbrowserTabs 645 .verticalMode 646 ? [event.screenY, window.screenY, window.screenY + window.outerHeight] 647 : [event.screenX, window.screenX, window.screenX + window.outerWidth]; 648 649 if (tabAxisPos > tabAxisStart && tabAxisPos < tabAxisEnd) { 650 // also avoid detaching if the tab was dropped too close to 651 // the tabbar (half a tab) 652 let rect = window.windowUtils.getBoundsWithoutFlushing( 653 this._tabbrowserTabs.arrowScrollbox 654 ); 655 let crossAxisPos = this._tabbrowserTabs.verticalMode 656 ? event.screenX 657 : event.screenY; 658 let crossAxisStart, crossAxisEnd; 659 if (this._tabbrowserTabs.verticalMode) { 660 if ( 661 (RTL_UI && this._tabbrowserTabs._sidebarPositionStart) || 662 (!RTL_UI && !this._tabbrowserTabs._sidebarPositionStart) 663 ) { 664 crossAxisStart = 665 window.mozInnerScreenX + rect.right - 1.5 * rect.width; 666 crossAxisEnd = window.screenX + window.outerWidth; 667 } else { 668 crossAxisStart = window.screenX; 669 crossAxisEnd = 670 window.mozInnerScreenX + rect.left + 1.5 * rect.width; 671 } 672 } else { 673 crossAxisStart = window.screenY; 674 crossAxisEnd = window.mozInnerScreenY + rect.top + 1.5 * rect.height; 675 } 676 if (crossAxisPos > crossAxisStart && crossAxisPos < crossAxisEnd) { 677 return; 678 } 679 } 680 681 // screen.availLeft et. al. only check the screen that this window is on, 682 // but we want to look at the screen the tab is being dropped onto. 683 var screen = event.screen; 684 var availX = {}, 685 availY = {}, 686 availWidth = {}, 687 availHeight = {}; 688 // Get available rect in desktop pixels. 689 screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight); 690 availX = availX.value; 691 availY = availY.value; 692 availWidth = availWidth.value; 693 availHeight = availHeight.value; 694 695 // Compute the final window size in desktop pixels ensuring that the new 696 // window entirely fits within `screen`. 697 let ourCssToDesktopScale = 698 window.devicePixelRatio / window.desktopToDeviceScale; 699 let screenCssToDesktopScale = 700 screen.defaultCSSScaleFactor / screen.contentsScaleFactor; 701 702 // NOTE(emilio): Multiplying the sizes here for screenCssToDesktopScale 703 // means that we'll try to create a window that has the same amount of CSS 704 // pixels than our current window, not the same amount of device pixels. 705 // There are pros and cons of both conversions, though this matches the 706 // pre-existing intended behavior. 707 var winWidth = Math.min( 708 window.outerWidth * screenCssToDesktopScale, 709 availWidth 710 ); 711 var winHeight = Math.min( 712 window.outerHeight * screenCssToDesktopScale, 713 availHeight 714 ); 715 716 // This is slightly tricky: _dragData.offsetX/Y is an offset in CSS 717 // pixels. Since we're doing the sizing above based on those, we also need 718 // to apply the offset with pixels relative to the screen's scale rather 719 // than our scale. 720 var left = Math.min( 721 Math.max( 722 event.screenX * ourCssToDesktopScale - 723 draggedTab._dragData.offsetX * screenCssToDesktopScale, 724 availX 725 ), 726 availX + availWidth - winWidth 727 ); 728 var top = Math.min( 729 Math.max( 730 event.screenY * ourCssToDesktopScale - 731 draggedTab._dragData.offsetY * screenCssToDesktopScale, 732 availY 733 ), 734 availY + availHeight - winHeight 735 ); 736 737 // Convert back left and top to our CSS pixel space. 738 left /= ourCssToDesktopScale; 739 top /= ourCssToDesktopScale; 740 741 delete draggedTab._dragData; 742 743 if (gBrowser.tabs.length == 1) { 744 // resize _before_ move to ensure the window fits the new screen. if 745 // the window is too large for its screen, the window manager may do 746 // automatic repositioning. 747 // 748 // Since we're resizing before moving to our new screen, we need to use 749 // sizes relative to the current screen. If we moved, then resized, then 750 // we could avoid this special-case and share this with the else branch 751 // below... 752 winWidth /= ourCssToDesktopScale; 753 winHeight /= ourCssToDesktopScale; 754 755 window.resizeTo(winWidth, winHeight); 756 window.moveTo(left, top); 757 window.focus(); 758 } else { 759 // We're opening a new window in a new screen, so make sure to use sizes 760 // relative to the new screen. 761 winWidth /= screenCssToDesktopScale; 762 winHeight /= screenCssToDesktopScale; 763 764 let props = { screenX: left, screenY: top, suppressanimation: 1 }; 765 gBrowser.replaceTabsWithWindow(draggedTab, props); 766 } 767 event.stopPropagation(); 768 } 769 770 handle_dragleave(event) { 771 this.#dragTime = 0; 772 773 // This does not work at all (see bug 458613) 774 var target = event.relatedTarget; 775 while (target && target != this._tabbrowserTabs) { 776 target = target.parentNode; 777 } 778 if (target) { 779 return; 780 } 781 782 this._tabDropIndicator.hidden = true; 783 event.stopPropagation(); 784 } 785 786 // Utilities 787 788 get _rtlMode() { 789 return !this._tabbrowserTabs.verticalMode && RTL_UI; 790 } 791 792 #setMovingTabMode(movingTab) { 793 this._tabbrowserTabs.toggleAttribute("movingtab", movingTab); 794 gNavToolbox.toggleAttribute("movingtab", movingTab); 795 } 796 797 _getDropIndex(event) { 798 let item = this._getDragTarget(event); 799 if (!item) { 800 return this._tabbrowserTabs.dragAndDropElements.length; 801 } 802 let isBeforeMiddle; 803 804 let elementForSize = elementToMove(item); 805 if (this._tabbrowserTabs.verticalMode) { 806 let middle = 807 elementForSize.screenY + 808 elementForSize.getBoundingClientRect().height / 2; 809 isBeforeMiddle = event.screenY < middle; 810 } else { 811 let middle = 812 elementForSize.screenX + 813 elementForSize.getBoundingClientRect().width / 2; 814 isBeforeMiddle = this._rtlMode 815 ? event.screenX > middle 816 : event.screenX < middle; 817 } 818 return item.elementIndex + (isBeforeMiddle ? 0 : 1); 819 } 820 821 /** 822 * Returns the tab, tab group label or split view wrapper where an event happened, 823 * it didn't occur on a tab or tab group label. 824 * 825 * @param {Event} event 826 * The event for which we want to know on which element it happened. 827 * @param {object} options 828 * @param {boolean} options.ignoreSides 829 * If set to true: events will only be associated with an element if they 830 * happened on its central part (from 25% to 75%); if they happened on the 831 * left or right sides of the tab, the method will return null. 832 */ 833 _getDragTarget(event, { ignoreSides = false } = {}) { 834 let { target } = event; 835 while (target) { 836 if ( 837 isTab(target) || 838 isTabGroupLabel(target) || 839 isSplitViewWrapper(target) 840 ) { 841 break; 842 } 843 target = target.parentNode; 844 } 845 if (target && ignoreSides) { 846 let { width, height } = target.getBoundingClientRect(); 847 if ( 848 event.screenX < target.screenX + width * 0.25 || 849 event.screenX > target.screenX + width * 0.75 || 850 ((event.screenY < target.screenY + height * 0.25 || 851 event.screenY > target.screenY + height * 0.75) && 852 this._tabbrowserTabs.verticalMode) 853 ) { 854 return null; 855 } 856 } 857 return target; 858 } 859 860 #isMovingTab() { 861 return this._tabbrowserTabs.hasAttribute("movingtab"); 862 } 863 864 // Tab groups 865 866 /** 867 * When a tab group is being dragged, it fully collapses, even if it 868 * contains the active tab. Since all of its tabs will become invisible, 869 * the cache of visible tabs needs to be updated. Similarly, when the user 870 * stops dragging the tab group, it needs to return to normal, which may 871 * result in grouped tabs becoming visible again. 872 * 873 * @param {MozTabbrowserTabGroup} tabGroup 874 * @param {boolean} isDragging 875 */ 876 _setIsDraggingTabGroup(tabGroup, isDragging) { 877 tabGroup.isBeingDragged = isDragging; 878 this._tabbrowserTabs._invalidateCachedVisibleTabs(); 879 } 880 881 _expandGroupOnDrop(draggedTab) { 882 if ( 883 isTabGroupLabel(draggedTab) && 884 draggedTab._dragData?.expandGroupOnDrop 885 ) { 886 draggedTab.group.collapsed = false; 887 } 888 } 889 890 /** 891 * @param {MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement} dropElement 892 */ 893 _triggerDragOverGrouping(dropElement) { 894 this._clearDragOverGroupingTimer(); 895 896 this._tabbrowserTabs.toggleAttribute("movingtab-group", true); 897 this._tabbrowserTabs.removeAttribute("movingtab-ungroup"); 898 dropElement.toggleAttribute("dragover-groupTarget", true); 899 } 900 901 _clearDragOverGroupingTimer() { 902 if (this._dragOverGroupingTimer) { 903 clearTimeout(this._dragOverGroupingTimer); 904 this._dragOverGroupingTimer = 0; 905 } 906 } 907 908 _setDragOverGroupColor(groupColorCode) { 909 if (!groupColorCode) { 910 this._tabbrowserTabs.style.removeProperty("--dragover-tab-group-color"); 911 this._tabbrowserTabs.style.removeProperty( 912 "--dragover-tab-group-color-invert" 913 ); 914 this._tabbrowserTabs.style.removeProperty( 915 "--dragover-tab-group-color-pale" 916 ); 917 return; 918 } 919 920 this._tabbrowserTabs.style.setProperty( 921 "--dragover-tab-group-color", 922 `var(--tab-group-color-${groupColorCode})` 923 ); 924 this._tabbrowserTabs.style.setProperty( 925 "--dragover-tab-group-color-invert", 926 `var(--tab-group-color-${groupColorCode}-invert)` 927 ); 928 this._tabbrowserTabs.style.setProperty( 929 "--dragover-tab-group-color-pale", 930 `var(--tab-group-color-${groupColorCode}-pale)` 931 ); 932 } 933 934 /** 935 * @param {MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement} [element] 936 */ 937 _resetGroupTarget(element) { 938 element?.removeAttribute("dragover-groupTarget"); 939 } 940 941 // Drag start 942 943 startTabDrag(event, tab, { fromTabList = false } = {}) { 944 if (this.expandOnHover) { 945 // Temporarily disable MousePosTracker while dragging 946 MousePosTracker.removeListener(document.defaultView.SidebarController); 947 } 948 if (this._tabbrowserTabs.isContainerVerticalPinnedGrid(tab)) { 949 // In expanded vertical mode, the max number of pinned tabs per row is dynamic 950 // Set this before adjusting dragged tab's position 951 let pinnedTabs = this._tabbrowserTabs.visibleTabs.slice( 952 0, 953 gBrowser.pinnedTabCount 954 ); 955 let tabsPerRow = 0; 956 let position = RTL_UI 957 ? window.windowUtils.getBoundsWithoutFlushing( 958 this._tabbrowserTabs.pinnedTabsContainer 959 ).right 960 : 0; 961 for (let pinnedTab of pinnedTabs) { 962 let tabPosition; 963 let rect = window.windowUtils.getBoundsWithoutFlushing(pinnedTab); 964 if (RTL_UI) { 965 tabPosition = rect.right; 966 if (tabPosition > position) { 967 break; 968 } 969 } else { 970 tabPosition = rect.left; 971 if (tabPosition < position) { 972 break; 973 } 974 } 975 tabsPerRow++; 976 position = tabPosition; 977 } 978 this._maxTabsPerRow = tabsPerRow; 979 } 980 981 if (tab.multiselected) { 982 for (let multiselectedTab of gBrowser.selectedTabs.filter( 983 t => t.pinned != tab.pinned 984 )) { 985 gBrowser.removeFromMultiSelectedTabs(multiselectedTab); 986 } 987 } 988 989 let dataTransferOrderedTabs; 990 if (fromTabList || isTabGroupLabel(tab) || isSplitViewWrapper(tab)) { 991 // Dragging a group label or an item in the all tabs menu doesn't 992 // change the currently selected tabs, and it's not possible to select 993 // multiple tabs from the list, thus handle only the dragged tab in 994 // this case. 995 dataTransferOrderedTabs = [tab]; 996 } else { 997 this._tabbrowserTabs.selectedItem = tab; 998 let selectedTabs = gBrowser.selectedTabs; 999 let otherSelectedTabs = selectedTabs.filter( 1000 selectedTab => selectedTab != tab 1001 ); 1002 dataTransferOrderedTabs = [tab].concat(otherSelectedTabs); 1003 } 1004 1005 let dt = event.dataTransfer; 1006 for (let i = 0; i < dataTransferOrderedTabs.length; i++) { 1007 let dtTab = dataTransferOrderedTabs[i]; 1008 dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i); 1009 if (isTab(dtTab)) { 1010 let dtBrowser = dtTab.linkedBrowser; 1011 1012 // We must not set text/x-moz-url or text/plain data here, 1013 // otherwise trying to detach the tab by dropping it on the desktop 1014 // may result in an "internet shortcut" 1015 dt.mozSetDataAt( 1016 "text/x-moz-text-internal", 1017 dtBrowser.currentURI.spec, 1018 i 1019 ); 1020 } 1021 } 1022 1023 // Set the cursor to an arrow during tab drags. 1024 dt.mozCursor = "default"; 1025 1026 // Set the tab as the source of the drag, which ensures we have a stable 1027 // node to deliver the `dragend` event. See bug 1345473. 1028 dt.addElement(tab); 1029 1030 // Create a canvas to which we capture the current tab. 1031 // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired 1032 // canvas size (in CSS pixels) to the window's backing resolution in order 1033 // to get a full-resolution drag image for use on HiDPI displays. 1034 let scale = window.devicePixelRatio; 1035 let canvas = this._tabbrowserTabs._dndCanvas; 1036 if (!canvas) { 1037 this._tabbrowserTabs._dndCanvas = canvas = document.createElementNS( 1038 "http://www.w3.org/1999/xhtml", 1039 "canvas" 1040 ); 1041 canvas.style.width = "100%"; 1042 canvas.style.height = "100%"; 1043 canvas.mozOpaque = true; 1044 } 1045 1046 canvas.width = 160 * scale; 1047 canvas.height = 90 * scale; 1048 let toDrag = canvas; 1049 let dragImageOffset = -16; 1050 let browser = isTab(tab) && tab.linkedBrowser; 1051 if (isTabGroupLabel(tab)) { 1052 toDrag = tab; 1053 } else if (gMultiProcessBrowser) { 1054 var context = canvas.getContext("2d"); 1055 context.fillStyle = "white"; 1056 context.fillRect(0, 0, canvas.width, canvas.height); 1057 1058 let captureListener; 1059 let platform = AppConstants.platform; 1060 // On Windows and Mac we can update the drag image during a drag 1061 // using updateDragImage. On Linux, we can use a panel. 1062 if (platform == "win" || platform == "macosx") { 1063 captureListener = function () { 1064 dt.updateDragImage(canvas, dragImageOffset, dragImageOffset); 1065 }; 1066 } else { 1067 // Create a panel to use it in setDragImage 1068 // which will tell xul to render a panel that follows 1069 // the pointer while a dnd session is on. 1070 if (!this._tabbrowserTabs._dndPanel) { 1071 this._tabbrowserTabs._dndCanvas = canvas; 1072 this._tabbrowserTabs._dndPanel = document.createXULElement("panel"); 1073 this._tabbrowserTabs._dndPanel.className = "dragfeedback-tab"; 1074 this._tabbrowserTabs._dndPanel.setAttribute("type", "drag"); 1075 let wrapper = document.createElementNS( 1076 "http://www.w3.org/1999/xhtml", 1077 "div" 1078 ); 1079 wrapper.style.width = "160px"; 1080 wrapper.style.height = "90px"; 1081 wrapper.appendChild(canvas); 1082 this._tabbrowserTabs._dndPanel.appendChild(wrapper); 1083 document.documentElement.appendChild( 1084 this._tabbrowserTabs._dndPanel 1085 ); 1086 } 1087 toDrag = this._tabbrowserTabs._dndPanel; 1088 } 1089 // PageThumb is async with e10s but that's fine 1090 // since we can update the image during the dnd. 1091 PageThumbs.captureToCanvas(browser, canvas) 1092 .then(captureListener) 1093 .catch(e => console.error(e)); 1094 } else { 1095 // For the non e10s case we can just use PageThumbs 1096 // sync, so let's use the canvas for setDragImage. 1097 PageThumbs.captureToCanvas(browser, canvas).catch(e => 1098 console.error(e) 1099 ); 1100 dragImageOffset = dragImageOffset * scale; 1101 } 1102 dt.setDragImage(toDrag, dragImageOffset, dragImageOffset); 1103 1104 // _dragData.offsetX/Y give the coordinates that the mouse should be 1105 // positioned relative to the corner of the new window created upon 1106 // dragend such that the mouse appears to have the same position 1107 // relative to the corner of the dragged tab. 1108 let clientPos = ele => { 1109 const rect = ele.getBoundingClientRect(); 1110 return this._tabbrowserTabs.verticalMode ? rect.top : rect.left; 1111 }; 1112 1113 let tabOffset = clientPos(tab) - clientPos(this._tabbrowserTabs); 1114 1115 let movingTabs = tab.multiselected ? gBrowser.selectedTabs : [tab]; 1116 let movingTabsSet = new Set(movingTabs); 1117 1118 let dropEffect = this.getDropEffectForTabDrag(event); 1119 let isMovingInTabStrip = !fromTabList && dropEffect == "move"; 1120 let collapseTabGroupDuringDrag = 1121 isMovingInTabStrip && isTabGroupLabel(tab) && !tab.group.collapsed; 1122 1123 tab._dragData = { 1124 offsetX: this._tabbrowserTabs.verticalMode 1125 ? event.screenX - window.screenX 1126 : event.screenX - window.screenX - tabOffset, 1127 offsetY: this._tabbrowserTabs.verticalMode 1128 ? event.screenY - window.screenY - tabOffset 1129 : event.screenY - window.screenY, 1130 scrollPos: 1131 this._tabbrowserTabs.verticalMode && tab.pinned 1132 ? this._tabbrowserTabs.pinnedTabsContainer.scrollPosition 1133 : this._tabbrowserTabs.arrowScrollbox.scrollPosition, 1134 screenX: event.screenX, 1135 screenY: event.screenY, 1136 movingTabs, 1137 movingTabsSet, 1138 fromTabList, 1139 tabGroupCreationColor: gBrowser.tabGroupMenu.nextUnusedColor, 1140 expandGroupOnDrop: collapseTabGroupDuringDrag, 1141 }; 1142 if (this._rtlMode) { 1143 // Reverse order to handle positioning in `_updateTabStylesOnDrag` 1144 // and animation in `_animateTabMove` 1145 tab._dragData.movingTabs.reverse(); 1146 } 1147 1148 if (isMovingInTabStrip) { 1149 this.#setMovingTabMode(true); 1150 1151 if (tab.multiselected) { 1152 this._moveTogetherSelectedTabs(tab); 1153 } else if (isTabGroupLabel(tab)) { 1154 this._setIsDraggingTabGroup(tab.group, true); 1155 1156 if (collapseTabGroupDuringDrag) { 1157 tab.group.collapsed = true; 1158 } 1159 } 1160 } 1161 1162 event.stopPropagation(); 1163 1164 if (fromTabList) { 1165 Glean.browserUiInteraction.allTabsPanelDragstartTabEventCount.add(1); 1166 } 1167 } 1168 1169 /* In order to to drag tabs between both the pinned arrowscrollbox (pinned tab container) 1170 and unpinned arrowscrollbox (tabbrowser-arrowscrollbox), the dragged tabs need to be 1171 positioned absolutely. This results in a shift in the layout, filling the empty space. 1172 This function updates the position and widths of elements affected by this layout shift 1173 when the tab is first selected to be dragged. 1174 */ 1175 _updateTabStylesOnDrag(tab, dropEffect) { 1176 let tabStripItemElement = elementToMove(tab); 1177 tabStripItemElement.style.pointerEvents = 1178 dropEffect == "copy" ? "auto" : ""; 1179 if (tabStripItemElement.hasAttribute("dragtarget")) { 1180 return; 1181 } 1182 let isPinned = tab.pinned; 1183 let numPinned = gBrowser.pinnedTabCount; 1184 let dragAndDropElements = this._tabbrowserTabs.dragAndDropElements; 1185 let isGrid = this._tabbrowserTabs.isContainerVerticalPinnedGrid(tab); 1186 let periphery = document.getElementById( 1187 "tabbrowser-arrowscrollbox-periphery" 1188 ); 1189 1190 if (isPinned && this._tabbrowserTabs.verticalMode) { 1191 this._tabbrowserTabs.pinnedTabsContainer.setAttribute("dragActive", ""); 1192 } 1193 1194 // Ensure tab containers retain size while tabs are dragged out of the layout 1195 let pinnedRect = window.windowUtils.getBoundsWithoutFlushing( 1196 this._tabbrowserTabs.pinnedTabsContainer.scrollbox 1197 ); 1198 let pinnedContainerRect = window.windowUtils.getBoundsWithoutFlushing( 1199 this._tabbrowserTabs.pinnedTabsContainer 1200 ); 1201 let unpinnedRect = window.windowUtils.getBoundsWithoutFlushing( 1202 this._tabbrowserTabs.arrowScrollbox.scrollbox 1203 ); 1204 let tabContainerRect = window.windowUtils.getBoundsWithoutFlushing( 1205 this._tabbrowserTabs 1206 ); 1207 1208 if (this._tabbrowserTabs.pinnedTabsContainer.firstChild) { 1209 this._tabbrowserTabs.pinnedTabsContainer.scrollbox.style.height = 1210 pinnedRect.height + "px"; 1211 // Use "minHeight" so as not to interfere with user preferences for height. 1212 this._tabbrowserTabs.pinnedTabsContainer.style.minHeight = 1213 pinnedContainerRect.height + "px"; 1214 this._tabbrowserTabs.pinnedTabsContainer.scrollbox.style.width = 1215 pinnedRect.width + "px"; 1216 } 1217 this._tabbrowserTabs.arrowScrollbox.scrollbox.style.height = 1218 unpinnedRect.height + "px"; 1219 this._tabbrowserTabs.arrowScrollbox.scrollbox.style.width = 1220 unpinnedRect.width + "px"; 1221 1222 let { movingTabs, movingTabsSet, expandGroupOnDrop } = tab._dragData; 1223 /** @type {(MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement)[]} */ 1224 let suppressTransitionsFor = []; 1225 /** @type {Map<MozTabbrowserTab, DOMRect>} */ 1226 const pinnedTabsOrigBounds = new Map(); 1227 1228 for (let t of dragAndDropElements) { 1229 t = elementToMove(t); 1230 let tabRect = window.windowUtils.getBoundsWithoutFlushing(t); 1231 1232 // record where all the pinned tabs were before we position:absolute the moving tabs 1233 if (isGrid && t.pinned) { 1234 pinnedTabsOrigBounds.set(t, tabRect); 1235 } 1236 // Prevent flex rules from resizing non dragged tabs while the dragged 1237 // tabs are positioned absolutely 1238 if (tabRect.width) { 1239 t.style.maxWidth = tabRect.width + "px"; 1240 } 1241 // Prevent non-moving tab strip items from performing any animations 1242 // at the very beginning of the drag operation; this prevents them 1243 // from appearing to move while the dragged tabs are positioned absolutely 1244 let isTabInCollapsingGroup = expandGroupOnDrop && t.group == tab.group; 1245 if (!movingTabsSet.has(t) && !isTabInCollapsingGroup) { 1246 t.animationsEnabled = false; 1247 suppressTransitionsFor.push(t); 1248 } 1249 } 1250 1251 if (suppressTransitionsFor.length) { 1252 window 1253 .promiseDocumentFlushed(() => {}) 1254 .then(() => { 1255 window.requestAnimationFrame(() => { 1256 for (let t of suppressTransitionsFor) { 1257 t.animationsEnabled = true; 1258 } 1259 }); 1260 }); 1261 } 1262 1263 // Use .tab-group-label-container or .tabbrowser-tab for size/position 1264 // calculations. 1265 let rect = 1266 window.windowUtils.getBoundsWithoutFlushing(tabStripItemElement); 1267 // Vertical tabs live under the #sidebar-main element which gets animated and has a 1268 // transform style property, making it the containing block for all its descendants. 1269 // Position:absolute elements need to account for this when updating position using 1270 // other measurements whose origin is the viewport or documentElement's 0,0 1271 let movingTabsOffsetX = window.windowUtils.getBoundsWithoutFlushing( 1272 tabStripItemElement.offsetParent 1273 ).x; 1274 1275 let movingTabsIndex = movingTabs.findIndex(t => t._tPos == tab._tPos); 1276 // Update moving tabs absolute position based on original dragged tab position 1277 // Moving tabs with a lower index are moved before the dragged tab and moving 1278 // tabs with a higher index are moved after the dragged tab. 1279 let position = 0; 1280 // Position moving tabs after dragged tab 1281 for (let movingTab of movingTabs.slice(movingTabsIndex)) { 1282 movingTab = elementToMove(movingTab); 1283 movingTab.style.width = rect.width + "px"; 1284 // "dragtarget" contains the following rules which must only be set AFTER the above 1285 // elements have been adjusted. {z-index: 3 !important, position: absolute !important} 1286 movingTab.setAttribute("dragtarget", ""); 1287 if (isTabGroupLabel(tab)) { 1288 if (this._tabbrowserTabs.verticalMode) { 1289 movingTab.style.top = rect.top - tabContainerRect.top + "px"; 1290 } else { 1291 movingTab.style.left = rect.left - movingTabsOffsetX + "px"; 1292 movingTab.style.height = rect.height + "px"; 1293 } 1294 } else if (isGrid) { 1295 movingTab.style.top = rect.top - pinnedRect.top + "px"; 1296 movingTab.style.left = 1297 rect.left - movingTabsOffsetX + position + "px"; 1298 position += rect.width; 1299 } else if (this._tabbrowserTabs.verticalMode) { 1300 movingTab.style.top = 1301 rect.top - tabContainerRect.top + position + "px"; 1302 position += rect.height; 1303 } else if (this._rtlMode) { 1304 movingTab.style.left = 1305 rect.left - movingTabsOffsetX - position + "px"; 1306 position -= rect.width; 1307 } else { 1308 movingTab.style.left = 1309 rect.left - movingTabsOffsetX + position + "px"; 1310 position += rect.width; 1311 } 1312 } 1313 // Reset position so we can next handle moving tabs before the dragged tab 1314 if (this._tabbrowserTabs.verticalMode) { 1315 position = -rect.height; 1316 } else if (this._rtlMode) { 1317 position = rect.width; 1318 } else { 1319 position = -rect.width; 1320 } 1321 // Position moving tabs before dragged tab 1322 for (let movingTab of movingTabs.slice(0, movingTabsIndex).reverse()) { 1323 movingTab.style.width = rect.width + "px"; 1324 movingTab.setAttribute("dragtarget", ""); 1325 if (this._tabbrowserTabs.verticalMode) { 1326 movingTab.style.top = 1327 rect.top - tabContainerRect.top + position + "px"; 1328 position -= rect.height; 1329 } else if (this._rtlMode) { 1330 movingTab.style.left = 1331 rect.left - movingTabsOffsetX - position + "px"; 1332 position += rect.width; 1333 } else { 1334 movingTab.style.left = 1335 rect.left - movingTabsOffsetX + position + "px"; 1336 position -= rect.width; 1337 } 1338 } 1339 1340 if ( 1341 !isPinned && 1342 this._tabbrowserTabs.arrowScrollbox.hasAttribute("overflowing") 1343 ) { 1344 if (this._tabbrowserTabs.verticalMode) { 1345 periphery.style.marginBlockStart = 1346 rect.height * movingTabs.length + "px"; 1347 } else { 1348 periphery.style.marginInlineStart = 1349 rect.width * movingTabs.length + "px"; 1350 } 1351 } else if ( 1352 isPinned && 1353 this._tabbrowserTabs.pinnedTabsContainer.hasAttribute("overflowing") 1354 ) { 1355 let pinnedPeriphery = document.createXULElement("hbox"); 1356 pinnedPeriphery.id = "pinned-tabs-container-periphery"; 1357 pinnedPeriphery.style.width = "100%"; 1358 pinnedPeriphery.style.marginBlockStart = 1359 (isGrid && numPinned % this._maxTabsPerRow == 1 1360 ? rect.height 1361 : rect.height * movingTabs.length) + "px"; 1362 this._tabbrowserTabs.pinnedTabsContainer.appendChild(pinnedPeriphery); 1363 } 1364 1365 let setElPosition = el => { 1366 let elRect = window.windowUtils.getBoundsWithoutFlushing(el); 1367 if (this._tabbrowserTabs.verticalMode && elRect.top > rect.top) { 1368 el.style.top = movingTabs.length * rect.height + "px"; 1369 } else if (!this._tabbrowserTabs.verticalMode) { 1370 if (!this._rtlMode && elRect.left > rect.left) { 1371 el.style.left = movingTabs.length * rect.width + "px"; 1372 } else if (this._rtlMode && elRect.left < rect.left) { 1373 el.style.left = movingTabs.length * -rect.width + "px"; 1374 } 1375 } 1376 }; 1377 1378 let setGridElPosition = el => { 1379 let origBounds = pinnedTabsOrigBounds.get(el); 1380 if (!origBounds) { 1381 // No bounds saved for this pinned tab 1382 return; 1383 } 1384 // We use getBoundingClientRect and force a reflow as we need to know their new positions 1385 // after making the moving tabs position:absolute 1386 let newBounds = el.getBoundingClientRect(); 1387 let shiftX = origBounds.x - newBounds.x; 1388 let shiftY = origBounds.y - newBounds.y; 1389 1390 el.style.left = shiftX + "px"; 1391 el.style.top = shiftY + "px"; 1392 }; 1393 1394 // Update tabs in the same container as the dragged tabs so as not 1395 // to fill the space when the dragged tabs become absolute 1396 for (let t of dragAndDropElements) { 1397 let tabIsPinned = t.pinned; 1398 t = elementToMove(t); 1399 if (!t.hasAttribute("dragtarget")) { 1400 if ( 1401 (!isPinned && !tabIsPinned) || 1402 (tabIsPinned && isPinned && !isGrid) 1403 ) { 1404 setElPosition(t); 1405 } else if (isGrid && tabIsPinned && isPinned) { 1406 setGridElPosition(t); 1407 } 1408 } 1409 } 1410 1411 if (this._tabbrowserTabs.expandOnHover) { 1412 // Query the expanded width from sidebar launcher to ensure tabs aren't 1413 // cut off (Bug 1974037). 1414 const { SidebarController } = tab.ownerGlobal; 1415 SidebarController.expandOnHoverComplete.then(async () => { 1416 const width = await window.promiseDocumentFlushed( 1417 () => SidebarController.sidebarMain.clientWidth 1418 ); 1419 requestAnimationFrame(() => { 1420 for (const t of movingTabs) { 1421 t.style.width = width + "px"; 1422 } 1423 // Allow scrollboxes to grow to expanded sidebar width. 1424 this._tabbrowserTabs.arrowScrollbox.scrollbox.style.width = ""; 1425 this._tabbrowserTabs.pinnedTabsContainer.scrollbox.style.width = ""; 1426 }); 1427 }); 1428 } 1429 1430 // Handle the new tab button filling the space when the dragged tab 1431 // position becomes absolute 1432 if (!this._tabbrowserTabs.overflowing && !isPinned) { 1433 if (this._tabbrowserTabs.verticalMode) { 1434 periphery.style.top = `${Math.round(movingTabs.length * rect.height)}px`; 1435 } else if (this._rtlMode) { 1436 periphery.style.left = `${Math.round(movingTabs.length * -rect.width)}px`; 1437 } else { 1438 periphery.style.left = `${Math.round(movingTabs.length * rect.width)}px`; 1439 } 1440 } 1441 } 1442 1443 /** 1444 * Move together all selected tabs around the tab in param. 1445 */ 1446 _moveTogetherSelectedTabs(tab) { 1447 let draggedTabIndex = tab.elementIndex; 1448 let selectedTabs = gBrowser.selectedTabs; 1449 if (selectedTabs.some(t => t.pinned != tab.pinned)) { 1450 throw new Error( 1451 "Cannot move together a mix of pinned and unpinned tabs." 1452 ); 1453 } 1454 let animate = !gReduceMotion; 1455 1456 tab._moveTogetherSelectedTabsData = { 1457 finished: !animate, 1458 }; 1459 1460 let addAnimationData = (movingTab, isBeforeSelectedTab) => { 1461 let lowerIndex = Math.min(movingTab.elementIndex, draggedTabIndex) + 1; 1462 let higherIndex = Math.max(movingTab.elementIndex, draggedTabIndex); 1463 let middleItems = this._tabbrowserTabs.dragAndDropElements 1464 .slice(lowerIndex, higherIndex) 1465 .filter(item => !item.multiselected); 1466 if (!middleItems.length) { 1467 // movingTab is already at the right position and thus doesn't need 1468 // to be animated. 1469 return; 1470 } 1471 1472 movingTab._moveTogetherSelectedTabsData = { 1473 translatePos: 0, 1474 animate: true, 1475 }; 1476 movingTab.toggleAttribute("multiselected-move-together", true); 1477 1478 let postTransitionCleanup = () => { 1479 movingTab._moveTogetherSelectedTabsData.animate = false; 1480 }; 1481 if (gReduceMotion) { 1482 postTransitionCleanup(); 1483 } else { 1484 let onTransitionEnd = transitionendEvent => { 1485 if ( 1486 transitionendEvent.propertyName != "transform" || 1487 transitionendEvent.originalTarget != movingTab 1488 ) { 1489 return; 1490 } 1491 movingTab.removeEventListener("transitionend", onTransitionEnd); 1492 postTransitionCleanup(); 1493 }; 1494 1495 movingTab.addEventListener("transitionend", onTransitionEnd); 1496 } 1497 1498 // Add animation data for tabs and tab group labels between movingTab 1499 // (multiselected tab moving towards the dragged tab) and draggedTab. Those items 1500 // in the middle should move in the opposite direction of movingTab. 1501 1502 let movingTabSize = 1503 movingTab.getBoundingClientRect()[ 1504 this._tabbrowserTabs.verticalMode ? "height" : "width" 1505 ]; 1506 1507 for (let middleItem of middleItems) { 1508 if (isTab(middleItem)) { 1509 if (middleItem.pinned != movingTab.pinned) { 1510 // Don't mix pinned and unpinned tabs 1511 break; 1512 } 1513 if (middleItem.multiselected) { 1514 // Skip because this multiselected tab should 1515 // be shifted towards the dragged Tab. 1516 continue; 1517 } 1518 } 1519 middleItem = elementToMove(middleItem); 1520 let middleItemSize = 1521 middleItem.getBoundingClientRect()[ 1522 this._tabbrowserTabs.verticalMode ? "height" : "width" 1523 ]; 1524 1525 if (!middleItem._moveTogetherSelectedTabsData?.translatePos) { 1526 middleItem._moveTogetherSelectedTabsData = { translatePos: 0 }; 1527 } 1528 movingTab._moveTogetherSelectedTabsData.translatePos += 1529 isBeforeSelectedTab ? middleItemSize : -middleItemSize; 1530 middleItem._moveTogetherSelectedTabsData.translatePos = 1531 isBeforeSelectedTab ? -movingTabSize : movingTabSize; 1532 1533 middleItem.toggleAttribute("multiselected-move-together", true); 1534 } 1535 }; 1536 1537 let tabIndex = selectedTabs.indexOf(tab); 1538 1539 // Animate left or top selected tabs 1540 for (let i = 0; i < tabIndex; i++) { 1541 let movingTab = selectedTabs[i]; 1542 if (animate) { 1543 addAnimationData(movingTab, true); 1544 } else { 1545 gBrowser.moveTabBefore(movingTab, tab); 1546 } 1547 } 1548 1549 // Animate right or bottom selected tabs 1550 for (let i = selectedTabs.length - 1; i > tabIndex; i--) { 1551 let movingTab = selectedTabs[i]; 1552 if (animate) { 1553 addAnimationData(movingTab, false); 1554 } else { 1555 gBrowser.moveTabAfter(movingTab, tab); 1556 } 1557 } 1558 1559 // Slide the relevant tabs to their new position. 1560 for (let item of this._tabbrowserTabs.dragAndDropElements) { 1561 item = elementToMove(item); 1562 if (item._moveTogetherSelectedTabsData?.translatePos) { 1563 let translatePos = 1564 (this._rtlMode ? -1 : 1) * 1565 item._moveTogetherSelectedTabsData.translatePos; 1566 item.style.transform = `translate${ 1567 this._tabbrowserTabs.verticalMode ? "Y" : "X" 1568 }(${translatePos}px)`; 1569 } 1570 } 1571 } 1572 1573 #isAnimatingMoveTogetherSelectedTabs() { 1574 for (let tab of gBrowser.selectedTabs) { 1575 if (tab._moveTogetherSelectedTabsData?.animate) { 1576 return true; 1577 } 1578 } 1579 return false; 1580 } 1581 1582 finishMoveTogetherSelectedTabs(tab) { 1583 if ( 1584 !tab._moveTogetherSelectedTabsData || 1585 tab._moveTogetherSelectedTabsData.finished 1586 ) { 1587 return; 1588 } 1589 1590 tab._moveTogetherSelectedTabsData.finished = true; 1591 1592 let selectedTabs = gBrowser.selectedTabs; 1593 let tabIndex = selectedTabs.indexOf(tab); 1594 1595 // Moving left or top tabs 1596 for (let i = 0; i < tabIndex; i++) { 1597 gBrowser.moveTabBefore(selectedTabs[i], tab); 1598 } 1599 1600 // Moving right or bottom tabs 1601 for (let i = selectedTabs.length - 1; i > tabIndex; i--) { 1602 gBrowser.moveTabAfter(selectedTabs[i], tab); 1603 } 1604 1605 for (let item of this._tabbrowserTabs.dragAndDropElements) { 1606 item = elementToMove(item); 1607 item.style.transform = ""; 1608 item.removeAttribute("multiselected-move-together"); 1609 delete item._moveTogetherSelectedTabsData; 1610 } 1611 } 1612 1613 // Drag over 1614 1615 _animateExpandedPinnedTabMove(event) { 1616 let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0); 1617 let dragData = draggedTab._dragData; 1618 let movingTabs = dragData.movingTabs; 1619 1620 dragData.animLastScreenX ??= dragData.screenX; 1621 dragData.animLastScreenY ??= dragData.screenY; 1622 1623 let screenX = event.screenX; 1624 let screenY = event.screenY; 1625 1626 if ( 1627 screenY == dragData.animLastScreenY && 1628 screenX == dragData.animLastScreenX 1629 ) { 1630 return; 1631 } 1632 1633 let tabs = this._tabbrowserTabs.visibleTabs.slice( 1634 0, 1635 gBrowser.pinnedTabCount 1636 ); 1637 1638 let directionX = screenX > dragData.animLastScreenX; 1639 let directionY = screenY > dragData.animLastScreenY; 1640 dragData.animLastScreenY = screenY; 1641 dragData.animLastScreenX = screenX; 1642 1643 let { width: tabWidth, height: tabHeight } = 1644 draggedTab.getBoundingClientRect(); 1645 let shiftSizeX = tabWidth * movingTabs.length; 1646 let shiftSizeY = tabHeight; 1647 dragData.tabWidth = tabWidth; 1648 dragData.tabHeight = tabHeight; 1649 1650 // Move the dragged tab based on the mouse position. 1651 let firstTabInRow; 1652 let lastTabInRow; 1653 let lastTab = tabs.at(-1); 1654 let periphery = document.getElementById( 1655 "tabbrowser-arrowscrollbox-periphery" 1656 ); 1657 if (RTL_UI) { 1658 firstTabInRow = 1659 tabs.length >= this._maxTabsPerRow 1660 ? tabs[this._maxTabsPerRow - 1] 1661 : lastTab; 1662 lastTabInRow = tabs[0]; 1663 } else { 1664 firstTabInRow = tabs[0]; 1665 lastTabInRow = 1666 tabs.length >= this._maxTabsPerRow 1667 ? tabs[this._maxTabsPerRow - 1] 1668 : lastTab; 1669 } 1670 let lastMovingTabScreenX = movingTabs.at(-1).screenX; 1671 let lastMovingTabScreenY = movingTabs.at(-1).screenY; 1672 let firstMovingTabScreenX = movingTabs[0].screenX; 1673 let firstMovingTabScreenY = movingTabs[0].screenY; 1674 let translateX = screenX - dragData.screenX; 1675 let translateY = screenY - dragData.screenY; 1676 let firstBoundX = firstTabInRow.screenX - firstMovingTabScreenX; 1677 let firstBoundY = this._tabbrowserTabs.screenY - firstMovingTabScreenY; 1678 let lastBoundX = 1679 lastTabInRow.screenX + 1680 lastTabInRow.getBoundingClientRect().width - 1681 (lastMovingTabScreenX + tabWidth); 1682 let lastBoundY = periphery.screenY - (lastMovingTabScreenY + tabHeight); 1683 translateX = Math.min(Math.max(translateX, firstBoundX), lastBoundX); 1684 translateY = Math.min(Math.max(translateY, firstBoundY), lastBoundY); 1685 1686 // Center the tab under the cursor if the tab is not under the cursor while dragging 1687 if ( 1688 screen < draggedTab.screenY + translateY || 1689 screen > draggedTab.screenY + tabHeight + translateY 1690 ) { 1691 translateY = screen - draggedTab.screenY - tabHeight / 2; 1692 } 1693 1694 for (let tab of movingTabs) { 1695 tab.style.transform = `translate(${translateX}px, ${translateY}px)`; 1696 } 1697 1698 dragData.translateX = translateX; 1699 dragData.translateY = translateY; 1700 1701 // Determine what tab we're dragging over. 1702 // * Single tab dragging: Point of reference is the center of the dragged tab. If that 1703 // point touches a background tab, the dragged tab would take that 1704 // tab's position when dropped. 1705 // * Multiple tabs dragging: All dragged tabs are one "giant" tab with two 1706 // points of reference (center of tabs on the extremities). When 1707 // mouse is moving from top to bottom, the bottom reference gets activated, 1708 // otherwise the top reference will be used. Everything else works the same 1709 // as single tab dragging. 1710 // * We're doing a binary search in order to reduce the amount of 1711 // tabs we need to check. 1712 1713 tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab); 1714 let firstTabCenterX = firstMovingTabScreenX + translateX + tabWidth / 2; 1715 let lastTabCenterX = lastMovingTabScreenX + translateX + tabWidth / 2; 1716 let tabCenterX = directionX ? lastTabCenterX : firstTabCenterX; 1717 let firstTabCenterY = firstMovingTabScreenY + translateY + tabHeight / 2; 1718 let lastTabCenterY = lastMovingTabScreenY + translateY + tabHeight / 2; 1719 let tabCenterY = directionY ? lastTabCenterY : firstTabCenterY; 1720 1721 let shiftNumber = this._maxTabsPerRow - movingTabs.length; 1722 1723 let getTabShift = (tab, dropIndex) => { 1724 if ( 1725 tab.elementIndex < draggedTab.elementIndex && 1726 tab.elementIndex >= dropIndex 1727 ) { 1728 // If tab is at the end of a row, shift back and down 1729 let tabRow = Math.ceil((tab.elementIndex + 1) / this._maxTabsPerRow); 1730 let shiftedTabRow = Math.ceil( 1731 (tab.elementIndex + 1 + movingTabs.length) / this._maxTabsPerRow 1732 ); 1733 if (tab.elementIndex && tabRow != shiftedTabRow) { 1734 return [ 1735 RTL_UI ? tabWidth * shiftNumber : -tabWidth * shiftNumber, 1736 shiftSizeY, 1737 ]; 1738 } 1739 return [RTL_UI ? -shiftSizeX : shiftSizeX, 0]; 1740 } 1741 if ( 1742 tab.elementIndex > draggedTab.elementIndex && 1743 tab.elementIndex < dropIndex 1744 ) { 1745 // If tab is not index 0 and at the start of a row, shift across and up 1746 let tabRow = Math.floor(tab.elementIndex / this._maxTabsPerRow); 1747 let shiftedTabRow = Math.floor( 1748 (tab.elementIndex - movingTabs.length) / this._maxTabsPerRow 1749 ); 1750 if (tab.elementIndex && tabRow != shiftedTabRow) { 1751 return [ 1752 RTL_UI ? -tabWidth * shiftNumber : tabWidth * shiftNumber, 1753 -shiftSizeY, 1754 ]; 1755 } 1756 return [RTL_UI ? shiftSizeX : -shiftSizeX, 0]; 1757 } 1758 return [0, 0]; 1759 }; 1760 1761 let low = 0; 1762 let high = tabs.length - 1; 1763 let newIndex = -1; 1764 let oldIndex = 1765 dragData.animDropElementIndex ?? movingTabs[0].elementIndex; 1766 while (low <= high) { 1767 let mid = Math.floor((low + high) / 2); 1768 if (tabs[mid] == draggedTab && ++mid > high) { 1769 break; 1770 } 1771 let [shiftX, shiftY] = getTabShift(tabs[mid], oldIndex); 1772 screenX = tabs[mid].screenX + shiftX; 1773 screenY = tabs[mid].screenY + shiftY; 1774 1775 if (screenY + tabHeight < tabCenterY) { 1776 low = mid + 1; 1777 } else if (screenY > tabCenterY) { 1778 high = mid - 1; 1779 } else if ( 1780 RTL_UI ? screenX + tabWidth < tabCenterX : screenX > tabCenterX 1781 ) { 1782 high = mid - 1; 1783 } else if ( 1784 RTL_UI ? screenX > tabCenterX : screenX + tabWidth < tabCenterX 1785 ) { 1786 low = mid + 1; 1787 } else { 1788 newIndex = tabs[mid].elementIndex; 1789 break; 1790 } 1791 } 1792 1793 if (newIndex >= oldIndex && newIndex < tabs.length) { 1794 newIndex++; 1795 } 1796 1797 if (newIndex < 0) { 1798 newIndex = oldIndex; 1799 } 1800 1801 if (newIndex == dragData.animDropElementIndex) { 1802 return; 1803 } 1804 1805 dragData.animDropElementIndex = newIndex; 1806 dragData.dropElement = tabs[Math.min(newIndex, tabs.length - 1)]; 1807 dragData.dropBefore = newIndex < tabs.length; 1808 1809 // Shift background tabs to leave a gap where the dragged tab 1810 // would currently be dropped. 1811 for (let tab of tabs) { 1812 if (tab != draggedTab) { 1813 let [shiftX, shiftY] = getTabShift(tab, newIndex); 1814 tab.style.transform = 1815 shiftX || shiftY ? `translate(${shiftX}px, ${shiftY}px)` : ""; 1816 } 1817 } 1818 } 1819 1820 // eslint-disable-next-line complexity 1821 _animateTabMove(event) { 1822 let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0); 1823 let dragData = draggedTab._dragData; 1824 let movingTabs = dragData.movingTabs; 1825 let movingTabsSet = dragData.movingTabsSet; 1826 1827 dragData.animLastScreenPos ??= this._tabbrowserTabs.verticalMode 1828 ? dragData.screenY 1829 : dragData.screenX; 1830 let screen = this._tabbrowserTabs.verticalMode 1831 ? event.screenY 1832 : event.screenX; 1833 if (screen == dragData.animLastScreenPos) { 1834 return; 1835 } 1836 let screenForward = screen > dragData.animLastScreenPos; 1837 dragData.animLastScreenPos = screen; 1838 1839 this._clearDragOverGroupingTimer(); 1840 this.#clearPinnedDropIndicatorTimer(); 1841 1842 let isPinned = draggedTab.pinned; 1843 let numPinned = gBrowser.pinnedTabCount; 1844 let dragAndDropElements = this._tabbrowserTabs.dragAndDropElements; 1845 let tabs = dragAndDropElements.slice( 1846 isPinned ? 0 : numPinned, 1847 isPinned ? numPinned : undefined 1848 ); 1849 1850 if (this._rtlMode) { 1851 tabs.reverse(); 1852 } 1853 1854 let bounds = ele => window.windowUtils.getBoundsWithoutFlushing(ele); 1855 let logicalForward = screenForward != this._rtlMode; 1856 let screenAxis = this._tabbrowserTabs.verticalMode 1857 ? "screenY" 1858 : "screenX"; 1859 let size = this._tabbrowserTabs.verticalMode ? "height" : "width"; 1860 let translateAxis = this._tabbrowserTabs.verticalMode 1861 ? "translateY" 1862 : "translateX"; 1863 let { width: tabWidth, height: tabHeight } = bounds(draggedTab); 1864 let tabSize = this._tabbrowserTabs.verticalMode ? tabHeight : tabWidth; 1865 let translateX = event.screenX - dragData.screenX; 1866 let translateY = event.screenY - dragData.screenY; 1867 1868 dragData.tabWidth = tabWidth; 1869 dragData.tabHeight = tabHeight; 1870 dragData.translateX = translateX; 1871 dragData.translateY = translateY; 1872 1873 // Move the dragged tab based on the mouse position. 1874 let periphery = document.getElementById( 1875 "tabbrowser-arrowscrollbox-periphery" 1876 ); 1877 let lastMovingTab = movingTabs.at(-1); 1878 let firstMovingTab = movingTabs[0]; 1879 let endEdge = ele => ele[screenAxis] + bounds(ele)[size]; 1880 let lastMovingTabScreen = endEdge(lastMovingTab); 1881 let firstMovingTabScreen = firstMovingTab[screenAxis]; 1882 let shiftSize = lastMovingTabScreen - firstMovingTabScreen; 1883 let translate = screen - dragData[screenAxis]; 1884 1885 // Constrain the range over which the moving tabs can move between the edge of the tabstrip and periphery. 1886 // Add 1 to periphery so we don't overlap it. 1887 let startBound = this._rtlMode 1888 ? endEdge(periphery) + 1 - firstMovingTabScreen 1889 : this._tabbrowserTabs[screenAxis] - firstMovingTabScreen; 1890 let endBound = this._rtlMode 1891 ? endEdge(this._tabbrowserTabs) - lastMovingTabScreen 1892 : periphery[screenAxis] - 1 - lastMovingTabScreen; 1893 translate = Math.min(Math.max(translate, startBound), endBound); 1894 1895 // Center the tab under the cursor if the tab is not under the cursor while dragging 1896 let draggedTabScreenAxis = draggedTab[screenAxis] + translate; 1897 if ( 1898 (screen < draggedTabScreenAxis || 1899 screen > draggedTabScreenAxis + tabSize) && 1900 draggedTabScreenAxis + tabSize < endBound && 1901 draggedTabScreenAxis > startBound 1902 ) { 1903 translate = screen - draggedTab[screenAxis] - tabSize / 2; 1904 // Ensure, after the above calculation, we are still within bounds 1905 translate = Math.min(Math.max(translate, startBound), endBound); 1906 } 1907 1908 if (!gBrowser.pinnedTabCount && !this._dragToPinPromoCard.shouldRender) { 1909 let pinnedDropIndicatorMargin = parseFloat( 1910 window.getComputedStyle(this._pinnedDropIndicator).marginInline 1911 ); 1912 this._checkWithinPinnedContainerBounds({ 1913 firstMovingTabScreen, 1914 lastMovingTabScreen, 1915 pinnedTabsStartEdge: this._rtlMode 1916 ? endEdge(this._tabbrowserTabs.arrowScrollbox) + 1917 pinnedDropIndicatorMargin 1918 : this[screenAxis], 1919 pinnedTabsEndEdge: this._rtlMode 1920 ? endEdge(this._tabbrowserTabs) 1921 : this._tabbrowserTabs.arrowScrollbox[screenAxis] - 1922 pinnedDropIndicatorMargin, 1923 translate, 1924 draggedTab, 1925 }); 1926 } 1927 1928 for (let item of movingTabs) { 1929 item = elementToMove(item); 1930 item.style.transform = `${translateAxis}(${translate}px)`; 1931 } 1932 1933 dragData.translatePos = translate; 1934 1935 tabs = tabs.filter(t => !movingTabsSet.has(t) || t == draggedTab); 1936 1937 /** 1938 * When the `draggedTab` is just starting to move, the `draggedTab` is in 1939 * its original location and the `dropElementIndex == draggedTab.elementIndex`. 1940 * Any tabs or tab group labels passed in as `item` will result in a 0 shift 1941 * because all of those items should also continue to appear in their original 1942 * locations. 1943 * 1944 * Once the `draggedTab` is more "backward" in the tab strip than its original 1945 * position, any tabs or tab group labels between the `draggedTab`'s original 1946 * `elementIndex` and the current `dropElementIndex` should shift "forward" 1947 * out of the way of the dragging tabs. 1948 * 1949 * When the `draggedTab` is more "forward" in the tab strip than its original 1950 * position, any tabs or tab group labels between the `draggedTab`'s original 1951 * `elementIndex` and the current `dropElementIndex` should shift "backward" 1952 * out of the way of the dragging tabs. 1953 * 1954 * @param {MozTabbrowserTab|MozTabbrowserTabGroup.label} item 1955 * @param {number} dropElementIndex 1956 * @returns {number} 1957 */ 1958 let getTabShift = (item, dropElementIndex) => { 1959 if ( 1960 item.elementIndex < draggedTab.elementIndex && 1961 item.elementIndex >= dropElementIndex 1962 ) { 1963 return this._rtlMode ? -shiftSize : shiftSize; 1964 } 1965 if ( 1966 item.elementIndex > draggedTab.elementIndex && 1967 item.elementIndex < dropElementIndex 1968 ) { 1969 return this._rtlMode ? shiftSize : -shiftSize; 1970 } 1971 return 0; 1972 }; 1973 1974 let oldDropElementIndex = 1975 dragData.animDropElementIndex ?? movingTabs[0].elementIndex; 1976 1977 /** 1978 * Returns the higher % by which one element overlaps another 1979 * in the tab strip. 1980 * 1981 * When element 1 is further forward in the tab strip: 1982 * 1983 * p1 p2 p1+s1 p2+s2 1984 * | | | | 1985 * --------------------------------- 1986 * ======================== 1987 * s1 1988 * =================== 1989 * s2 1990 * ========== 1991 * overlap 1992 * 1993 * When element 2 is further forward in the tab strip: 1994 * 1995 * p2 p1 p2+s2 p1+s1 1996 * | | | | 1997 * --------------------------------- 1998 * ======================== 1999 * s2 2000 * =================== 2001 * s1 2002 * ========== 2003 * overlap 2004 * 2005 * @param {number} p1 2006 * Position (x or y value in screen coordinates) of element 1. 2007 * @param {number} s1 2008 * Size (width or height) of element 1. 2009 * @param {number} p2 2010 * Position (x or y value in screen coordinates) of element 2. 2011 * @param {number} s2 2012 * Size (width or height) of element 1. 2013 * @returns {number} 2014 * Percent between 0.0 and 1.0 (inclusive) of element 1 or element 2 2015 * that is overlapped by the other element. If the elements have 2016 * different sizes, then this returns the larger overlap percentage. 2017 */ 2018 function greatestOverlap(p1, s1, p2, s2) { 2019 let overlapSize; 2020 if (p1 < p2) { 2021 // element 1 starts first 2022 overlapSize = p1 + s1 - p2; 2023 } else { 2024 // element 2 starts first 2025 overlapSize = p2 + s2 - p1; 2026 } 2027 2028 // No overlap if size is <= 0 2029 if (overlapSize <= 0) { 2030 return 0; 2031 } 2032 2033 // Calculate the overlap fraction from each element's perspective. 2034 let overlapPercent = Math.max(overlapSize / s1, overlapSize / s2); 2035 2036 return Math.min(overlapPercent, 1); 2037 } 2038 2039 /** 2040 * Determine what tab/tab group label we're dragging over. 2041 * 2042 * When dragging right or downwards, the reference point for overlap is 2043 * the right or bottom edge of the most forward moving tab. 2044 * 2045 * When dragging left or upwards, the reference point for overlap is the 2046 * left or top edge of the most backward moving tab. 2047 * 2048 * @returns {Element|null} 2049 * The tab or tab group label that should be used to visually shift tab 2050 * strip elements out of the way of the dragged tab(s) during a drag 2051 * operation. Note: this is not used to determine where the dragged 2052 * tab(s) will be dropped, it is only used for visual animation at this 2053 * time. 2054 */ 2055 let getOverlappedElement = () => { 2056 let point = 2057 (screenForward ? lastMovingTabScreen : firstMovingTabScreen) + 2058 translate; 2059 let low = 0; 2060 let high = tabs.length - 1; 2061 while (low <= high) { 2062 let mid = Math.floor((low + high) / 2); 2063 if (tabs[mid] == draggedTab && ++mid > high) { 2064 break; 2065 } 2066 let element = tabs[mid]; 2067 let elementForSize = elementToMove(element); 2068 screen = 2069 elementForSize[screenAxis] + 2070 getTabShift(element, oldDropElementIndex); 2071 2072 if (screen > point) { 2073 high = mid - 1; 2074 } else if (screen + bounds(elementForSize)[size] < point) { 2075 low = mid + 1; 2076 } else { 2077 return element; 2078 } 2079 } 2080 return null; 2081 }; 2082 2083 let dropElement = getOverlappedElement(); 2084 2085 let newDropElementIndex; 2086 if (dropElement) { 2087 newDropElementIndex = dropElement.elementIndex; 2088 } else { 2089 // When the dragged element(s) moves past a tab strip item, the dragged 2090 // element's leading edge starts dragging over empty space, resulting in 2091 // no overlapping `dropElement`. In these cases, try to fall back to the 2092 // previous animation drop element index to avoid unstable animations 2093 // (tab strip items snapping back and forth to shift out of the way of 2094 // the dragged element(s)). 2095 newDropElementIndex = oldDropElementIndex; 2096 2097 // We always want to have a `dropElement` so that we can determine where to 2098 // logically drop the dragged element(s). 2099 // 2100 // It's tempting to set `dropElement` to 2101 // `this.dragAndDropElements.at(oldDropElementIndex)`, and that is correct 2102 // for most cases, but there are edge cases: 2103 // 2104 // 1) the drop element index range needs to be one larger than the number of 2105 // items that can move in the tab strip. The simplest example is when all 2106 // tabs are ungrouped and unpinned: for 5 tabs, the drop element index needs 2107 // to be able to go from 0 (become the first tab) to 5 (become the last tab). 2108 // `this.dragAndDropElements.at(5)` would be `undefined` when dragging to the 2109 // end of the tab strip. In this specific case, it works to fall back to 2110 // setting the drop element to the last tab. 2111 // 2112 // 2) the `elementIndex` values of the tab strip items do not change during 2113 // the drag operation. When dragging the last tab or multiple tabs at the end 2114 // of the tab strip, having `dropElement` fall back to the last tab makes the 2115 // drop element one of the moving tabs. This can have some unexpected behavior 2116 // if not careful. Falling back to the last tab that's not moving (instead of 2117 // just the last tab) helps ensure that `dropElement` is always a stable target 2118 // to drop next to. 2119 // 2120 // 3) all of the elements in the tab strip are moving, in which case there can't 2121 // be a drop element and it should stay `undefined`. 2122 // 2123 // 4) we just started dragging and the `oldDropElementIndex` has its default 2124 // valuë of `movingTabs[0].elementIndex`. In this case, the drop element 2125 // shouldn't be a moving tab, so keep it `undefined`. 2126 let lastPossibleDropElement = this._rtlMode 2127 ? tabs.find(t => t != draggedTab) 2128 : tabs.findLast(t => t != draggedTab); 2129 let maxElementIndexForDropElement = 2130 lastPossibleDropElement?.elementIndex; 2131 if (Number.isInteger(maxElementIndexForDropElement)) { 2132 let index = Math.min( 2133 oldDropElementIndex, 2134 maxElementIndexForDropElement 2135 ); 2136 let oldDropElementCandidate = 2137 this._tabbrowserTabs.dragAndDropElements.at(index); 2138 if (!movingTabsSet.has(oldDropElementCandidate)) { 2139 dropElement = oldDropElementCandidate; 2140 } 2141 } 2142 } 2143 2144 let moveOverThreshold; 2145 let overlapPercent; 2146 let dropBefore; 2147 if (dropElement) { 2148 let dropElementForOverlap = elementToMove(dropElement); 2149 2150 let dropElementScreen = dropElementForOverlap[screenAxis]; 2151 let dropElementPos = 2152 dropElementScreen + getTabShift(dropElement, oldDropElementIndex); 2153 let dropElementSize = bounds(dropElementForOverlap)[size]; 2154 let firstMovingTabPos = firstMovingTabScreen + translate; 2155 overlapPercent = greatestOverlap( 2156 firstMovingTabPos, 2157 shiftSize, 2158 dropElementPos, 2159 dropElementSize 2160 ); 2161 2162 moveOverThreshold = gBrowser._tabGroupsEnabled 2163 ? Services.prefs.getIntPref( 2164 "browser.tabs.dragDrop.moveOverThresholdPercent" 2165 ) / 100 2166 : 0.5; 2167 moveOverThreshold = Math.min(1, Math.max(0, moveOverThreshold)); 2168 let shouldMoveOver = overlapPercent > moveOverThreshold; 2169 if (logicalForward && shouldMoveOver) { 2170 newDropElementIndex++; 2171 } else if (!logicalForward && !shouldMoveOver) { 2172 newDropElementIndex++; 2173 if (newDropElementIndex > oldDropElementIndex) { 2174 // FIXME: Not quite sure what's going on here, but this check 2175 // prevents jittery back-and-forth movement of background tabs 2176 // in certain cases. 2177 newDropElementIndex = oldDropElementIndex; 2178 } 2179 } 2180 2181 // Recalculate the overlap with the updated drop index for when the 2182 // drop element moves over. 2183 dropElementPos = 2184 dropElementScreen + getTabShift(dropElement, newDropElementIndex); 2185 overlapPercent = greatestOverlap( 2186 firstMovingTabPos, 2187 shiftSize, 2188 dropElementPos, 2189 dropElementSize 2190 ); 2191 dropBefore = firstMovingTabPos < dropElementPos; 2192 if (this._rtlMode) { 2193 dropBefore = !dropBefore; 2194 } 2195 2196 // If dragging a group over another group, don't make it look like it is 2197 // possible to drop the dragged group inside the other group. 2198 if ( 2199 isTabGroupLabel(draggedTab) && 2200 dropElement?.group && 2201 (!dropElement.group.collapsed || 2202 (dropElement.group.collapsed && dropElement.group.hasActiveTab)) 2203 ) { 2204 let overlappedGroup = dropElement.group; 2205 2206 if (isTabGroupLabel(dropElement)) { 2207 dropBefore = true; 2208 newDropElementIndex = dropElement.elementIndex; 2209 } else { 2210 dropBefore = false; 2211 let lastVisibleTabInGroup = overlappedGroup.tabs.findLast( 2212 tab => tab.visible 2213 ); 2214 newDropElementIndex = lastVisibleTabInGroup.elementIndex + 1; 2215 } 2216 2217 dropElement = overlappedGroup; 2218 } 2219 2220 // Constrain drop direction at the boundary between pinned and 2221 // unpinned tabs so that they don't mix together. 2222 let isOutOfBounds = isPinned 2223 ? dropElement.elementIndex >= numPinned 2224 : dropElement.elementIndex < numPinned; 2225 if (isOutOfBounds) { 2226 // Drop after last pinned tab 2227 dropElement = this._tabbrowserTabs.dragAndDropElements[numPinned - 1]; 2228 dropBefore = false; 2229 } 2230 } 2231 2232 if ( 2233 gBrowser._tabGroupsEnabled && 2234 isTab(draggedTab) && 2235 !isPinned && 2236 (!numPinned || newDropElementIndex >= numPinned) 2237 ) { 2238 let dragOverGroupingThreshold = 1 - moveOverThreshold; 2239 let groupingDelay = Services.prefs.getIntPref( 2240 "browser.tabs.dragDrop.createGroup.delayMS" 2241 ); 2242 2243 // When dragging tab(s) over an ungrouped tab, signal to the user 2244 // that dropping the tab(s) will create a new tab group. 2245 let shouldCreateGroupOnDrop = 2246 !movingTabsSet.has(dropElement) && 2247 isTab(dropElement) && 2248 !dropElement?.group && 2249 overlapPercent > dragOverGroupingThreshold; 2250 2251 // When dragging tab(s) over a collapsed tab group label, signal to the 2252 // user that dropping the tab(s) will add them to the group. 2253 let shouldDropIntoCollapsedTabGroup = 2254 isTabGroupLabel(dropElement) && 2255 dropElement.group.collapsed && 2256 overlapPercent > dragOverGroupingThreshold; 2257 2258 if (shouldCreateGroupOnDrop) { 2259 this._dragOverGroupingTimer = setTimeout(() => { 2260 this._triggerDragOverGrouping(dropElement); 2261 dragData.shouldCreateGroupOnDrop = true; 2262 this._setDragOverGroupColor(dragData.tabGroupCreationColor); 2263 }, groupingDelay); 2264 } else if (shouldDropIntoCollapsedTabGroup) { 2265 this._dragOverGroupingTimer = setTimeout(() => { 2266 this._triggerDragOverGrouping(dropElement); 2267 dragData.shouldDropIntoCollapsedTabGroup = true; 2268 this._setDragOverGroupColor(dropElement.group.color); 2269 }, groupingDelay); 2270 } else { 2271 this._tabbrowserTabs.removeAttribute("movingtab-group"); 2272 this._resetGroupTarget( 2273 document.querySelector("[dragover-groupTarget]") 2274 ); 2275 2276 delete dragData.shouldCreateGroupOnDrop; 2277 delete dragData.shouldDropIntoCollapsedTabGroup; 2278 2279 // Default to dropping into `dropElement`'s tab group, if it exists. 2280 let dropElementGroup = dropElement?.group; 2281 let colorCode = dropElementGroup?.color; 2282 2283 let lastUnmovingTabInGroup = dropElementGroup?.tabs.findLast( 2284 t => !movingTabsSet.has(t) 2285 ); 2286 if ( 2287 isTab(dropElement) && 2288 dropElementGroup && 2289 dropElement == lastUnmovingTabInGroup && 2290 !dropBefore && 2291 overlapPercent < dragOverGroupingThreshold 2292 ) { 2293 // Dragging tab over the last tab of a tab group, but not enough 2294 // for it to drop into the tab group. Drop it after the tab group instead. 2295 dropElement = dropElementGroup; 2296 colorCode = undefined; 2297 } else if (isTabGroupLabel(dropElement)) { 2298 if (dropBefore) { 2299 // Dropping right before the tab group. 2300 dropElement = dropElementGroup; 2301 colorCode = undefined; 2302 } else if (dropElementGroup.collapsed) { 2303 // Dropping right after the collapsed tab group. 2304 dropElement = dropElementGroup; 2305 colorCode = undefined; 2306 } else { 2307 // Dropping right before the first tab in the tab group. 2308 dropElement = dropElementGroup.tabs[0]; 2309 dropBefore = true; 2310 } 2311 } 2312 this._setDragOverGroupColor(colorCode); 2313 this._tabbrowserTabs.toggleAttribute( 2314 "movingtab-addToGroup", 2315 colorCode 2316 ); 2317 this._tabbrowserTabs.toggleAttribute("movingtab-ungroup", !colorCode); 2318 } 2319 } 2320 2321 if ( 2322 newDropElementIndex == oldDropElementIndex && 2323 dropBefore == dragData.dropBefore && 2324 dropElement == dragData.dropElement 2325 ) { 2326 return; 2327 } 2328 2329 dragData.dropElement = dropElement; 2330 dragData.dropBefore = dropBefore; 2331 dragData.animDropElementIndex = newDropElementIndex; 2332 2333 // Shift background tabs to leave a gap where the dragged tab 2334 // would currently be dropped. 2335 for (let item of tabs) { 2336 if (item == draggedTab) { 2337 continue; 2338 } 2339 2340 let shift = getTabShift(item, newDropElementIndex); 2341 let transform = shift ? `${translateAxis}(${shift}px)` : ""; 2342 item = elementToMove(item); 2343 item.style.transform = transform; 2344 } 2345 } 2346 2347 _checkWithinPinnedContainerBounds({ 2348 firstMovingTabScreen, 2349 lastMovingTabScreen, 2350 pinnedTabsStartEdge, 2351 pinnedTabsEndEdge, 2352 translate, 2353 draggedTab, 2354 }) { 2355 // Display the pinned drop indicator based on the position of the moving tabs. 2356 // If the indicator is not yet shown, display once we are within a pinned tab width/height 2357 // distance. 2358 let firstMovingTabPosition = firstMovingTabScreen + translate; 2359 let lastMovingTabPosition = lastMovingTabScreen + translate; 2360 // Approximation of half pinned tabs width and height in horizontal or grid mode (40) is a sufficient 2361 // buffer to display the pinned drop indicator slightly before dragging over it. Exact value is 2362 // not necessary. 2363 let buffer = 20; 2364 let inPinnedRange = this._rtlMode 2365 ? lastMovingTabPosition >= pinnedTabsStartEdge 2366 : firstMovingTabPosition <= pinnedTabsEndEdge; 2367 let inVisibleRange = this._rtlMode 2368 ? lastMovingTabPosition >= pinnedTabsStartEdge - buffer 2369 : firstMovingTabPosition <= pinnedTabsEndEdge + buffer; 2370 let isVisible = this._pinnedDropIndicator.hasAttribute("visible"); 2371 let isInteractive = this._pinnedDropIndicator.hasAttribute("interactive"); 2372 2373 if ( 2374 this.#pinnedDropIndicatorTimeout && 2375 !inPinnedRange && 2376 !inVisibleRange && 2377 !isVisible && 2378 !isInteractive 2379 ) { 2380 this.#resetPinnedDropIndicator(); 2381 } else if ( 2382 isTab(draggedTab) && 2383 ((inVisibleRange && !isVisible) || (inPinnedRange && !isInteractive)) 2384 ) { 2385 // On drag into pinned container 2386 let tabbrowserTabsRect = window.windowUtils.getBoundsWithoutFlushing( 2387 this._tabbrowserTabs 2388 ); 2389 if (!this._tabbrowserTabs.verticalMode) { 2390 // The tabbrowser container expands with the expansion of the 2391 // drop indicator - prevent that by setting maxWidth first. 2392 this._tabbrowserTabs.style.maxWidth = tabbrowserTabsRect.width + "px"; 2393 } 2394 if (isVisible) { 2395 this._pinnedDropIndicator.setAttribute("interactive", ""); 2396 } else if (!this.#pinnedDropIndicatorTimeout) { 2397 let interactionDelay = Services.prefs.getIntPref( 2398 "browser.tabs.dragDrop.pinInteractionCue.delayMS" 2399 ); 2400 this.#pinnedDropIndicatorTimeout = setTimeout(() => { 2401 if (this.#isMovingTab()) { 2402 this._pinnedDropIndicator.setAttribute("visible", ""); 2403 this._pinnedDropIndicator.setAttribute("interactive", ""); 2404 } 2405 }, interactionDelay); 2406 } 2407 } else if (!inPinnedRange) { 2408 this._pinnedDropIndicator.removeAttribute("interactive"); 2409 } 2410 } 2411 2412 #clearPinnedDropIndicatorTimer() { 2413 if (this.#pinnedDropIndicatorTimeout) { 2414 clearTimeout(this.#pinnedDropIndicatorTimeout); 2415 this.#pinnedDropIndicatorTimeout = null; 2416 } 2417 } 2418 2419 #resetPinnedDropIndicator() { 2420 this.#clearPinnedDropIndicatorTimer(); 2421 this._pinnedDropIndicator.removeAttribute("visible"); 2422 this._pinnedDropIndicator.removeAttribute("interactive"); 2423 } 2424 2425 finishAnimateTabMove() { 2426 if (!this.#isMovingTab()) { 2427 return; 2428 } 2429 2430 this.#setMovingTabMode(false); 2431 2432 for (let item of this._tabbrowserTabs.dragAndDropElements) { 2433 this._resetGroupTarget(item); 2434 item = elementToMove(item); 2435 item.style.transform = ""; 2436 } 2437 this._tabbrowserTabs.removeAttribute("movingtab-group"); 2438 this._tabbrowserTabs.removeAttribute("movingtab-ungroup"); 2439 this._tabbrowserTabs.removeAttribute("movingtab-addToGroup"); 2440 this._setDragOverGroupColor(null); 2441 this._clearDragOverGroupingTimer(); 2442 this.#resetPinnedDropIndicator(); 2443 } 2444 2445 // Drop 2446 2447 // If the tab is dropped in another window, we need to pass in the original window document 2448 _resetTabsAfterDrop(draggedTabDocument = document) { 2449 if (this._tabbrowserTabs.expandOnHover) { 2450 // Re-enable MousePosTracker after dropping 2451 MousePosTracker.addListener(document.defaultView.SidebarController); 2452 } 2453 2454 let pinnedDropIndicator = draggedTabDocument.getElementById( 2455 "pinned-drop-indicator" 2456 ); 2457 pinnedDropIndicator.removeAttribute("visible"); 2458 pinnedDropIndicator.removeAttribute("interactive"); 2459 draggedTabDocument.ownerGlobal.gBrowser.tabContainer.style.maxWidth = ""; 2460 let allTabs = draggedTabDocument.getElementsByClassName("tabbrowser-tab"); 2461 for (let tab of allTabs) { 2462 tab.style.width = ""; 2463 tab.style.left = ""; 2464 tab.style.top = ""; 2465 tab.style.maxWidth = ""; 2466 tab.removeAttribute("dragtarget"); 2467 } 2468 for (let label of draggedTabDocument.getElementsByClassName( 2469 "tab-group-label-container" 2470 )) { 2471 label.style.width = ""; 2472 label.style.height = ""; 2473 label.style.left = ""; 2474 label.style.top = ""; 2475 label.style.maxWidth = ""; 2476 label.removeAttribute("dragtarget"); 2477 } 2478 let periphery = draggedTabDocument.getElementById( 2479 "tabbrowser-arrowscrollbox-periphery" 2480 ); 2481 periphery.style.marginBlockStart = ""; 2482 periphery.style.marginInlineStart = ""; 2483 periphery.style.left = ""; 2484 periphery.style.top = ""; 2485 let pinnedTabsContainer = draggedTabDocument.getElementById( 2486 "pinned-tabs-container" 2487 ); 2488 let pinnedPeriphery = draggedTabDocument.getElementById( 2489 "pinned-tabs-container-periphery" 2490 ); 2491 pinnedPeriphery && pinnedTabsContainer.removeChild(pinnedPeriphery); 2492 pinnedTabsContainer.removeAttribute("dragActive"); 2493 pinnedTabsContainer.style.minHeight = ""; 2494 draggedTabDocument.defaultView.SidebarController.updatePinnedTabsHeightOnResize(); 2495 pinnedTabsContainer.scrollbox.style.height = ""; 2496 pinnedTabsContainer.scrollbox.style.width = ""; 2497 let arrowScrollbox = draggedTabDocument.getElementById( 2498 "tabbrowser-arrowscrollbox" 2499 ); 2500 arrowScrollbox.scrollbox.style.height = ""; 2501 arrowScrollbox.scrollbox.style.width = ""; 2502 for (let groupLabel of draggedTabDocument.getElementsByClassName( 2503 "tab-group-label-container" 2504 )) { 2505 groupLabel.style.left = ""; 2506 groupLabel.style.top = ""; 2507 } 2508 } 2509 2510 /** 2511 * @param {DragEvent} event 2512 * @returns {typeof DataTransfer.prototype.dropEffect} 2513 */ 2514 getDropEffectForTabDrag(event) { 2515 var dt = event.dataTransfer; 2516 2517 let isMovingTab = dt.mozItemCount > 0; 2518 for (let i = 0; i < dt.mozItemCount; i++) { 2519 // tabs are always added as the first type 2520 let types = dt.mozTypesAt(0); 2521 if (types[0] != TAB_DROP_TYPE) { 2522 isMovingTab = false; 2523 break; 2524 } 2525 } 2526 2527 if (isMovingTab) { 2528 let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0); 2529 if ( 2530 (isTab(sourceNode) || 2531 isTabGroupLabel(sourceNode) || 2532 isSplitViewWrapper(sourceNode)) && 2533 sourceNode.ownerGlobal.isChromeWindow && 2534 sourceNode.ownerDocument.documentElement.getAttribute("windowtype") == 2535 "navigator:browser" 2536 ) { 2537 // Do not allow transfering a private tab to a non-private window 2538 // and vice versa. 2539 if ( 2540 PrivateBrowsingUtils.isWindowPrivate(window) != 2541 PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerGlobal) 2542 ) { 2543 return "none"; 2544 } 2545 2546 if ( 2547 window.gMultiProcessBrowser != 2548 sourceNode.ownerGlobal.gMultiProcessBrowser 2549 ) { 2550 return "none"; 2551 } 2552 2553 if ( 2554 window.gFissionBrowser != sourceNode.ownerGlobal.gFissionBrowser 2555 ) { 2556 return "none"; 2557 } 2558 2559 return dt.dropEffect == "copy" ? "copy" : "move"; 2560 } 2561 } 2562 2563 if (Services.droppedLinkHandler.canDropLink(event, true)) { 2564 return "link"; 2565 } 2566 return "none"; 2567 } 2568 }; 2569 }