__MSG_config_openOptionsInTab_label__ +
+__MSG_config_sidebarPosition_caption__ + + + +
+__MSG_config_sidebarPosition_description__
+ + +__MSG_config_labelOverflowStyle_caption__ + + +
+ + + + + + ++ __MSG_config_autoAttachOnAnyOtherTrigger_caution__
+ +
+
__MSG_config_successorTabControlLevel_legacyDescription__
+
+
__MSG_config_parentTabOperationBehaviorMode_noteForPermanentlyConsistentBehaviors__
+__MSG_config_closeParentBehavior_insideSidebar__
+__MSG_config_closeParentBehavior_outsideSidebar__
+__MSG_config_moveParentBehavior_outsideSidebar__
+__MSG_config_parentTabOperationBehaviorMode_consistent_caption__
+__MSG_config_parentTabOperationBehaviorMode_consistent_notes__
+
+
+
+
+
+
+
+
+
+
__MSG_config_tabDragBehavior_description__
+| __MSG_config_tabDragBehavior_label__ | +__MSG_config_tabDragBehaviorShift_label__ | +__MSG_config_tabDragBehavior_label_behaviorInsideSidebar__ | +__MSG_config_tabDragBehavior_label_behaviorOutsideSidebar__ | +
|---|---|---|---|
+ __MSG_config_showTabDragBehaviorNotification_label__
__MSG_config_tabDragBehavior_noteForDragstartOutsideSidebar__
+__MSG_config_useCachedTree_description__
+__MSG_config_persistCachedTree_description__
+__MSG_config_userStyleRules_description_before__ + __MSG_config_userStyleRules_description_link_label__ + __MSG_config_userStyleRules_description_after__
+__MSG_config_userStyleRules_themeRules_description__
+ +__MSG_config_userStyleRules_themeRules_description_alphaVariations__
+__MSG_config_tooLargeUserStyleRulesCaution__
+ + +__MSG_config_addons_description_before__ + __MSG_config_addons_description_link_label__ + __MSG_config_addons_description_after__
+| __MSG_config_externalAddonPermissions_header_name__ | +__MSG_config_externalAddonPermissions_header_incognito__ | +__MSG_config_externalAddonPermissions_header_permissions__ | +
|---|
+
__MSG_config_colorScheme_caption__
+
+ +
+__MSG_blank_allUrlsPermissionRequiredMessage__
diff --git a/waterfox/browser/components/sidebar/resources/group-tab.html b/waterfox/browser/components/sidebar/resources/group-tab.html new file mode 100644 index 000000000000..560ed731e492 --- /dev/null +++ b/waterfox/browser/components/sidebar/resources/group-tab.html @@ -0,0 +1,319 @@ + + + + + +[+ +
];+ + +
{
+
+};+ diff --git a/waterfox/browser/components/sidebar/resources/module/InContentPanel.js b/waterfox/browser/components/sidebar/resources/module/InContentPanel.js new file mode 100644 index 000000000000..82cf04200b27 --- /dev/null +++ b/waterfox/browser/components/sidebar/resources/module/InContentPanel.js @@ -0,0 +1,598 @@ +/* +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ +'use strict'; + +// This is the base class of implementations to show custom UI on contents. + +// This script can be loaded in three ways: +// * REGULAR case: +// loaded into a public webpage +// * SIDEBAR case: +// loaded into the TST sidebar + +export default class InContentPanel { + static TYPE = 'in-content-panel'; + get type() { + return this.constructor.TYPE; + } + + panel; + root; + windowId; // for SIDEBAR case + + // -moz-platform @media rules looks unavailable on Web contents... + isWindows = /^Win/i.test(navigator.platform); + isLinux = /Linux/i.test(navigator.platform); + isMac = /^Mac/i.test(navigator.platform); + + get styleRules() { + return ` + .in-content-panel-root { + --in-content-panel-show-hide-animation: opacity 0.1s ease-out; + --in-content-panel-scale: 1; /* Web contents may be zoomed by the user, and we need to cancel the zoom effect. */ + --max-32bit-integer: 2147483647; + background: transparent; + border: 0 none; + bottom: auto; + height: 0px; + left: 0; + opacity: 1; + overflow: hidden; + position: fixed; + right: 0; + top: 0; + transition: var(--in-content-panel-show-hide-animation); + width: 100%; + z-index: var(--max-32bit-integer); + + .in-content-panel { + /* https://searchfox.org/mozilla-central/rev/dfaf02d68a7cb018b6cad7e189f450352e2cde04/toolkit/themes/shared/popup.css#11-63 */ + color-scheme: light dark; + + --panel-background: Menu; + --panel-color: MenuText; + --panel-padding-block: calc(4px / var(--in-content-panel-scale)); + --panel-padding: var(--panel-padding-block) 0; + --panel-border-radius: calc(4px / var(--in-content-panel-scale)); + --panel-border-color: ThreeDShadow; + --panel-width: initial; + + --panel-shadow-margin: 0px; + --panel-shadow: 0px 0px var(--panel-shadow-margin) hsla(0,0%,0%,.2); + -moz-window-input-region-margin: var(--panel-shadow-margin); + margin: calc(-1 * var(--panel-shadow-margin)); + + /* Panel design token theming */ + --background-color-canvas: var(--panel-background); + + /*@media (-moz-platform: linux) {*/ + ${this.isLinux ? '' : '/*'} + --panel-border-radius: calc(8px / var(--in-content-panel-scale)); + --panel-padding-block: calc(3px / var(--in-content-panel-scale)); + + @media (prefers-contrast) { + --panel-border-color: color-mix(in srgb, currentColor 60%, transparent); + } + ${this.isLinux ? '' : '*/'} + /*}*/ + + /*@media (-moz-platform: linux) or (-moz-platform: windows) {*/ + ${this.isLinux || this.isWindows ? '' : '/*'} + --panel-shadow-margin: calc(4px / var(--in-content-panel-scale)); + ${this.isLinux || this.isWindows ? '' : '*/'} + /*}*/ + + /* On some linux WMs we need to draw square menus because alpha is not available */ + @media /*(-moz-platform: linux) and*/ (not (-moz-gtk-csd-transparency-available)) { + ${this.isLinux ? '' : '/*'} + --panel-shadow-margin: 0px !important; + --panel-border-radius: 0px !important; + ${this.isLinux ? '' : '*/'} + } + + /*@media (-moz-platform: macos) {*/ + ${this.isMac ? '' : '/*'} + appearance: auto; + -moz-default-appearance: menupopup; + background-color: Menu; + --panel-background: white /* https://searchfox.org/mozilla-central/rev/86c208f86f35d53dc824f18f8e540fe5b0663870/browser/themes/shared/browser-colors.css#89 https://searchfox.org/mozilla-central/rev/86c208f86f35d53dc824f18f8e540fe5b0663870/toolkit/themes/shared/global-shared.css#128 */; + --panel-border-color: transparent; + --panel-border-radius: calc(6px / var(--in-content-panel-scale)); + ${this.isMac ? '' : '*/'} + /*}*/ + + /* https://searchfox.org/mozilla-central/rev/dfaf02d68a7cb018b6cad7e189f450352e2cde04/browser/themes/shared/tabbrowser/tab-hover-preview.css#5 */ + --panel-width: min(100%, calc(${this.BASE_PANEL_WIDTH}px / var(--in-content-panel-scale))); + --panel-padding: 0; + + /* https://searchfox.org/mozilla-central/rev/b576bae69c6f3328d2b08108538cbbf535b1b99d/toolkit/themes/shared/global-shared.css#111 */ + /* https://searchfox.org/mozilla-central/rev/b576bae69c6f3328d2b08108538cbbf535b1b99d/browser/themes/shared/browser-colors.css#90 */ + --panel-border-color: light-dark(rgb(240, 240, 244), rgb(82, 82, 94)); + + + @media (prefers-color-scheme: dark) { + --panel-background: ${this.isMac ? 'rgb(66, 65, 77)' /* https://searchfox.org/mozilla-central/rev/86c208f86f35d53dc824f18f8e540fe5b0663870/browser/themes/shared/browser-colors.css#89 https://searchfox.org/mozilla-central/rev/86c208f86f35d53dc824f18f8e540fe5b0663870/toolkit/themes/shared/global-shared.css#128 */ : 'var(--dark-popup)'}; + --panel-color: var(--dark-popup-text); + --panel-border-color: var(--dark-popup-border); + } + + background: var(--panel-background); + border: var(--panel-border-color) solid calc(1px / var(--in-content-panel-scale)); + border-radius: var(--panel-border-radius); + box-shadow: var(--panel-shadow); + box-sizing: border-box; + color: var(--panel-color); + direction: ltr; + font: Message-Box; + left: auto; + line-height: 1.5; + margin-block-start: 0px; + max-width: var(--panel-width); + min-width: var(--panel-width); + opacity: 0; + padding: 0; + position: fixed; + right: auto; + z-index: var(--max-32bit-integer); + + &.rtl { + direction: rtl; + } + &.animation { + transition: var(--in-content-panel-show-hide-animation), + left 0.1s ease-out, + margin-block-start 0.1s ease-out, + right 0.1s ease-out; + } + &.open { + opacity: 1; + } + + &.updating, + & .updating { + visibility: hidden; + } + } + + .in-content-panel-contents { + max-width: calc(var(--panel-width) - (2px / var(--in-content-panel-scale))); + min-width: calc(var(--panel-width) - (2px / var(--in-content-panel-scale))); + } + + .in-content-panel-contents { + max-height: calc(var(--panel-max-height) - (2px / var(--in-content-panel-scale))); + } + } + `; + } + + constructor(givenRoot, ...args) { + this.lastTimestamp = 0; + this.lastTimestampFor = new Map(); + + this.BASE_PANEL_WIDTH = '280px'; + + try { + this.init(givenRoot, ...args); + + browser.runtime.sendMessage({ + type: `ws:${this.type}:ready`, + }); + } + catch (error) { + console.log('TST In Content Panel fatal error: ', error); + this.root = this.onMessageSelf = this.destroySelf = null; + } + } + init(givenRoot) { // this can be overridden by subclasses + this.destroySelf = this.destroy.bind(this); + this.onMessageSelf = this.onMessage.bind(this); + + this.root = givenRoot || document.documentElement; + this.root.classList.add('in-content-panel-root'); + + const style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.textContent = this.styleRules; + this.root.appendChild(style); + + browser.runtime.onMessage.addListener(this.onMessageSelf); + window.addEventListener('unload', this.destroySelf, { once: true }); + window.addEventListener('pagehide', this.destroySelf, { once: true }); + } + + async onBeforeShow(_message, _sender) {} // this can be overridden by subclasses + + onMessage(message, sender) { + if ((this.windowId && + message?.windowId != this.windowId)) + return; + + if (message?.logging) + console.log(`${message.type}: `, message); + + switch (message?.type) { + case `ws:${this.type}:show`: + return (async () => { + await this.onBeforeShow(message, sender); + if (message.timestamp < this.lastTimestamp || + message.timestamp < (this.lastTimestampFor.get(message.targetId) || 0)) { + if (message?.logging) + console.log(`${this.type} show ${message.targetId}: expired, give up to show/update `, message.timestamp); + return true; + } + if (message?.logging) + console.log(`${this.type} show ${message.targetId}: invoked, let's show/update `, message.timestamp); + this.lastTimestamp = message.timestamp; + this.lastTimestampFor.set(message.targetId, message.timestamp); + this.prepareUI(); + this.updateUI(message); + this.panel.classList.add('open'); + return true; + })(); + + case `ws:${this.type}:hide`: + return (async () => { + // Ensure the order of messages: "show" for new target => + // "hide" for previous target. + await new Promise(requestAnimationFrame); + if (!this.panel || + (message.targetId && + this.panel.dataset.targetId != message.targetId)) { + if (message?.logging) + console.log(`${this.type} hide ${message.targetId}: already hidden, nothing to do `, message.timestamp); + if (!this.panel && !message.targetId) { // on initial case + this.lastTimestamp = message.timestamp; + } + if (message.targetId) { + this.lastTimestampFor.set(message.targetId, message.timestamp); + } + return; + } + if (message.timestamp < this.lastTimestamp || + (message.targetId && + message.timestamp < (this.lastTimestampFor.get(message.targetId) || 0))) { + if (message?.logging) + console.log(`${this.type} hide ${message.targetId}: expired, give up to hide `, message.timestamp); + return true; + } + if (message?.logging) + console.log(`${this.type} hide ${message.targetId}: invoked, let's hide `, message.timestamp); + this.lastTimestamp = message.timestamp; + if (message.targetId) { + this.lastTimestampFor.set(message.targetId, message.timestamp); + } + this.panel.classList.remove('open'); + return true; + })(); + + case 'ws:notify-sidebar-closed': + if (this.panel) { + this.panel.classList.remove('open'); + } + break; + } + } + + onBeforeDestroy() {} // this can be overridden by subclasses + + destroy() { + this.onBeforeDestroy(); + + if (!this.onMessageSelf) + return; + + if (this.panel) { + this.panel.parentNode.removeChild(this.panel); + this.panel = null; + } + + browser.runtime.onMessage.removeListener(this.onMessageSelf); + window.removeEventListener('unload', this.destroySelf); + window.removeEventListener('pagehide', this.destroySelf); + + this.lastTimestampFor.clear(); + this.root = this.onMessageSelf = this.destroySelf = null; + } + + get UISource() { // this can be overridden by subclasses + return ''; + } + + prepareUI() { + if (this.panel) { + return; + } + this.root.insertAdjacentHTML('beforeend', ` +
__MSG_message_startup_description_1____MSG_message_startup_description_key____MSG_message_startup_description_2____MSG_extensionName____MSG_message_startup_description_3__
+__MSG_message_startup_description_sync_before____MSG_message_startup_description_sync_link____MSG_message_startup_description_sync_after__
+__MSG_config_addons_description_before____MSG_config_addons_description_link_label____MSG_config_addons_description_after__
+__MSG_config_theme_description_before____MSG_config_theme_description_link_label____MSG_config_theme_description_after__
+__MSG_message_startup_history_before____MSG_message_startup_history_link_label____MSG_message_startup_history_after__
+ +__MSG_message_startup_requestPermissions_description__
+ + +__MSG_message_startup_userChromeCss_notify__ +
__MSG_message_startup_userChromeCss_description_1____MSG_message_startup_userChromeCss_description_link_label____MSG_message_startup_userChromeCss_description_2____MSG_message_startup_userChromeCss_description_note____MSG_message_startup_userChromeCss_description_3__
+${sanitizeForHTMLText(debugTooltip)}`;
+ return;
+ }
+ }
+
+ this.tooltip = this.$TST.defaultTooltipText;
+ this.tooltipWithDescendants = this.$TST.tooltipTextWithDescendants;
+ this.tooltipHtml = this.$TST.tooltipHtml;
+ this.tooltipHtmlWithDescendants = this.$TST.tooltipHtmlWithDescendants;
+
+ const appliedTooltipText = this.appliedTooltipText;
+ this.hasCustomTooltip = (
+ appliedTooltipText !== null &&
+ appliedTooltipText != this.$TST.defaultTooltipText
+ );
+ //console.log('this.useTabPreviewTooltip ', { useTabPreviewTooltip: this.useTabPreviewTooltip, canRunScript, canInjectScriptToTab, hasCustomTooltip: this.hasCustomTooltip });
+
+ const tooltipText = configs.debug ?
+ debugTooltip :
+ (this.useTabPreviewTooltip &&
+ (canInjectScriptToTab ||
+ !(this.hasCustomTooltip && configs.showCollapsedDescendantsByLegacyTooltipOnSidebar))) ?
+ null :
+ appliedTooltipText;
+ if (typeof tooltipText == 'string')
+ this.$TST.setAttribute('title', tooltipText);
+ else
+ this.$TST.removeAttribute('title');
+ }
+
+ get appliedTooltipText() {
+ if (configs.showCollapsedDescendantsByTooltip &&
+ this.$TST.subtreeCollapsed &&
+ this.$TST.hasChild) {
+ return this.tooltipWithDescendants;
+ }
+
+ const highPriorityTooltipText = this.$TST.highPriorityTooltipText;
+ if (typeof highPriorityTooltipText == 'string') {
+ if (highPriorityTooltipText)
+ return highPriorityTooltipText;
+
+ return null;
+ }
+
+ let tooltip = null;
+
+ const raw = this.$TST.raw;
+ if (this.classList.contains('faviconized') ||
+ this.overflow ||
+ this.tooltip != raw.title)
+ tooltip = this.tooltip;
+ else
+ tooltip = null;
+
+ const lowPriorityTooltipText = this.$TST.lowPriorityTooltipText;
+ if (typeof lowPriorityTooltipText == 'string' &&
+ !this.getAttribute('title')) {
+ if (lowPriorityTooltipText)
+ tooltip = lowPriorityTooltipText;
+ else
+ tooltip = null;
+ }
+ return tooltip;
+ }
+
+ get appliedTooltipHtml() {
+ if (configs.showCollapsedDescendantsByTooltip &&
+ this.$TST.subtreeCollapsed &&
+ this.$TST.hasChild) {
+ return this.tooltipHtmlWithDescendants;
+ }
+
+ const highPriorityTooltipText = this.$TST.highPriorityTooltipText;
+ if (typeof highPriorityTooltipText == 'string') {
+ if (highPriorityTooltipText)
+ return sanitizeForHTMLText(highPriorityTooltipText);
+
+ return null;
+ }
+
+ let tooltip = null;
+
+ const raw = this.$TST.raw;
+ if (this.classList.contains('faviconized') ||
+ this.overflow ||
+ this.tooltip != raw.title)
+ tooltip = this.tooltipHtml;
+ else
+ tooltip = null;
+
+ const lowPriorityTooltipText = this.$TST.lowPriorityTooltipText;
+ if (typeof lowPriorityTooltipText == 'string' &&
+ !this.getAttribute('title')) {
+ if (lowPriorityTooltipText)
+ tooltip = sanitizeForHTMLText(lowPriorityTooltipText);
+ else
+ tooltip = null;
+ }
+ return tooltip;
+ }
+
+ _initExtraItemsContainers() {
+ if (!this.extraItemsContainerIndentRoot) {
+ this.extraItemsContainerIndentRoot = this.querySelector(`.${Constants.kEXTRA_ITEMS_CONTAINER}.indent`).attachShadow({ mode: 'open' });
+ this.extraItemsContainerIndentRoot.itemById = new Map();
+ }
+ if (!this.extraItemsContainerBehindRoot) {
+ this.extraItemsContainerBehindRoot = this.querySelector(`.${Constants.kEXTRA_ITEMS_CONTAINER}.behind`).attachShadow({ mode: 'open' });
+ this.extraItemsContainerBehindRoot.itemById = new Map();
+ }
+ if (!this.extraItemsContainerFrontRoot) {
+ this.extraItemsContainerFrontRoot = this.querySelector(`.${Constants.kEXTRA_ITEMS_CONTAINER}.front`).attachShadow({ mode: 'open' });
+ this.extraItemsContainerFrontRoot.itemById = new Map();
+ }
+ if (!this.extraItemsContainerAboveRoot) {
+ this.extraItemsContainerAboveRoot = this.querySelector(`.${Constants.kEXTRA_ITEMS_CONTAINER}.above`).attachShadow({ mode: 'open' });
+ this.extraItemsContainerAboveRoot.itemById = new Map();
+ }
+ if (!this.extraItemsContainerBelowRoot) {
+ this.extraItemsContainerBelowRoot = this.querySelector(`.${Constants.kEXTRA_ITEMS_CONTAINER}.below`).attachShadow({ mode: 'open' });
+ this.extraItemsContainerBelowRoot.itemById = new Map();
+ }
+ }
+
+ _startListening() {
+ if (this.__onMouseOver)
+ return;
+ this.addEventListener('mouseover', this.__onMouseOver = this._onMouseOver.bind(this));
+ this.addEventListener('mouseenter', this.__onMouseEnter = this._onMouseEnter.bind(this));
+ this.substanceElement?.addEventListener('mouseenter', this.__onMouseEnter);
+ this.addEventListener('mouseleave', this.__onMouseLeave = this._onMouseLeave.bind(this));
+ this.substanceElement?.addEventListener('mouseleave', this.__onMouseLeave);
+ window.addEventListener('resize', this.__onWindowResize = this._onWindowResize.bind(this));
+ configs.$addObserver(this.__onConfigChange = this._onConfigChange.bind(this));
+ }
+
+ _endListening() {
+ if (!this.__onMouseOver)
+ return;
+ this.removeEventListener('mouseover', this.__onMouseOver);
+ this.__onMouseOver = null;
+ this.removeEventListener('mouseenter', this.__onMouseEnter);
+ this.substanceElement?.removeEventListener('mouseenter', this.__onMouseEnter);
+ this.__onMouseEnter = null;
+ this.removeEventListener('mouseleave', this.__onMouseLeave);
+ this.substanceElement?.removeEventListener('mouseleave', this.__onMouseLeave);
+ this.__onMouseLeave = null;
+ window.removeEventListener('resize', this.__onWindowResize);
+ this.__onWindowResize = null;
+ configs.$removeObserver(this.__onConfigChange);
+ this.__onConfigChange = null;
+ }
+
+ _onMouseOver(_event) {
+ this._updateTabAndAncestorsTooltip(this.$TST.raw);
+ }
+
+ _onMouseEnter(event) {
+ if (this.classList.contains('faviconized') != (event.target == this))
+ return;
+ if (this._reservedUpdateTooltip) {
+ this.removeEventListener('mouseover', this._reservedUpdateTooltip);
+ this._updateTooltip();
+ }
+ const tabSubstanceEnterEvent = new MouseEvent(kEVENT_TREE_ITEM_SUBSTANCE_ENTER, {
+ ...event,
+ clientX: event.clientX,
+ clientY: event.clientY,
+ screenX: event.screenX,
+ screenY: event.screenY,
+ bubbles: true,
+ composed: true,
+ });
+ this.dispatchEvent(tabSubstanceEnterEvent);
+ }
+
+ _onMouseLeave(event) {
+ if (this.classList.contains('faviconized') != (event.target == this))
+ return;
+ const tabSubstanceLeaveEvent = new UIEvent(kEVENT_TREE_ITEM_SUBSTANCE_LEAVE, {
+ ...event,
+ bubbles: true,
+ composed: true,
+ });
+ this.dispatchEvent(tabSubstanceLeaveEvent);
+ }
+
+ _onWindowResize(_event) {
+ this.invalidateTooltip();
+ }
+
+ _onConfigChange(changedKey) {
+ switch (changedKey) {
+ case 'showCollapsedDescendantsByTooltip':
+ this.invalidateTooltip();
+ break;
+
+ case 'labelOverflowStyle':
+ this.updateOverflow();
+ break;
+ }
+ }
+
+ _updateTabAndAncestorsTooltip(tab) {
+ if (!TabsStore.ensureLivingItem(tab))
+ return;
+ for (const updateTab of [tab].concat(tab.$TST.ancestors)) {
+ const tabElement = updateTab.$TST.element;
+ if (!tabElement)
+ continue;
+ tabElement.invalidateTooltip();
+ // on the "fade" mode, overflow style was already updated,
+ // so we don' need to update the status here.
+ if (configs.labelOverflowStyle != 'fade')
+ tabElement.updateOverflow();
+ }
+ }
+
+ _updateDescendantsHighlighted() {
+ if (!this.$TST) // called before binding on restoration from cache
+ return;
+
+ const children = this.$TST.children;
+ if (!this.$TST.hasChild) {
+ this.$TST.removeState(Constants.kTAB_STATE_SOME_DESCENDANTS_HIGHLIGHTED);
+ this.$TST.removeState(Constants.kTAB_STATE_ALL_DESCENDANTS_HIGHLIGHTED);
+ return;
+ }
+ let someHighlighted = false;
+ let allHighlighted = true;
+ for (const child of children) {
+ if (child.$TST.states.has(Constants.kTAB_STATE_HIGHLIGHTED)) {
+ someHighlighted = true;
+ allHighlighted = (
+ allHighlighted &&
+ (!child.$TST.hasChild ||
+ child.$TST.states.has(Constants.kTAB_STATE_ALL_DESCENDANTS_HIGHLIGHTED))
+ );
+ }
+ else {
+ if (!someHighlighted &&
+ child.$TST.states.has(Constants.kTAB_STATE_SOME_DESCENDANTS_HIGHLIGHTED)) {
+ someHighlighted = true;
+ }
+ allHighlighted = false;
+ }
+ }
+ if (someHighlighted) {
+ this.$TST.addState(Constants.kTAB_STATE_SOME_DESCENDANTS_HIGHLIGHTED);
+ this.$TST.toggleState(Constants.kTAB_STATE_ALL_DESCENDANTS_HIGHLIGHTED, allHighlighted);
+ }
+ else {
+ this.$TST.removeState(Constants.kTAB_STATE_SOME_DESCENDANTS_HIGHLIGHTED);
+ this.$TST.removeState(Constants.kTAB_STATE_ALL_DESCENDANTS_HIGHLIGHTED);
+ }
+ }
+
+ _updateCollapseExpandState() {
+ if (!this.$TST) // called before binding on restoration from cache
+ return;
+
+ const classList = this.classList;
+ const parent = this.$TST.parent;
+ if (this.$TST.collapsed ||
+ (parent &&
+ (parent.$TST.collapsed ||
+ parent.$TST.subtreeCollapsed))) {
+ if (!classList.contains(Constants.kTAB_STATE_COLLAPSED))
+ classList.add(Constants.kTAB_STATE_COLLAPSED);
+ if (!classList.contains(Constants.kTAB_STATE_COLLAPSED_DONE))
+ classList.add(Constants.kTAB_STATE_COLLAPSED_DONE);
+ }
+ else {
+ if (classList.contains(Constants.kTAB_STATE_COLLAPSED))
+ classList.remove(Constants.kTAB_STATE_COLLAPSED);
+ if (classList.contains(Constants.kTAB_STATE_COLLAPSED_DONE))
+ classList.remove(Constants.kTAB_STATE_COLLAPSED_DONE);
+ }
+ }
+
+ _updateTabProperties() {
+ if (!this.$TST) // called before binding on restoration from cache
+ return;
+
+ const raw = this.$TST.raw;
+ const classList = this.classList;
+
+ this.label = raw.$TST.title;
+
+ const tab = this.$TST.tab;
+ if (tab) {
+ const openerOfGroupTab = tab && this.$TST.isGroupTab && Tab.getOpenerFromGroupTab(tab);
+ this.favIconUrl = openerOfGroupTab?.favIconUrl || tab?.favIconUrl;
+
+ for (const state of classList) {
+ if (IGNORE_CLASSES.has(state) ||
+ NATIVE_PROPERTIES.has(state))
+ continue;
+ if (!this.$TST.states.has(state))
+ classList.remove(state);
+ }
+ for (const state of this.$TST.states) {
+ if (IGNORE_CLASSES.has(state))
+ continue;
+ if (!classList.contains(state))
+ classList.add(state);
+ }
+
+ for (const state of NATIVE_PROPERTIES) {
+ if (raw[state] == classList.contains(state))
+ continue;
+ classList.toggle(state, raw[state]);
+ }
+
+ if (this.$TST.childIds.length > 0)
+ this.setAttribute(Constants.kCHILDREN, `|${this.$TST.childIds.join('|')}|`);
+ else
+ this.removeAttribute(Constants.kCHILDREN);
+
+ if (this.$TST.parentId)
+ this.setAttribute(Constants.kPARENT, this.$TST.parentId);
+ else
+ this.removeAttribute(Constants.kPARENT);
+
+ const alreadyGrouped = this.$TST.getAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER) || '';
+ if (this.getAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER) != alreadyGrouped)
+ this.setAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER, alreadyGrouped);
+
+ const opener = this.$TST.getAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID) || '';
+ if (this.getAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID) != opener)
+ this.setAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID, opener);
+
+ const uri = this.$TST.getAttribute(Constants.kCURRENT_URI) || tab?.url;
+ if (this.getAttribute(Constants.kCURRENT_URI) != uri)
+ this.setAttribute(Constants.kCURRENT_URI, uri);
+
+ const favIconUri = this.$TST.getAttribute(Constants.kCURRENT_FAVICON_URI) || tab?.favIconUrl;
+ if (this.getAttribute(Constants.kCURRENT_FAVICON_URI) != favIconUri)
+ this.setAttribute(Constants.kCURRENT_FAVICON_URI, favIconUri);
+
+ const level = this.$TST.getAttribute(Constants.kLEVEL) || 0;
+ if (this.getAttribute(Constants.kLEVEL) != level)
+ this.setAttribute(Constants.kLEVEL, level);
+
+ const id = this.$TST.uniqueId.id;
+ if (this.getAttribute(Constants.kPERSISTENT_ID) != id)
+ this.setAttribute(Constants.kPERSISTENT_ID, id);
+
+ if (this.$TST.subtreeCollapsed) {
+ if (!classList.contains(Constants.kTAB_STATE_SUBTREE_COLLAPSED))
+ classList.add(Constants.kTAB_STATE_SUBTREE_COLLAPSED);
+ }
+ else {
+ if (classList.contains(Constants.kTAB_STATE_SUBTREE_COLLAPSED))
+ classList.remove(Constants.kTAB_STATE_SUBTREE_COLLAPSED);
+ }
+ }
+
+ const group = this.$TST.nativeTabGroup || this.$TST.group;
+ if (group) {
+ this.style.setProperty('--tab-group-color', `var(--tab-group-color-${group.color})`);
+ this.style.setProperty('--tab-group-color-pale', `var(--tab-group-color-${group.color}-pale)`);
+ this.style.setProperty('--tab-group-color-invert', `var(--tab-group-color-${group.color}-invert)`);
+ }
+ if (this.$TST.group) {
+ classList.toggle(Constants.kTAB_STATE_SUBTREE_COLLAPSED, group.collapsed);
+ }
+ }
+
+ get favIconUrl() {
+ if (!this.initialized)
+ return null;
+
+ return this.favicon.src;
+ }
+
+ set favIconUrl(url) {
+ this._favIconUrl = url;
+ if (!this.initialized || !this.$TST)
+ return url;
+
+ if (!url || url.startsWith('data:')) { // we don't need to use the helper for data: URI.
+ this.favicon.src = url;
+ this.favicon.classList.remove('error');
+ return url;
+ }
+
+ TabFavIconHelper.loadToImage({
+ image: this.favicon,
+ tab: this.$TST.tab,
+ url
+ });
+ return url;
+ }
+
+ get overflow() {
+ const label = this._labelElement;
+ return label?.overflow;
+ }
+
+ get label() {
+ const label = this._labelElement;
+ return label ? label.value : null;
+ }
+ set label(value) {
+ const label = this._labelElement;
+ if (label)
+ label.value = value;
+
+ this.dataset.title = value; // for custom CSS https://github.com/piroor/treestyletab/issues/2242
+
+ if (!this.$TST) // called before binding on restoration from cache
+ return;
+
+ this.invalidateTooltip();
+ if (this.$TST.collapsed) {
+ this._labelElement.invalidateOverflow();
+ this._needToUpdateOverflow = true;
+ }
+ }
+}
diff --git a/waterfox/browser/components/sidebar/sidebar/components/TreeItemLabelElement.js b/waterfox/browser/components/sidebar/sidebar/components/TreeItemLabelElement.js
new file mode 100644
index 000000000000..0da7d8217dfd
--- /dev/null
+++ b/waterfox/browser/components/sidebar/sidebar/components/TreeItemLabelElement.js
@@ -0,0 +1,234 @@
+/*
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+*/
+
+import {
+ watchOverflowStateChange,
+} from '/common/common.js';
+import * as Constants from '/common/constants.js';
+
+export const kTREE_ITEM_LABEL_ELEMENT_NAME = 'tab-label';
+
+const KLABEL_CLASS_NAME = 'label';
+const kCONTENT_CLASS_NAME = `${KLABEL_CLASS_NAME}-content`;
+
+const kATTR_NAME_VALUE = 'value';
+
+//****************************************************************************
+// isRTL https://github.com/kavirajk/isRTL
+// The MIT License (MIT)
+// Copyright (c) 2013 dhilipsiva
+const rtlChars = [
+ /* arabic ranges*/
+ '\u0600-\u06FF',
+ '\u0750-\u077F',
+ '\uFB50-\uFDFF',
+ '\uFE70-\uFEFF',
+ /* hebrew range*/
+ '\u05D0-\u05FF'
+].join('');
+
+const reRTL = new RegExp(`[${rtlChars}]`, 'g');
+
+function isRTL(text) {
+ if (!text)
+ return false;
+ if (/^\s*\u200f[^\u200e]/.test(text)) // title starting with right-to-left-mark
+ return true;
+ const textCount = text.replace(/[0-9\s\\\/.,\-+="']/g, '').length; // remove multilengual characters from count
+ const rtlCount = (text.match(reRTL) || []).length;
+ return rtlCount >= (textCount-rtlCount) && textCount > 0;
+};
+//****************************************************************************
+
+export class TreeItemLabelElement extends HTMLElement {
+ static define() {
+ window.customElements.define(kTREE_ITEM_LABEL_ELEMENT_NAME, TreeItemLabelElement);
+ }
+
+ static get observedAttributes() {
+ return [kATTR_NAME_VALUE];
+ }
+
+ constructor() {
+ super();
+
+ // We should initialize private properties with blank value for better performance with a fixed shape.
+ this.__unwatch = null;
+ }
+
+ connectedCallback() {
+ this.setAttribute('role', 'button');
+
+ if (this.initialized) {
+ this._startListening();
+ this.applyAttributes();
+ this.updateTextContent();
+ return;
+ }
+
+ // I make ensure to call these operation only once conservatively because:
+ // * If we do these operations in a constructor of this class, Gecko throws `NotSupportedError: Operation is not supported`.
+ // * I'm not familiar with details of the spec, but this is not Gecko's bug.
+ // See https://dom.spec.whatwg.org/#concept-create-element
+ // "6. If result has children, then throw a "NotSupportedError" DOMException."
+ // * `connectedCallback()` may be called multiple times by append/remove operations.
+ //
+ // FIXME:
+ // Ideally, these descendants should be in shadow tree. Thus I don't change these element to custom elements.
+ // However, I hesitate to do it at this moment by these reasons.
+ // If we move these to shadow tree,
+ // * We need some rewrite our style.
+ // * This includes that we need to move almost CSS code into this file as a string.
+ // * I'm not sure about that whether we should require [CSS Shadow Parts](https://bugzilla.mozilla.org/show_bug.cgi?id=1559074).
+ // * I suspect we can resolve almost problems by using CSS Custom Properties.
+
+ // We preserve this class for backward compatibility with other addons.
+ this.classList.add(KLABEL_CLASS_NAME);
+
+ const content = this.appendChild(document.createElement('span'));
+ content.classList.add(kCONTENT_CLASS_NAME);
+
+ this._startListening();
+ this.applyAttributes();
+ this.updateTextContent();
+ }
+
+ disconnectedCallback() {
+ this._overflowChangeListeners.clear();
+ this._endListening();
+ }
+
+ get initialized() {
+ return !!this._content;
+ }
+
+ attributeChangedCallback(name, oldValue, newValue, _namespace) {
+ if (oldValue === newValue) {
+ return;
+ }
+
+ switch (name) {
+ case kATTR_NAME_VALUE:
+ this.updateTextContent();
+ break;
+
+ default:
+ throw new RangeError(`Handling \`${name}\` attribute has not been defined.`);
+ }
+ }
+
+ applyAttributes() {
+ // for convenience on customization with custom user styles
+ this._content.setAttribute(Constants.kAPI_TAB_ID, this.getAttribute(Constants.kAPI_TAB_ID));
+ this._content.setAttribute(Constants.kAPI_WINDOW_ID, this.getAttribute(Constants.kAPI_WINDOW_ID));
+ this._content.dataset.index = this.dataset.index;
+ }
+
+ updateTextContent() {
+ const content = this._content;
+ if (!content)
+ return;
+ content.textContent = this.getAttribute(kATTR_NAME_VALUE) || '';
+ this.classList.toggle('rtl', isRTL(content.textContent));
+ this.invalidateOverflow();
+ // Don't touch to offsetWidth if not needed - touching it will triggers indent animation unexpectedly
+ this.closest('tab-item[type="group"]')?.style.setProperty('--tab-label-width', `${content.offsetWidth}px`);
+ }
+
+ updateOverflow() {
+ // Accessing to the real size of the element triggers layouting and hits the performance,
+ // like https://github.com/piroor/treestyletab/issues/3477 .
+ // So we need to throttle the process for better formance.
+ if (this.updateOverflow.invoked)
+ return;
+ this.updateOverflow.invoked = true;
+ window.requestAnimationFrame(() => {
+ this.updateOverflow.invoked = false;
+ if (!this.closest('body')) // already detached from document!
+ return;
+ const tab = this.owner;
+ const overflow = tab && !tab.pinned && this._content.offsetWidth > this.offsetWidth;
+ this.classList.toggle('overflow', overflow);
+ // Don't touch to offsetWidth if not needed - touching it will triggers indent animation unexpectedly
+ this.closest('tab-item[type="group"]')?.style.setProperty('--tab-label-width', `${this._content.offsetWidth}px`);
+ });
+ }
+
+ invalidateOverflow() {
+ this.updateOverflow.invoked = false;
+ }
+
+ get _content() {
+ return this.querySelector(`.${kCONTENT_CLASS_NAME}`);
+ }
+
+ get value() {
+ return this.getAttribute(kATTR_NAME_VALUE);
+ }
+ set value(value) {
+ this.setAttribute(kATTR_NAME_VALUE, value);
+ }
+
+ get overflow() {
+ return this.classList.contains('overflow');
+ }
+
+ _startListening() {
+ if (this.__unwatch)
+ return;
+
+ // Accessing to the real size of the element triggers layouting and hits the performance,
+ // like https://github.com/piroor/treestyletab/issues/3557 .
+ // So we need to throttle the process for better formance.
+ if (this._startListening_invoked)
+ return;
+ this._startListening_invoked = true;
+ window.requestAnimationFrame(() => {
+ this._startListening_invoked = false;
+ if (!this.closest('body')) // already detached from document!
+ return;
+ this.__unwatch = watchOverflowStateChange({
+ target: this,
+ horizontal: true,
+ onOverflow: () => this._onOverflow(),
+ onUnderflow: () => this._onUnderflow(),
+ });
+ });
+ }
+
+ _endListening() {
+ if (!this.__unwatch)
+ return;
+ this.__unwatch();
+ this.__unwatch = null;
+ }
+
+ _onOverflow() {
+ this.classList.add('overflow');
+ for (const listener of this._overflowChangeListeners) {
+ listener();
+ }
+ }
+
+ _onUnderflow() {
+ this.classList.remove('overflow');
+ for (const listener of this._overflowChangeListeners) {
+ listener();
+ }
+ }
+
+ addOverflowChangeListener(listener) {
+ this._overflowChangeListeners.add(listener);
+ }
+
+ removeOverflowChangeListener(listener) {
+ this._overflowChangeListeners.delete(listener);
+ }
+
+ get _overflowChangeListeners() {
+ return this.__overflowChangeListeners || (this.__overflowChangeListeners = new Set());
+ }
+}
diff --git a/waterfox/browser/components/sidebar/sidebar/drag-and-drop.js b/waterfox/browser/components/sidebar/sidebar/drag-and-drop.js
new file mode 100644
index 000000000000..0ee2c6083445
--- /dev/null
+++ b/waterfox/browser/components/sidebar/sidebar/drag-and-drop.js
@@ -0,0 +1,1814 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Tree Style Tab.
+ *
+ * The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
+ * Portions created by the Initial Developer are Copyright (C) 2010-2025
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s): YUKI "Piro" Hiroshi