From 81ec621cad4fa76885e0d6488f532a4963e181bb Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Sun, 1 Jun 2025 21:42:33 +0300 Subject: [PATCH 01/19] feat: Drag and Drop link to split --- src/browser/app/profile/features.inc | 1 + src/zen/split-view/ZenViewSplitter.mjs | 282 +++++++++++++++++++++++++ src/zen/split-view/zen-decks.css | 52 +++++ 3 files changed, 335 insertions(+) diff --git a/src/browser/app/profile/features.inc b/src/browser/app/profile/features.inc index f92e9ba985..4396ef7c1d 100644 --- a/src/browser/app/profile/features.inc +++ b/src/browser/app/profile/features.inc @@ -134,6 +134,7 @@ pref('zen.workspaces.debug', true); // Zen Split View pref('zen.splitView.enable-tab-drop', true); +pref('zen.splitView.enable-link-drop', true); pref('zen.splitView.min-resize-width', 7); pref('zen.splitView.rearrange-hover-size', 24); diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index c67b4fa71f..00b380395b 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -79,6 +79,10 @@ class ZenViewSplitter extends ZenDOMOperatedFeature { MAX_TABS = 4; + // Link drag and drop + _linkDropZone = null; + _isLinkDragging = false; + init() { this.handleTabEvent = this._handleTabEvent.bind(this); @@ -123,6 +127,11 @@ class ZenViewSplitter extends ZenDOMOperatedFeature { tabBox.addEventListener('dragover', this.onBrowserDragOverToSplit.bind(this)); this.onBrowserDragEndToSplit = this.onBrowserDragEndToSplit.bind(this); } + + // If enabled initialize the link drag and drop + if (Services.prefs.getBoolPref('zen.splitView.enable-link-drop')) { + this.#initLinkDragDropSplit(); + } } insertIntoContextMenu() { @@ -1894,6 +1903,279 @@ class ZenViewSplitter extends ZenDOMOperatedFeature { } return true; } + + #initLinkDragDropSplit() { + this._handleLinkDragEnter = this._handleLinkDragEnter.bind(this); + this._handleLinkDragLeave = this._handleLinkDragLeave.bind(this); + this._handleLinkDragDrop = this._handleLinkDragDrop.bind(this); + this._handleLinkDragEnd = this._handleLinkDragEnd.bind(this); + + const tabBox = document.getElementById('tabbrowser-tabbox'); + + tabBox.addEventListener('dragenter', this._handleLinkDragEnter, true); + tabBox.addEventListener('dragleave', this._handleLinkDragLeave, false); + tabBox.addEventListener('drop', this._handleLinkDragDrop, false); + tabBox.addEventListener('dragend', this._handleLinkDragEnd, false); + } + + _createLinkDropZone() { + if (this._linkDropZone) return; + + this._linkDropZone = document.createXULElement('box'); + this._linkDropZone.id = 'zen-drop-link-zone'; + + const content = document.createXULElement('vbox'); + content.setAttribute('align', 'center'); + content.setAttribute('pack', 'center'); + content.setAttribute('flex', '1'); + + const text = document.createXULElement('description'); + text.setAttribute('value', 'Drop link to split'); // Localization! data-l10n-id + + content.appendChild(text); + this._linkDropZone.appendChild(content); + + this._linkDropZone.addEventListener('dragover', (event) => { + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = 'link'; + if (!this._linkDropZone.hasAttribute('has-focus')) { + this._linkDropZone.setAttribute('has-focus', 'true'); + } + }); + + this._linkDropZone.addEventListener('dragleave', (event) => { + event.stopPropagation(); + if (!this._linkDropZone.contains(event.relatedTarget)) { + this._linkDropZone.removeAttribute('has-focus'); + } + }); + + this._linkDropZone.addEventListener('drop', this._handleDropForSplit.bind(this)); + + const tabBox = document.getElementById('tabbrowser-tabbox'); + tabBox.appendChild(this._linkDropZone); + } + + _showLinkDropZone() { + if (!this._linkDropZone) this._createLinkDropZone(); + + this._linkDropZone.setAttribute('enabled', 'true'); + } + + _hideLinkDropZone(force = false) { + if (!this._linkDropZone || !this._linkDropZone.hasAttribute('enabled')) return; + + if (this._isLinkDragging && !force) return; + + this._linkDropZone.removeAttribute('enabled'); + this._linkDropZone.removeAttribute('has-focus'); + } + + _validateURI(dataTransfer) { + let dt = dataTransfer; + + const URL_TYPES = ['text/uri-list', 'text/x-moz-url', 'text/plain']; + + const FIXUP_FLAGS = Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS; + + const matchedType = URL_TYPES.find((type) => { + const raw = dt.getData(type); + return typeof raw === 'string' && raw.trim().length > 0; + }); + + const uriString = dt.getData(matchedType).trim(); + + const info = Services.uriFixup.getFixupURIInfo(uriString, FIXUP_FLAGS); + + if (!info || !info.fixedURI) { + return null; + } + + return info.fixedURI.spec; + } + + _handleLinkDragEnter(event) { + // If rearrangeViewEnabled - don't do anything + if (this.rearrangeViewEnabled) { + return; + } + + const shouldBeDisabled = !this.canOpenLinkInSplitView(); + if (shouldBeDisabled) return; + + // If the target is our drop zone or one of its children, or already active, do nothing here. + if ( + this._linkDropZone && + (this._linkDropZone.contains(event.target) || this._linkDropZone.hasAttribute('enabled')) + ) { + return; + } + + // If the data is not a valid URI, we don't want to do anything + if (!this._validateURI(event.dataTransfer)) { + return; + } + + this._isLinkDragging = true; + this._showLinkDropZone(); + + event.preventDefault(); + event.stopPropagation(); + } + + _handleLinkDragLeave(event) { + if ( + event.target === document.documentElement || + (event.clientX <= 0 && event.clientY <= 0) || + event.clientX >= window.innerWidth || + event.clientY >= window.innerHeight + ) { + if (this._linkDropZone && !this._linkDropZone.contains(event.relatedTarget)) { + this._isLinkDragging = false; + this._hideLinkDropZone(); + } + } + } + + _handleLinkDragDrop(event) { + if (!this._linkDropZone || !this._linkDropZone.contains(event.target)) { + if (this._linkDropZone && this._linkDropZone.hasAttribute('enabled')) { + this._isLinkDragging = false; + this._hideLinkDropZone(true); // true for forced hiding + } + } + } + + _handleLinkDragEnd(event) { + this._isLinkDragging = false; + this._hideLinkDropZone(true); // true for forced hiding + } + + _handleDropForSplit(event) { + let linkDropZone = this._linkDropZone; + event.preventDefault(); + event.stopPropagation(); + + const url = this._validateURI(event.dataTransfer); + + if (!url) { + this._hideDropZoneAndResetState(); + return; + } + + const currentTab = gZenGlanceManager.getTabOrGlanceParent(gBrowser.selectedTab); + const newTab = this.openAndSwitchToTab(url, { inBackground: false }); + + if (!newTab) { + this._hideDropZoneAndResetState(); + return; + } + + const linkDropSide = this._calculateDropSide(event, linkDropZone); + + this._createOrUpdateSplitViewWithSide(currentTab, newTab, linkDropSide); + + this._hideDropZoneAndResetState(); + } + _calculateDropSide(event, linkDropZone) { + const rect = linkDropZone.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + const width = rect.width; + const height = rect.height; + + const edgeSizeRatio = 0.3; // 30% of the size, maybe increase to 35% + const hEdge = width * edgeSizeRatio; + const vEdge = height * edgeSizeRatio; + + const isInLeftEdge = x < hEdge; + const isInRightEdge = x > width - hEdge; + const isInTopEdge = y < vEdge; + const isInBottomEdge = y > height - vEdge; + + if (isInTopEdge) { + if (isInLeftEdge && x / width < y / height) return 'left'; // More left in angle + if (isInRightEdge && (width - x) / width < y / height) return 'right'; // More right in angle + return 'top'; + } + if (isInBottomEdge) { + if (isInLeftEdge && x / width < (height - y) / height) return 'left'; + if (isInRightEdge && (width - x) / width < (height - y) / height) return 'right'; + return 'bottom'; + } + if (isInLeftEdge) { + return 'left'; + } + if (isInRightEdge) { + return 'right'; + } + return 'center'; + } + + _createOrUpdateSplitViewWithSide(currentTab, newTab, linkDropSide) { + const SIDES = ['left', 'right', 'top', 'bottom']; + const groupIndex = this._data.findIndex((group) => group.tabs.includes(currentTab)); + + if (groupIndex > -1) { + const group = this._data[groupIndex]; + + if (group.tabs.length >= this.MAX_TABS) { + console.warn(`Cannot add tab to split, MAX_TABS (${this.MAX_TABS}) reached.`); + return; + } + + const splitViewGroup = this._getSplitViewGroup(group.tabs); + if (splitViewGroup && newTab.group !== splitViewGroup) { + this._moveTabsToContainer([newTab], currentTab); + gBrowser.moveTabToGroup(newTab, splitViewGroup); + } + + if (!group.tabs.includes(newTab)) { + group.tabs.push(newTab); + + const targetNode = this.getSplitNodeFromTab(currentTab); + const isValidSide = SIDES.includes(linkDropSide); + + if (targetNode && isValidSide) { + this.splitIntoNode(targetNode, new SplitLeafNode(newTab, 50), linkDropSide, 0.5); + } else { + const parentNode = targetNode?.parent || group.layoutTree; + this.addTabToSplit(newTab, parentNode, false); + } + + this.activateSplitView(group, true); + } + return; + } + + const splitConfig = { + left: { tabs: [newTab, currentTab], gridType: 'vsep', initialIndex: 0 }, + right: { tabs: [currentTab, newTab], gridType: 'vsep', initialIndex: 1 }, + top: { tabs: [newTab, currentTab], gridType: 'hsep', initialIndex: 0 }, + bottom: { tabs: [currentTab, newTab], gridType: 'hsep', initialIndex: 1 }, + }; + + const { + tabs: tabsToSplit, + gridType, + initialIndex, + } = splitConfig[linkDropSide] || { + // If linkDropSide is invalid should use the default "vsep" + tabs: [currentTab, newTab], + gridType: 'vsep', + initialIndex: 1, + }; + + this.splitTabs(tabsToSplit, gridType, initialIndex); + } + + _hideDropZoneAndResetState() { + if (this._linkDropZone && this._linkDropZone.hasAttribute('enabled')) { + this._isLinkDragging = false; + this._hideLinkDropZone(true); + } + } } window.gZenViewSplitter = new ZenViewSplitter(); diff --git a/src/zen/split-view/zen-decks.css b/src/zen/split-view/zen-decks.css index b2397bc8db..c268928dac 100644 --- a/src/zen/split-view/zen-decks.css +++ b/src/zen/split-view/zen-decks.css @@ -221,3 +221,55 @@ transition-delay: 0s; } } + +#zen-drop-link-zone { + position: fixed; + top: 50%; + left: 50%; + width: 260px; + height: 150px; + + background: var(--zen-branding-bg); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 18px; + box-shadow: + rgba(0, 0, 0, 0.45) 0px 15px 35px, + rgba(255, 255, 255, 0.04) 0px 1px 0px inset; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + z-index: 999; + opacity: 0; + transform: translate(-50%, -50%) translateY(80px) scale(0.1); + transition: + transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), + opacity 0.38s ease; + pointer-events: none; + + &[enabled='true'] { + opacity: 1; + transform: translate(-50%, -50%) translateY(0) scale(1); + pointer-events: auto; + } + + &[has-focus='true'] { + transform: translate(-50%, -50%) translateY(0) scale(1.03); + transition: + transform 0.25s cubic-bezier(0.22, 1, 0.36, 1), + border-color 0.25s ease; + border-color: var(--zen-primary-color); + } + + & text { + font-size: 14px; + line-height: 1.4; + text-align: center; + color: rgba(255, 255, 255, 0.85); + font-weight: 500; + letter-spacing: 0.2px; + user-select: none; + } +} From 9cc475fa9302fbc8d0862a7af70a02c31b8ea849 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Mon, 2 Jun 2025 00:01:02 +0300 Subject: [PATCH 02/19] Moved all the logic to class ZenSplitViewLinkDrop --- src/zen/split-view/ZenViewSplitter.mjs | 580 +++++++++++++------------ 1 file changed, 302 insertions(+), 278 deletions(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 00b380395b..6ef9263402 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -63,6 +63,305 @@ class SplitNode extends SplitLeafNode { } } +class ZenSplitViewLinkDrop { + #zenViewSplitter; + _linkDropZone = null; + _isLinkDragging = false; + + #handleLinkDragEnter; + #handleLinkDragLeave; + #handleLinkDragDrop; + #handleLinkDragEnd; + + constructor(zenViewSplitter) { + this.#zenViewSplitter = zenViewSplitter; + } + + init() { + if (!Services.prefs.getBoolPref('zen.splitView.enable-link-drop')) return; + + this.#handleLinkDragEnter = this._handleLinkDragEnter.bind(this); + this.#handleLinkDragLeave = this._handleLinkDragLeave.bind(this); + this.#handleLinkDragDrop = this._handleLinkDragDrop.bind(this); + this.#handleLinkDragEnd = this._handleLinkDragEnd.bind(this); + + const tabBox = document.getElementById('tabbrowser-tabbox'); + + tabBox.addEventListener('dragenter', this.#handleLinkDragEnter, true); + tabBox.addEventListener('dragleave', this.#handleLinkDragLeave, false); + tabBox.addEventListener('drop', this.#handleLinkDragDrop, false); + tabBox.addEventListener('dragend', this.#handleLinkDragEnd, false); + } + + _createLinkDropZone() { + if (this._linkDropZone) return; + + this._linkDropZone = document.createXULElement('box'); + this._linkDropZone.id = 'zen-drop-link-zone'; + + const content = document.createXULElement('vbox'); + content.setAttribute('align', 'center'); + content.setAttribute('pack', 'center'); + content.setAttribute('flex', '1'); + + const text = document.createXULElement('description'); + text.setAttribute('value', 'Drop link to split'); // Localization! data-l10n-id + + content.appendChild(text); + this._linkDropZone.appendChild(content); + + this._linkDropZone.addEventListener('dragover', (event) => { + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = 'link'; + if (!this._linkDropZone.hasAttribute('has-focus')) { + this._linkDropZone.setAttribute('has-focus', 'true'); + } + }); + + this._linkDropZone.addEventListener('dragleave', (event) => { + event.stopPropagation(); + if (!this._linkDropZone.contains(event.relatedTarget)) { + this._linkDropZone.removeAttribute('has-focus'); + } + }); + + this._linkDropZone.addEventListener('drop', this._handleDropForSplit.bind(this)); + + const tabBox = document.getElementById('tabbrowser-tabbox'); + tabBox.appendChild(this._linkDropZone); + } + + _showLinkDropZone() { + if (!this._linkDropZone) this._createLinkDropZone(); + + this._linkDropZone.setAttribute('enabled', 'true'); + } + + _hideLinkDropZone(force = false) { + if (!this._linkDropZone || !this._linkDropZone.hasAttribute('enabled')) return; + + if (this._isLinkDragging && !force) return; + + this._linkDropZone.removeAttribute('enabled'); + this._linkDropZone.removeAttribute('has-focus'); + } + + _validateURI(dataTransfer) { + let dt = dataTransfer; + + const URL_TYPES = ['text/uri-list', 'text/x-moz-url', 'text/plain']; + + let fixupFlags = + Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | + Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; + + const matchedType = URL_TYPES.find((type) => { + const raw = dt.getData(type); + return typeof raw === 'string' && raw.trim().length > 0; + }); + + const uriString = dt.getData(matchedType).trim(); + + const info = Services.uriFixup.getFixupURIInfo(uriString, fixupFlags); + + if (!info || !info.fixedURI) { + return null; + } + + return info.fixedURI.spec; + } + + _handleLinkDragEnter(event) { + // If rearrangeViewEnabled - don't do anything + if (this.#zenViewSplitter.rearrangeViewEnabled) { + return; + } + + const shouldBeDisabled = !this.#zenViewSplitter.canOpenLinkInSplitView(); + if (shouldBeDisabled) return; + + // If the target is our drop zone or one of its children, or already active, do nothing here. + if ( + this._linkDropZone && + (this._linkDropZone.contains(event.target) || this._linkDropZone.hasAttribute('enabled')) + ) { + return; + } + + // If the data is not a valid URI, we don't want to do anything + if (!this._validateURI(event.dataTransfer)) { + return; + } + + this._isLinkDragging = true; + this._showLinkDropZone(); + + event.preventDefault(); + event.stopPropagation(); + } + + _handleLinkDragLeave(event) { + if ( + event.target === document.documentElement || + (event.clientX <= 0 && event.clientY <= 0) || + event.clientX >= window.innerWidth || + event.clientY >= window.innerHeight + ) { + if (this._linkDropZone && !this._linkDropZone.contains(event.relatedTarget)) { + this._isLinkDragging = false; + this._hideLinkDropZone(); + } + } + } + + _handleLinkDragDrop(event) { + if (!this._linkDropZone || !this._linkDropZone.contains(event.target)) { + if (this._linkDropZone && this._linkDropZone.hasAttribute('enabled')) { + this._isLinkDragging = false; + this._hideLinkDropZone(true); // true for forced hiding + } + } + } + + _handleLinkDragEnd(event) { + this._isLinkDragging = false; + this._hideLinkDropZone(true); // true for forced hiding + } + + _handleDropForSplit(event) { + let linkDropZone = this._linkDropZone; + event.preventDefault(); + event.stopPropagation(); + + const url = this._validateURI(event.dataTransfer); + + if (!url) { + this._hideDropZoneAndResetState(); + return; + } + + const currentTab = gZenGlanceManager.getTabOrGlanceParent(gBrowser.selectedTab); + const newTab = this.#zenViewSplitter.openAndSwitchToTab(url, { inBackground: false }); + + if (!newTab) { + this._hideDropZoneAndResetState(); + return; + } + + const linkDropSide = this._calculateDropSide(event, linkDropZone); + + this._createOrUpdateSplitViewWithSide(currentTab, newTab, linkDropSide); + + this._hideDropZoneAndResetState(); + } + _calculateDropSide(event, linkDropZone) { + const rect = linkDropZone.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + const width = rect.width; + const height = rect.height; + + const edgeSizeRatio = 0.3; // 30% of the size, maybe increase to 35% + const hEdge = width * edgeSizeRatio; + const vEdge = height * edgeSizeRatio; + + const isInLeftEdge = x < hEdge; + const isInRightEdge = x > width - hEdge; + const isInTopEdge = y < vEdge; + const isInBottomEdge = y > height - vEdge; + + if (isInTopEdge) { + if (isInLeftEdge && x / width < y / height) return 'left'; // More left in angle + if (isInRightEdge && (width - x) / width < y / height) return 'right'; // More right in angle + return 'top'; + } + if (isInBottomEdge) { + if (isInLeftEdge && x / width < (height - y) / height) return 'left'; + if (isInRightEdge && (width - x) / width < (height - y) / height) return 'right'; + return 'bottom'; + } + if (isInLeftEdge) { + return 'left'; + } + if (isInRightEdge) { + return 'right'; + } + return 'center'; + } + + _createOrUpdateSplitViewWithSide(currentTab, newTab, linkDropSide) { + const SIDES = ['left', 'right', 'top', 'bottom']; + const groupIndex = this.#zenViewSplitter._data.findIndex((group) => + group.tabs.includes(currentTab) + ); + + if (groupIndex > -1) { + const group = this.#zenViewSplitter._data[groupIndex]; + + if (group.tabs.length >= this.#zenViewSplitter.MAX_TABS) { + console.warn(`Cannot add tab to split, MAX_TABS (${this.#zenViewSplitter.MAX_TABS}) reached.`); + return; + } + + const splitViewGroup = this.#zenViewSplitter._getSplitViewGroup(group.tabs); + if (splitViewGroup && newTab.group !== splitViewGroup) { + this.#zenViewSplitter._moveTabsToContainer([newTab], currentTab); + gBrowser.moveTabToGroup(newTab, splitViewGroup); + } + + if (!group.tabs.includes(newTab)) { + group.tabs.push(newTab); + + const targetNode = this.#zenViewSplitter.getSplitNodeFromTab(currentTab); + const isValidSide = SIDES.includes(linkDropSide); + + if (targetNode && isValidSide) { + this.#zenViewSplitter.splitIntoNode( + targetNode, + new SplitLeafNode(newTab, 50), + linkDropSide, + 0.5 + ); + } else { + const parentNode = targetNode?.parent || group.layoutTree; + this.#zenViewSplitter.addTabToSplit(newTab, parentNode, false); + } + + this.#zenViewSplitter.activateSplitView(group, true); + } + return; + } + + const splitConfig = { + left: { tabs: [newTab, currentTab], gridType: 'vsep', initialIndex: 0 }, + right: { tabs: [currentTab, newTab], gridType: 'vsep', initialIndex: 1 }, + top: { tabs: [newTab, currentTab], gridType: 'hsep', initialIndex: 0 }, + bottom: { tabs: [currentTab, newTab], gridType: 'hsep', initialIndex: 1 }, + }; + + const { + tabs: tabsToSplit, + gridType, + initialIndex, + } = splitConfig[linkDropSide] || { + // If linkDropSide is invalid should use the default "vsep" + tabs: [currentTab, newTab], + gridType: 'vsep', + initialIndex: 1, + }; + + this.#zenViewSplitter.splitTabs(tabsToSplit, gridType, initialIndex); + } + + _hideDropZoneAndResetState() { + if (this._linkDropZone && this._linkDropZone.hasAttribute('enabled')) { + this._isLinkDragging = false; + this._hideLinkDropZone(true); + } + } +} + class ZenViewSplitter extends ZenDOMOperatedFeature { currentView = -1; _data = []; @@ -79,9 +378,7 @@ class ZenViewSplitter extends ZenDOMOperatedFeature { MAX_TABS = 4; - // Link drag and drop - _linkDropZone = null; - _isLinkDragging = false; + #ZenSplitViewLinkDrop; init() { this.handleTabEvent = this._handleTabEvent.bind(this); @@ -128,9 +425,9 @@ class ZenViewSplitter extends ZenDOMOperatedFeature { this.onBrowserDragEndToSplit = this.onBrowserDragEndToSplit.bind(this); } - // If enabled initialize the link drag and drop if (Services.prefs.getBoolPref('zen.splitView.enable-link-drop')) { - this.#initLinkDragDropSplit(); + this.#ZenSplitViewLinkDrop = new ZenSplitViewLinkDrop(this); + this.#ZenSplitViewLinkDrop.init(); } } @@ -1903,279 +2200,6 @@ class ZenViewSplitter extends ZenDOMOperatedFeature { } return true; } - - #initLinkDragDropSplit() { - this._handleLinkDragEnter = this._handleLinkDragEnter.bind(this); - this._handleLinkDragLeave = this._handleLinkDragLeave.bind(this); - this._handleLinkDragDrop = this._handleLinkDragDrop.bind(this); - this._handleLinkDragEnd = this._handleLinkDragEnd.bind(this); - - const tabBox = document.getElementById('tabbrowser-tabbox'); - - tabBox.addEventListener('dragenter', this._handleLinkDragEnter, true); - tabBox.addEventListener('dragleave', this._handleLinkDragLeave, false); - tabBox.addEventListener('drop', this._handleLinkDragDrop, false); - tabBox.addEventListener('dragend', this._handleLinkDragEnd, false); - } - - _createLinkDropZone() { - if (this._linkDropZone) return; - - this._linkDropZone = document.createXULElement('box'); - this._linkDropZone.id = 'zen-drop-link-zone'; - - const content = document.createXULElement('vbox'); - content.setAttribute('align', 'center'); - content.setAttribute('pack', 'center'); - content.setAttribute('flex', '1'); - - const text = document.createXULElement('description'); - text.setAttribute('value', 'Drop link to split'); // Localization! data-l10n-id - - content.appendChild(text); - this._linkDropZone.appendChild(content); - - this._linkDropZone.addEventListener('dragover', (event) => { - event.preventDefault(); - event.stopPropagation(); - event.dataTransfer.dropEffect = 'link'; - if (!this._linkDropZone.hasAttribute('has-focus')) { - this._linkDropZone.setAttribute('has-focus', 'true'); - } - }); - - this._linkDropZone.addEventListener('dragleave', (event) => { - event.stopPropagation(); - if (!this._linkDropZone.contains(event.relatedTarget)) { - this._linkDropZone.removeAttribute('has-focus'); - } - }); - - this._linkDropZone.addEventListener('drop', this._handleDropForSplit.bind(this)); - - const tabBox = document.getElementById('tabbrowser-tabbox'); - tabBox.appendChild(this._linkDropZone); - } - - _showLinkDropZone() { - if (!this._linkDropZone) this._createLinkDropZone(); - - this._linkDropZone.setAttribute('enabled', 'true'); - } - - _hideLinkDropZone(force = false) { - if (!this._linkDropZone || !this._linkDropZone.hasAttribute('enabled')) return; - - if (this._isLinkDragging && !force) return; - - this._linkDropZone.removeAttribute('enabled'); - this._linkDropZone.removeAttribute('has-focus'); - } - - _validateURI(dataTransfer) { - let dt = dataTransfer; - - const URL_TYPES = ['text/uri-list', 'text/x-moz-url', 'text/plain']; - - const FIXUP_FLAGS = Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS; - - const matchedType = URL_TYPES.find((type) => { - const raw = dt.getData(type); - return typeof raw === 'string' && raw.trim().length > 0; - }); - - const uriString = dt.getData(matchedType).trim(); - - const info = Services.uriFixup.getFixupURIInfo(uriString, FIXUP_FLAGS); - - if (!info || !info.fixedURI) { - return null; - } - - return info.fixedURI.spec; - } - - _handleLinkDragEnter(event) { - // If rearrangeViewEnabled - don't do anything - if (this.rearrangeViewEnabled) { - return; - } - - const shouldBeDisabled = !this.canOpenLinkInSplitView(); - if (shouldBeDisabled) return; - - // If the target is our drop zone or one of its children, or already active, do nothing here. - if ( - this._linkDropZone && - (this._linkDropZone.contains(event.target) || this._linkDropZone.hasAttribute('enabled')) - ) { - return; - } - - // If the data is not a valid URI, we don't want to do anything - if (!this._validateURI(event.dataTransfer)) { - return; - } - - this._isLinkDragging = true; - this._showLinkDropZone(); - - event.preventDefault(); - event.stopPropagation(); - } - - _handleLinkDragLeave(event) { - if ( - event.target === document.documentElement || - (event.clientX <= 0 && event.clientY <= 0) || - event.clientX >= window.innerWidth || - event.clientY >= window.innerHeight - ) { - if (this._linkDropZone && !this._linkDropZone.contains(event.relatedTarget)) { - this._isLinkDragging = false; - this._hideLinkDropZone(); - } - } - } - - _handleLinkDragDrop(event) { - if (!this._linkDropZone || !this._linkDropZone.contains(event.target)) { - if (this._linkDropZone && this._linkDropZone.hasAttribute('enabled')) { - this._isLinkDragging = false; - this._hideLinkDropZone(true); // true for forced hiding - } - } - } - - _handleLinkDragEnd(event) { - this._isLinkDragging = false; - this._hideLinkDropZone(true); // true for forced hiding - } - - _handleDropForSplit(event) { - let linkDropZone = this._linkDropZone; - event.preventDefault(); - event.stopPropagation(); - - const url = this._validateURI(event.dataTransfer); - - if (!url) { - this._hideDropZoneAndResetState(); - return; - } - - const currentTab = gZenGlanceManager.getTabOrGlanceParent(gBrowser.selectedTab); - const newTab = this.openAndSwitchToTab(url, { inBackground: false }); - - if (!newTab) { - this._hideDropZoneAndResetState(); - return; - } - - const linkDropSide = this._calculateDropSide(event, linkDropZone); - - this._createOrUpdateSplitViewWithSide(currentTab, newTab, linkDropSide); - - this._hideDropZoneAndResetState(); - } - _calculateDropSide(event, linkDropZone) { - const rect = linkDropZone.getBoundingClientRect(); - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; - const width = rect.width; - const height = rect.height; - - const edgeSizeRatio = 0.3; // 30% of the size, maybe increase to 35% - const hEdge = width * edgeSizeRatio; - const vEdge = height * edgeSizeRatio; - - const isInLeftEdge = x < hEdge; - const isInRightEdge = x > width - hEdge; - const isInTopEdge = y < vEdge; - const isInBottomEdge = y > height - vEdge; - - if (isInTopEdge) { - if (isInLeftEdge && x / width < y / height) return 'left'; // More left in angle - if (isInRightEdge && (width - x) / width < y / height) return 'right'; // More right in angle - return 'top'; - } - if (isInBottomEdge) { - if (isInLeftEdge && x / width < (height - y) / height) return 'left'; - if (isInRightEdge && (width - x) / width < (height - y) / height) return 'right'; - return 'bottom'; - } - if (isInLeftEdge) { - return 'left'; - } - if (isInRightEdge) { - return 'right'; - } - return 'center'; - } - - _createOrUpdateSplitViewWithSide(currentTab, newTab, linkDropSide) { - const SIDES = ['left', 'right', 'top', 'bottom']; - const groupIndex = this._data.findIndex((group) => group.tabs.includes(currentTab)); - - if (groupIndex > -1) { - const group = this._data[groupIndex]; - - if (group.tabs.length >= this.MAX_TABS) { - console.warn(`Cannot add tab to split, MAX_TABS (${this.MAX_TABS}) reached.`); - return; - } - - const splitViewGroup = this._getSplitViewGroup(group.tabs); - if (splitViewGroup && newTab.group !== splitViewGroup) { - this._moveTabsToContainer([newTab], currentTab); - gBrowser.moveTabToGroup(newTab, splitViewGroup); - } - - if (!group.tabs.includes(newTab)) { - group.tabs.push(newTab); - - const targetNode = this.getSplitNodeFromTab(currentTab); - const isValidSide = SIDES.includes(linkDropSide); - - if (targetNode && isValidSide) { - this.splitIntoNode(targetNode, new SplitLeafNode(newTab, 50), linkDropSide, 0.5); - } else { - const parentNode = targetNode?.parent || group.layoutTree; - this.addTabToSplit(newTab, parentNode, false); - } - - this.activateSplitView(group, true); - } - return; - } - - const splitConfig = { - left: { tabs: [newTab, currentTab], gridType: 'vsep', initialIndex: 0 }, - right: { tabs: [currentTab, newTab], gridType: 'vsep', initialIndex: 1 }, - top: { tabs: [newTab, currentTab], gridType: 'hsep', initialIndex: 0 }, - bottom: { tabs: [currentTab, newTab], gridType: 'hsep', initialIndex: 1 }, - }; - - const { - tabs: tabsToSplit, - gridType, - initialIndex, - } = splitConfig[linkDropSide] || { - // If linkDropSide is invalid should use the default "vsep" - tabs: [currentTab, newTab], - gridType: 'vsep', - initialIndex: 1, - }; - - this.splitTabs(tabsToSplit, gridType, initialIndex); - } - - _hideDropZoneAndResetState() { - if (this._linkDropZone && this._linkDropZone.hasAttribute('enabled')) { - this._isLinkDragging = false; - this._hideLinkDropZone(true); - } - } } window.gZenViewSplitter = new ZenViewSplitter(); From d8910beef3590456a622a1b7f1fd8ea268aad518 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Mon, 2 Jun 2025 00:01:53 +0300 Subject: [PATCH 03/19] npm run pretty --- src/zen/split-view/ZenViewSplitter.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 6ef9263402..bef17e0ada 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -153,8 +153,7 @@ class ZenSplitViewLinkDrop { const URL_TYPES = ['text/uri-list', 'text/x-moz-url', 'text/plain']; let fixupFlags = - Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | - Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; + Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; const matchedType = URL_TYPES.find((type) => { const raw = dt.getData(type); @@ -300,7 +299,9 @@ class ZenSplitViewLinkDrop { const group = this.#zenViewSplitter._data[groupIndex]; if (group.tabs.length >= this.#zenViewSplitter.MAX_TABS) { - console.warn(`Cannot add tab to split, MAX_TABS (${this.#zenViewSplitter.MAX_TABS}) reached.`); + console.warn( + `Cannot add tab to split, MAX_TABS (${this.#zenViewSplitter.MAX_TABS}) reached.` + ); return; } From fa141a50ced43825bec26cbcec69a6449b0ec7a6 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Mon, 2 Jun 2025 00:09:21 +0300 Subject: [PATCH 04/19] Removed optional if statement --- src/zen/split-view/ZenViewSplitter.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index bef17e0ada..436734cd43 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -78,8 +78,6 @@ class ZenSplitViewLinkDrop { } init() { - if (!Services.prefs.getBoolPref('zen.splitView.enable-link-drop')) return; - this.#handleLinkDragEnter = this._handleLinkDragEnter.bind(this); this.#handleLinkDragLeave = this._handleLinkDragLeave.bind(this); this.#handleLinkDragDrop = this._handleLinkDragDrop.bind(this); From 43712aad5ff3c7e2219d3a2b139c498ae77a1341 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mauro-balades@users.noreply.github.com> Date: Fri, 6 Jun 2025 13:49:55 +0200 Subject: [PATCH 05/19] Move pref to the correct feature config file Signed-off-by: mr. m <91018726+mauro-balades@users.noreply.github.com> --- src/browser/app/profile/features/split-view.inc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browser/app/profile/features/split-view.inc b/src/browser/app/profile/features/split-view.inc index 09afcb2b3a..c820152222 100644 --- a/src/browser/app/profile/features/split-view.inc +++ b/src/browser/app/profile/features/split-view.inc @@ -3,5 +3,6 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. pref('zen.splitView.enable-tab-drop', true); +pref('zen.splitView.enable-link-drop', true); pref('zen.splitView.min-resize-width', 7); pref('zen.splitView.rearrange-hover-size', 24); From 1620aa14735f70f4f4db5b796d1ead5dfe65abe5 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Fri, 6 Jun 2025 23:15:14 +0300 Subject: [PATCH 06/19] Add l10n for split view link drop zone --- src/zen/split-view/ZenViewSplitter.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 436734cd43..029ffed982 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -103,7 +103,7 @@ class ZenSplitViewLinkDrop { content.setAttribute('flex', '1'); const text = document.createXULElement('description'); - text.setAttribute('value', 'Drop link to split'); // Localization! data-l10n-id + text.setAttribute('data-l10n-id', 'zen-drop-link-zone-label'); content.appendChild(text); this._linkDropZone.appendChild(content); From 46ce1ae520b3ace926ed838f2448994b23808700 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Sat, 7 Jun 2025 00:34:13 +0300 Subject: [PATCH 07/19] Reworked the logic instead of hiding now removing --- src/zen/split-view/ZenViewSplitter.mjs | 63 ++++++-------------------- src/zen/split-view/zen-decks.css | 13 ++---- 2 files changed, 18 insertions(+), 58 deletions(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 029ffed982..ed47ccc6a5 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -66,7 +66,6 @@ class SplitNode extends SplitLeafNode { class ZenSplitViewLinkDrop { #zenViewSplitter; _linkDropZone = null; - _isLinkDragging = false; #handleLinkDragEnter; #handleLinkDragLeave; @@ -92,8 +91,6 @@ class ZenSplitViewLinkDrop { } _createLinkDropZone() { - if (this._linkDropZone) return; - this._linkDropZone = document.createXULElement('box'); this._linkDropZone.id = 'zen-drop-link-zone'; @@ -129,20 +126,11 @@ class ZenSplitViewLinkDrop { const tabBox = document.getElementById('tabbrowser-tabbox'); tabBox.appendChild(this._linkDropZone); } - - _showLinkDropZone() { - if (!this._linkDropZone) this._createLinkDropZone(); - - this._linkDropZone.setAttribute('enabled', 'true'); - } - - _hideLinkDropZone(force = false) { - if (!this._linkDropZone || !this._linkDropZone.hasAttribute('enabled')) return; - - if (this._isLinkDragging && !force) return; - - this._linkDropZone.removeAttribute('enabled'); - this._linkDropZone.removeAttribute('has-focus'); + _removeLinkDropZone() { + if (this._linkDropZone) { + this._linkDropZone.remove(); + this._linkDropZone = null; + } } _validateURI(dataTransfer) { @@ -178,11 +166,8 @@ class ZenSplitViewLinkDrop { const shouldBeDisabled = !this.#zenViewSplitter.canOpenLinkInSplitView(); if (shouldBeDisabled) return; - // If the target is our drop zone or one of its children, or already active, do nothing here. - if ( - this._linkDropZone && - (this._linkDropZone.contains(event.target) || this._linkDropZone.hasAttribute('enabled')) - ) { + // If _linkDropZone is already created, we don't want to do anything + if (this._linkDropZone) { return; } @@ -191,8 +176,7 @@ class ZenSplitViewLinkDrop { return; } - this._isLinkDragging = true; - this._showLinkDropZone(); + this._createLinkDropZone(); event.preventDefault(); event.stopPropagation(); @@ -206,24 +190,19 @@ class ZenSplitViewLinkDrop { event.clientY >= window.innerHeight ) { if (this._linkDropZone && !this._linkDropZone.contains(event.relatedTarget)) { - this._isLinkDragging = false; - this._hideLinkDropZone(); + this._removeLinkDropZone(); } } } _handleLinkDragDrop(event) { if (!this._linkDropZone || !this._linkDropZone.contains(event.target)) { - if (this._linkDropZone && this._linkDropZone.hasAttribute('enabled')) { - this._isLinkDragging = false; - this._hideLinkDropZone(true); // true for forced hiding - } + this._removeLinkDropZone(); } } _handleLinkDragEnd(event) { - this._isLinkDragging = false; - this._hideLinkDropZone(true); // true for forced hiding + this._removeLinkDropZone(); } _handleDropForSplit(event) { @@ -234,7 +213,7 @@ class ZenSplitViewLinkDrop { const url = this._validateURI(event.dataTransfer); if (!url) { - this._hideDropZoneAndResetState(); + this._removeLinkDropZone(); return; } @@ -242,7 +221,7 @@ class ZenSplitViewLinkDrop { const newTab = this.#zenViewSplitter.openAndSwitchToTab(url, { inBackground: false }); if (!newTab) { - this._hideDropZoneAndResetState(); + this._removeLinkDropZone(); return; } @@ -250,7 +229,7 @@ class ZenSplitViewLinkDrop { this._createOrUpdateSplitViewWithSide(currentTab, newTab, linkDropSide); - this._hideDropZoneAndResetState(); + this._removeLinkDropZone(); } _calculateDropSide(event, linkDropZone) { const rect = linkDropZone.getBoundingClientRect(); @@ -296,13 +275,6 @@ class ZenSplitViewLinkDrop { if (groupIndex > -1) { const group = this.#zenViewSplitter._data[groupIndex]; - if (group.tabs.length >= this.#zenViewSplitter.MAX_TABS) { - console.warn( - `Cannot add tab to split, MAX_TABS (${this.#zenViewSplitter.MAX_TABS}) reached.` - ); - return; - } - const splitViewGroup = this.#zenViewSplitter._getSplitViewGroup(group.tabs); if (splitViewGroup && newTab.group !== splitViewGroup) { this.#zenViewSplitter._moveTabsToContainer([newTab], currentTab); @@ -352,13 +324,6 @@ class ZenSplitViewLinkDrop { this.#zenViewSplitter.splitTabs(tabsToSplit, gridType, initialIndex); } - - _hideDropZoneAndResetState() { - if (this._linkDropZone && this._linkDropZone.hasAttribute('enabled')) { - this._isLinkDragging = false; - this._hideLinkDropZone(true); - } - } } class ZenViewSplitter extends ZenDOMOperatedFeature { diff --git a/src/zen/split-view/zen-decks.css b/src/zen/split-view/zen-decks.css index c268928dac..33d0734ae2 100644 --- a/src/zen/split-view/zen-decks.css +++ b/src/zen/split-view/zen-decks.css @@ -242,18 +242,13 @@ justify-content: center; z-index: 999; - opacity: 0; - transform: translate(-50%, -50%) translateY(80px) scale(0.1); + opacity: 1; + transform: translate(-50%, -50%) translateY(0) scale(1); + transition: transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.38s ease; - pointer-events: none; - - &[enabled='true'] { - opacity: 1; - transform: translate(-50%, -50%) translateY(0) scale(1); - pointer-events: auto; - } + pointer-events: auto; &[has-focus='true'] { transform: translate(-50%, -50%) translateY(0) scale(1.03); From 1e5254c731ad3f6853e028e0efb804968e3e2a39 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Sat, 7 Jun 2025 15:02:38 +0300 Subject: [PATCH 08/19] Fix in some cases `uriFixup` throws an exception --- src/zen/split-view/ZenViewSplitter.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index ed47ccc6a5..cfc17aaff6 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -148,6 +148,10 @@ class ZenSplitViewLinkDrop { const uriString = dt.getData(matchedType).trim(); + if (!uriString) { + return null; + } + const info = Services.uriFixup.getFixupURIInfo(uriString, fixupFlags); if (!info || !info.fixedURI) { From 66ec8e408ec0b955139d77e5bdbf2449b72cd3b6 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Sat, 7 Jun 2025 16:40:19 +0300 Subject: [PATCH 09/19] Fix opening tab for left and top sides --- src/zen/split-view/ZenViewSplitter.mjs | 28 ++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index cfc17aaff6..277d903185 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -66,6 +66,7 @@ class SplitNode extends SplitLeafNode { class ZenSplitViewLinkDrop { #zenViewSplitter; _linkDropZone = null; + _lastSplitSide = "right"; #handleLinkDragEnter; #handleLinkDragLeave; @@ -271,11 +272,11 @@ class ZenSplitViewLinkDrop { } _createOrUpdateSplitViewWithSide(currentTab, newTab, linkDropSide) { - const SIDES = ['left', 'right', 'top', 'bottom']; const groupIndex = this.#zenViewSplitter._data.findIndex((group) => group.tabs.includes(currentTab) ); + // Add to existing split view group if (groupIndex > -1) { const group = this.#zenViewSplitter._data[groupIndex]; @@ -289,9 +290,13 @@ class ZenSplitViewLinkDrop { group.tabs.push(newTab); const targetNode = this.#zenViewSplitter.getSplitNodeFromTab(currentTab); + const SIDES = ['left', 'right', 'top', 'bottom']; const isValidSide = SIDES.includes(linkDropSide); + // If targetNode and split side are valid we perform adding a new tab next to the current tab if (targetNode && isValidSide) { + this._lastSplitSide = linkDropSide; + this.#zenViewSplitter.splitIntoNode( targetNode, new SplitLeafNode(newTab, 50), @@ -299,8 +304,10 @@ class ZenSplitViewLinkDrop { 0.5 ); } else { + // If we can't determine the side, add the tab considering the previous side + const shouldPrepend = ['left', 'top'].includes(this._lastSplitSide); const parentNode = targetNode?.parent || group.layoutTree; - this.#zenViewSplitter.addTabToSplit(newTab, parentNode, false); + this.#zenViewSplitter.addTabToSplit(newTab, parentNode, shouldPrepend); } this.#zenViewSplitter.activateSplitView(group, true); @@ -315,16 +322,21 @@ class ZenSplitViewLinkDrop { bottom: { tabs: [currentTab, newTab], gridType: 'hsep', initialIndex: 1 }, }; + + // If linkDropSide is invalid should use the default "vsep" + const defaultConfig = { + tabs: [currentTab, newTab], + gridType: 'vsep', + initialIndex: 1, + } + const { tabs: tabsToSplit, gridType, initialIndex, - } = splitConfig[linkDropSide] || { - // If linkDropSide is invalid should use the default "vsep" - tabs: [currentTab, newTab], - gridType: 'vsep', - initialIndex: 1, - }; + } = splitConfig[linkDropSide] || defaultConfig; + + this._lastSplitSide = linkDropSide; this.#zenViewSplitter.splitTabs(tabsToSplit, gridType, initialIndex); } From df3b4bbc3a7f1aab21158ba42269f6f672a96b85 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Sun, 15 Jun 2025 23:12:51 +0300 Subject: [PATCH 10/19] Add auto close after timeout --- src/zen/split-view/ZenViewSplitter.mjs | 32 ++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index e60c18f6e5..0bdabb7c81 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -66,7 +66,9 @@ class SplitNode extends SplitLeafNode { class ZenSplitViewLinkDrop { #zenViewSplitter; _linkDropZone = null; - _lastSplitSide = "right"; + _linkDropTimer = null; + _linkDropTimeout = Services.prefs.getIntPref('zen.splitView.link-drop-timeout', 1000); + _lastSplitSide = 'right'; #handleLinkDragEnter; #handleLinkDragLeave; @@ -110,6 +112,8 @@ class ZenSplitViewLinkDrop { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'link'; + + clearTimeout(this._linkDropTimer); if (!this._linkDropZone.hasAttribute('has-focus')) { this._linkDropZone.setAttribute('has-focus', 'true'); } @@ -119,6 +123,12 @@ class ZenSplitViewLinkDrop { event.stopPropagation(); if (!this._linkDropZone.contains(event.relatedTarget)) { this._linkDropZone.removeAttribute('has-focus'); + + this._linkDropTimer = setTimeout(() => { + if (!this._linkDropZone.hasAttribute('has-focus')) { + this._removeLinkDropZone(); + } + }, this._linkDropTimeout); } }); @@ -128,6 +138,8 @@ class ZenSplitViewLinkDrop { tabBox.appendChild(this._linkDropZone); } _removeLinkDropZone() { + clearTimeout(this._linkDropTimer); + if (this._linkDropZone) { this._linkDropZone.remove(); this._linkDropZone = null; @@ -163,6 +175,9 @@ class ZenSplitViewLinkDrop { } _handleLinkDragEnter(event) { + event.preventDefault(); + event.stopPropagation(); + // If rearrangeViewEnabled - don't do anything if (this.#zenViewSplitter.rearrangeViewEnabled) { return; @@ -183,11 +198,16 @@ class ZenSplitViewLinkDrop { this._createLinkDropZone(); - event.preventDefault(); - event.stopPropagation(); + this._linkDropTimer = setTimeout(() => { + if (!this._linkDropZone.hasAttribute('has-focus')) { + this._removeLinkDropZone(); + } + }, this._linkDropTimeout); } _handleLinkDragLeave(event) { + clearTimeout(this._linkDropTimer); + if ( event.target === document.documentElement || (event.clientX <= 0 && event.clientY <= 0) || @@ -202,15 +222,18 @@ class ZenSplitViewLinkDrop { _handleLinkDragDrop(event) { if (!this._linkDropZone || !this._linkDropZone.contains(event.target)) { + clearTimeout(this._linkDropTimer); this._removeLinkDropZone(); } } _handleLinkDragEnd(event) { + clearTimeout(this._linkDropTimer); this._removeLinkDropZone(); } _handleDropForSplit(event) { + clearTimeout(this._linkDropTimer); let linkDropZone = this._linkDropZone; event.preventDefault(); event.stopPropagation(); @@ -322,13 +345,12 @@ class ZenSplitViewLinkDrop { bottom: { tabs: [currentTab, newTab], gridType: 'hsep', initialIndex: 1 }, }; - // If linkDropSide is invalid should use the default "vsep" const defaultConfig = { tabs: [currentTab, newTab], gridType: 'vsep', initialIndex: 1, - } + }; const { tabs: tabsToSplit, From 1e572587b34dd44484dc51943416549ff9286346 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Sun, 15 Jun 2025 23:20:32 +0300 Subject: [PATCH 11/19] Refactor linkDropZone events --- src/zen/split-view/ZenViewSplitter.mjs | 49 +++++++++++++------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 0bdabb7c81..87abefb42b 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -108,35 +108,36 @@ class ZenSplitViewLinkDrop { content.appendChild(text); this._linkDropZone.appendChild(content); - this._linkDropZone.addEventListener('dragover', (event) => { - event.preventDefault(); - event.stopPropagation(); - event.dataTransfer.dropEffect = 'link'; - - clearTimeout(this._linkDropTimer); - if (!this._linkDropZone.hasAttribute('has-focus')) { - this._linkDropZone.setAttribute('has-focus', 'true'); - } - }); - - this._linkDropZone.addEventListener('dragleave', (event) => { - event.stopPropagation(); - if (!this._linkDropZone.contains(event.relatedTarget)) { - this._linkDropZone.removeAttribute('has-focus'); - - this._linkDropTimer = setTimeout(() => { - if (!this._linkDropZone.hasAttribute('has-focus')) { - this._removeLinkDropZone(); - } - }, this._linkDropTimeout); - } - }); - + this._linkDropZone.addEventListener('dragover', this._handleDragOver.bind(this)); + this._linkDropZone.addEventListener('dragleave', this._handleDragLeave.bind(this)); this._linkDropZone.addEventListener('drop', this._handleDropForSplit.bind(this)); const tabBox = document.getElementById('tabbrowser-tabbox'); tabBox.appendChild(this._linkDropZone); } + _handleDragOver(event) { + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = 'link'; + + clearTimeout(this._linkDropTimer); + if (!this._linkDropZone.hasAttribute('has-focus')) { + this._linkDropZone.setAttribute('has-focus', 'true'); + } + } + + _handleDragLeave(event) { + event.stopPropagation(); + if (!this._linkDropZone.contains(event.relatedTarget)) { + this._linkDropZone.removeAttribute('has-focus'); + + this._linkDropTimer = setTimeout(() => { + if (!this._linkDropZone.hasAttribute('has-focus')) { + this._removeLinkDropZone(); + } + }, this._linkDropTimeout); + } + } _removeLinkDropZone() { clearTimeout(this._linkDropTimer); From 0d50fe7872398a9ad2511244119cd8113311d867 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Sun, 15 Jun 2025 23:25:38 +0300 Subject: [PATCH 12/19] Refactor _createOrUpdateSplitViewWithSide --- src/zen/split-view/ZenViewSplitter.mjs | 65 +++++++++++++------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 87abefb42b..ba982a215c 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -300,45 +300,48 @@ class ZenSplitViewLinkDrop { group.tabs.includes(currentTab) ); - // Add to existing split view group if (groupIndex > -1) { - const group = this.#zenViewSplitter._data[groupIndex]; - - const splitViewGroup = this.#zenViewSplitter._getSplitViewGroup(group.tabs); - if (splitViewGroup && newTab.group !== splitViewGroup) { - this.#zenViewSplitter._moveTabsToContainer([newTab], currentTab); - gBrowser.moveTabToGroup(newTab, splitViewGroup); - } + this._addToExistingGroup(groupIndex, currentTab, newTab, linkDropSide); + } else { + this._createNewSplitGroup(currentTab, newTab, linkDropSide); + } + } - if (!group.tabs.includes(newTab)) { - group.tabs.push(newTab); + _addToExistingGroup(groupIndex, currentTab, newTab, linkDropSide) { + const group = this.#zenViewSplitter._data[groupIndex]; + const splitViewGroup = this.#zenViewSplitter._getSplitViewGroup(group.tabs); - const targetNode = this.#zenViewSplitter.getSplitNodeFromTab(currentTab); - const SIDES = ['left', 'right', 'top', 'bottom']; - const isValidSide = SIDES.includes(linkDropSide); + if (splitViewGroup && newTab.group !== splitViewGroup) { + this.#zenViewSplitter._moveTabsToContainer([newTab], currentTab); + gBrowser.moveTabToGroup(newTab, splitViewGroup); + } - // If targetNode and split side are valid we perform adding a new tab next to the current tab - if (targetNode && isValidSide) { - this._lastSplitSide = linkDropSide; + if (!group.tabs.includes(newTab)) { + group.tabs.push(newTab); - this.#zenViewSplitter.splitIntoNode( - targetNode, - new SplitLeafNode(newTab, 50), - linkDropSide, - 0.5 - ); - } else { - // If we can't determine the side, add the tab considering the previous side - const shouldPrepend = ['left', 'top'].includes(this._lastSplitSide); - const parentNode = targetNode?.parent || group.layoutTree; - this.#zenViewSplitter.addTabToSplit(newTab, parentNode, shouldPrepend); - } + const targetNode = this.#zenViewSplitter.getSplitNodeFromTab(currentTab); + const SIDES = ['left', 'right', 'top', 'bottom']; + const isValidSide = SIDES.includes(linkDropSide); - this.#zenViewSplitter.activateSplitView(group, true); + if (targetNode && isValidSide) { + this._lastSplitSide = linkDropSide; + this.#zenViewSplitter.splitIntoNode( + targetNode, + new SplitLeafNode(newTab, 50), + linkDropSide, + 0.5 + ); + } else { + const shouldPrepend = ['left', 'top'].includes(this._lastSplitSide); + const parentNode = targetNode?.parent || group.layoutTree; + this.#zenViewSplitter.addTabToSplit(newTab, parentNode, shouldPrepend); } - return; + + this.#zenViewSplitter.activateSplitView(group, true); } + } + _createNewSplitGroup(currentTab, newTab, linkDropSide) { const splitConfig = { left: { tabs: [newTab, currentTab], gridType: 'vsep', initialIndex: 0 }, right: { tabs: [currentTab, newTab], gridType: 'vsep', initialIndex: 1 }, @@ -346,7 +349,6 @@ class ZenSplitViewLinkDrop { bottom: { tabs: [currentTab, newTab], gridType: 'hsep', initialIndex: 1 }, }; - // If linkDropSide is invalid should use the default "vsep" const defaultConfig = { tabs: [currentTab, newTab], gridType: 'vsep', @@ -360,7 +362,6 @@ class ZenSplitViewLinkDrop { } = splitConfig[linkDropSide] || defaultConfig; this._lastSplitSide = linkDropSide; - this.#zenViewSplitter.splitTabs(tabsToSplit, gridType, initialIndex); } } From 6828930332697f4e199d48083aaf679539991e39 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Mon, 16 Jun 2025 21:14:30 +0300 Subject: [PATCH 13/19] Add animation to link drop zone --- src/zen/split-view/ZenViewSplitter.mjs | 33 ++++++++++++++++++++++---- src/zen/split-view/zen-decks.css | 32 +++++++++++++++---------- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index ba982a215c..c28e7e9887 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -114,13 +114,25 @@ class ZenSplitViewLinkDrop { const tabBox = document.getElementById('tabbrowser-tabbox'); tabBox.appendChild(this._linkDropZone); + + gZenUIManager.motion.animate(this._linkDropZone, { + opacity: [0, 1], + x: ['-50%', '-50%'], + y: ['-40%', '-50%'], + scale: [0.1, 1], + duration: 0.15, + ease: [0.16, 1, 0.3, 1], + }); } _handleDragOver(event) { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'link'; - clearTimeout(this._linkDropTimer); + + const side = this._calculateDropSide(event, this._linkDropZone); + this._linkDropZone.setAttribute('drop-side', side); + if (!this._linkDropZone.hasAttribute('has-focus')) { this._linkDropZone.setAttribute('has-focus', 'true'); } @@ -129,6 +141,7 @@ class ZenSplitViewLinkDrop { _handleDragLeave(event) { event.stopPropagation(); if (!this._linkDropZone.contains(event.relatedTarget)) { + this._linkDropZone.removeAttribute('drop-side'); this._linkDropZone.removeAttribute('has-focus'); this._linkDropTimer = setTimeout(() => { @@ -139,12 +152,22 @@ class ZenSplitViewLinkDrop { } } _removeLinkDropZone() { + if (!this._linkDropZone) return; clearTimeout(this._linkDropTimer); - if (this._linkDropZone) { - this._linkDropZone.remove(); - this._linkDropZone = null; - } + gZenUIManager.motion + .animate(this._linkDropZone, { + opacity: [1, 0], + x: ['-50%', '-50%'], + y: ['-40%', '-50%'], + scale: [1, 0.1], + duration: 0.15, + ease: [0.16, 1, 0.3, 1], + }) + .then(() => { + this._linkDropZone.remove(); + this._linkDropZone = null; + }); } _validateURI(dataTransfer) { diff --git a/src/zen/split-view/zen-decks.css b/src/zen/split-view/zen-decks.css index 33d0734ae2..69ef21460e 100644 --- a/src/zen/split-view/zen-decks.css +++ b/src/zen/split-view/zen-decks.css @@ -230,7 +230,7 @@ height: 150px; background: var(--zen-branding-bg); - border: 1px solid rgba(255, 255, 255, 0.08); + border: 2px solid transparent; border-radius: 18px; box-shadow: rgba(0, 0, 0, 0.45) 0px 15px 35px, @@ -242,20 +242,28 @@ justify-content: center; z-index: 999; - opacity: 1; - transform: translate(-50%, -50%) translateY(0) scale(1); - - transition: - transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), - opacity 0.38s ease; + opacity: 0; + transform: translate(-50%, -40%) scale(0.95); pointer-events: auto; &[has-focus='true'] { - transform: translate(-50%, -50%) translateY(0) scale(1.03); - transition: - transform 0.25s cubic-bezier(0.22, 1, 0.36, 1), - border-color 0.25s ease; - border-color: var(--zen-primary-color); + border-color: var(--zen-colors-border); + } + + &[drop-side='left'] { + border-left-color: var(--zen-primary-color); + } + + &[drop-side='right'] { + border-right-color: var(--zen-primary-color); + } + + &[drop-side='top'] { + border-top-color: var(--zen-primary-color); + } + + &[drop-side='bottom'] { + border-bottom-color: var(--zen-primary-color); } & text { From 2b157a0c8f04dcac51e83ed6f48c73e397e356c1 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Tue, 17 Jun 2025 20:55:03 +0300 Subject: [PATCH 14/19] Add node size alignment --- src/zen/split-view/ZenViewSplitter.mjs | 76 ++++++++++---------------- 1 file changed, 29 insertions(+), 47 deletions(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index c28e7e9887..62f7871646 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -66,31 +66,19 @@ class SplitNode extends SplitLeafNode { class ZenSplitViewLinkDrop { #zenViewSplitter; _linkDropZone = null; - _linkDropTimer = null; - _linkDropTimeout = Services.prefs.getIntPref('zen.splitView.link-drop-timeout', 1000); _lastSplitSide = 'right'; - #handleLinkDragEnter; - #handleLinkDragLeave; - #handleLinkDragDrop; - #handleLinkDragEnd; - constructor(zenViewSplitter) { this.#zenViewSplitter = zenViewSplitter; } init() { - this.#handleLinkDragEnter = this._handleLinkDragEnter.bind(this); - this.#handleLinkDragLeave = this._handleLinkDragLeave.bind(this); - this.#handleLinkDragDrop = this._handleLinkDragDrop.bind(this); - this.#handleLinkDragEnd = this._handleLinkDragEnd.bind(this); - const tabBox = document.getElementById('tabbrowser-tabbox'); - tabBox.addEventListener('dragenter', this.#handleLinkDragEnter, true); - tabBox.addEventListener('dragleave', this.#handleLinkDragLeave, false); - tabBox.addEventListener('drop', this.#handleLinkDragDrop, false); - tabBox.addEventListener('dragend', this.#handleLinkDragEnd, false); + tabBox.addEventListener('dragenter', this._handleLinkDragEnter.bind(this)); + tabBox.addEventListener('dragleave', this._handleLinkDragLeave.bind(this)); + tabBox.addEventListener('drop', this._handleLinkDragDrop.bind(this)); + tabBox.addEventListener('dragend', this._handleLinkDragEnd.bind(this)); } _createLinkDropZone() { @@ -128,8 +116,6 @@ class ZenSplitViewLinkDrop { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'link'; - clearTimeout(this._linkDropTimer); - const side = this._calculateDropSide(event, this._linkDropZone); this._linkDropZone.setAttribute('drop-side', side); @@ -143,17 +129,10 @@ class ZenSplitViewLinkDrop { if (!this._linkDropZone.contains(event.relatedTarget)) { this._linkDropZone.removeAttribute('drop-side'); this._linkDropZone.removeAttribute('has-focus'); - - this._linkDropTimer = setTimeout(() => { - if (!this._linkDropZone.hasAttribute('has-focus')) { - this._removeLinkDropZone(); - } - }, this._linkDropTimeout); } } _removeLinkDropZone() { if (!this._linkDropZone) return; - clearTimeout(this._linkDropTimer); gZenUIManager.motion .animate(this._linkDropZone, { @@ -221,17 +200,9 @@ class ZenSplitViewLinkDrop { } this._createLinkDropZone(); - - this._linkDropTimer = setTimeout(() => { - if (!this._linkDropZone.hasAttribute('has-focus')) { - this._removeLinkDropZone(); - } - }, this._linkDropTimeout); } _handleLinkDragLeave(event) { - clearTimeout(this._linkDropTimer); - if ( event.target === document.documentElement || (event.clientX <= 0 && event.clientY <= 0) || @@ -246,18 +217,15 @@ class ZenSplitViewLinkDrop { _handleLinkDragDrop(event) { if (!this._linkDropZone || !this._linkDropZone.contains(event.target)) { - clearTimeout(this._linkDropTimer); this._removeLinkDropZone(); } } _handleLinkDragEnd(event) { - clearTimeout(this._linkDropTimer); this._removeLinkDropZone(); } _handleDropForSplit(event) { - clearTimeout(this._linkDropTimer); let linkDropZone = this._linkDropZone; event.preventDefault(); event.stopPropagation(); @@ -279,10 +247,11 @@ class ZenSplitViewLinkDrop { const linkDropSide = this._calculateDropSide(event, linkDropZone); - this._createOrUpdateSplitViewWithSide(currentTab, newTab, linkDropSide); + this._dispatchSplitAction(currentTab, newTab, linkDropSide); this._removeLinkDropZone(); } + _calculateDropSide(event, linkDropZone) { const rect = linkDropZone.getBoundingClientRect(); const x = event.clientX - rect.left; @@ -290,9 +259,10 @@ class ZenSplitViewLinkDrop { const width = rect.width; const height = rect.height; - const edgeSizeRatio = 0.3; // 30% of the size, maybe increase to 35% - const hEdge = width * edgeSizeRatio; - const vEdge = height * edgeSizeRatio; + // Defines the size of the "active" zone near the edges (30%) + const EDGE_SIZE_RATIO = 0.3; + const hEdge = width * EDGE_SIZE_RATIO; + const vEdge = height * EDGE_SIZE_RATIO; const isInLeftEdge = x < hEdge; const isInRightEdge = x > width - hEdge; @@ -300,11 +270,14 @@ class ZenSplitViewLinkDrop { const isInBottomEdge = y > height - vEdge; if (isInTopEdge) { - if (isInLeftEdge && x / width < y / height) return 'left'; // More left in angle - if (isInRightEdge && (width - x) / width < y / height) return 'right'; // More right in angle + // If the cursor is in a top corner, determine which side it's proportionally "closer" to + // This comparison decides if it's a side drop or a top drop + if (isInLeftEdge && x / width < y / height) return 'left'; + if (isInRightEdge && (width - x) / width < y / height) return 'right'; return 'top'; } if (isInBottomEdge) { + // Similar logic for the bottom corners if (isInLeftEdge && x / width < (height - y) / height) return 'left'; if (isInRightEdge && (width - x) / width < (height - y) / height) return 'right'; return 'bottom'; @@ -315,10 +288,12 @@ class ZenSplitViewLinkDrop { if (isInRightEdge) { return 'right'; } + + // If the cursor is not in any edge zone, it's considered the center return 'center'; } - _createOrUpdateSplitViewWithSide(currentTab, newTab, linkDropSide) { + _dispatchSplitAction(currentTab, newTab, linkDropSide) { const groupIndex = this.#zenViewSplitter._data.findIndex((group) => group.tabs.includes(currentTab) ); @@ -343,20 +318,27 @@ class ZenSplitViewLinkDrop { group.tabs.push(newTab); const targetNode = this.#zenViewSplitter.getSplitNodeFromTab(currentTab); - const SIDES = ['left', 'right', 'top', 'bottom']; - const isValidSide = SIDES.includes(linkDropSide); + const parentNode = targetNode?.parent || group.layoutTree; + const isValidSide = ['left', 'right', 'top', 'bottom'].includes(linkDropSide); if (targetNode && isValidSide) { this._lastSplitSide = linkDropSide; + this.#zenViewSplitter.splitIntoNode( targetNode, - new SplitLeafNode(newTab, 50), + new SplitLeafNode(newTab), linkDropSide, 0.5 ); + + // Rebalance sizes + const newSize = 100 / parentNode.children.length; + parentNode.children.forEach((child) => { + child.sizeInParent = newSize; + }); } else { + // If linkDropSide is center, then open a new tab at the start/end const shouldPrepend = ['left', 'top'].includes(this._lastSplitSide); - const parentNode = targetNode?.parent || group.layoutTree; this.#zenViewSplitter.addTabToSplit(newTab, parentNode, shouldPrepend); } From 609f6ca59a2e5b314d43720e03fce4eb82db5f01 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Sat, 28 Jun 2025 16:31:08 +0300 Subject: [PATCH 15/19] Fix SplitLeafNode -> nsSplitLeafNode --- src/zen/split-view/ZenViewSplitter.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 2af09675e0..0055b9f738 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -326,7 +326,7 @@ class ZenSplitViewLinkDrop { this.#zenViewSplitter.splitIntoNode( targetNode, - new SplitLeafNode(newTab), + new nsSplitLeafNode(newTab), linkDropSide, 0.5 ); From 1f9c58ef9dfd4aa730135e29043fa66723786c7a Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Thu, 10 Jul 2025 19:35:32 +0300 Subject: [PATCH 16/19] feat: Enhance drag and drop zone visuals and animations --- src/zen/split-view/ZenViewSplitter.mjs | 32 ++++++++++---- src/zen/split-view/zen-decks.css | 59 +++++++++++++++++++++----- 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 1d4a697b21..46e93a2b45 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -82,6 +82,9 @@ class ZenSplitViewLinkDrop { } _createLinkDropZone() { + const wrapper = document.createXULElement('box'); + wrapper.id = 'zen-drop-link-wrapper'; + this._linkDropZone = document.createXULElement('box'); this._linkDropZone.id = 'zen-drop-link-zone'; @@ -96,17 +99,17 @@ class ZenSplitViewLinkDrop { content.appendChild(text); this._linkDropZone.appendChild(content); + wrapper.appendChild(this._linkDropZone); + this._linkDropZone.addEventListener('dragover', this._handleDragOver.bind(this)); this._linkDropZone.addEventListener('dragleave', this._handleDragLeave.bind(this)); this._linkDropZone.addEventListener('drop', this._handleDropForSplit.bind(this)); const tabBox = document.getElementById('tabbrowser-tabbox'); - tabBox.appendChild(this._linkDropZone); + tabBox.appendChild(wrapper); gZenUIManager.motion.animate(this._linkDropZone, { opacity: [0, 1], - x: ['-50%', '-50%'], - y: ['-40%', '-50%'], scale: [0.1, 1], duration: 0.15, ease: [0.16, 1, 0.3, 1], @@ -121,30 +124,43 @@ class ZenSplitViewLinkDrop { if (!this._linkDropZone.hasAttribute('has-focus')) { this._linkDropZone.setAttribute('has-focus', 'true'); + gZenUIManager.motion.animate(this._linkDropZone, { + scale: [1, 1.2], + duration: 0.15, + }); } } _handleDragLeave(event) { event.stopPropagation(); if (!this._linkDropZone.contains(event.relatedTarget)) { - this._linkDropZone.removeAttribute('drop-side'); - this._linkDropZone.removeAttribute('has-focus'); + gZenUIManager.motion + .animate(this._linkDropZone, { + scale: [1.2, 1], + duration: 0.15, + }) + .then(() => { + this._linkDropZone.removeAttribute('has-focus'); + this._linkDropZone.removeAttribute('drop-side'); + }); } } _removeLinkDropZone() { if (!this._linkDropZone) return; + const wrapper = this._linkDropZone.parentElement; + gZenUIManager.motion .animate(this._linkDropZone, { opacity: [1, 0], - x: ['-50%', '-50%'], - y: ['-40%', '-50%'], scale: [1, 0.1], duration: 0.15, ease: [0.16, 1, 0.3, 1], }) .then(() => { - this._linkDropZone.remove(); + if (wrapper) { + wrapper.remove(); + } this._linkDropZone = null; }); } diff --git a/src/zen/split-view/zen-decks.css b/src/zen/split-view/zen-decks.css index 713fe8c5b8..ab8377c163 100644 --- a/src/zen/split-view/zen-decks.css +++ b/src/zen/split-view/zen-decks.css @@ -222,32 +222,71 @@ } } -#zen-drop-link-zone { +@keyframes zen-rotate-orbit { + 0% { + transform: translate(-50%, -50%) rotate(0deg) translateX(8px) rotate(0deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg) translateX(8px) rotate(-360deg); + } +} + +#zen-drop-link-wrapper { position: fixed; top: 50%; left: 50%; - width: 260px; - height: 150px; + width: 200px; + height: 260px; + z-index: 999; + pointer-events: none; + animation: zen-rotate-orbit 2.5s linear infinite; +} +#zen-drop-link-zone { + width: 100%; + height: 100%; background: var(--zen-branding-bg); border: 2px solid transparent; border-radius: 18px; - box-shadow: - rgba(0, 0, 0, 0.45) 0px 15px 35px, - rgba(255, 255, 255, 0.04) 0px 1px 0px inset; + box-shadow: rgba(0, 0, 0, 0.45) 0px 15px 35px; display: flex; flex-direction: column; align-items: center; justify-content: center; - z-index: 999; opacity: 0; - transform: translate(-50%, -40%) scale(0.95); pointer-events: auto; + transition: + background-color 0.3s ease, + border-color 0.3s ease; + will-change: transform; + + &::before { + content: ''; + position: absolute; + top: 6px; + left: 6px; + right: 6px; + bottom: 6px; + + border-radius: 12px; + + border: 2px dashed rgba(255, 255, 255, 0.2); + transition: border-color 0.3s ease; + pointer-events: none; + } + &[has-focus='true'] { border-color: var(--zen-colors-border); + background: color-mix(in srgb, var(--zen-branding-bg), var(--zen-primary-color) 30%); + &::before { + border: 2px dashed color-mix(in srgb, var(--zen-primary-color), transparent 30%); + } + & description { + color: color-mix(in srgb, rgba(255, 255, 255, 0.85), var(--zen-primary-color) 50%); + } } &[drop-side='left'] { @@ -266,12 +305,12 @@ border-bottom-color: var(--zen-primary-color); } - & text { + & description { font-size: 14px; line-height: 1.4; text-align: center; color: rgba(255, 255, 255, 0.85); - font-weight: 500; + font-weight: 600; letter-spacing: 0.2px; user-select: none; } From b4e45ec93ec59407b2e24619c2004edf80f1e1ad Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Fri, 11 Jul 2025 00:38:42 +0300 Subject: [PATCH 17/19] feat: Enhance link drop zone with dynamic icon and glance functionality --- src/zen/split-view/ZenViewSplitter.mjs | 81 +++++++++++++++++++++++--- src/zen/split-view/zen-decks.css | 27 +++++---- 2 files changed, 87 insertions(+), 21 deletions(-) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 46e93a2b45..927e078665 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -68,6 +68,19 @@ class ZenSplitViewLinkDrop { _linkDropZone = null; _lastSplitSide = 'right'; + _svgIcon = new DOMParser().parseFromString( + ` + + + + + + `, + 'image/svg+xml' + ).documentElement; + _svgIconLeftFill = null; + _svgIconRightFill = null; + constructor(zenViewSplitter) { this.#zenViewSplitter = zenViewSplitter; } @@ -93,8 +106,17 @@ class ZenSplitViewLinkDrop { content.setAttribute('pack', 'center'); content.setAttribute('flex', '1'); + this._svgIcon.id = 'zen-drop-link-icon'; + + this._svgIconLeftFill = this._svgIcon.querySelector('#left-fill'); + this._svgIconRightFill = this._svgIcon.querySelector('#right-fill'); + + content.appendChild(this._svgIcon); + this._updateIconForSide('center'); + const text = document.createXULElement('description'); text.setAttribute('data-l10n-id', 'zen-drop-link-zone-label'); + text.style.marginTop = '8px'; content.appendChild(text); this._linkDropZone.appendChild(content); @@ -115,6 +137,30 @@ class ZenSplitViewLinkDrop { ease: [0.16, 1, 0.3, 1], }); } + + _updateIconForSide(side) { + this._svgIcon.style.transform = 'rotate(0deg)'; + this._svgIconLeftFill.style.opacity = '1'; + this._svgIconRightFill.style.opacity = '1'; + + switch (side) { + case 'left': + this._svgIconRightFill.style.opacity = '0'; + break; + case 'right': + this._svgIconLeftFill.style.opacity = '0'; + break; + case 'top': + this._svgIcon.style.transform = 'rotate(90deg)'; + this._svgIconRightFill.style.opacity = '0'; + break; + case 'bottom': + this._svgIcon.style.transform = 'rotate(-90deg)'; + this._svgIconRightFill.style.opacity = '0'; + break; + } + } + _handleDragOver(event) { event.preventDefault(); event.stopPropagation(); @@ -122,10 +168,12 @@ class ZenSplitViewLinkDrop { const side = this._calculateDropSide(event, this._linkDropZone); this._linkDropZone.setAttribute('drop-side', side); + this._updateIconForSide(side); + if (!this._linkDropZone.hasAttribute('has-focus')) { this._linkDropZone.setAttribute('has-focus', 'true'); gZenUIManager.motion.animate(this._linkDropZone, { - scale: [1, 1.2], + scale: [1, 1.1], duration: 0.15, }); } @@ -134,9 +182,10 @@ class ZenSplitViewLinkDrop { _handleDragLeave(event) { event.stopPropagation(); if (!this._linkDropZone.contains(event.relatedTarget)) { + this._updateIconForSide('center'); gZenUIManager.motion .animate(this._linkDropZone, { - scale: [1.2, 1], + scale: [1.1, 1], duration: 0.15, }) .then(() => { @@ -145,6 +194,7 @@ class ZenSplitViewLinkDrop { }); } } + _removeLinkDropZone() { if (!this._linkDropZone) return; @@ -158,10 +208,10 @@ class ZenSplitViewLinkDrop { ease: [0.16, 1, 0.3, 1], }) .then(() => { - if (wrapper) { - wrapper.remove(); - } + wrapper.remove(); this._linkDropZone = null; + this._svgIconLeftFill = null; + this._svgIconRightFill = null; }); } @@ -263,6 +313,23 @@ class ZenSplitViewLinkDrop { const linkDropSide = this._calculateDropSide(event, linkDropZone); + if (linkDropSide === 'center') { + const rect = event.target.getBoundingClientRect(); + gZenGlanceManager.openGlance( + { + clientX: rect.left, + clientY: rect.top, + width: rect.width, + height: rect.height, + }, + newTab, + currentTab + ); + + this._removeLinkDropZone(); + return; + } + this._dispatchSplitAction(currentTab, newTab, linkDropSide); this._removeLinkDropZone(); @@ -352,10 +419,6 @@ class ZenSplitViewLinkDrop { parentNode.children.forEach((child) => { child.sizeInParent = newSize; }); - } else { - // If linkDropSide is center, then open a new tab at the start/end - const shouldPrepend = ['left', 'top'].includes(this._lastSplitSide); - this.#zenViewSplitter.addTabToSplit(newTab, parentNode, shouldPrepend); } this.#zenViewSplitter.activateSplitView(group, true); diff --git a/src/zen/split-view/zen-decks.css b/src/zen/split-view/zen-decks.css index ab8377c163..9f916227dc 100644 --- a/src/zen/split-view/zen-decks.css +++ b/src/zen/split-view/zen-decks.css @@ -224,10 +224,10 @@ @keyframes zen-rotate-orbit { 0% { - transform: translate(-50%, -50%) rotate(0deg) translateX(8px) rotate(0deg); + transform: translate(-50%, -50%) rotate(0deg) translateX(6px) rotate(0deg); } 100% { - transform: translate(-50%, -50%) rotate(360deg) translateX(8px) rotate(-360deg); + transform: translate(-50%, -50%) rotate(360deg) translateX(6px) rotate(-360deg); } } @@ -238,8 +238,7 @@ width: 200px; height: 260px; z-index: 999; - pointer-events: none; - animation: zen-rotate-orbit 2.5s linear infinite; + animation: zen-rotate-orbit 3s linear infinite; } #zen-drop-link-zone { @@ -259,8 +258,8 @@ pointer-events: auto; transition: - background-color 0.3s ease, - border-color 0.3s ease; + background-color 0.15s ease, + border-color 0.15s ease; will-change: transform; &::before { @@ -272,9 +271,8 @@ bottom: 6px; border-radius: 12px; - - border: 2px dashed rgba(255, 255, 255, 0.2); - transition: border-color 0.3s ease; + border: 2px dashed color-mix(in srgb, var(--toolbox-textcolor), transparent 20%); + transition: border-color 0.15s ease; pointer-events: none; } @@ -282,10 +280,10 @@ border-color: var(--zen-colors-border); background: color-mix(in srgb, var(--zen-branding-bg), var(--zen-primary-color) 30%); &::before { - border: 2px dashed color-mix(in srgb, var(--zen-primary-color), transparent 30%); + border: 2px dashed color-mix(in srgb, var(--zen-primary-color), transparent 20%); } & description { - color: color-mix(in srgb, rgba(255, 255, 255, 0.85), var(--zen-primary-color) 50%); + color: color-mix(in srgb, var(--toolbox-textcolor), var(--zen-primary-color) 50%); } } @@ -305,11 +303,16 @@ border-bottom-color: var(--zen-primary-color); } + #zen-drop-link-icon { + width: 48px; + height: 48px; + } + & description { font-size: 14px; line-height: 1.4; text-align: center; - color: rgba(255, 255, 255, 0.85); + color: color-mix(in srgb, var(--toolbox-textcolor), transparent 20%); font-weight: 600; letter-spacing: 0.2px; user-select: none; From 6e6960fb14c4110ae2a4b9914d151a131b8446a9 Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Fri, 11 Jul 2025 19:20:18 +0300 Subject: [PATCH 18/19] test: add browser tests for link darg and drop behavior --- src/zen/tests/moz.build | 1 + src/zen/tests/split_view/browser.toml | 11 ++ ...browser_link_drop_add_to_existing_split.js | 29 +++++ .../browser_link_drop_center_opens_glance.js | 24 ++++ .../browser_link_drop_split_right.js | 25 ++++ .../split_view/browser_link_drop_split_top.js | 25 ++++ src/zen/tests/split_view/head.js | 115 ++++++++++++++++++ 7 files changed, 230 insertions(+) create mode 100644 src/zen/tests/split_view/browser.toml create mode 100644 src/zen/tests/split_view/browser_link_drop_add_to_existing_split.js create mode 100644 src/zen/tests/split_view/browser_link_drop_center_opens_glance.js create mode 100644 src/zen/tests/split_view/browser_link_drop_split_right.js create mode 100644 src/zen/tests/split_view/browser_link_drop_split_top.js create mode 100644 src/zen/tests/split_view/head.js diff --git a/src/zen/tests/moz.build b/src/zen/tests/moz.build index a68349d9af..2702deff1e 100644 --- a/src/zen/tests/moz.build +++ b/src/zen/tests/moz.build @@ -7,6 +7,7 @@ BROWSER_CHROME_MANIFESTS += [ "container_essentials/browser.toml", "glance/browser.toml", "pinned/browser.toml", + "split_view/browser.toml", "urlbar/browser.toml", "welcome/browser.toml", "workspaces/browser.toml", diff --git a/src/zen/tests/split_view/browser.toml b/src/zen/tests/split_view/browser.toml new file mode 100644 index 0000000000..ef5b0b20ff --- /dev/null +++ b/src/zen/tests/split_view/browser.toml @@ -0,0 +1,11 @@ +[DEFAULT] +prefs = ["zen.splitView.enable-link-drop=true"] + +support-files = [ + "head.js", +] + +["browser_link_drop_split_right.js"] +["browser_link_drop_split_top.js"] +["browser_link_drop_center_opens_glance.js"] +["browser_link_drop_add_to_existing_split.js"] diff --git a/src/zen/tests/split_view/browser_link_drop_add_to_existing_split.js b/src/zen/tests/split_view/browser_link_drop_add_to_existing_split.js new file mode 100644 index 0000000000..e59185b35e --- /dev/null +++ b/src/zen/tests/split_view/browser_link_drop_add_to_existing_split.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +add_task(async function test_link_drop_add_to_existing_split() { + info('Starting test: Adding a link to an existing split view.'); + const initialTab = gBrowser.selectedTab; + + await simulateLinkDragAndDrop('right'); // Create an initial split + + await TestUtils.waitForCondition( + () => gZenViewSplitter.splitViewActive && getNonEmptyTabCount() === 2, + 'Wait for initial split view to become active' + ); + + // Drop another link to add to the existing split + await simulateLinkDragAndDrop('left'); + + await TestUtils.waitForCondition( + () => getNonEmptyTabCount() === 3, + 'Wait for the third tab to be added' + ); + + const groupData = gZenViewSplitter._data.find((g) => g.tabs.includes(initialTab)); + is(groupData.tabs.length, 3, 'The group should now contain three tabs.'); + + await cleanupSplitView(); +}); diff --git a/src/zen/tests/split_view/browser_link_drop_center_opens_glance.js b/src/zen/tests/split_view/browser_link_drop_center_opens_glance.js new file mode 100644 index 0000000000..9608648cbb --- /dev/null +++ b/src/zen/tests/split_view/browser_link_drop_center_opens_glance.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +add_task(async function test_link_drop_center_opens_glance() { + info('Starting test: Dropping a link in the center opens a glance view.'); + is(getNonEmptyTabCount(), 1, 'Should start with one tab.'); + ok(!gZenViewSplitter.splitViewActive, 'Split view should not be active initially.'); + + await simulateLinkDragAndDrop('center'); + + const glanceTab = await TestUtils.waitForCondition( + () => gBrowser.tabs.find((t) => t.hasAttribute('zen-glance-tab')), + 'Wait for glance tab to appear' + ); + + ok(glanceTab, 'Glance tab should be created.'); + is(getNonEmptyTabCount(), 2, 'Should have two tabs after opening glance.'); + ok(!gZenViewSplitter.splitViewActive, 'Split view should not be activated for a center drop.'); + + await cleanupSplitView(); +}); + diff --git a/src/zen/tests/split_view/browser_link_drop_split_right.js b/src/zen/tests/split_view/browser_link_drop_split_right.js new file mode 100644 index 0000000000..c44913bc63 --- /dev/null +++ b/src/zen/tests/split_view/browser_link_drop_split_right.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +add_task(async function test_link_drop_split_right() { + info('Starting test: Splitting a new view by dropping a link on the right.'); + const initialTab = gBrowser.selectedTab; + is(getNonEmptyTabCount(), 1, 'Should start with one tab.'); + ok(!gZenViewSplitter.splitViewActive, 'Split view should not be active initially.'); + + await simulateLinkDragAndDrop('right'); + + await TestUtils.waitForCondition( + () => gZenViewSplitter.splitViewActive, + 'Wait for split view to become active' + ); + + is(getNonEmptyTabCount(), 2, 'Should have two tabs after split.'); + const groupData = gZenViewSplitter._data.find((g) => g.tabs.includes(initialTab)); + ok(groupData, 'A split view group should be created.'); + is(groupData.gridType, 'vsep', 'Grid type should be vertical split.'); + + await cleanupSplitView(); +}); diff --git a/src/zen/tests/split_view/browser_link_drop_split_top.js b/src/zen/tests/split_view/browser_link_drop_split_top.js new file mode 100644 index 0000000000..882cce822a --- /dev/null +++ b/src/zen/tests/split_view/browser_link_drop_split_top.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +add_task(async function test_link_drop_split_top() { + info('Starting test: Splitting a new view by dropping a link on the top.'); + const initialTab = gBrowser.selectedTab; + is(getNonEmptyTabCount(), 1, 'Should start with one tab.'); + + await simulateLinkDragAndDrop('top'); + + await TestUtils.waitForCondition( + () => gZenViewSplitter.splitViewActive, + 'Wait for split view to become active' + ); + + is(getNonEmptyTabCount(), 2, 'Should have two tabs after split.'); + const groupData = gZenViewSplitter._data.find((g) => g.tabs.includes(initialTab)); + ok(groupData, 'A split view group should be created.'); + is(groupData.gridType, 'hsep', 'Grid type should be horizontal split.'); + + await cleanupSplitView(); +}); + diff --git a/src/zen/tests/split_view/head.js b/src/zen/tests/split_view/head.js new file mode 100644 index 0000000000..a6c78fa8b9 --- /dev/null +++ b/src/zen/tests/split_view/head.js @@ -0,0 +1,115 @@ +/* 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/. */ + +/** + * Returns the count of tabs that do not have the 'zen-empty-tab' attribute. + * + * @returns {number} The number of non-empty tabs. + */ +function getNonEmptyTabCount() { + return gBrowser.tabs.filter(tab => !tab.hasAttribute('zen-empty-tab')).length; +} + +/** + * Simulates dragging and dropping a link to test split view. + * + * @param {string} side The side to drop on ('left', 'right', 'top', 'bottom', 'center'). + * @param {string} [url='https://example.com/'] The URL to be dragged. + */ +async function simulateLinkDragAndDrop(side, url = 'https://example.com/') { + const tabBox = document.getElementById('tabbrowser-tabbox'); + const dataTransfer = new DataTransfer(); + dataTransfer.setData('text/uri-list', url); + dataTransfer.setData('text/plain', url); + + // 1. Enter the tab area to trigger drop zone creation. + tabBox.dispatchEvent(new DragEvent('dragenter', { bubbles: true, view: window, dataTransfer })); + + const dropZone = await TestUtils.waitForCondition( + () => document.getElementById('zen-drop-link-zone'), + 'Wait for link drop zone to appear' + ); + + // 2. Calculate coordinates for the drop. + const rect = dropZone.getBoundingClientRect(); + const edgeRatio = 0.2; // Should be less than EDGE_SIZE_RATIO in the source + const position = { + left: rect.left + rect.width * edgeRatio, + right: rect.right - rect.width * edgeRatio, + top: rect.top + rect.height * edgeRatio, + bottom: rect.bottom - rect.height * edgeRatio, + hCenter: rect.left + rect.width / 2, + vCenter: rect.top + rect.height / 2, + }; + + const coords = { + left: { clientX: position.left, clientY: position.vCenter }, + right: { clientX: position.right, clientY: position.vCenter }, + top: { clientX: position.hCenter, clientY: position.top }, + bottom: { clientX: position.hCenter, clientY: position.bottom }, + center: { clientX: position.hCenter, clientY: position.vCenter }, + }; + + // 3. Drag over the drop zone to set the side. + dropZone.dispatchEvent( + new DragEvent('dragover', { bubbles: true, view: window, dataTransfer, ...coords[side] }) + ); + await TestUtils.waitForCondition( + () => dropZone.getAttribute('drop-side') === side, + `Wait for drop-side to be '${side}'` + ); + + // 4. Drop. + dropZone.dispatchEvent( + new DragEvent('drop', { bubbles: true, view: window, dataTransfer, ...coords[side] }) + ); + + // 5. End drag to clean up. + tabBox.dispatchEvent(new DragEvent('dragend', { bubbles: true, view: window, dataTransfer })); +} + +/** + * Cleans up any split or glance views after a test and ensures only one clean tab remains. + */ +async function cleanupSplitView() { + // 1. Ensure all split views are closed. + if (gZenViewSplitter.splitViewActive) { + gZenViewSplitter.unsplitCurrentView(); + await TestUtils.waitForCondition( + () => !gZenViewSplitter.splitViewActive, + 'Wait for split view to become inactive during cleanup' + ); + } + + // 2. Close any active glance view. + const glanceTab = gBrowser.tabs.find((t) => t.hasAttribute('zen-glance-tab')); + if (glanceTab) { + await BrowserTestUtils.removeTab(glanceTab); + await TestUtils.waitForCondition( + () => gBrowser.tabs.find((t) => !t.hasAttribute('zen-glance-tab')), // Check if tab is removed from DOM + 'Wait for glance tab to be removed during cleanup' + ); + } + + // 3. Create a new, clean tab that will be the only tab remaining. + let newTab = await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'about:blank', true); + gBrowser.selectedTab = newTab; + + // 4. Remove all other tabs, excluding the newly created one. + // Convert gBrowser.tabs to an array to avoid issues with live collections changing during iteration. + const tabsToRemove = Array.from(gBrowser.tabs).filter( + tab => tab !== newTab + ); + + for (const tab of tabsToRemove) { + // BrowserTestUtils.removeTab correctly waits for the tab to be removed. + await BrowserTestUtils.removeTab(tab); + } + + // Final check: ensure exactly one non-empty tab remains and it is the new tab. + await TestUtils.waitForCondition( + () => getNonEmptyTabCount() === 1 && gBrowser.selectedTab === newTab, + 'Wait for only one non-empty tab to remain after cleanup, and it should be the new tab.' + ); +} From 9524d8a0a4e857a4121757a7f452098fbfdcae4c Mon Sep 17 00:00:00 2001 From: octaviusz <50177704+octaviusz@users.noreply.github.com> Date: Fri, 11 Jul 2025 19:25:11 +0300 Subject: [PATCH 19/19] fix: npm run pretty --- .../split_view/browser_link_drop_center_opens_glance.js | 1 - src/zen/tests/split_view/browser_link_drop_split_top.js | 1 - src/zen/tests/split_view/head.js | 6 ++---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/zen/tests/split_view/browser_link_drop_center_opens_glance.js b/src/zen/tests/split_view/browser_link_drop_center_opens_glance.js index 9608648cbb..77573a4079 100644 --- a/src/zen/tests/split_view/browser_link_drop_center_opens_glance.js +++ b/src/zen/tests/split_view/browser_link_drop_center_opens_glance.js @@ -21,4 +21,3 @@ add_task(async function test_link_drop_center_opens_glance() { await cleanupSplitView(); }); - diff --git a/src/zen/tests/split_view/browser_link_drop_split_top.js b/src/zen/tests/split_view/browser_link_drop_split_top.js index 882cce822a..a565dd2eb9 100644 --- a/src/zen/tests/split_view/browser_link_drop_split_top.js +++ b/src/zen/tests/split_view/browser_link_drop_split_top.js @@ -22,4 +22,3 @@ add_task(async function test_link_drop_split_top() { await cleanupSplitView(); }); - diff --git a/src/zen/tests/split_view/head.js b/src/zen/tests/split_view/head.js index a6c78fa8b9..99ebfe11c8 100644 --- a/src/zen/tests/split_view/head.js +++ b/src/zen/tests/split_view/head.js @@ -8,7 +8,7 @@ * @returns {number} The number of non-empty tabs. */ function getNonEmptyTabCount() { - return gBrowser.tabs.filter(tab => !tab.hasAttribute('zen-empty-tab')).length; + return gBrowser.tabs.filter((tab) => !tab.hasAttribute('zen-empty-tab')).length; } /** @@ -98,9 +98,7 @@ async function cleanupSplitView() { // 4. Remove all other tabs, excluding the newly created one. // Convert gBrowser.tabs to an array to avoid issues with live collections changing during iteration. - const tabsToRemove = Array.from(gBrowser.tabs).filter( - tab => tab !== newTab - ); + const tabsToRemove = Array.from(gBrowser.tabs).filter((tab) => tab !== newTab); for (const tab of tabsToRemove) { // BrowserTestUtils.removeTab correctly waits for the tab to be removed.