mirror of
https://github.com/zen-browser/desktop.git
synced 2025-07-08 01:19:59 +02:00
chore: Continued working on containerized essentials, b=(no-bug), c=tabs, workspaces
This commit is contained in:
parent
8136387a75
commit
47fbae7e0d
3 changed files with 456 additions and 28 deletions
390
src/zen/@types/zen.d.ts
vendored
390
src/zen/@types/zen.d.ts
vendored
|
@ -49,3 +49,393 @@ interface nsIXPCComponents extends nsISupports {
|
|||
readonly Constructor: (aClass: any, aIID: any, aFlags: any) => any;
|
||||
returnCode: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* TS-TODO - Needs typing.
|
||||
*
|
||||
* This file contains type stubs for loading things from Gecko. All of these
|
||||
* types should be used in the correct places eventually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Namespace anything that has its types mocked out here. These definitions are
|
||||
* only "good enough" to get the type checking to pass in this directory.
|
||||
* Eventually some more structured solution should be found. This namespace is
|
||||
* global and makes sure that all the definitions inside do not clash with
|
||||
* naming.
|
||||
*/
|
||||
declare namespace MockedExports {
|
||||
/**
|
||||
* This interface teaches ChromeUtils.importESModule how to find modules.
|
||||
*/
|
||||
interface KnownModules {
|
||||
Services: typeof import('Services');
|
||||
'resource://gre/modules/AppConstants.sys.mjs': typeof import('resource://gre/modules/AppConstants.sys.mjs');
|
||||
'resource:///modules/CustomizableUI.sys.mjs': typeof import('resource:///modules/CustomizableUI.sys.mjs');
|
||||
'resource:///modules/CustomizableWidgets.sys.mjs': typeof import('resource:///modules/CustomizableWidgets.sys.mjs');
|
||||
}
|
||||
|
||||
interface ChromeUtils {
|
||||
/**
|
||||
* This function reads the KnownModules and resolves which import to use.
|
||||
* If you are getting the TS2345 error:
|
||||
*
|
||||
* Argument of type '"resource:///.../file.sys.mjs"' is not assignable to
|
||||
* parameter of type
|
||||
*
|
||||
* Then add the file path to the KnownModules above.
|
||||
*/
|
||||
importESModule: <S extends keyof KnownModules>(module: S) => KnownModules[S];
|
||||
defineESModuleGetters: (target: any, mappings: any) => void;
|
||||
}
|
||||
|
||||
interface MessageManager {
|
||||
loadFrameScript(url: string, flag: boolean): void;
|
||||
sendAsyncMessage: (event: string, data: any) => void;
|
||||
addMessageListener: (event: string, listener: (event: any) => void) => void;
|
||||
}
|
||||
|
||||
// This is the thing in window.gBrowser, defined in
|
||||
// https://searchfox.org/mozilla-central/source/browser/base/content/tabbrowser.js
|
||||
interface Browser {
|
||||
addWebTab: (url: string, options: any) => BrowserTab;
|
||||
contentPrincipal: any;
|
||||
selectedTab: BrowserTab;
|
||||
selectedBrowser?: ChromeBrowser;
|
||||
messageManager: MessageManager;
|
||||
ownerDocument?: ChromeDocument;
|
||||
tabs: BrowserTab[];
|
||||
}
|
||||
|
||||
interface BrowserGroup {
|
||||
readonly tabs: BrowserTab[];
|
||||
readonly group?: BrowserGroup;
|
||||
}
|
||||
|
||||
// This is a tab in a browser, defined in
|
||||
// https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/base/content/tabbrowser.js#2580
|
||||
interface BrowserTab extends XULElement {
|
||||
linkedBrowser: ChromeBrowser;
|
||||
readonly group?: BrowserGroup;
|
||||
}
|
||||
|
||||
interface BrowserWindow extends Window {
|
||||
gBrowser: Browser;
|
||||
focus(): void;
|
||||
}
|
||||
|
||||
// The thing created in https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/base/content/tabbrowser.js#2088
|
||||
// This is linked to BrowserTab.
|
||||
interface ChromeBrowser {
|
||||
browsingContext?: BrowsingContext;
|
||||
browserId: number;
|
||||
}
|
||||
|
||||
interface BrowsingContext {
|
||||
/**
|
||||
* A unique identifier for the browser element that is hosting this
|
||||
* BrowsingContext tree. Every BrowsingContext in the element's tree will
|
||||
* return the same ID in all processes and it will remain stable regardless of
|
||||
* process changes. When a browser element's frameloader is switched to
|
||||
* another browser element this ID will remain the same but hosted under the
|
||||
* under the new browser element.
|
||||
* We are using this identifier for getting the active tab ID and passing to
|
||||
* the profiler back-end. See `getActiveBrowserID` for the usage.
|
||||
*/
|
||||
browserId: number;
|
||||
}
|
||||
|
||||
type GetPref<T> = (prefName: string, defaultValue?: T) => T;
|
||||
type SetPref<T> = (prefName: string, value?: T) => T;
|
||||
type nsIPrefBranch = {
|
||||
clearUserPref: (prefName: string) => void;
|
||||
getStringPref: GetPref<string>;
|
||||
setStringPref: SetPref<string>;
|
||||
getCharPref: GetPref<string>;
|
||||
setCharPref: SetPref<string>;
|
||||
getIntPref: GetPref<number>;
|
||||
setIntPref: SetPref<number>;
|
||||
getBoolPref: GetPref<boolean>;
|
||||
setBoolPref: SetPref<boolean>;
|
||||
addObserver: (aDomain: string, aObserver: PrefObserver, aHoldWeak?: boolean) => void;
|
||||
removeObserver: (aDomain: string, aObserver: PrefObserver) => void;
|
||||
};
|
||||
|
||||
type PrefObserverFunction = (aSubject: nsIPrefBranch, aTopic: 'nsPref:changed', aData: string) => unknown;
|
||||
type PrefObserver = PrefObserverFunction | { observe: PrefObserverFunction };
|
||||
|
||||
interface nsIURI {}
|
||||
|
||||
interface SharedLibrary {
|
||||
start: number;
|
||||
end: number;
|
||||
offset: number;
|
||||
name: string;
|
||||
path: string;
|
||||
debugName: string;
|
||||
debugPath: string;
|
||||
breakpadId: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
interface ProfileGenerationAdditionalInformation {
|
||||
sharedLibraries: SharedLibrary[];
|
||||
}
|
||||
|
||||
interface ProfileAndAdditionalInformation {
|
||||
profile: ArrayBuffer;
|
||||
additionalInformation?: ProfileGenerationAdditionalInformation;
|
||||
}
|
||||
|
||||
type Services = {
|
||||
env: {
|
||||
set: (name: string, value: string) => void;
|
||||
get: (name: string) => string;
|
||||
exists: (name: string) => boolean;
|
||||
};
|
||||
prefs: nsIPrefBranch;
|
||||
profiler: {
|
||||
StartProfiler: (
|
||||
entryCount: number,
|
||||
interval: number,
|
||||
features: string[],
|
||||
filters?: string[],
|
||||
activeTabId?: number,
|
||||
duration?: number
|
||||
) => void;
|
||||
StopProfiler: () => void;
|
||||
IsPaused: () => boolean;
|
||||
Pause: () => void;
|
||||
Resume: () => void;
|
||||
IsSamplingPaused: () => boolean;
|
||||
PauseSampling: () => void;
|
||||
ResumeSampling: () => void;
|
||||
GetFeatures: () => string[];
|
||||
getProfileDataAsync: (sinceTime?: number) => Promise<object>;
|
||||
getProfileDataAsArrayBuffer: (sinceTime?: number) => Promise<ArrayBuffer>;
|
||||
getProfileDataAsGzippedArrayBuffer: (sinceTime?: number) => Promise<ProfileAndAdditionalInformation>;
|
||||
IsActive: () => boolean;
|
||||
sharedLibraries: SharedLibrary[];
|
||||
};
|
||||
platform: string;
|
||||
obs: {
|
||||
addObserver: (observer: object, type: string) => void;
|
||||
removeObserver: (observer: object, type: string) => void;
|
||||
};
|
||||
wm: {
|
||||
getMostRecentWindow: (name: string) => BrowserWindow;
|
||||
getMostRecentNonPBWindow: (name: string) => BrowserWindow;
|
||||
};
|
||||
focus: {
|
||||
activeWindow: BrowserWindow;
|
||||
};
|
||||
io: {
|
||||
newURI(url: string): nsIURI;
|
||||
};
|
||||
scriptSecurityManager: any;
|
||||
startup: {
|
||||
quit: (optionsBitmask: number) => void;
|
||||
eForceQuit: number;
|
||||
eRestart: number;
|
||||
};
|
||||
};
|
||||
|
||||
const EventEmitter: {
|
||||
decorate: (target: object) => void;
|
||||
};
|
||||
|
||||
const AppConstantsSYSMJS: {
|
||||
AppConstants: {
|
||||
platform: string;
|
||||
};
|
||||
};
|
||||
|
||||
interface BrowsingContextStub {}
|
||||
interface PrincipalStub {}
|
||||
|
||||
interface WebChannelTarget {
|
||||
browsingContext: BrowsingContextStub;
|
||||
browser: Browser;
|
||||
eventTarget: null;
|
||||
principal: PrincipalStub;
|
||||
}
|
||||
|
||||
interface FaviconData {
|
||||
uri: nsIURI;
|
||||
dataLen: number;
|
||||
data: number[];
|
||||
mimeType: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
const PlaceUtilsSYSMJS: {
|
||||
PlacesUtils: {
|
||||
promiseFaviconData: (pageUrl: string | URL | nsIURI, preferredWidth?: number) => Promise<FaviconData>;
|
||||
// TS-TODO: Add the rest.
|
||||
};
|
||||
};
|
||||
|
||||
// TS-TODO
|
||||
const CustomizableUISYSMJS: any;
|
||||
const CustomizableWidgetsSYSMJS: any;
|
||||
const PanelMultiViewSYSMJS: any;
|
||||
|
||||
const LoaderESM: {
|
||||
require: (path: string) => any;
|
||||
};
|
||||
|
||||
const Services: Services;
|
||||
|
||||
// This class is needed by the Cc importing mechanism. e.g.
|
||||
// Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
||||
class nsIFilePicker {}
|
||||
|
||||
interface FilePicker {
|
||||
init: (browsingContext: BrowsingContext, title: string, mode: number) => void;
|
||||
open: (callback: (rv: number) => unknown) => void;
|
||||
// The following are enum values.
|
||||
modeGetFolder: number;
|
||||
returnOK: number;
|
||||
file: {
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Cc {
|
||||
'@mozilla.org/filepicker;1': {
|
||||
createInstance(instance: nsIFilePicker): FilePicker;
|
||||
};
|
||||
}
|
||||
|
||||
interface Ci {
|
||||
nsIFilePicker: nsIFilePicker;
|
||||
}
|
||||
|
||||
interface Cu {
|
||||
exportFunction: (fn: Function, scope: object, options?: object) => void;
|
||||
cloneInto: (value: any, scope: object, options?: object) => void;
|
||||
isInAutomation: boolean;
|
||||
}
|
||||
|
||||
interface FluentLocalization {
|
||||
/**
|
||||
* This function sets the attributes data-l10n-id and possibly data-l10n-args
|
||||
* on the element.
|
||||
*/
|
||||
setAttributes(target: Element, id?: string, args?: Record<string, string>): void;
|
||||
}
|
||||
}
|
||||
|
||||
interface PathUtilsInterface {
|
||||
split: (path: string) => string[];
|
||||
isAbsolute: (path: string) => boolean;
|
||||
}
|
||||
|
||||
declare module 'Services' {
|
||||
export = MockedExports.Services;
|
||||
}
|
||||
|
||||
declare module 'ChromeUtils' {
|
||||
export = ChromeUtils;
|
||||
}
|
||||
|
||||
declare var ChromeUtils: MockedExports.ChromeUtils;
|
||||
|
||||
declare var PathUtils: PathUtilsInterface;
|
||||
|
||||
// These global objects can be used directly in JSM files only.
|
||||
declare var Cu: MockedExports.Cu;
|
||||
declare var Cc: MockedExports.Cc;
|
||||
declare var Ci: MockedExports.Ci;
|
||||
declare var Services: MockedExports.Services;
|
||||
|
||||
/**
|
||||
* This is a variant on the normal Document, as it contains chrome-specific properties.
|
||||
*/
|
||||
declare interface ChromeDocument extends Document {
|
||||
/**
|
||||
* Create a XUL element of a specific type. Right now this function
|
||||
* only refines iframes, but more tags could be added.
|
||||
*/
|
||||
createXULElement: ((type: 'iframe') => XULIframeElement) & ((type: string) => XULElement);
|
||||
|
||||
/**
|
||||
* This is a fluent instance connected to this document.
|
||||
*/
|
||||
l10n: MockedExports.FluentLocalization;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a variant on the HTMLElement, as it contains chrome-specific properties.
|
||||
*/
|
||||
declare interface ChromeHTMLElement extends HTMLElement {
|
||||
ownerDocument: ChromeDocument;
|
||||
}
|
||||
|
||||
declare interface XULIframeElement extends XULElement {
|
||||
contentWindow: Window;
|
||||
src: string;
|
||||
}
|
||||
|
||||
// `declare interface Window` is TypeScript way to let us implicitely extend and
|
||||
// augment the already existing Window interface defined in the TypeScript library.
|
||||
// This makes it possible to define properties that exist in the window object
|
||||
// while in a privileged context. We assume that all of the environments we run
|
||||
// in this project will be pribileged, that's why we take this shortcut of
|
||||
// globally extending the Window type.
|
||||
// See the ChromeOnly attributes in https://searchfox.org/mozilla-central/rev/896042a1a71066254ceb5291f016ca3dbca21cb7/dom/webidl/Window.webidl#391
|
||||
//
|
||||
// openWebLinkIn and openTrustedLinkIn aren't in all privileged windows, but
|
||||
// they're also defined in the privileged environments we're dealing with in
|
||||
// this project, so they're defined here for convenience.
|
||||
declare interface Window {
|
||||
browsingContext: MockedExports.BrowsingContext;
|
||||
openWebLinkIn: (
|
||||
url: string,
|
||||
where: 'current' | 'tab' | 'tabshifted' | 'window' | 'save',
|
||||
options?: Partial<{
|
||||
// Not all possible options are present, please add more if/when needed.
|
||||
userContextId: number;
|
||||
forceNonPrivate: boolean;
|
||||
relatedToCurrent: boolean;
|
||||
resolveOnContentBrowserCreated: (contentBrowser: MockedExports.ChromeBrowser) => unknown;
|
||||
}>
|
||||
) => void;
|
||||
openTrustedLinkIn: (
|
||||
url: string,
|
||||
where: 'current' | 'tab' | 'tabshifted' | 'window' | 'save',
|
||||
options?: Partial<{
|
||||
// Not all possible options are present, please add more if/when needed.
|
||||
userContextId: number;
|
||||
forceNonPrivate: boolean;
|
||||
relatedToCurrent: boolean;
|
||||
resolveOnContentBrowserCreated: (contentBrowser: MockedExports.ChromeBrowser) => unknown;
|
||||
}>
|
||||
) => void;
|
||||
}
|
||||
|
||||
declare class ChromeWorker extends Worker {}
|
||||
|
||||
declare interface MenuListElement extends XULElement {
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
declare interface XULCommandEvent extends Event {
|
||||
target: XULElement;
|
||||
}
|
||||
|
||||
declare interface XULElementWithCommandHandler {
|
||||
addEventListener: (type: 'command', handler: (event: XULCommandEvent) => void, isCapture?: boolean) => void;
|
||||
removeEventListener: (type: 'command', handler: (event: XULCommandEvent) => void, isCapture?: boolean) => void;
|
||||
}
|
||||
|
||||
declare type nsIPrefBranch = MockedExports.nsIPrefBranch;
|
||||
|
||||
// chrome context-only DOM isInstance method
|
||||
// XXX: This hackishly extends Function because there is no way to extend DOM constructors.
|
||||
// Callers should manually narrow the type when needed.
|
||||
// See also https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/222
|
||||
interface Function {
|
||||
isInstance(obj: any): boolean;
|
||||
}
|
||||
|
|
|
@ -800,7 +800,7 @@
|
|||
if (tabsTarget === gBrowser.tabs.at(-1)) {
|
||||
newIndex++;
|
||||
}
|
||||
gBrowser.moveTabTo(draggedTab, newIndex, { forceUngrouped: true });
|
||||
gBrowser.moveTabTo(draggedTab, { tabIndex: newIndex, forceUngrouped: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -255,7 +255,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
|
|||
if (!this.containerSpecificEssentials) {
|
||||
container = 0;
|
||||
}
|
||||
let essentialsContainer = document.querySelector(`.zen-essentials-container[container="${container}"]`);
|
||||
let essentialsContainer = document.querySelector(`.zen-essentials-container[container="${container}"]:not([clone])`);
|
||||
if (!essentialsContainer) {
|
||||
essentialsContainer = document.createXULElement('vbox');
|
||||
essentialsContainer.className = 'zen-essentials-container zen-workspace-tabs-section';
|
||||
|
@ -657,7 +657,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
|
|||
if (this._initialTab) {
|
||||
this.moveTabToWorkspace(this._initialTab, this.activeWorkspace);
|
||||
gBrowser.selectedTab = this._initialTab;
|
||||
gBrowser.moveTabTo(this._initialTab, 0, { forceUngrouped: true });
|
||||
gBrowser.moveTabTo(this._initialTab, { forceUngrouped: true, tabIndex: 0 });
|
||||
this._initialTab._possiblyEmpty = false;
|
||||
this._initialTab = null;
|
||||
}
|
||||
|
@ -1735,23 +1735,26 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
|
|||
const workspaces = await this._workspaces();
|
||||
const newWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === newWorkspace.uuid);
|
||||
const clonedEssentials = [];
|
||||
const essentialsContainerMap = {};
|
||||
if (shouldAnimate) {
|
||||
if (shouldAnimate && this.containerSpecificEssentials) {
|
||||
for (const workspace of workspaces.workspaces) {
|
||||
const essentialsContainer = this.getEssentialsSection(workspace.containerTabId);
|
||||
if (clonedEssentials[clonedEssentials.length - 1]?.contextId == workspace.containerTabId) {
|
||||
clonedEssentials[clonedEssentials.length - 1].repeat++;
|
||||
clonedEssentials[clonedEssentials.length - 1].workspaces.push(workspace);
|
||||
continue;
|
||||
}
|
||||
essentialsContainer.setAttribute('hidden', 'true');
|
||||
const essentialsClone = essentialsContainer.cloneNode(true);
|
||||
essentialsClone.removeAttribute('hidden');
|
||||
essentialsClone.setAttribute('cloned', 'true');
|
||||
clonedEssentials.push({
|
||||
container: essentialsClone,
|
||||
workspaceId: workspace.uuid,
|
||||
workspaces: [workspace],
|
||||
contextId: workspace.containerTabId,
|
||||
originalContainer: essentialsContainer,
|
||||
repeat: 0,
|
||||
});
|
||||
essentialsContainer.parentNode.appendChild(essentialsClone);
|
||||
// +0 to convert null to 0
|
||||
essentialsContainerMap[workspace.containerTabId + 0] = essentialsContainer;
|
||||
}
|
||||
}
|
||||
for (const element of document.querySelectorAll('.zen-workspace-tabs-section')) {
|
||||
|
@ -1778,16 +1781,57 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
|
|||
}
|
||||
)
|
||||
);
|
||||
if (element.parentNode.id === 'zen-current-workspace-indicator-container') {
|
||||
// Get essential container clone for this workspace
|
||||
const clonedEssential = clonedEssentials.find((cloned) => cloned.workspaceId === elementWorkspaceId);
|
||||
if (clonedEssential && !clonedEssential.animating) {
|
||||
clonedEssential.animating = true; // Avoid motion hanging due to animating the same element twice
|
||||
}
|
||||
if (offset === 0) {
|
||||
element.setAttribute('active', 'true');
|
||||
if (tabToSelect != gBrowser.selectedTab) {
|
||||
gBrowser.selectedTab = tabToSelect;
|
||||
}
|
||||
} else {
|
||||
element.removeAttribute('active');
|
||||
}
|
||||
}
|
||||
if (this.containerSpecificEssentials) {
|
||||
// Animate essentials
|
||||
for (const cloned of clonedEssentials) {
|
||||
const container = cloned.container;
|
||||
const essentialsWorkspacess = cloned.workspaces;
|
||||
const repeats = cloned.repeat;
|
||||
const containerId = cloned.contextId;
|
||||
// Animate like the workspaces above expect essentials are a bit more
|
||||
// complicated because they are not based on workspaces but on containers
|
||||
// So, if we have the following arangement:
|
||||
// | [workspace1] [workspace2] [workspace3] [workspace4]
|
||||
// | [container1] [container1] [container2] [container1]
|
||||
// And if we are changing from workspace 1 to workspace 4,
|
||||
// we should be doing the following:
|
||||
// First container (repeat 2 times) will stay in place until
|
||||
// we reach container 3, then animate to the left and container 2
|
||||
// also move to the left after that while container 1 in workspace 4
|
||||
// will slide in from the right
|
||||
|
||||
// Get the index from first and last workspace
|
||||
const firstWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === essentialsWorkspacess[0].uuid);
|
||||
const lastWorkspaceIndex = workspaces.workspaces.findIndex(
|
||||
(w) => w.uuid === essentialsWorkspacess[essentialsWorkspacess.length - 1].uuid
|
||||
);
|
||||
const isGoingLeft = newWorkspaceIndex > lastWorkspaceIndex;
|
||||
const isGoingInsideSameContainer = essentialsWorkspacess.some((w) => w.uuid === newWorkspace.uuid);
|
||||
if (isGoingInsideSameContainer) {
|
||||
continue; // We dont want to animate if we are going inside the same container
|
||||
}
|
||||
const firstOffset = -(newWorkspaceIndex - firstWorkspaceIndex - (isGoingLeft ? repeats : -repeats)) * 100;
|
||||
const lastOffset = -(newWorkspaceIndex - lastWorkspaceIndex - (isGoingLeft ? repeats : -repeats)) * 100;
|
||||
const newTransform = `translateX(${firstOffset}%)`;
|
||||
const existingTransform = `translateX(${lastOffset}%)`;
|
||||
const stepsInBetween = Math.abs(lastWorkspaceIndex - firstWorkspaceIndex);
|
||||
if (shouldAnimate) {
|
||||
container.style.transform = newTransform;
|
||||
animations.push(
|
||||
gZenUIManager.motion.animate(
|
||||
clonedEssential.container,
|
||||
container,
|
||||
{
|
||||
transform: existingTransform ? [existingTransform, newTransform] : newTransform,
|
||||
transform: [existingTransform, new Array(stepsInBetween).fill(newTransform).join(',')],
|
||||
},
|
||||
{
|
||||
type: 'spring',
|
||||
|
@ -1799,15 +1843,6 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (offset === 0) {
|
||||
element.setAttribute('active', 'true');
|
||||
if (tabToSelect != gBrowser.selectedTab) {
|
||||
gBrowser.selectedTab = tabToSelect;
|
||||
}
|
||||
} else {
|
||||
element.removeAttribute('active');
|
||||
}
|
||||
}
|
||||
await Promise.all(animations);
|
||||
if (shouldAnimate) {
|
||||
for (const cloned of clonedEssentials) {
|
||||
|
@ -2391,6 +2426,9 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
|
|||
}
|
||||
const containers = [...essentialsContainer, ...pinnedContainers, ...normalContainers];
|
||||
for (const container of containers) {
|
||||
if (container.hasAttribute('cloned')) {
|
||||
continue;
|
||||
}
|
||||
for (const tab of container.children) {
|
||||
if (tab.tagName === 'tab') {
|
||||
tabs.push(tab);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue