mirror of
https://github.com/zen-browser/components.git
synced 2025-07-08 20:09:57 +02:00
refactor: Remove unused type declarations and package.json dependencies
This commit is contained in:
parent
de5090fe9c
commit
828ecbbf10
10 changed files with 1326 additions and 2546 deletions
5
@types/common.d.ts
vendored
5
@types/common.d.ts
vendored
|
@ -1,5 +0,0 @@
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
debug: boolean;
|
|
||||||
browserName: string;
|
|
||||||
};
|
|
525
@types/gecko.d.ts
vendored
525
@types/gecko.d.ts
vendored
|
@ -1,525 +0,0 @@
|
||||||
/* 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/. */
|
|
||||||
|
|
||||||
// https://github.com/Floorp-Projects/Floorp-core/blob/95ce9eb44c06a682e9d6379eaa4a5d1bbd5db6cd/%40types/gecko.d.ts#L198
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.import how to find modules.
|
|
||||||
*/
|
|
||||||
interface KnownModules {
|
|
||||||
"resource://gre/modules/FileUtils.sys.mjs": FileUtils;
|
|
||||||
"resource://gre/modules/FileUtils.jsm": FileUtils;
|
|
||||||
|
|
||||||
Services: Services;
|
|
||||||
"resource://gre/modules/Services.jsm": Services;
|
|
||||||
"resource://gre/modules/AppConstants.sys.mjs": typeof AppConstantsSYSMJS;
|
|
||||||
"resource:///modules/CustomizableUI.sys.mjs": typeof CustomizableUISYSMJS;
|
|
||||||
"resource:///modules/CustomizableWidgets.sys.mjs": typeof CustomizableWidgetsSYSMJS;
|
|
||||||
"resource://devtools/shared/loader/Loader.sys.mjs": typeof LoaderESM;
|
|
||||||
//"resource://devtools/client/performance-new/shared/background.jsm.js": typeof import("resource://devtools/client/performance-new/shared/background.jsm.js");
|
|
||||||
//"resource://devtools/client/performance-new/shared/symbolication.jsm.js": typeof import("resource://devtools/client/performance-new/shared/symbolication.jsm.js");
|
|
||||||
"resource://devtools/shared/loader/browser-loader.js": any;
|
|
||||||
//"resource://devtools/client/performance-new/popup/menu-button.jsm.js": typeof import("resource://devtools/client/performance-new/popup/menu-button.jsm.js");
|
|
||||||
//"resource://devtools/client/performance-new/shared/typescript-lazy-load.jsm.js": typeof import("resource://devtools/client/performance-new/shared/typescript-lazy-load.jsm.js");
|
|
||||||
//"resource://devtools/client/performance-new/popup/logic.jsm.js": typeof import("resource://devtools/client/performance-new/popup/logic.jsm.js");
|
|
||||||
"resource:///modules/PanelMultiView.sys.mjs": typeof PanelMultiViewSYSMJS;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Services = {
|
|
||||||
Services: import("firefox").Services;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FileUtils = {
|
|
||||||
FileUtils: IFileUtils;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IFileUtils {
|
|
||||||
MODE_RDONLY: number;
|
|
||||||
MODE_WRONLY: number;
|
|
||||||
MODE_RDWR: number;
|
|
||||||
MODE_CREATE: number;
|
|
||||||
MODE_APPEND: number;
|
|
||||||
MODE_TRUNCATE: number;
|
|
||||||
|
|
||||||
PERMS_FILE: number;
|
|
||||||
PERMS_DIRECTORY: number;
|
|
||||||
/**
|
|
||||||
* Gets a file at the specified hierarchy under a nsIDirectoryService key.
|
|
||||||
* @param key
|
|
||||||
* The Directory Service Key to start from
|
|
||||||
* @param pathArray
|
|
||||||
* An array of path components to locate beneath the directory
|
|
||||||
* specified by |key|. The last item in this array must be the
|
|
||||||
* leaf name of a file.
|
|
||||||
* @return nsIFile object for the file specified. The file is NOT created
|
|
||||||
* if it does not exist, however all required directories along
|
|
||||||
* the way are if pathArray has more than one item.
|
|
||||||
*/
|
|
||||||
getFile: (key: any, pathArray: any) => any;
|
|
||||||
/**
|
|
||||||
* Gets a directory at the specified hierarchy under a nsIDirectoryService
|
|
||||||
* key.
|
|
||||||
* @param key
|
|
||||||
* The Directory Service Key to start from
|
|
||||||
* @param pathArray
|
|
||||||
* An array of path components to locate beneath the directory
|
|
||||||
* specified by |key|
|
|
||||||
* @param shouldCreate
|
|
||||||
* true if the directory hierarchy specified in |pathArray|
|
|
||||||
* should be created if it does not exist, false otherwise.
|
|
||||||
* @return nsIFile object for the location specified.
|
|
||||||
*/
|
|
||||||
getDir: (key, pathArray, shouldCreate) => any;
|
|
||||||
/**
|
|
||||||
* Opens a file output stream for writing.
|
|
||||||
* @param file
|
|
||||||
* The file to write to.
|
|
||||||
* @param modeFlags
|
|
||||||
* (optional) File open flags. Can be undefined.
|
|
||||||
* @returns nsIFileOutputStream to write to.
|
|
||||||
* @note The stream is initialized with the DEFER_OPEN behavior flag.
|
|
||||||
* See nsIFileOutputStream.
|
|
||||||
*/
|
|
||||||
openFileOutputStream: (file, modeFlags) => any;
|
|
||||||
/**
|
|
||||||
* Opens an atomic file output stream for writing.
|
|
||||||
* @param file
|
|
||||||
* The file to write to.
|
|
||||||
* @param modeFlags
|
|
||||||
* (optional) File open flags. Can be undefined.
|
|
||||||
* @returns nsIFileOutputStream to write to.
|
|
||||||
* @note The stream is initialized with the DEFER_OPEN behavior flag.
|
|
||||||
* See nsIFileOutputStream.
|
|
||||||
* OpeanAtomicFileOutputStream is generally better than openSafeFileOutputStream
|
|
||||||
* baecause flushing is not needed in most of the issues.
|
|
||||||
*/
|
|
||||||
openAtomicFileOutputStream: (file, modeFlags) => any;
|
|
||||||
/**
|
|
||||||
* Opens a safe file output stream for writing.
|
|
||||||
* @param file
|
|
||||||
* The file to write to.
|
|
||||||
* @param modeFlags
|
|
||||||
* (optional) File open flags. Can be undefined.
|
|
||||||
* @returns nsIFileOutputStream to write to.
|
|
||||||
* @note The stream is initialized with the DEFER_OPEN behavior flag.
|
|
||||||
* See nsIFileOutputStream.
|
|
||||||
*/
|
|
||||||
openSafeFileOutputStream: (file, modeFlags) => any;
|
|
||||||
|
|
||||||
_initFileOutputStream: (fos, file, modeFlags) => any;
|
|
||||||
/**
|
|
||||||
* Closes an atomic file output stream.
|
|
||||||
* @param stream
|
|
||||||
* The stream to close.
|
|
||||||
*/
|
|
||||||
|
|
||||||
closeAtomicFileOutputStream: <
|
|
||||||
Stream extends
|
|
||||||
import("firefox").Components_Interfaces["nsISafeOutputStream"] & any,
|
|
||||||
>(
|
|
||||||
stream: Stream
|
|
||||||
) => void;
|
|
||||||
/**
|
|
||||||
* Closes a safe file output stream.
|
|
||||||
* @param stream
|
|
||||||
* The stream to close.
|
|
||||||
*/
|
|
||||||
closeSafeFileOutputStream: <
|
|
||||||
Stream extends
|
|
||||||
import("firefox").Components_Interfaces["nsISafeOutputStream"] & any,
|
|
||||||
>(
|
|
||||||
stream: Stream
|
|
||||||
) => void;
|
|
||||||
File: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: add to window
|
|
||||||
interface MozXULElement {
|
|
||||||
/**
|
|
||||||
* Allows eager deterministic construction of XUL elements with XBL attached, by
|
|
||||||
* parsing an element tree and returning a DOM fragment to be inserted in the
|
|
||||||
* document before any of the inner elements is referenced by JavaScript.
|
|
||||||
*
|
|
||||||
* This process is required instead of calling the createElement method directly
|
|
||||||
* because bindings get attached when:
|
|
||||||
*
|
|
||||||
* 1. the node gets a layout frame constructed, or
|
|
||||||
* 2. the node gets its JavaScript reflector created, if it's in the document,
|
|
||||||
*
|
|
||||||
* whichever happens first. The createElement method would return a JavaScript
|
|
||||||
* reflector, but the element wouldn't be in the document, so the node wouldn't
|
|
||||||
* get XBL attached. After that point, even if the node is inserted into a
|
|
||||||
* document, it won't get XBL attached until either the frame is constructed or
|
|
||||||
* the reflector is garbage collected and the element is touched again.
|
|
||||||
*
|
|
||||||
* @param {string} str
|
|
||||||
* String with the XML representation of XUL elements.
|
|
||||||
* @param {string[]} [entities]
|
|
||||||
* An array of DTD URLs containing entity definitions.
|
|
||||||
*
|
|
||||||
* @return {DocumentFragment} `DocumentFragment` instance containing
|
|
||||||
* the corresponding element tree, including element nodes
|
|
||||||
* but excluding any text node.
|
|
||||||
*/
|
|
||||||
parseXULToFragment: (str: string, entities: string[]) => DocumentFragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.jsm"' is not assignable to parameter
|
|
||||||
* of type
|
|
||||||
*
|
|
||||||
* Then add the file path to the KnownModules above.
|
|
||||||
*/
|
|
||||||
import: <S extends keyof KnownModules>(module: S) => KnownModules[S];
|
|
||||||
importESModule: <S extends keyof KnownModules>(
|
|
||||||
module: S
|
|
||||||
) => KnownModules[S];
|
|
||||||
defineModuleGetter: (target: any, variable: string, path: string) => void;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Browser extends HTMLElement {
|
|
||||||
addWebTab: (url: string, options: any) => BrowserTab;
|
|
||||||
contentPrincipal: any;
|
|
||||||
selectedTab: BrowserTab;
|
|
||||||
selectedBrowser?: ChromeBrowser;
|
|
||||||
messageManager: MessageManager;
|
|
||||||
ownerDocument?: ChromeDocument;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BrowserTab extends HTMLElement {
|
|
||||||
linkedBrowser: Browser;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChromeWindow {
|
|
||||||
gBrowser: Browser;
|
|
||||||
focus(): void;
|
|
||||||
openWebLinkIn(
|
|
||||||
url: string,
|
|
||||||
where: "current" | "tab" | "window",
|
|
||||||
options: Partial<{
|
|
||||||
// Not all possible options are present, please add more if/when needed.
|
|
||||||
userContextId: number;
|
|
||||||
forceNonPrivate: boolean;
|
|
||||||
resolveOnContentBrowserCreated: (
|
|
||||||
contentBrowser: ChromeBrowser
|
|
||||||
) => unknown;
|
|
||||||
}>
|
|
||||||
): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChromeBrowser {
|
|
||||||
browsingContext?: BrowsingContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 PrefObserverFunction = (
|
|
||||||
aSubject: import("./firefox/modules/libpref/nsIPrefBranch").nsIPrefBranch,
|
|
||||||
aTopic: "nsPref:changed",
|
|
||||||
aData: string
|
|
||||||
) => unknown;
|
|
||||||
type PrefObserver = PrefObserverFunction | { observe: PrefObserverFunction };
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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: (window: Window, 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 {
|
|
||||||
// /**
|
|
||||||
// * This function reads the KnownModules and resolves which import to use.
|
|
||||||
// * If you are getting the TS2345 error:
|
|
||||||
// *
|
|
||||||
// * Argument of type '"resource:///.../file.jsm"' is not assignable to parameter
|
|
||||||
// * of type
|
|
||||||
// *
|
|
||||||
// * Then add the file path to the KnownModules above.
|
|
||||||
// */
|
|
||||||
// import: <S extends keyof KnownModules>(module: S) => KnownModules[S];
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// export module "resource://devtools/client/shared/vendor/react.js" {
|
|
||||||
// import * as React from "react";
|
|
||||||
// export = React;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "resource://devtools/client/shared/vendor/react-dom-factories.js" {
|
|
||||||
// import * as ReactDomFactories from "react-dom-factories";
|
|
||||||
// export = ReactDomFactories;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "resource://devtools/client/shared/vendor/redux.js" {
|
|
||||||
// import * as Redux from "redux";
|
|
||||||
// export = Redux;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "resource://devtools/client/shared/vendor/react-redux.js" {
|
|
||||||
// import * as ReactRedux from "react-redux";
|
|
||||||
// export = ReactRedux;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "resource://devtools/shared/event-emitter2.js" {
|
|
||||||
// export = MockedExports.EventEmitter;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "Services" {
|
|
||||||
// export = Services;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "ChromeUtils" {
|
|
||||||
// export = ChromeUtils;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "resource://gre/modules/AppConstants.sys.mjs" {
|
|
||||||
// export = MockedExports.AppConstantsSYSMJS;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "resource://devtools/client/performance-new/shared/background.jsm.js" {
|
|
||||||
// import * as Background from "devtools/client/performance-new/shared/background.jsm.js";
|
|
||||||
// export = Background;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "resource://devtools/client/performance-new/shared/symbolication.jsm.js" {
|
|
||||||
// import * as PerfSymbolication from "devtools/client/performance-new/shared/symbolication.jsm.js";
|
|
||||||
// export = PerfSymbolication;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "resource:///modules/CustomizableUI.sys.mjs" {
|
|
||||||
// export = MockedExports.CustomizableUISYSMJS;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "resource:///modules/CustomizableWidgets.sys.mjs" {
|
|
||||||
// export = MockedExports.CustomizableWidgetsSYSMJS;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "resource:///modules/PanelMultiView.sys.mjs" {
|
|
||||||
// export = MockedExports.PanelMultiViewSYSMJS;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// declare module "resource://devtools/shared/loader/Loader.sys.mjs" {
|
|
||||||
// export = MockedExports.LoaderESM;
|
|
||||||
// }
|
|
||||||
|
|
||||||
declare var ChromeUtils: MockedExports.ChromeUtils;
|
|
||||||
|
|
||||||
declare var PathUtils: PathUtilsInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 XULElement extends HTMLElement {
|
|
||||||
ownerDocument: ChromeDocument;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare interface XULIframeElement extends XULElement {
|
|
||||||
contentWindow: ChromeWindow;
|
|
||||||
src: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare interface ChromeWindow extends Window {
|
|
||||||
openWebLinkIn: (
|
|
||||||
url: string,
|
|
||||||
where: "current" | "tab" | "tabshifted" | "window" | "save",
|
|
||||||
// TS-TODO
|
|
||||||
params?: unknown
|
|
||||||
) => void;
|
|
||||||
openTrustedLinkIn: (
|
|
||||||
url: string,
|
|
||||||
where: "current" | "tab" | "tabshifted" | "window" | "save",
|
|
||||||
// TS-TODO
|
|
||||||
params?: 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "resource://gre/modules/FileUtils.sys.mjs" {
|
|
||||||
const FileUtils: MockedExports.IFileUtils;
|
|
||||||
export { FileUtils };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extend "window" to extend "ChromeWindow"
|
|
||||||
declare global {
|
|
||||||
interface Window extends ChromeWindow {}
|
|
||||||
}
|
|
13
@types/split-views.d.ts
vendored
13
@types/split-views.d.ts
vendored
|
@ -1,13 +0,0 @@
|
||||||
|
|
||||||
declare type SplitType = 'horizontal' | 'vertical' | 'grid';
|
|
||||||
|
|
||||||
declare interface SplitViewConfig extends Config {
|
|
||||||
splitIndicator: string; // e.g. "split-tab='true'"
|
|
||||||
defaultSplitView: SplitType;
|
|
||||||
};
|
|
||||||
|
|
||||||
declare interface SplitView {
|
|
||||||
type: SplitType;
|
|
||||||
tabs: MockedExports.BrowserTab[];
|
|
||||||
id: number;
|
|
||||||
};
|
|
1551
package-lock.json
generated
1551
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"name": "components",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Some components used by @zen-browser and @Floorp-Projects as an attempt to make firefox forks a better place",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"check": "tsc"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.5.4"
|
|
||||||
}
|
|
||||||
}
|
|
551
src/ZenSidebarManager.mjs
Normal file
551
src/ZenSidebarManager.mjs
Normal file
|
@ -0,0 +1,551 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export var gZenBrowserManagerSidebar = {
|
||||||
|
_sidebarElement: null,
|
||||||
|
_currentPanel: null,
|
||||||
|
_lastOpenedPanel: null,
|
||||||
|
_hasChangedConfig: true,
|
||||||
|
_splitterElement: null,
|
||||||
|
_hSplitterElement: null,
|
||||||
|
_hasRegisteredPinnedClickOutside: false,
|
||||||
|
_isDragging: false,
|
||||||
|
contextTab: null,
|
||||||
|
|
||||||
|
DEFAULT_MOBILE_USER_AGENT: "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36 Edg/114.0.1823.79",
|
||||||
|
MAX_SIDEBAR_PANELS: 8, // +1 for the add panel button
|
||||||
|
MAX_RUNS: 3,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.update();
|
||||||
|
this.close(); // avoid caching
|
||||||
|
this.listenForPrefChanges();
|
||||||
|
this.insertIntoContextMenu();
|
||||||
|
},
|
||||||
|
|
||||||
|
get sidebarData() {
|
||||||
|
let services = Services.prefs.getStringPref("zen.sidebar.data");
|
||||||
|
if (services === "") {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return JSON.parse(services);
|
||||||
|
},
|
||||||
|
|
||||||
|
get shouldCloseOnBlur() {
|
||||||
|
return Services.prefs.getBoolPref("zen.sidebar.close-on-blur");
|
||||||
|
},
|
||||||
|
|
||||||
|
listenForPrefChanges() {
|
||||||
|
Services.prefs.addObserver("zen.sidebar.data", this.handleEvent.bind(this));
|
||||||
|
Services.prefs.addObserver("zen.sidebar.enabled", this.handleEvent.bind(this));
|
||||||
|
|
||||||
|
let sidebar = document.getElementById("zen-sidebar-web-panel");
|
||||||
|
this.splitterElement.addEventListener("mousedown", (function(event) {
|
||||||
|
let computedStyle = window.getComputedStyle(sidebar);
|
||||||
|
let maxWidth = parseInt(computedStyle.getPropertyValue("max-width").replace("px", ""));
|
||||||
|
let minWidth = parseInt(computedStyle.getPropertyValue("min-width").replace("px", ""));
|
||||||
|
|
||||||
|
if (!this._isDragging) { // Prevent multiple resizes
|
||||||
|
this._isDragging = true;
|
||||||
|
let sidebarWidth = sidebar.getBoundingClientRect().width;
|
||||||
|
let startX = event.clientX;
|
||||||
|
let startWidth = sidebarWidth;
|
||||||
|
let mouseMove = (function(e) {
|
||||||
|
let newWidth = startWidth + e.clientX - startX;
|
||||||
|
if (newWidth <= minWidth+10) {
|
||||||
|
newWidth = minWidth+1;
|
||||||
|
} else if (newWidth >= maxWidth-10) {
|
||||||
|
newWidth = maxWidth-1;
|
||||||
|
}
|
||||||
|
sidebar.style.width = `${newWidth}px`;
|
||||||
|
});
|
||||||
|
let mouseUp = (function() {
|
||||||
|
this.handleEvent();
|
||||||
|
this._isDragging = false;
|
||||||
|
document.removeEventListener("mousemove", mouseMove);
|
||||||
|
document.removeEventListener("mouseup", mouseUp);
|
||||||
|
}).bind(this);
|
||||||
|
document.addEventListener("mousemove", mouseMove);
|
||||||
|
document.addEventListener("mouseup", mouseUp);
|
||||||
|
}
|
||||||
|
}).bind(this));
|
||||||
|
|
||||||
|
this.hSplitterElement.addEventListener("mousedown", (function(event) {
|
||||||
|
let computedStyle = window.getComputedStyle(sidebar);
|
||||||
|
const parent = sidebar.parentElement;
|
||||||
|
// relative to avoid the top margin
|
||||||
|
// 20px is the padding
|
||||||
|
let parentRelativeHeight = parent.getBoundingClientRect().height - parent.getBoundingClientRect().top + 20;
|
||||||
|
let minHeight = parseInt(computedStyle.getPropertyValue("min-height").replace("px", ""));
|
||||||
|
if (!this._isDragging) { // Prevent multiple resizes
|
||||||
|
this._isDragging = true;
|
||||||
|
let sidebarHeight = sidebar.getBoundingClientRect().height;
|
||||||
|
let startY = event.clientY;
|
||||||
|
let startHeight = sidebarHeight;
|
||||||
|
let mouseMove = (function(e) {
|
||||||
|
let newHeight = startHeight + e.clientY - startY;
|
||||||
|
if (newHeight <= minHeight+10) {
|
||||||
|
newHeight = minHeight+1;
|
||||||
|
} else if (newHeight >= parentRelativeHeight) { // 10px is the padding
|
||||||
|
newHeight = parentRelativeHeight;
|
||||||
|
}
|
||||||
|
sidebar.style.height = `${newHeight}px`;
|
||||||
|
});
|
||||||
|
let mouseUp = (function() {
|
||||||
|
this.handleEvent();
|
||||||
|
this._isDragging = false;
|
||||||
|
document.removeEventListener("mousemove", mouseMove);
|
||||||
|
document.removeEventListener("mouseup", mouseUp);
|
||||||
|
}).bind(this);
|
||||||
|
document.addEventListener("mousemove", mouseMove);
|
||||||
|
document.addEventListener("mouseup", mouseUp);
|
||||||
|
}
|
||||||
|
}).bind(this));
|
||||||
|
|
||||||
|
this.handleEvent();
|
||||||
|
},
|
||||||
|
|
||||||
|
get isFloating() {
|
||||||
|
return document.getElementById("zen-sidebar-web-panel").hasAttribute("pinned");
|
||||||
|
},
|
||||||
|
|
||||||
|
handleEvent() {
|
||||||
|
this._hasChangedConfig = true;
|
||||||
|
this.update();
|
||||||
|
this._hasChangedConfig = false;
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/11565471/removing-event-listener-which-was-added-with-bind
|
||||||
|
var clickOutsideHandler = this._handleClickOutside.bind(this);
|
||||||
|
let isFloating = this.isFloating;
|
||||||
|
if (isFloating && !this._hasRegisteredPinnedClickOutside) {
|
||||||
|
document.addEventListener("mouseup", clickOutsideHandler);
|
||||||
|
this._hasRegisteredPinnedClickOutside = true;
|
||||||
|
} else if (!isFloating && this._hasRegisteredPinnedClickOutside) {
|
||||||
|
document.removeEventListener("mouseup", clickOutsideHandler);
|
||||||
|
this._hasRegisteredPinnedClickOutside = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = document.getElementById("zen-sidepanel-button");
|
||||||
|
if (Services.prefs.getBoolPref("zen.sidebar.enabled")) {
|
||||||
|
button.removeAttribute("hidden");
|
||||||
|
} else {
|
||||||
|
button.setAttribute("hidden", "true");
|
||||||
|
this._closeSidebarPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_handleClickOutside(event) {
|
||||||
|
let sidebar = document.getElementById("zen-sidebar-web-panel");
|
||||||
|
if (!sidebar.hasAttribute("pinned") || this._isDragging || !this.shouldCloseOnBlur) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let target = event.target;
|
||||||
|
const closestSelector = [
|
||||||
|
"#zen-sidebar-web-panel",
|
||||||
|
"#zen-sidebar-panels-wrapper",
|
||||||
|
"#zenWebPanelContextMenu",
|
||||||
|
"#zen-sidebar-web-panel-splitter",
|
||||||
|
"#contentAreaContextMenu"
|
||||||
|
].join(", ");
|
||||||
|
if (target.closest(closestSelector)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (!this._currentPanel) {
|
||||||
|
this._currentPanel = this._lastOpenedPanel;
|
||||||
|
}
|
||||||
|
if (document.getElementById("zen-sidebar-web-panel").hasAttribute("hidden")) {
|
||||||
|
this.open();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
open() {
|
||||||
|
let sidebar = document.getElementById("zen-sidebar-web-panel");
|
||||||
|
sidebar.removeAttribute("hidden");
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this._updateWebPanels();
|
||||||
|
this._updateSidebarButton();
|
||||||
|
this._updateWebPanel();
|
||||||
|
this._updateButtons();
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateSidebarButton() {
|
||||||
|
let button = document.getElementById("zen-sidepanel-button");
|
||||||
|
if (!document.getElementById("zen-sidebar-web-panel").hasAttribute("hidden")) {
|
||||||
|
button.setAttribute("open", "true");
|
||||||
|
} else {
|
||||||
|
button.removeAttribute("open");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateWebPanels() {
|
||||||
|
if (Services.prefs.getBoolPref("zen.sidebar.enabled")) {
|
||||||
|
this.sidebarElement.removeAttribute("hidden");
|
||||||
|
} else {
|
||||||
|
this.sidebarElement.setAttribute("hidden", "true");
|
||||||
|
this._closeSidebarPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = this.sidebarData;
|
||||||
|
if (!data.data || !data.index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sidebarElement.innerHTML = "";
|
||||||
|
for (let site of data.index) {
|
||||||
|
let panel = data.data[site];
|
||||||
|
if (!panel || !panel.url) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let button = document.createXULElement("toolbarbutton");
|
||||||
|
button.classList.add("zen-sidebar-panel-button", "toolbarbutton-1", "chromeclass-toolbar-additional");
|
||||||
|
button.setAttribute("flex", "1");
|
||||||
|
button.setAttribute("zen-sidebar-id", site);
|
||||||
|
button.setAttribute("context", "zenWebPanelContextMenu");
|
||||||
|
this._getWebPanelIcon(panel.url, button);
|
||||||
|
button.addEventListener("click", this._handleClick.bind(this));
|
||||||
|
this.sidebarElement.appendChild(button);
|
||||||
|
}
|
||||||
|
const addButton = document.getElementById("zen-sidebar-add-panel-button");
|
||||||
|
if (data.index.length < this.MAX_SIDEBAR_PANELS) {
|
||||||
|
addButton.removeAttribute("hidden");
|
||||||
|
} else {
|
||||||
|
addButton.setAttribute("hidden", "true");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async _openAddPanelDialog() {
|
||||||
|
let dialogURL = "chrome://browser/content/places/zenNewWebPanel.xhtml";
|
||||||
|
let features = "centerscreen,chrome,modal,resizable=no";
|
||||||
|
let aParentWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
|
||||||
|
if (aParentWindow?.gDialogBox) {
|
||||||
|
await aParentWindow.gDialogBox.open(dialogURL, {});
|
||||||
|
} else {
|
||||||
|
aParentWindow.openDialog(dialogURL, "", features, {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_setPinnedToElements() {
|
||||||
|
let sidebar = document.getElementById("zen-sidebar-web-panel");
|
||||||
|
sidebar.setAttribute("pinned", "true");
|
||||||
|
document.getElementById("zen-sidebar-web-panel-pinned").setAttribute("pinned", "true");
|
||||||
|
},
|
||||||
|
|
||||||
|
_removePinnedFromElements() {
|
||||||
|
let sidebar = document.getElementById("zen-sidebar-web-panel");
|
||||||
|
sidebar.removeAttribute("pinned");
|
||||||
|
document.getElementById("zen-sidebar-web-panel-pinned").removeAttribute("pinned");
|
||||||
|
},
|
||||||
|
|
||||||
|
_closeSidebarPanel() {
|
||||||
|
let sidebar = document.getElementById("zen-sidebar-web-panel");
|
||||||
|
sidebar.setAttribute("hidden", "true");
|
||||||
|
this._lastOpenedPanel = this._currentPanel;
|
||||||
|
this._currentPanel = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_handleClick(event) {
|
||||||
|
let target = event.target;
|
||||||
|
let panelId = target.getAttribute("zen-sidebar-id");
|
||||||
|
if (this._currentPanel === panelId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._currentPanel = panelId;
|
||||||
|
this._updateWebPanel();
|
||||||
|
},
|
||||||
|
|
||||||
|
_createNewPanel(url) {
|
||||||
|
let data = this.sidebarData;
|
||||||
|
let newName = "p" + new Date().getTime();
|
||||||
|
data.index.push(newName);
|
||||||
|
data.data[newName] = {
|
||||||
|
url: url,
|
||||||
|
ua: false,
|
||||||
|
};
|
||||||
|
Services.prefs.setStringPref("zen.sidebar.data", JSON.stringify(data));
|
||||||
|
this._currentPanel = newName;
|
||||||
|
this.open();
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateButtons() {
|
||||||
|
for (let button of this.sidebarElement.querySelectorAll(".zen-sidebar-panel-button")) {
|
||||||
|
if (button.getAttribute("zen-sidebar-id") === this._currentPanel) {
|
||||||
|
button.setAttribute("selected", "true");
|
||||||
|
} else {
|
||||||
|
button.removeAttribute("selected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_hideAllWebPanels() {
|
||||||
|
let sidebar = document.getElementById("zen-sidebar-web-panel");
|
||||||
|
for (let browser of sidebar.querySelectorAll("browser[zen-sidebar-id]")) {
|
||||||
|
browser.setAttribute("hidden", "true");
|
||||||
|
browser.docShellIsActive = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get introductionPanel() {
|
||||||
|
return document.getElementById("zen-sidebar-introduction-panel");
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateWebPanel() {
|
||||||
|
this._updateButtons();
|
||||||
|
let sidebar = document.getElementById("zen-sidebar-web-panel");
|
||||||
|
this._hideAllWebPanels();
|
||||||
|
if (!this._currentPanel) {
|
||||||
|
this.introductionPanel.removeAttribute("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.introductionPanel.setAttribute("hidden", "true");
|
||||||
|
let existantWebview = this._getCurrentBrowser();
|
||||||
|
if (existantWebview) {
|
||||||
|
existantWebview.docShellIsActive = true;
|
||||||
|
existantWebview.removeAttribute("hidden");
|
||||||
|
document.getElementById("zen-sidebar-web-panel-title").textContent = existantWebview.contentTitle;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data = this._getWebPanelData(this._currentPanel);
|
||||||
|
let browser = this._createWebPanelBrowser(data);
|
||||||
|
let browserContainers = document.getElementById("zen-sidebar-web-panel-browser-containers");
|
||||||
|
browserContainers.appendChild(browser);
|
||||||
|
if (data.ua) {
|
||||||
|
browser.browsingContext.customUserAgent = this.DEFAULT_MOBILE_USER_AGENT;
|
||||||
|
}
|
||||||
|
browser.docShellIsActive = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getWebPanelData(id) {
|
||||||
|
let data = this.sidebarData;
|
||||||
|
let panel = data.data[id];
|
||||||
|
if (!panel || !panel.url) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
...panel,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
_createWebPanelBrowser(data) {
|
||||||
|
const titleContainer = document.getElementById("zen-sidebar-web-panel-title");
|
||||||
|
titleContainer.textContent = "Loading...";
|
||||||
|
let browser = gBrowser.createBrowser({});
|
||||||
|
browser.setAttribute("disablefullscreen", "true");
|
||||||
|
browser.setAttribute("src", data.url);
|
||||||
|
browser.setAttribute("zen-sidebar-id", data.id);
|
||||||
|
browser.setAttribute("disableglobalhistory", "true");
|
||||||
|
browser.setAttribute("autoscroll", "false");
|
||||||
|
browser.setAttribute("autocompletepopup", "PopupAutoComplete");
|
||||||
|
browser.setAttribute("contextmenu", "contentAreaContextMenu");
|
||||||
|
browser.setAttribute("disablesecurity", "true");
|
||||||
|
browser.addEventListener("pagetitlechanged", (function(event) {
|
||||||
|
let browser = event.target;
|
||||||
|
let title = browser.contentTitle;
|
||||||
|
if (!title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let id = browser.getAttribute("zen-sidebar-id");
|
||||||
|
if (id === this._currentPanel) {
|
||||||
|
titleContainer.textContent = title;
|
||||||
|
}
|
||||||
|
}).bind(this));
|
||||||
|
return browser;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getWebPanelIcon(url, element) {
|
||||||
|
let { preferredURI } = Services.uriFixup.getFixupURIInfo(url);
|
||||||
|
element.setAttribute("image", `page-icon:${preferredURI.spec}`);
|
||||||
|
fetch(`https://s2.googleusercontent.com/s2/favicons?domain_url=${preferredURI.spec}`).then(async response => {
|
||||||
|
if (response.ok) {
|
||||||
|
let blob = await response.blob();
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.onload = function() {
|
||||||
|
element.setAttribute("image", reader.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_getBrowserById(id) {
|
||||||
|
let sidebar = document.getElementById("zen-sidebar-web-panel");
|
||||||
|
return sidebar.querySelector(`browser[zen-sidebar-id="${id}"]`);
|
||||||
|
},
|
||||||
|
|
||||||
|
_getCurrentBrowser() {
|
||||||
|
return this._getBrowserById(this._currentPanel);
|
||||||
|
},
|
||||||
|
|
||||||
|
reload() {
|
||||||
|
let browser = this._getCurrentBrowser();
|
||||||
|
if (browser) {
|
||||||
|
browser.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
forward() {
|
||||||
|
let browser = this._getCurrentBrowser();
|
||||||
|
if (browser) {
|
||||||
|
browser.goForward();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
back() {
|
||||||
|
let browser = this._getCurrentBrowser();
|
||||||
|
if (browser) {
|
||||||
|
browser.goBack();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
home() {
|
||||||
|
let browser = this._getCurrentBrowser();
|
||||||
|
if (browser) {
|
||||||
|
browser.gotoIndex();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this._hideAllWebPanels();
|
||||||
|
this._closeSidebarPanel();
|
||||||
|
this._updateSidebarButton();
|
||||||
|
},
|
||||||
|
|
||||||
|
togglePinned(elem) {
|
||||||
|
let sidebar = document.getElementById("zen-sidebar-web-panel");
|
||||||
|
if (sidebar.hasAttribute("pinned")) {
|
||||||
|
this._removePinnedFromElements();
|
||||||
|
} else {
|
||||||
|
this._setPinnedToElements();
|
||||||
|
}
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
get sidebarElement() {
|
||||||
|
if (!this._sidebarElement) {
|
||||||
|
this._sidebarElement = document.getElementById("zen-sidebar-panels-sites");
|
||||||
|
}
|
||||||
|
return this._sidebarElement;
|
||||||
|
},
|
||||||
|
|
||||||
|
get splitterElement() {
|
||||||
|
if (!this._splitterElement) {
|
||||||
|
this._splitterElement = document.getElementById("zen-sidebar-web-panel-splitter");
|
||||||
|
}
|
||||||
|
return this._splitterElement;
|
||||||
|
},
|
||||||
|
|
||||||
|
get hSplitterElement() {
|
||||||
|
if (!this._hSplitterElement) {
|
||||||
|
this._hSplitterElement = document.getElementById("zen-sidebar-web-panel-hsplitter");
|
||||||
|
}
|
||||||
|
return this._hSplitterElement;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
|
||||||
|
updateContextMenu(aPopupMenu) {
|
||||||
|
let panel =
|
||||||
|
aPopupMenu.triggerNode &&
|
||||||
|
(aPopupMenu.triggerNode || aPopupMenu.triggerNode.closest("toolbarbutton[zen-sidebar-id]"));
|
||||||
|
if (!panel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let id = panel.getAttribute("zen-sidebar-id");
|
||||||
|
this.contextTab = id;
|
||||||
|
let data = this._getWebPanelData(id);
|
||||||
|
let browser = this._getBrowserById(id);
|
||||||
|
let isMuted = browser && browser.audioMuted;
|
||||||
|
let mutedContextItem = document.getElementById("context_zenToggleMuteWebPanel");
|
||||||
|
document.l10n.setAttributes(mutedContextItem,
|
||||||
|
!isMuted ? "zen-web-side-panel-context-mute-panel" : "zen-web-side-panel-context-unmute-panel");
|
||||||
|
if (!isMuted) {
|
||||||
|
mutedContextItem.setAttribute("muted", "true");
|
||||||
|
} else {
|
||||||
|
mutedContextItem.removeAttribute("muted");
|
||||||
|
}
|
||||||
|
document.l10n.setAttributes(document.getElementById("context_zenToogleUAWebPanel"),
|
||||||
|
data.ua ? "zen-web-side-panel-context-disable-ua" : "zen-web-side-panel-context-enable-ua");
|
||||||
|
if (!browser) {
|
||||||
|
document.getElementById("context_zenUnloadWebPanel").setAttribute("disabled", "true");
|
||||||
|
} else {
|
||||||
|
document.getElementById("context_zenUnloadWebPanel").removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
contextOpenNewTab() {
|
||||||
|
let browser = this._getBrowserById(this.contextTab);
|
||||||
|
let data = this.sidebarData;
|
||||||
|
let panel = data.data[this.contextTab];
|
||||||
|
let url = (browser == null) ? panel.url : browser.currentURI.spec;
|
||||||
|
gZenUIManager.openAndChangeToTab(url);
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
contextToggleMuteAudio() {
|
||||||
|
let browser = this._getBrowserById(this.contextTab);
|
||||||
|
if (browser.audioMuted) {
|
||||||
|
browser.unmute();
|
||||||
|
} else {
|
||||||
|
browser.mute();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
contextToggleUserAgent() {
|
||||||
|
let browser = this._getBrowserById(this.contextTab);
|
||||||
|
browser.browsingContext.customUserAgent = browser.browsingContext.customUserAgent ? null : this.DEFAULT_MOBILE_USER_AGENT;
|
||||||
|
let data = this.sidebarData;
|
||||||
|
data.data[this.contextTab].ua = !data.data[this.contextTab].ua;
|
||||||
|
Services.prefs.setStringPref("zen.sidebar.data", JSON.stringify(data));
|
||||||
|
browser.reload();
|
||||||
|
},
|
||||||
|
|
||||||
|
contextDelete() {
|
||||||
|
let data = this.sidebarData;
|
||||||
|
delete data.data[this.contextTab];
|
||||||
|
data.index = data.index.filter(id => id !== this.contextTab);
|
||||||
|
let browser = this._getBrowserById(this.contextTab);
|
||||||
|
if (browser) {
|
||||||
|
browser.remove();
|
||||||
|
}
|
||||||
|
this._currentPanel = null;
|
||||||
|
this._lastOpenedPanel = null;
|
||||||
|
this.update();
|
||||||
|
Services.prefs.setStringPref("zen.sidebar.data", JSON.stringify(data));
|
||||||
|
},
|
||||||
|
|
||||||
|
contextUnload() {
|
||||||
|
let browser = this._getBrowserById(this.contextTab);
|
||||||
|
browser.remove();
|
||||||
|
this._closeSidebarPanel();
|
||||||
|
this.close();
|
||||||
|
this._lastOpenedPanel = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
insertIntoContextMenu() {
|
||||||
|
const sibling = document.getElementById("context-stripOnShareLink");
|
||||||
|
const menuitem = document.createXULElement("menuitem");
|
||||||
|
menuitem.setAttribute("id", "context-zenAddToWebPanel");
|
||||||
|
menuitem.setAttribute("hidden", "true");
|
||||||
|
menuitem.setAttribute("oncommand", "gZenBrowserManagerSidebar.addPanelFromContextMenu();");
|
||||||
|
menuitem.setAttribute("data-l10n-id", "zen-web-side-panel-context-add-to-panel");
|
||||||
|
sibling.insertAdjacentElement("afterend", menuitem);
|
||||||
|
},
|
||||||
|
|
||||||
|
addPanelFromContextMenu() {
|
||||||
|
const url = gContextMenu.linkURL || gContextMenu.target.ownerDocument.location.href;
|
||||||
|
this._createNewPanel(url);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
gZenBrowserManagerSidebar.init();
|
349
src/ZenViewSplitter.mjs
Normal file
349
src/ZenViewSplitter.mjs
Normal file
|
@ -0,0 +1,349 @@
|
||||||
|
|
||||||
|
export var gZenViewSplitter = {
|
||||||
|
/**
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* tabs: [
|
||||||
|
* tab1,
|
||||||
|
* tab2,
|
||||||
|
* tab3,
|
||||||
|
* ],
|
||||||
|
* gridType: "vsep" | "hsep" | "grid",
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
_data: [],
|
||||||
|
currentView: -1,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Services.prefs.setBoolPref("zen.splitView.working", false);
|
||||||
|
window.addEventListener("TabClose", this);
|
||||||
|
this.initializeUI();
|
||||||
|
console.log("ZenViewSplitter initialized");
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeUI() {
|
||||||
|
this.insertIntoContextMenu();
|
||||||
|
this.initializeUpdateContextMenuItems();
|
||||||
|
this.initializeTabContextMenu();
|
||||||
|
this.initializeZenSplitBox();
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeZenSplitBox() {
|
||||||
|
const fragment = window.MozXULElement.parseXULToFragment(`
|
||||||
|
<hbox id="zen-split-views-box"
|
||||||
|
hidden="true"
|
||||||
|
role="button"
|
||||||
|
class="urlbar-page-action"
|
||||||
|
onclick="gZenViewSplitter.openSplitViewPanel(event);">
|
||||||
|
<image id="zen-split-views-button"
|
||||||
|
class="urlbar-icon"/>
|
||||||
|
</hbox>`);
|
||||||
|
document.getElementById("star-button-box").after(fragment);
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeTabContextMenu() {
|
||||||
|
const fragment = window.MozXULElement.parseXULToFragment(`
|
||||||
|
<menuseparator/>
|
||||||
|
<menuitem id="context_zenSplitTabs"
|
||||||
|
data-lazy-l10n-id="tab-zen-split-tabs"
|
||||||
|
oncommand="gZenViewSplitter.contextSplitTabs();"/>
|
||||||
|
`);
|
||||||
|
document.getElementById("tabContextMenu").appendChild(fragment);
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeUpdateContextMenuItems() {
|
||||||
|
const contentAreaContextMenu = document.getElementById("tabContextMenu");
|
||||||
|
contentAreaContextMenu.addEventListener("popupshowing", () => {
|
||||||
|
const tabCountInfo = JSON.stringify({
|
||||||
|
tabCount: window.gBrowser.selectedTabs.length,
|
||||||
|
});
|
||||||
|
document
|
||||||
|
.getElementById("context_zenSplitTabs")
|
||||||
|
.setAttribute("data-l10n-args", tabCountInfo);
|
||||||
|
document.getElementById("context_zenSplitTabs").disabled =
|
||||||
|
!this.contextCanSplitTabs();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleEvent(event) {
|
||||||
|
switch (event.type) {
|
||||||
|
case "TabClose":
|
||||||
|
this.onTabClose(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
insertIntoContextMenu() {
|
||||||
|
const sibling = document.getElementById("context-stripOnShareLink");
|
||||||
|
const menuitem = document.createXULElement("menuitem");
|
||||||
|
menuitem.setAttribute("id", "context-zenSplitLink");
|
||||||
|
menuitem.setAttribute("hidden", "true");
|
||||||
|
menuitem.setAttribute("oncommand", "gZenViewSplitter.contextSplitLink();");
|
||||||
|
menuitem.setAttribute("data-l10n-id", "zen-split-link");
|
||||||
|
const separator = document.createXULElement("menuseparator");
|
||||||
|
sibling.insertAdjacentElement("afterend", menuitem);
|
||||||
|
sibling.insertAdjacentElement("afterend", separator);
|
||||||
|
},
|
||||||
|
|
||||||
|
get tabBrowserPanel() {
|
||||||
|
if (!this._tabBrowserPanel) {
|
||||||
|
this._tabBrowserPanel = document.getElementById("tabbrowser-tabpanels");
|
||||||
|
}
|
||||||
|
return this._tabBrowserPanel;
|
||||||
|
},
|
||||||
|
|
||||||
|
onTabClose(event) {
|
||||||
|
const tab = event.target;
|
||||||
|
let index = this._data.findIndex((group) => group.tabs.includes(tab));
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dataTab = this._data[index].tabs;
|
||||||
|
dataTab.splice(dataTab.indexOf(tab), 1);
|
||||||
|
tab._zenSplitted = false;
|
||||||
|
tab.linkedBrowser.zenModeActive = false;
|
||||||
|
let container = tab.linkedBrowser.closest(".browserSidebarContainer");
|
||||||
|
container.removeAttribute("zen-split");
|
||||||
|
if (!event.forUnsplit) {
|
||||||
|
tab.linkedBrowser.docShellIsActive = false;
|
||||||
|
container.style.display = "none";
|
||||||
|
} else {
|
||||||
|
container.style.gridArea = "1 / 1";
|
||||||
|
}
|
||||||
|
if (dataTab.length < 2) {
|
||||||
|
this._data.splice(index, 1);
|
||||||
|
if (this.currentView == index) {
|
||||||
|
console.assert(dataTab.length == 1, "Data tab length is not 1");
|
||||||
|
this.currentView = -1;
|
||||||
|
this.tabBrowserPanel.removeAttribute("zen-split-view");
|
||||||
|
this.tabBrowserPanel.style.gridTemplateAreas = "";
|
||||||
|
this.tabBrowserPanel.style.gridGap = "0px";
|
||||||
|
Services.prefs.setBoolPref("zen.splitView.working", false);
|
||||||
|
for (const tab of dataTab) {
|
||||||
|
let container = tab.linkedBrowser.closest(".browserSidebarContainer");
|
||||||
|
container.removeAttribute("zen-split");
|
||||||
|
container.style.gridArea = "1 / 1";
|
||||||
|
tab._zenSplitted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let lastTab = dataTab[dataTab.length - 1];
|
||||||
|
this._showSplitView(lastTab);
|
||||||
|
},
|
||||||
|
|
||||||
|
contextSplitLink() {
|
||||||
|
const url = gContextMenu.linkURL || gContextMenu.target.ownerDocument.location.href;
|
||||||
|
const tab = gBrowser.selectedTab;
|
||||||
|
const newTab = gZenUIManager.openAndChangeToTab(url);
|
||||||
|
this.splitTabs([tab, newTab]);
|
||||||
|
},
|
||||||
|
|
||||||
|
onLocationChange(browser) {
|
||||||
|
let tab = gBrowser.getTabForBrowser(browser);
|
||||||
|
this.updateSplitViewButton(!(tab && tab._zenSplitted));
|
||||||
|
if (!tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._showSplitView(tab);
|
||||||
|
},
|
||||||
|
|
||||||
|
splitTabs(tabs) {
|
||||||
|
if (tabs.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check if any tab is already split
|
||||||
|
for (const tab of tabs) {
|
||||||
|
if (tab._zenSplitted) {
|
||||||
|
let index = this._data.findIndex((group) => group.tabs.includes(tab));
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._showSplitView(tab);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._data.push({
|
||||||
|
tabs,
|
||||||
|
gridType: "grid",
|
||||||
|
});
|
||||||
|
gBrowser.selectedTab = tabs[0];
|
||||||
|
this._showSplitView(tabs[0]);
|
||||||
|
},
|
||||||
|
|
||||||
|
_showSplitView(tab) {
|
||||||
|
const splitData = this._data.find((group) => group.tabs.includes(tab));
|
||||||
|
function modifyDecks(tabs, add) {
|
||||||
|
for (const tab of tabs) {
|
||||||
|
tab.linkedBrowser.zenModeActive = add;
|
||||||
|
tab.linkedBrowser.docShellIsActive = add;
|
||||||
|
let browser = tab.linkedBrowser.closest(".browserSidebarContainer");
|
||||||
|
if (add) {
|
||||||
|
browser.setAttribute("zen-split", "true");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
browser.removeAttribute("zen-split");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleClick = (tab) => {
|
||||||
|
return ((event) => {
|
||||||
|
gBrowser.selectedTab = tab;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if (!splitData || (this.currentView >= 0 && !this._data[this.currentView].tabs.includes(tab))) {
|
||||||
|
this.updateSplitViewButton(true);
|
||||||
|
if (this.currentView < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const tab of this._data[this.currentView].tabs) {
|
||||||
|
//tab._zenSplitted = false;
|
||||||
|
let container = tab.linkedBrowser.closest(".browserSidebarContainer");
|
||||||
|
container.removeAttribute("zen-split-active");
|
||||||
|
container.classList.remove("deck-selected");
|
||||||
|
console.assert(container, "No container found for tab");
|
||||||
|
container.removeEventListener("click", handleClick(tab));
|
||||||
|
container.style.gridArea = "";
|
||||||
|
}
|
||||||
|
this.tabBrowserPanel.removeAttribute("zen-split-view");
|
||||||
|
this.tabBrowserPanel.style.gridTemplateAreas = "";
|
||||||
|
Services.prefs.setBoolPref("zen.splitView.working", false);
|
||||||
|
modifyDecks(this._data[this.currentView].tabs, false);
|
||||||
|
// console.log("Setting the active tab to be active", gBrowser.selectedTab);
|
||||||
|
gBrowser.selectedTab.linkedBrowser.docShellIsActive = true; // Make sure the active tab is active
|
||||||
|
this.currentView = -1;
|
||||||
|
if (!splitData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.tabBrowserPanel.setAttribute("zen-split-view", "true");
|
||||||
|
Services.prefs.setBoolPref("zen.splitView.working", true);
|
||||||
|
this.currentView = this._data.indexOf(splitData);
|
||||||
|
let gridType = splitData.gridType || "grid"; // TODO: let user decide the grid type
|
||||||
|
let i = 0;
|
||||||
|
// 2 rows, infinite columns
|
||||||
|
let currentRowGridArea = ["", ""/* first row, second row */];
|
||||||
|
let numberOfRows = 0;
|
||||||
|
for (const _tab of splitData.tabs) {
|
||||||
|
_tab._zenSplitted = true;
|
||||||
|
let container = _tab.linkedBrowser.closest(".browserSidebarContainer");
|
||||||
|
console.assert(container, "No container found for tab");
|
||||||
|
container.removeAttribute("zen-split-active");
|
||||||
|
if (_tab == tab) {
|
||||||
|
container.setAttribute("zen-split-active", "true");
|
||||||
|
}
|
||||||
|
container.setAttribute("zen-split-anim", "true");
|
||||||
|
container.addEventListener("click", handleClick(_tab));
|
||||||
|
// Set the grid type for the container. If the grid type is not "grid", then set the grid type contain
|
||||||
|
// each column or row. If it's "grid", then try to create
|
||||||
|
if (gridType == "grid") {
|
||||||
|
// Each 2 tabs, create a new row
|
||||||
|
if (i % 2 == 0) {
|
||||||
|
currentRowGridArea[0] += ` tab${i + 1}`;
|
||||||
|
} else {
|
||||||
|
currentRowGridArea[1] += ` tab${i + 1}`;
|
||||||
|
numberOfRows++;
|
||||||
|
}
|
||||||
|
container.style.gridArea = `tab${i + 1}`;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (gridType == "grid") {
|
||||||
|
if ((numberOfRows < splitData.tabs.length / 2) && (splitData.tabs.length != 2)) {
|
||||||
|
// Make the last tab occupy the last row
|
||||||
|
currentRowGridArea[1] += ` tab${i}`;
|
||||||
|
}
|
||||||
|
if (gridType == "grid" && (splitData.tabs.length === 2)) {
|
||||||
|
currentRowGridArea[0] = `tab1 tab2`;
|
||||||
|
currentRowGridArea[1] = "";
|
||||||
|
}
|
||||||
|
this.tabBrowserPanel.style.gridTemplateAreas = `'${currentRowGridArea[0]}'`;
|
||||||
|
if (currentRowGridArea[1] != "") {
|
||||||
|
this.tabBrowserPanel.style.gridTemplateAreas += ` '${currentRowGridArea[1]}'`;
|
||||||
|
}
|
||||||
|
} else if (gridType == "vsep") {
|
||||||
|
this.tabBrowserPanel.style.gridTemplateAreas = `'${splitData.tabs.map((_, i) => `tab${i + 1}`).join(" ")}'`;
|
||||||
|
} else if (gridType == "hsep") {
|
||||||
|
this.tabBrowserPanel.style.gridTemplateAreas = `${splitData.tabs.map((_, i) => `'tab${i + 1}'`).join(" ")}`;
|
||||||
|
}
|
||||||
|
modifyDecks(splitData.tabs, true);
|
||||||
|
this.updateSplitViewButton(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
contextSplitTabs() {
|
||||||
|
let tabs = gBrowser.selectedTabs;
|
||||||
|
this.splitTabs(tabs);
|
||||||
|
},
|
||||||
|
|
||||||
|
contextCanSplitTabs() {
|
||||||
|
if (gBrowser.selectedTabs.length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check if any tab is already split
|
||||||
|
for (const tab of gBrowser.selectedTabs) {
|
||||||
|
if (tab._zenSplitted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Panel and url button
|
||||||
|
|
||||||
|
updateSplitViewButton(hidden) {
|
||||||
|
let button = document.getElementById("zen-split-views-box");
|
||||||
|
if (hidden) {
|
||||||
|
button.setAttribute("hidden", "true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
button.removeAttribute("hidden");
|
||||||
|
},
|
||||||
|
|
||||||
|
get _modifierElement() {
|
||||||
|
if (!this.__modifierElement) {
|
||||||
|
let wrapper = document.getElementById("template-zen-split-view-modifier");
|
||||||
|
const panel = wrapper.content.firstElementChild;
|
||||||
|
wrapper.replaceWith(wrapper.content);
|
||||||
|
this.__modifierElement = panel;
|
||||||
|
}
|
||||||
|
return this.__modifierElement;
|
||||||
|
},
|
||||||
|
|
||||||
|
async openSplitViewPanel(event) {
|
||||||
|
let panel = this._modifierElement;
|
||||||
|
let target = event.target.parentNode;
|
||||||
|
for (const gridType of ["hsep", "vsep", "grid", "unsplit"]) {
|
||||||
|
let selector = panel.querySelector(`.zen-split-view-modifier-preview.${gridType}`);
|
||||||
|
selector.classList.remove("active");
|
||||||
|
if (this.currentView >= 0 && this._data[this.currentView].gridType == gridType) {
|
||||||
|
selector.classList.add("active");
|
||||||
|
}
|
||||||
|
if (this.__hasSetMenuListener) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
selector.addEventListener("click", ((gridType) => {
|
||||||
|
if (gridType === "unsplit") {
|
||||||
|
let currentTab = gBrowser.selectedTab;
|
||||||
|
let tabs = this._data[this.currentView].tabs;
|
||||||
|
for (const tab of tabs) {
|
||||||
|
this.onTabClose({ target: tab, forUnsplit: true });
|
||||||
|
}
|
||||||
|
gBrowser.selectedTab = currentTab;
|
||||||
|
panel.hidePopup();
|
||||||
|
this.updateSplitViewButton(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._data[this.currentView].gridType = gridType;
|
||||||
|
this._showSplitView(gBrowser.selectedTab);
|
||||||
|
panel.hidePopup();
|
||||||
|
}).bind(this, gridType));
|
||||||
|
}
|
||||||
|
this.__hasSetMenuListener = true;
|
||||||
|
PanelMultiView.openPopup(panel, target, {
|
||||||
|
position: "bottomright topright",
|
||||||
|
triggerEvent: event,
|
||||||
|
}).catch(console.error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
gZenViewSplitter.init();
|
426
src/ZenWorkspaces.mjs
Normal file
426
src/ZenWorkspaces.mjs
Normal file
|
@ -0,0 +1,426 @@
|
||||||
|
|
||||||
|
export var ZenWorkspaces = {
|
||||||
|
async init() {
|
||||||
|
let docElement = document.documentElement;
|
||||||
|
if (docElement.getAttribute("chromehidden").includes("toolbar")
|
||||||
|
|| docElement.getAttribute("chromehidden").includes("menubar")
|
||||||
|
|| docElement.hasAttribute("privatebrowsingmode")) {
|
||||||
|
console.warn("ZenWorkspaces: !!! ZenWorkspaces is disabled in hidden windows !!!");
|
||||||
|
return; // We are in a hidden window, don't initialize ZenWorkspaces
|
||||||
|
}
|
||||||
|
console.log("ZenWorkspaces: Initializing ZenWorkspaces...");
|
||||||
|
await this.initializeWorkspaces();
|
||||||
|
console.log("ZenWorkspaces: ZenWorkspaces initialized");
|
||||||
|
},
|
||||||
|
|
||||||
|
get workspaceEnabled() {
|
||||||
|
return Services.prefs.getBoolPref("zen.workspaces.enabled", false);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Wrorkspaces saving/loading
|
||||||
|
get _storeFile() {
|
||||||
|
return PathUtils.join(
|
||||||
|
PathUtils.profileDir,
|
||||||
|
"zen-workspaces",
|
||||||
|
"Workspaces.json",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async _workspaces() {
|
||||||
|
if (!this._workspaceCache) {
|
||||||
|
this._workspaceCache = await IOUtils.readJSON(this._storeFile);
|
||||||
|
if (!this._workspaceCache.workspaces) {
|
||||||
|
this._workspaceCache.workspaces = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this._workspaceCache;
|
||||||
|
},
|
||||||
|
|
||||||
|
onWorkspacesEnabledChanged() {
|
||||||
|
if (this.workspaceEnabled) {
|
||||||
|
this.initializeWorkspaces();
|
||||||
|
} else {
|
||||||
|
this._workspaceCache = null;
|
||||||
|
document.getElementById("zen-workspaces-button")?.remove();
|
||||||
|
for (let tab of gBrowser.tabs) {
|
||||||
|
gBrowser.showTab(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async initializeWorkspaces() {
|
||||||
|
Services.prefs.addObserver("zen.workspaces.enabled", this.onWorkspacesEnabledChanged.bind(this));
|
||||||
|
this.initializeWorkspacesButton();
|
||||||
|
let file = new FileUtils.File(this._storeFile);
|
||||||
|
if (!file.exists()) {
|
||||||
|
await IOUtils.writeJSON(this._storeFile, {});
|
||||||
|
}
|
||||||
|
if (this.workspaceEnabled) {
|
||||||
|
let workspaces = await this._workspaces();
|
||||||
|
if (workspaces.workspaces.length === 0) {
|
||||||
|
await this.createAndSaveWorkspace("Default Workspace", true);
|
||||||
|
} else {
|
||||||
|
let activeWorkspace = workspaces.workspaces.find(workspace => workspace.default);
|
||||||
|
if (!activeWorkspace) {
|
||||||
|
activeWorkspace = workspaces.workspaces.find(workspace => workspace.used);
|
||||||
|
activeWorkspace.used = true;
|
||||||
|
await this.saveWorkspaces();
|
||||||
|
}
|
||||||
|
if (!activeWorkspace) {
|
||||||
|
activeWorkspace = workspaces.workspaces[0];
|
||||||
|
activeWorkspace.used = true;
|
||||||
|
await this.saveWorkspaces();
|
||||||
|
}
|
||||||
|
await this.changeWorkspace(activeWorkspace);
|
||||||
|
}
|
||||||
|
this._initializeWorkspaceIcons();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_initializeWorkspaceIcons() {
|
||||||
|
const kIcons = ["🏠", "📄", "💹", "💼", "📧", "✅", "👥"];
|
||||||
|
let container = document.getElementById("PanelUI-zen-workspaces-create-icons-container");
|
||||||
|
for (let icon of kIcons) {
|
||||||
|
let button = document.createXULElement("toolbarbutton");
|
||||||
|
button.className = "toolbarbutton-1";
|
||||||
|
button.setAttribute("label", icon);
|
||||||
|
button.onclick = ((event) => {
|
||||||
|
for (let button of container.children) {
|
||||||
|
button.removeAttribute("selected");
|
||||||
|
}
|
||||||
|
button.setAttribute("selected", "true");
|
||||||
|
}).bind(this, button);
|
||||||
|
container.appendChild(button);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveWorkspace(workspaceData) {
|
||||||
|
let json = await IOUtils.readJSON(this._storeFile);
|
||||||
|
if (typeof json.workspaces === "undefined") {
|
||||||
|
json.workspaces = [];
|
||||||
|
}
|
||||||
|
json.workspaces.push(workspaceData);
|
||||||
|
console.log("ZenWorkspaces: Saving workspace", workspaceData);
|
||||||
|
await IOUtils.writeJSON(this._storeFile, json);
|
||||||
|
this._workspaceCache = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeWorkspace(windowID) {
|
||||||
|
let json = await this._workspaces();
|
||||||
|
console.log("ZenWorkspaces: Removing workspace", windowID);
|
||||||
|
await this.changeWorkspace(json.workspaces.find(workspace => workspace.uuid !== windowID));
|
||||||
|
this._deleteAllTabsInWorkspace(windowID);
|
||||||
|
json.workspaces = json.workspaces.filter(workspace => workspace.uuid !== windowID);
|
||||||
|
await this.unsafeSaveWorkspaces(json);
|
||||||
|
await this._propagateWorkspaceData();
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveWorkspaces() {
|
||||||
|
await IOUtils.writeJSON(this._storeFile, await this._workspaces());
|
||||||
|
this._workspaceCache = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async unsafeSaveWorkspaces(workspaces) {
|
||||||
|
await IOUtils.writeJSON(this._storeFile, workspaces);
|
||||||
|
this._workspaceCache = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Workspaces dialog UI management
|
||||||
|
|
||||||
|
openSaveDialog() {
|
||||||
|
let parentPanel = document.getElementById("PanelUI-zen-workspaces-multiview");
|
||||||
|
PanelUI.showSubView("PanelUI-zen-workspaces-create", parentPanel);
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelWorkspaceCreation() {
|
||||||
|
let parentPanel = document.getElementById("PanelUI-zen-workspaces-multiview");
|
||||||
|
parentPanel.goBack();
|
||||||
|
},
|
||||||
|
|
||||||
|
workspaceHasIcon(workspace) {
|
||||||
|
return typeof workspace.icon !== "undefined" && workspace.icon !== "";
|
||||||
|
},
|
||||||
|
|
||||||
|
getWorkspaceIcon(workspace) {
|
||||||
|
if (this.workspaceHasIcon(workspace)) {
|
||||||
|
return workspace.icon;
|
||||||
|
}
|
||||||
|
return workspace.name[0].toUpperCase();
|
||||||
|
},
|
||||||
|
|
||||||
|
async _propagateWorkspaceData() {
|
||||||
|
let currentContainer = document.getElementById("PanelUI-zen-workspaces-current-info");
|
||||||
|
let workspaceList = document.getElementById("PanelUI-zen-workspaces-list");
|
||||||
|
const createWorkspaceElement = (workspace) => {
|
||||||
|
let element = document.createXULElement("toolbarbutton");
|
||||||
|
element.className = "subviewbutton";
|
||||||
|
element.setAttribute("tooltiptext", workspace.name);
|
||||||
|
element.setAttribute("zen-workspace-id", workspace.uuid);
|
||||||
|
//element.setAttribute("context", "zenWorkspaceActionsMenu");
|
||||||
|
let childs = window.MozXULElement.parseXULToFragment(`
|
||||||
|
<div class="zen-workspace-icon">
|
||||||
|
${this.getWorkspaceIcon(workspace)}
|
||||||
|
</div>
|
||||||
|
<div class="zen-workspace-name">
|
||||||
|
${workspace.name}
|
||||||
|
</div>
|
||||||
|
<toolbarbutton closemenu="none" class="toolbarbutton-1 zen-workspace-actions">
|
||||||
|
<image class="toolbarbutton-icon" id="zen-workspace-actions-menu-icon"></image>
|
||||||
|
</toolbarbutton>
|
||||||
|
`);
|
||||||
|
childs.querySelector(".zen-workspace-actions").addEventListener("command", ((event) => {
|
||||||
|
let button = event.target;
|
||||||
|
this._contextMenuId = button.closest("toolbarbutton[zen-workspace-id]").getAttribute("zen-workspace-id");
|
||||||
|
const popup = button.ownerDocument.getElementById(
|
||||||
|
"zenWorkspaceActionsMenu"
|
||||||
|
);
|
||||||
|
popup.openPopup(button, "after_end");
|
||||||
|
}).bind(this));
|
||||||
|
element.appendChild(childs);
|
||||||
|
element.onclick = (async () => {
|
||||||
|
if (event.target.closest(".zen-workspace-actions")) {
|
||||||
|
return; // Ignore clicks on the actions button
|
||||||
|
}
|
||||||
|
await this.changeWorkspace(workspace)
|
||||||
|
let panel = document.getElementById("PanelUI-zen-workspaces");
|
||||||
|
PanelMultiView.hidePopup(panel);
|
||||||
|
}).bind(this, workspace);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
let workspaces = await this._workspaces();
|
||||||
|
let activeWorkspace = workspaces.workspaces.find(workspace => workspace.used);
|
||||||
|
currentContainer.innerHTML = "";
|
||||||
|
workspaceList.innerHTML = "";
|
||||||
|
workspaceList.parentNode.style.display = "flex";
|
||||||
|
if (workspaces.workspaces.length - 1 <= 0) {
|
||||||
|
workspaceList.innerHTML = "No workspaces available";
|
||||||
|
workspaceList.setAttribute("empty", "true");
|
||||||
|
} else {
|
||||||
|
workspaceList.removeAttribute("empty");
|
||||||
|
}
|
||||||
|
if (activeWorkspace) {
|
||||||
|
let currentWorkspace = createWorkspaceElement(activeWorkspace);
|
||||||
|
currentContainer.appendChild(currentWorkspace);
|
||||||
|
}
|
||||||
|
for (let workspace of workspaces.workspaces) {
|
||||||
|
if (workspace.used) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let workspaceElement = createWorkspaceElement(workspace);
|
||||||
|
workspaceList.appendChild(workspaceElement);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async openWorkspacesDialog(event) {
|
||||||
|
if (!this.workspaceEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let target = event.target;
|
||||||
|
let panel = document.getElementById("PanelUI-zen-workspaces");
|
||||||
|
await this._propagateWorkspaceData();
|
||||||
|
PanelMultiView.openPopup(panel, target, {
|
||||||
|
position: "bottomright topright",
|
||||||
|
triggerEvent: event,
|
||||||
|
}).catch(console.error);
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeWorkspacesButton() {
|
||||||
|
if (!this.workspaceEnabled) {
|
||||||
|
return;
|
||||||
|
} else if (document.getElementById("zen-workspaces-button")) {
|
||||||
|
let button = document.getElementById("zen-workspaces-button");
|
||||||
|
button.removeAttribute("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let browserTabs = document.getElementById("tabbrowser-tabs");
|
||||||
|
let button = document.createElement("toolbarbutton");
|
||||||
|
button.id = "zen-workspaces-button";
|
||||||
|
button.className = "toolbarbutton-1 chromeclass-toolbar-additional";
|
||||||
|
button.setAttribute("label", "Workspaces");
|
||||||
|
button.setAttribute("tooltiptext", "Workspaces");
|
||||||
|
button.onclick = this.openWorkspacesDialog.bind(this);
|
||||||
|
browserTabs.insertAdjacentElement("beforebegin", button);
|
||||||
|
},
|
||||||
|
|
||||||
|
async _updateWorkspacesButton() {
|
||||||
|
let button = document.getElementById("zen-workspaces-button");
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let activeWorkspace = (await this._workspaces()).workspaces.find(workspace => workspace.used);
|
||||||
|
if (activeWorkspace) {
|
||||||
|
button.innerHTML = `
|
||||||
|
<div class="zen-workspace-sidebar-icon">
|
||||||
|
${this.getWorkspaceIcon(activeWorkspace)}
|
||||||
|
</div>
|
||||||
|
<div class="zen-workspace-sidebar-name">
|
||||||
|
${activeWorkspace.name}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
if (!this.workspaceHasIcon(activeWorkspace)) {
|
||||||
|
button.querySelector(".zen-workspace-sidebar-icon").setAttribute("no-icon", "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Workspaces management
|
||||||
|
|
||||||
|
get _workspaceInput() {
|
||||||
|
return document.getElementById("PanelUI-zen-workspaces-create-input");
|
||||||
|
},
|
||||||
|
|
||||||
|
_deleteAllTabsInWorkspace(workspaceID) {
|
||||||
|
for (let tab of gBrowser.tabs) {
|
||||||
|
if (tab.getAttribute("zen-workspace-id") === workspaceID) {
|
||||||
|
gBrowser.removeTab(tab, {
|
||||||
|
animate: true,
|
||||||
|
skipSessionStore: true,
|
||||||
|
closeWindowWithLastTab: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_prepareNewWorkspace(window) {
|
||||||
|
document.documentElement.setAttribute("zen-workspace-id", window.uuid);
|
||||||
|
let tabCount = 0;
|
||||||
|
for (let tab of gBrowser.tabs) {
|
||||||
|
if (!tab.hasAttribute("zen-workspace-id")) {
|
||||||
|
tab.setAttribute("zen-workspace-id", window.uuid);
|
||||||
|
tabCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tabCount === 0) {
|
||||||
|
this._createNewTabForWorkspace(window);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_createNewTabForWorkspace(window) {
|
||||||
|
let tab = gZenUIManager.openAndChangeToTab(Services.prefs.getStringPref("browser.startup.homepage"));
|
||||||
|
tab.setAttribute("zen-workspace-id", window.uuid);
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveWorkspaceFromInput() {
|
||||||
|
// Go to the next view
|
||||||
|
let parentPanel = document.getElementById("PanelUI-zen-workspaces-multiview");
|
||||||
|
PanelUI.showSubView("PanelUI-zen-workspaces-create-icons", parentPanel);
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveWorkspaceFromIcon() {
|
||||||
|
let workspaceName = this._workspaceInput.value;
|
||||||
|
if (!workspaceName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._workspaceInput.value = "";
|
||||||
|
let icon = document.querySelector("#PanelUI-zen-workspaces-create-icons-container [selected]");
|
||||||
|
icon?.removeAttribute("selected");
|
||||||
|
await this.createAndSaveWorkspace(workspaceName, false, icon?.label);
|
||||||
|
document.getElementById("PanelUI-zen-workspaces").hidePopup(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
onWorkspaceNameChange(event) {
|
||||||
|
let button = document.getElementById("PanelUI-zen-workspaces-create-save");
|
||||||
|
if (this._workspaceInput.value === "") {
|
||||||
|
button.setAttribute("disabled", "true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
button.removeAttribute("disabled");
|
||||||
|
},
|
||||||
|
|
||||||
|
async changeWorkspace(window) {
|
||||||
|
if (!this.workspaceEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let firstTab = undefined;
|
||||||
|
let workspaces = await this._workspaces();
|
||||||
|
for (let workspace of workspaces.workspaces) {
|
||||||
|
workspace.used = workspace.uuid === window.uuid;
|
||||||
|
}
|
||||||
|
this.unsafeSaveWorkspaces(workspaces);
|
||||||
|
console.log("ZenWorkspaces: Changing workspace to", window.uuid);
|
||||||
|
for (let tab of gBrowser.tabs) {
|
||||||
|
if (tab.getAttribute("zen-workspace-id") === window.uuid && !tab.pinned) {
|
||||||
|
if (!firstTab) {
|
||||||
|
firstTab = tab;
|
||||||
|
gBrowser.selectedTab = firstTab;
|
||||||
|
}
|
||||||
|
gBrowser.showTab(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof firstTab === "undefined") {
|
||||||
|
this._createNewTabForWorkspace(window);
|
||||||
|
}
|
||||||
|
for (let tab of gBrowser.tabs) {
|
||||||
|
if (tab.getAttribute("zen-workspace-id") !== window.uuid) {
|
||||||
|
gBrowser.hideTab(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute("zen-workspace-id", window.uuid);
|
||||||
|
await this.saveWorkspaces();
|
||||||
|
await this._updateWorkspacesButton();
|
||||||
|
await this._propagateWorkspaceData();
|
||||||
|
},
|
||||||
|
|
||||||
|
_createWorkspaceData(name, isDefault, icon) {
|
||||||
|
let window = {
|
||||||
|
uuid: gZenUIManager.generateUuidv4(),
|
||||||
|
default: isDefault,
|
||||||
|
used: true,
|
||||||
|
icon: icon,
|
||||||
|
name: name,
|
||||||
|
};
|
||||||
|
this._prepareNewWorkspace(window);
|
||||||
|
return window;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createAndSaveWorkspace(name = "New Workspace", isDefault = false, icon = undefined) {
|
||||||
|
if (!this.workspaceEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let workspaceData = this._createWorkspaceData(name, isDefault, icon);
|
||||||
|
await this.saveWorkspace(workspaceData);
|
||||||
|
await this.changeWorkspace(workspaceData);
|
||||||
|
},
|
||||||
|
|
||||||
|
async onLocationChange(browser) {
|
||||||
|
let tab = gBrowser.getTabForBrowser(browser);
|
||||||
|
let workspaceID = tab.getAttribute("zen-workspace-id");
|
||||||
|
if (!workspaceID) {
|
||||||
|
let workspaces = await this._workspaces();
|
||||||
|
let activeWorkspace = workspaces.workspaces.find(workspace => workspace.used);
|
||||||
|
if (!activeWorkspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tab.setAttribute("zen-workspace-id", activeWorkspace.uuid);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Context menu management
|
||||||
|
|
||||||
|
_contextMenuId: null,
|
||||||
|
async updateContextMenu(_) {
|
||||||
|
console.assert(this._contextMenuId, "No context menu ID set");
|
||||||
|
document.querySelector(`#PanelUI-zen-workspaces [zen-workspace-id="${this._contextMenuId}"] .zen-workspace-actions`).setAttribute("active", "true");
|
||||||
|
const workspaces = await this._workspaces();
|
||||||
|
let deleteMenuItem = document.getElementById("context_zenDeleteWorkspace");
|
||||||
|
if (workspaces.workspaces.length <= 1 || workspaces.workspaces.find(workspace => workspace.uuid === this._contextMenuId).default) {
|
||||||
|
deleteMenuItem.setAttribute("disabled", "true");
|
||||||
|
} else {
|
||||||
|
deleteMenuItem.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onContextMenuClose() {
|
||||||
|
let target = document.querySelector(`#PanelUI-zen-workspaces [zen-workspace-id="${this._contextMenuId}"] .zen-workspace-actions`);
|
||||||
|
if (target) {
|
||||||
|
target.removeAttribute("active");
|
||||||
|
}
|
||||||
|
this._contextMenuId = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async contextDelete() {
|
||||||
|
await this.removeWorkspace(this._contextMenuId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ZenWorkspaces.init();
|
|
@ -1,330 +0,0 @@
|
||||||
|
|
||||||
class SplitViewsUtils {
|
|
||||||
/**
|
|
||||||
* @returns {HTMLDivElement}
|
|
||||||
*/
|
|
||||||
get tabBrowser() {
|
|
||||||
if (!this._tabBrowser) {
|
|
||||||
this._tabBrowser = document.getElementById('tabbrowser-tabpanels');
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
return this._tabBrowser;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SplitViewsBase extends SplitViewsUtils {
|
|
||||||
/**
|
|
||||||
* @type {SplitView[]}
|
|
||||||
*/
|
|
||||||
data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {SplitViewConfig} config
|
|
||||||
*/
|
|
||||||
constructor(config) {
|
|
||||||
super();
|
|
||||||
this.config = config;
|
|
||||||
this.data = [];
|
|
||||||
this.currentView = -1;
|
|
||||||
this.globalIdCounter = 0;
|
|
||||||
// Added to "navigator-toolbox" element
|
|
||||||
this.parentSplitIndicator = this.config.splitIndicator + '-view';
|
|
||||||
this.log('SplitViewsBase initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} message
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
log(message) {
|
|
||||||
console.log(`SplitViews: ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
get isActivated() {
|
|
||||||
return this.currentView !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
get activeView() {
|
|
||||||
if (!this.isActivated) {
|
|
||||||
throw new Error('No active view');
|
|
||||||
}
|
|
||||||
return this.data[this.currentView];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MockedExports.BrowserTab} tab
|
|
||||||
*/
|
|
||||||
getTabView(tab) {
|
|
||||||
return this.data.find(view => view.tabs.includes(tab));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MockedExports.BrowserTab} tab
|
|
||||||
*/
|
|
||||||
isTabSplit(tab) {
|
|
||||||
return tab.hasAttribute(this.config.splitIndicator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MockedExports.BrowserTab} tab
|
|
||||||
* @param {SplitType} type
|
|
||||||
* @param {MockedExports.BrowserTab[]} tabs
|
|
||||||
*/
|
|
||||||
changeSplitViewBase(tab, type, tabs) {
|
|
||||||
let view = this.getTabView(tab);
|
|
||||||
if (!view) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
view.type = type;
|
|
||||||
view.tabs.push(...tabs.filter(t => !view.tabs.includes(t)));
|
|
||||||
return view.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MockedExports.BrowserTab[]} tabs
|
|
||||||
* @param {SplitType} type
|
|
||||||
*/
|
|
||||||
createSplitViewBase(tabs, type) {
|
|
||||||
let view = {
|
|
||||||
id: this.globalIdCounter++,
|
|
||||||
type,
|
|
||||||
tabs,
|
|
||||||
};
|
|
||||||
this.data.push(view);
|
|
||||||
this.currentView = this.data.length - 1;
|
|
||||||
return view.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies the grid layout to the tabs.
|
|
||||||
*
|
|
||||||
* @param {MockedExports.BrowserTab[]} tabs - The tabs to apply the grid layout to.
|
|
||||||
* @param {string} gridType - The type of grid layout.
|
|
||||||
* @param {MockedExports.BrowserTab} activeTab - The active tab.
|
|
||||||
*/
|
|
||||||
applyGridLayout(tabs, gridType, activeTab) {
|
|
||||||
const gridAreas = this.calculateGridAreas(tabs, gridType);
|
|
||||||
this.tabBrowser.style.gridTemplateAreas = gridAreas;
|
|
||||||
|
|
||||||
tabs.forEach((tab, index) => {
|
|
||||||
tab.setAttribute(this.config.splitIndicator, "true");
|
|
||||||
const container = tab.linkedBrowser.closest(".browserSidebarContainer");
|
|
||||||
if (!container) {
|
|
||||||
throw new Error("Container not found");
|
|
||||||
}
|
|
||||||
this.styleContainer(container, tab === activeTab, index, gridType);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Styles the container for a tab.
|
|
||||||
*
|
|
||||||
* @param {Element} container - The container element.
|
|
||||||
* @param {boolean} isActive - Indicates if the tab is active.
|
|
||||||
* @param {number} index - The index of the tab.
|
|
||||||
* @param {string} gridType - The type of grid layout.
|
|
||||||
*/
|
|
||||||
styleContainer(container, isActive, index, gridType) {
|
|
||||||
container.removeAttribute("split-active");
|
|
||||||
container.setAttribute(this.config.splitIndicator, "true");
|
|
||||||
if (isActive) {
|
|
||||||
container.setAttribute("split-active", "true");
|
|
||||||
}
|
|
||||||
container.setAttribute("split-anim", "true");
|
|
||||||
container.addEventListener("click", this.handleTabClick);
|
|
||||||
|
|
||||||
if (gridType === "grid") {
|
|
||||||
// @ts-ignore
|
|
||||||
container.style.gridArea = `tab${index + 1}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the grid areas for the tabs.
|
|
||||||
*
|
|
||||||
* @param {MockedExports.BrowserTab[]} tabs - The tabs.
|
|
||||||
* @param {string} gridType - The type of grid layout.
|
|
||||||
* @returns {string} The calculated grid areas.
|
|
||||||
*/
|
|
||||||
calculateGridAreas(tabs, gridType) {
|
|
||||||
if (gridType === "grid") {
|
|
||||||
return this.calculateGridAreasForGrid(tabs);
|
|
||||||
}
|
|
||||||
if (gridType === "vsep") {
|
|
||||||
return `'${tabs.map((_, j) => `tab${j + 1}`).join(" ")}'`;
|
|
||||||
}
|
|
||||||
if (gridType === "hsep") {
|
|
||||||
return tabs.map((_, j) => `'tab${j + 1}'`).join(" ");
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the tab click event.
|
|
||||||
*
|
|
||||||
* @param {Event} event - The click event.
|
|
||||||
*/
|
|
||||||
handleTabClick(event) {
|
|
||||||
const container = event.currentTarget;
|
|
||||||
// @ts-ignore
|
|
||||||
const tab = window.gBrowser.tabs.find(
|
|
||||||
// @ts-ignore
|
|
||||||
t => t.linkedBrowser.closest(".browserSidebarContainer") === container
|
|
||||||
);
|
|
||||||
if (tab) {
|
|
||||||
// @ts-ignore
|
|
||||||
window.gBrowser.selectedTab = tab;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the grid areas for the tabs in a grid layout.
|
|
||||||
*
|
|
||||||
* @param {MockedExports.BrowserTab[]} tabs - The tabs.
|
|
||||||
* @returns {string} The calculated grid areas.
|
|
||||||
*/
|
|
||||||
calculateGridAreasForGrid(tabs) {
|
|
||||||
const rows = ["", ""];
|
|
||||||
tabs.forEach((_, i) => {
|
|
||||||
if (i % 2 === 0) {
|
|
||||||
rows[0] += ` tab${i + 1}`;
|
|
||||||
} else {
|
|
||||||
rows[1] += ` tab${i + 1}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (tabs.length === 2) {
|
|
||||||
return "'tab1 tab2'";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tabs.length % 2 !== 0) {
|
|
||||||
rows[1] += ` tab${tabs.length}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `'${rows[0].trim()}' '${rows[1].trim()}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} viewId
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
updateSplitView(viewId) {
|
|
||||||
let view = this.data.find(view => view.id === viewId);
|
|
||||||
this.log(`updateSplitView: ${viewId}`);
|
|
||||||
this.currentView = viewId;
|
|
||||||
if (!view) {
|
|
||||||
this.tabBrowser.removeAttribute(this.parentSplitIndicator);
|
|
||||||
throw new Error('TODO: Remove split view');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.tabBrowser.setAttribute(this.parentSplitIndicator, "true");
|
|
||||||
this.applyGridLayout(view.tabs, view.type, view.tabs[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MockedExports.BrowserTab[]} tabs
|
|
||||||
* @param {SplitType} type
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
createOrChangeSplitView(tabs, type) {
|
|
||||||
let activeTab = tabs.find(tab => this.isTabSplit(tab));
|
|
||||||
this.log(`createOrChangeSplitView: ${type}`);
|
|
||||||
let viewId = -1;
|
|
||||||
if (activeTab) {
|
|
||||||
viewId = this.changeSplitViewBase(activeTab, type, tabs);
|
|
||||||
} else {
|
|
||||||
viewId = this.createSplitViewBase(tabs, type);
|
|
||||||
}
|
|
||||||
this.updateSplitView(viewId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public API exposed by the module
|
|
||||||
export class SplitViews extends SplitViewsBase {
|
|
||||||
/**
|
|
||||||
* @param {SplitViewConfig} config
|
|
||||||
*/
|
|
||||||
constructor(config) {
|
|
||||||
super(config);
|
|
||||||
this.addEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListeners() {
|
|
||||||
window.addEventListener('TabClose', this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Event} event
|
|
||||||
*/
|
|
||||||
handleEvent(event) {
|
|
||||||
switch (event.type) {
|
|
||||||
case 'TabClose':
|
|
||||||
this.onTabClose(event);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Event} event
|
|
||||||
*/
|
|
||||||
// @ts-ignore
|
|
||||||
// @ts-ignore
|
|
||||||
onTabClose(event) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MockedExports.Browser} browser
|
|
||||||
*/
|
|
||||||
// @ts-ignore
|
|
||||||
// @ts-ignore
|
|
||||||
onLocationChange(browser) {
|
|
||||||
this.log('onLocationChange');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {SplitType} type
|
|
||||||
*/
|
|
||||||
// @ts-ignore
|
|
||||||
// @ts-ignore
|
|
||||||
tileCurrentView(type) {
|
|
||||||
this.log('tileCurrentView');
|
|
||||||
}
|
|
||||||
|
|
||||||
closeCurrentView() {
|
|
||||||
this.log('closeCurrentView');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MockedExports.BrowserTab} tab
|
|
||||||
*/
|
|
||||||
// @ts-ignore
|
|
||||||
// @ts-ignore
|
|
||||||
tabIsInActiveView(tab) {
|
|
||||||
this.log('tabIsInActiveView');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getActiveViewTabs() {
|
|
||||||
this.log('getActiveViewTabs');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getActiveViewType() {
|
|
||||||
if (!this.isActivated) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return this.activeView.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MockedExports.BrowserTab[]} tabs
|
|
||||||
* @param {SplitType} type
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
createSplitView(tabs, type = this.config.defaultSplitView) {
|
|
||||||
if (tabs.length < 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.createOrChangeSplitView(tabs, type);
|
|
||||||
}
|
|
||||||
};
|
|
106
tsconfig.json
106
tsconfig.json
|
@ -1,106 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
|
||||||
|
|
||||||
/* Projects */
|
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
|
||||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
|
||||||
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
|
|
||||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
|
|
||||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
|
||||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
|
||||||
|
|
||||||
/* Language and Environment */
|
|
||||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
|
||||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
|
||||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
|
||||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
|
||||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
|
||||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
|
||||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
|
|
||||||
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
|
|
||||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
|
||||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
|
||||||
// Make the type checking as strict as possible.
|
|
||||||
// TypeScript will check JS files only if they have a @ts-check comment in them.
|
|
||||||
"allowJs": true,
|
|
||||||
// Allow esnext syntax. Otherwise the default is ES5 only.
|
|
||||||
"target": "esnext",
|
|
||||||
"lib": ["esnext", "dom"],
|
|
||||||
|
|
||||||
/* Modules */
|
|
||||||
"module": "CommonJS", /* Specify what module code is generated. */
|
|
||||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
|
||||||
// "moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */
|
|
||||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
|
||||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
|
||||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
|
||||||
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
|
|
||||||
"typeRoots": ["./@types"], /* Specify type package names to be included without being referenced in a source file. */
|
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
|
||||||
// "resolveJsonModule": true, /* Enable importing .json files */
|
|
||||||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
|
||||||
|
|
||||||
/* JavaScript Support */
|
|
||||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
|
||||||
"checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
|
||||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
|
||||||
|
|
||||||
/* Emit */
|
|
||||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
|
||||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
|
||||||
"emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
|
||||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
|
||||||
// "outFile": "./dist/components.js", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
|
||||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
|
||||||
// "removeComments": true, /* Disable emitting comments. */
|
|
||||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
|
||||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
|
||||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
|
||||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
|
||||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
|
||||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
|
||||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
|
||||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
|
||||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
|
||||||
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
|
||||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
|
||||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
|
||||||
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
|
||||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
|
||||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
|
||||||
|
|
||||||
/* Interop Constraints */
|
|
||||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
|
||||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
|
||||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
|
|
||||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
|
||||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
|
||||||
|
|
||||||
/* Type Checking */
|
|
||||||
"strict": true, /* Enable all strict type-checking options. */
|
|
||||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
|
||||||
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
|
||||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
|
||||||
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
|
||||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
|
||||||
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
|
||||||
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
|
||||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
|
||||||
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
|
||||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
|
|
||||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
|
||||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
|
||||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
|
||||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
|
||||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
|
||||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
|
||||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
|
||||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
|
||||||
|
|
||||||
/* Completeness */
|
|
||||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
|
||||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue