diff --git a/base/content/browser-commands.js b/base/content/browser-commands.js index ce5d8e42fa..10c9ca44b6 100644 --- a/base/content/browser-commands.js +++ b/base/content/browser-commands.js @@ -200,14 +200,13 @@ var gDotCommands = { name: "browser.toolbar.toggle", action: ({ gDot, name }) => { - const toolbar = gDot.getToolbarByName(name); - if (!toolbar) { - throw new Error( - `Toolbar with name '${name}' could not be found!` - ); - } - - toolbar.toggleCollapsed(); + // const toolbar = gDot.getToolbarByName(name); + // if (!toolbar) { + // throw new Error( + // `Toolbar with name '${name}' could not be found!` + // ); + // } + // toolbar.toggleCollapsed(); }, enabled: () => true, diff --git a/base/content/browser-elements.js b/base/content/browser-elements.js index 8341001a15..22b5dfe5a8 100644 --- a/base/content/browser-elements.js +++ b/base/content/browser-elements.js @@ -34,6 +34,7 @@ let elements = { }; let noCallbackElements = [ + "chrome://dot/content/customizableui/components/customizable-element.js", "chrome://dot/content/widgets/browser-addressbar-panel.js", "chrome://dot/content/widgets/browser-customizable-area.js", "chrome://dot/content/widgets/browser-contextual-element.js", diff --git a/base/content/browser.js b/base/content/browser.js index 7f753382e2..d4003f0a9f 100644 --- a/base/content/browser.js +++ b/base/content/browser.js @@ -6,8 +6,8 @@ ChromeUtils.defineESModuleGetters(globalThis, { DotAppConstants: "resource://gre/modules/DotAppConstants.sys.mjs" }); -var { DotCustomizableUI } = ChromeUtils.importESModule( - "resource:///modules/DotCustomizableUI.sys.mjs" +var { BrowserCustomizable } = ChromeUtils.importESModule( + "resource:///modules/BrowserCustomizable.sys.mjs" ); var { NavigationHelper } = ChromeUtils.importESModule( @@ -33,17 +33,13 @@ var { NativeTitlebar } = ChromeUtils.importESModule( class BrowserApplication extends MozHTMLElement { constructor() { super(); - - this.mutationObserver = new MutationObserver( - this.observeMutations.bind(this) - ); - this.intersectionObserver = new IntersectionObserver( - this.observeToolbarIntersections.bind(this) - ); } _done = false; + /** @type {typeof BrowserCustomizable.prototype} */ + customizable = null; + /** @type {typeof BrowserTabs.prototype} */ tabs = null; @@ -88,233 +84,17 @@ class BrowserApplication extends MozHTMLElement { return window.matchMedia("(prefers-reduced-motion: reduce)").matches; } - /** - * Obtains an area element for an area ID - * @param {string} area - * @returns {HTMLElement} - */ - getAreaElement(area) { - if (area == "menubar" || area == "toolbar" || area == "navbar") { - return /** @type {BrowserToolbar} */ ( - html("browser-toolbar", { slot: area }) - ); - } - - return this.querySelector(`[slot=${area}]`); - } - - /** - * @type {Set} - */ - _toolbars = new Set(); - - /** - * An index sorted list of BrowserToolbars - * @type {BrowserToolbar[]} - */ - get toolbars() { - return Array.from(this._toolbars).sort( - (a, b) => a.getBoundingClientRect().y - b.getBoundingClientRect().y - ); - } - - /** - * Locates a toolbar by its name - * @param {string} name - * @returns {BrowserToolbar | null} - */ - getToolbarByName(name) { - return this.toolbars.find((tb) => tb.name === name); - } - - _forcingNativeCSD = false; - - /** @type {MutationCallback} */ - observeMutations(mutations) { - for (const mut of mutations) { - for (const node of mut.addedNodes) { - if ( - node instanceof BrowserToolbar && - !this._toolbars.has(node) - ) { - this.intersectionObserver.observe(node); - this._toolbars.add(node); - } - } - - for (const node of mut.removedNodes) { - if ( - node instanceof BrowserToolbar && - this._toolbars.has(node) - ) { - this.intersectionObserver.unobserve(node); - this._toolbars.delete(node); - } - } - } - } - - /** @type {IntersectionObserverCallback} */ - observeToolbarIntersections(intersections) { - for (const intersection of intersections) { - if (intersection.target instanceof BrowserToolbar) { - if ( - intersection.intersectionRatio === 0 || - intersection.intersectionRatio === 1 - ) { - this.computeInitialToolbar(); - } - } - } - } - - /** - * Recomputes the "initial" toolbar - */ - computeInitialToolbar() { - if (this._forcingNativeCSD && !window.fullScreen) { - this._forcingNativeCSD = false; - NativeTitlebar.set(false, false); - } - - let oldToolbars = []; - - for (const toolbar of this.toolbars) { - if (toolbar.hasAttribute("initial")) { - oldToolbars.push(toolbar); - } - } - - let foundNewInitial = false; - - for (const toolbar of this.toolbars) { - const bounds = toolbar.getBoundingClientRect(); - - toolbar.setAttribute( - "orientation", - bounds.width > bounds.height ? "horizontal" : "vertical" - ); - - // Skip toolbars that are hidden - if (bounds.width === 0 || bounds.height === 0) { - continue; - } - - // Once we have found a suitable toolbar to - // make initial, return early, we're done here - toolbar.toggleAttribute("initial", true); - oldToolbars.forEach((t) => t.removeAttribute("initial")); - gDot.style.setProperty( - "--browser-csd-height", - toolbar.getBoundingClientRect().height + "px" - ); - gDot.style.setProperty( - "--browser-csd-width", - gDot.shadowRoot - .querySelector("browser-window-controls") - .getBoundingClientRect().width + "px" - ); - - foundNewInitial = true; - return; - } - - // If we weren't able to find a single toolbar to make initial - // We will need to show the window controls over the top of everything - // - // If we're in fullscreen mode, we can skip this as we're expecting chrome - // to be hidden. - if (!foundNewInitial && !window.fullScreen) { - this._forcingNativeCSD = true; - NativeTitlebar.set(true, false); - } - } - - /** - * Fetches a slot from the Shadow DOM by name - * @param {string} name - */ - getSlot(name) { - return this.shadowRoot.querySelector(`slot[name=${name}]`); - } - - /** - * - * @param {string} name - * @param {number} [index] - The index to move the slot to, use undefined to move to end - */ - moveSlotTo(name, index) { - const slot = this.getSlot(name); - - if (typeof index == "undefined") { - this.shadowRoot.append(slot); - this.computeInitialToolbar(); - return; - } - - if (index <= 0) { - this.shadowRoot.prepend(slot); - this.computeInitialToolbar(); - return; - } - - const slots = Array.from(this.shadowRoot.childNodes).filter( - (n) => /** @type {Element} */ (n).tagName == "slot" && n !== slot - ); - - if (index >= slots.length) { - this.shadowRoot.append(slot); - this.computeInitialToolbar(); - return; - } - - const beforeSlot = slots[index]; - console.log("beforeSlot", beforeSlot); - - this.shadowRoot.insertBefore(slot, beforeSlot); - this.computeInitialToolbar(); - } - connectedCallback() { if (this.delayConnectedCallback()) return; this.attachShadow({ mode: "open" }); - const areas = ["menubar", "toolbar", "navbar", "web-contents"]; - for (const area of areas) { - if (!this.getSlot(area)) { - this.shadowRoot.appendChild( - html("slot", { name: area, part: area }) - ); - } - - const areaElement = this.getAreaElement(area); - - if (area == "web-contents") { - areaElement.hidden = false; - } - - this.append(areaElement); - } - - for (const node of this.childNodes) { - if (node instanceof BrowserToolbar && !this._toolbars.has(node)) { - this.intersectionObserver.observe(node); - this._toolbars.add(node); - } - } - this.shadowRoot.appendChild( html("link", { rel: "stylesheet", href: "chrome://dot/skin/browser.css" }) ); - - this.mutationObserver.observe(this, { - childList: true, - subtree: true - }); } /** @@ -325,12 +105,11 @@ class BrowserApplication extends MozHTMLElement { throw new Error("Browser cannot be initialized twice!"); } + gDot.customizable = new BrowserCustomizable(this); gDot.tabs = new BrowserTabs(window); gDot.search = new BrowserSearch(window); gDot.shortcuts = new BrowserShortcuts(window); - DotCustomizableUI.init(window); - gDotRoutines.init(); // Listens for changes to the reduced motion preference diff --git a/base/content/browser.xhtml b/base/content/browser.xhtml index 16a63acc1d..feb05391a7 100644 --- a/base/content/browser.xhtml +++ b/base/content/browser.xhtml @@ -92,9 +92,7 @@ - +
diff --git a/build/scripts/gen_js_module_types.js b/build/scripts/gen_js_module_types.js index 52ffdb56da..d67b8fbe5f 100644 --- a/build/scripts/gen_js_module_types.js +++ b/build/scripts/gen_js_module_types.js @@ -85,19 +85,21 @@ async function generateMozModules() { function declareModule(moduleURI, originURI, exportName) { const moduleType = toPascalCase(exportName); + const match = dotModules[moduleType]; + // Check if we have a dot module type for this module - if (dotModules[moduleType]) { + if (match) { customImports.add(moduleType); } else { imports.add(toPascalCase(exportName)); } return `declare module ${JSON.stringify(moduleURI)} {${ - dotModules[moduleType] ? `\n import * as M from "./${dotModules[moduleType]}";` : `` + match ? `\n import * as M from "./${match}";` : `` } export const ${toPascalCase(exportName)}: ${ - dotModules[moduleType] ? `typeof M.` : `` - }${toPascalCase(exportName)}; + match ? `typeof M.` : `` + }${toPascalCase(exportName)}; }\n\n`; } @@ -115,12 +117,17 @@ async function generateMozModules() { if (isSymlink) { origin = readlinkSync(module); } else { - const match = findOriginByName(module, [parse(module).ext.substring(1)]); + const match = findOriginByName(module, [ + parse(module).ext.substring(1) + ]); if (match) { origin = match; } else { - console.warn("WARN: Could not find origin for", basename(module)); + console.warn( + "WARN: Could not find origin for", + basename(module) + ); } } @@ -134,7 +141,9 @@ async function generateMozModules() { relativeURI = ".." + relativeURI.split(rootDir)[1]; } - const strippedModuleURI = module.split(resolve(binDir, "modules"))[1].substring(1); + const strippedModuleURI = module + .split(resolve(binDir, "modules"))[1] + .substring(1); [ `resource:///modules/${strippedModuleURI}`, @@ -195,7 +204,10 @@ export interface AllMozResourceBindings { ${Array.from(resourceBindings.keys()) .sort(alphabeticalSort) .map( - (i) => `${JSON.stringify(i)}: ${JSON.stringify(toPascalCase(resourceBindings.get(i)))}` + (i) => + `${JSON.stringify(i)}: ${JSON.stringify( + toPascalCase(resourceBindings.get(i)) + )}` ) .join(";\n ")} diff --git a/components/csd/content/browser-window-controls.css b/components/csd/content/browser-window-controls.css index 6ce237ab15..15cf4ec456 100644 --- a/components/csd/content/browser-window-controls.css +++ b/components/csd/content/browser-window-controls.css @@ -21,8 +21,6 @@ browser-window-controls[hidden] { } :host(browser-application)::part(csd) { - position: fixed; - right: 0; height: var(--browser-csd-height); max-height: var(--browser-csd-height); transition: 0.3s height cubic-bezier(0.19, 1, 0.22, 1), 0.3s max-height cubic-bezier(0.19, 1, 0.22, 1); diff --git a/components/customizableui/BrowserCustomizable.sys.mjs b/components/customizableui/BrowserCustomizable.sys.mjs new file mode 100644 index 0000000000..167488fcdb --- /dev/null +++ b/components/customizableui/BrowserCustomizable.sys.mjs @@ -0,0 +1,148 @@ +/* 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/. */ + +const { BrowserCustomizableInternal } = ChromeUtils.importESModule( + "resource://gre/modules/BrowserCustomizableInternal.sys.mjs" +); + +const { BrowserCustomizableShared: Shared } = ChromeUtils.importESModule( + "resource://gre/modules/BrowserCustomizableShared.sys.mjs" +); + +/** + * @param {Element} renderRoot + */ +export function BrowserCustomizable(renderRoot) { + this.init(renderRoot); +} + +BrowserCustomizable.prototype = { + /** @type {Window} */ + win: null, + + /** @type {typeof BrowserCustomizableInternal.prototype} */ + internal: null, + + /** + * The current stored customizable state + */ + state: {}, + + /** + * The element to render the customizable interface to + * @type {Element} + */ + renderRoot: null, + + /** @type {Element} */ + _root: null, + + /** + * The main root parent of all modules + */ + get root() { + return this._root; + }, + + /** + * Refetches and validates the customizable state + */ + async _updateState() { + const newState = await this.internal.parseConfig(); + + if (!newState) { + throw new Error("Failed to parse customizable state."); + } + + this.state = newState; + }, + + /** + * Repaints the whole browser interface + */ + async _paint() { + const templates = this.state.templates || {}; + + Shared.logger.log( + `Registering ${Object.keys(templates).length} templates...` + ); + try { + this.internal.registerNamedTemplates(templates); + } catch (e) { + throw new Error("Failure registering template components:\n" + e); + } + + Shared.logger.log("Registering root component..."); + try { + if (this.state.root[0] !== "root") { + throw new Error( + `Property 'root' must be a 'root' type component.` + ); + } + + const rootElement = this.internal.createComponentFromDefinition( + this.state.root, + { allowInternal: true } + ); + + this.renderRoot.shadowRoot.replaceChildren(); + + this._root = rootElement; + + this.renderRoot.shadowRoot.append( + ...this.internal.customizableStylesheets, + this._root + ); + } catch (e) { + throw new Error("Failure registering root component:\n" + e); + } + }, + + /** + * Updates the entire customizable interface + */ + async _update(boot = false) { + try { + await this._updateState(); + await this._paint(); + } catch (e) { + Shared.logger.error("Failure reading customizable state:", e); + + if (boot) { + await this.internal.resetConfig(); + await this._update(); + } + } + }, + + /** + * Initialises any observers needed for BrowserCustomizable + */ + _initObservers() { + Services.prefs.addObserver( + Shared.customizableStatePref, + (() => this._update()).bind(this) + ); + }, + + /** + * Initialises the BrowserCustomizable class + * @param {Element} renderRoot + */ + async init(renderRoot) { + if (this.renderRoot) + throw new Error( + "BrowserCustomizable cannot be initialised more than once!" + ); + this.renderRoot = renderRoot; + this.win = this.renderRoot.ownerGlobal; + + this.internal = new BrowserCustomizableInternal(this.win); + this.internal.ensureCustomizableComponents(); + + this._initObservers(); + + await this._update(true); + } +}; diff --git a/components/customizableui/BrowserCustomizableInternal.sys.mjs b/components/customizableui/BrowserCustomizableInternal.sys.mjs new file mode 100644 index 0000000000..a480a21b2c --- /dev/null +++ b/components/customizableui/BrowserCustomizableInternal.sys.mjs @@ -0,0 +1,389 @@ +/* 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/. */ + +const { BrowserCustomizableShared: Shared } = ChromeUtils.importESModule( + "resource://gre/modules/BrowserCustomizableShared.sys.mjs" +); + +const { JsonSchema } = ChromeUtils.importESModule( + "resource://gre/modules/JsonSchema.sys.mjs" +); + +/** + * @typedef {[string, Record, CustomizableComponentDefinition[] | Record]} CustomizableComponentDefinition + */ + +/** + * @param {Window} win + */ +export function BrowserCustomizableInternal(win) { + this.win = win; +} + +BrowserCustomizableInternal.prototype = { + /** + * The current customizable state + */ + state: null, + + /** + * The customizable state JSON schema object + */ + stateSchema: null, + + /** + * A map of reusable templates + * @type {Map} + */ + templates: new Map(), + + /** + * The base schema for validating component attributes + */ + attributesSchema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: {}, + $defs: { + length: { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + enum: ["fill", "hug"] + } + ] + }, + color: { + type: "string" + }, + mode: { + enum: ["icons", "text", "icons_text"] + }, + orientation: { + enum: ["horizontal", "vertical"] + } + } + }, + + /** + * Fetches the customizable state schema + */ + async ensureStateSchema() { + if (this.stateSchema) return; + + this.stateSchema = await fetch(Shared.customizableStateSchemaURI).then( + (r) => r.json() + ); + }, + + /** + * Validates some data using a JSON Schema + * @param {object} schema + * @param {any} data + */ + _validate(schema, data) { + const validator = new JsonSchema.Validator(schema); + + const { valid, errors } = validator.validate(data); + + if (valid) { + return data; + } else { + throw new Error( + "Failed to validate data using schema:\n" + + errors + .map((e) => ` ${e.keywordLocation}: ${e.error}`) + .join("\n") + ); + } + }, + + /** + * Parses the current customizable state preference + * @returns {Promise} + */ + async parseConfig() { + const serialized = Services.prefs.getStringPref( + Shared.customizableStatePref, + "{}" + ); + + let data = {}; + + try { + data = JSON.parse(serialized); + } catch (e) { + throw e; + } + + return this.ensureStateSchema().then((_) => { + return this._validate(this.stateSchema, data); + }); + }, + + /** + * Resets the customizable state preference to default + */ + async resetConfig() { + Shared.logger.warn("Resetting customizable state to default!"); + + Services.prefs.setStringPref(Shared.customizableStatePref, "{}"); + }, + + /** + * Stylesheets for the customizable root + */ + get customizableStylesheets() { + return ["chrome://dot/skin/browser.css"].map((sheet) => { + const stylesheet = this.win.document.createElement("link"); + stylesheet.setAttribute("rel", "stylesheet"); + stylesheet.setAttribute("href", sheet); + + return stylesheet; + }); + }, + + /** + * Connects attributes up to a customizable component + * @param {BrowserCustomizableElement} element + * @param {CustomizableComponentDefinition[1]} attributes + */ + connectComponentWith(element, attributes) { + const validated = this._validate( + { + ...this.attributesSchema, + ...(element.attributesSchema || {}), + additionalProperties: !element.attributesSchema + }, + attributes + ); + + for (const [key, value] of Object.entries(validated)) { + element.setAttribute(key, (value ?? "").toString()); + } + + return element; + }, + + /** + * Gets the toolbar button for a type + * @param {string} type + */ + getToolbarButtonByType(type) { + const doc = this.win.document; + + switch (type) { + case "back-button": + case "forward-button": + case "reload-button": + return doc.createElement("button", { is: type }); + default: + return doc.createElement("button", { is: "custom-button" }); + } + }, + + /** + * Obtains a component using its type + * @param {string} type + * @param {object} [attributes] + * @param {object} [options] + * @param {boolean} [options.allowInternal] + * @returns {BrowserCustomizableElement} + */ + getComponentByType(type, attributes, options) { + const doc = this.win.document; + + let element; + + if (type.charAt(0) == "@") { + element = this.templates.get(type.substring(1)).cloneNode(true); + } + + switch (type) { + case "root": + if (options.allowInternal) { + element = doc.createElement("customizable-root"); + + break; + } + case "toolbar": + element = doc.createElement("browser-toolbar"); + break; + case "toolbar-button": + element = this.getToolbarButtonByType(attributes.is); + break; + case "spring": + element = doc.createElement("browser-spring"); + break; + case "tab-strip": + element = doc.createElement("browser-tabs"); + break; + case "web-contents": + element = doc.createElement("customizable-web-contents"); + break; + case "stack": + case "shelf": + element = doc.createElement("div"); + element.classList.add(`customizable-${type}`); + break; + case "": + element = doc.createDocumentFragment(); + break; + default: + if (!element) { + throw new Error(`No component with type '${type}'.`); + } + } + + return /** @type {BrowserCustomizableElement} */ (element); + }, + + /** + * Appends children to a component + * @param {BrowserCustomizableElement} parentElement + * @param {string} slot + * @param {CustomizableComponentDefinition[2]} children + */ + appendChildrenTo(parentElement, children, slot = "content") { + if (!parentElement) return; + + if (Array.isArray(children)) { + for (let i = 0; i < children.length; i++) { + const child = children[i]; + + const childComponent = + this.createComponentFromDefinition(child); + + ( + parentElement.appendComponent || parentElement.appendChild + ).bind(parentElement)(childComponent); + } + } else { + for (const [slot, slottedChildren] of Object.entries(children)) { + this.appendChildrenTo(parentElement, slottedChildren, slot); + } + } + }, + + /** + * Creates a new customizable component + * @param {CustomizableComponentDefinition[0]} type + * @param {CustomizableComponentDefinition[1]} [attributes] + * @param {CustomizableComponentDefinition[2]} [children] + * @param {object} [creationOptions] + * @param {boolean} [creationOptions.allowInternal] + */ + createComponent(type, attributes, children, creationOptions) { + if (!attributes) attributes = {}; + if (!children) children = []; + + try { + const baseElement = this.getComponentByType( + type, + attributes, + creationOptions + ); + + if (baseElement) { + const component = this.connectComponentWith( + baseElement, + attributes + ); + this.appendChildrenTo(component, children); + + return component; + } else { + return null; + } + } catch (e) { + throw new Error(`Failed to create component '${type}':\n` + e); + } + }, + + /** + * Creates a new customizable component from a component definition + * @param {CustomizableComponentDefinition} definition + * @param {Parameters[3]} [options] + */ + createComponentFromDefinition(definition, options) { + return this.createComponent( + definition[0], + definition[1] || {}, + definition[2] || [], + options + ); + }, + + /** + * Registers a new reusable template + * @param {string} name + * @param {CustomizableComponentDefinition} template + */ + registerTemplate(name, template) { + if (this.templates.has(name)) { + throw new Error(`Template with name '${name}' already exists!`); + } + + const component = this.createComponentFromDefinition(template); + + this.templates.set(name, component); + }, + + /** + * Registers templates using a KV map + * @param {Record} templates + */ + registerNamedTemplates(templates) { + this.templates.clear(); + + for (const [name, template] of Object.entries(templates)) { + this.registerTemplate(name, template); + } + }, + + /** + * Creates a slot element with a name + * @param {string} name + */ + createSlot(name) { + const slot = this.win.document.createElement("slot"); + slot.setAttribute("name", name); + + return slot; + }, + + /** + * Loads a customizable component into the window + * @param {string} name + */ + _loadCustomizableComponent(name) { + if (this.win.customElements.get(name)) return; + + Services.scriptloader.loadSubScript( + `chrome://dot/content/customizableui/components/${name}.js`, + this.win + ); + }, + + /** + * Ensures the customizable components are loaded + */ + ensureCustomizableComponents() { + ["customizable-root", "customizable-web-contents"].forEach( + (component) => { + if (this.win.customElements.get(component)) return; + + this.win.customElements.setElementCreationCallback( + component, + () => { + this._loadCustomizableComponent(component); + } + ); + } + ); + } +}; diff --git a/components/customizableui/BrowserCustomizableShared.sys.mjs b/components/customizableui/BrowserCustomizableShared.sys.mjs new file mode 100644 index 0000000000..1fad8e1d5f --- /dev/null +++ b/components/customizableui/BrowserCustomizableShared.sys.mjs @@ -0,0 +1,52 @@ +/* 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/. */ + +const { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" +); + +const kLogger = new ConsoleAPI({ + maxLogLevel: "warn", + maxLogLevelPref: "dot.customizable.loglevel", + prefix: "BrowserCustomizable.sys.mjs" +}); + +export const BrowserCustomizableShared = { + /** + * WARNING! Do not change this value without creating + * the necessary migrations and upgrades to the user's state! + */ + customizableVersion: 1, + + /** + * The customizable state preference ID + */ + customizableStatePref: "dot.customizable.state", + + /** + * The customizable state schema URI + */ + customizableStateSchemaURI: + "chrome://dot/content/customizableui/schemas/customizable_state.schema.json", + + customizableComponentKeyRegex: /^@[a-zA-Z0-9]+([_-]?[a-zA-Z0-9])*$/, + + /** + * The global customizable logger object + * @type {Console} + */ + get logger() { + return kLogger; + }, + + /** + * Asserts whether an object is really an object + * @param {any} obj + */ + assertObject(obj) { + if (typeof obj !== "object" || Array.isArray(obj) || obj == null) { + throw new Error("Illegal object supplied"); + } + } +}; diff --git a/components/customizableui/DotCustomizableUI.sys.mjs b/components/customizableui/DotCustomizableUI.sys.mjs deleted file mode 100644 index 36baf63771..0000000000 --- a/components/customizableui/DotCustomizableUI.sys.mjs +++ /dev/null @@ -1,185 +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/. */ - -const kCustomizableAreasPrefId = "dot.customizable.area."; - -class _CustomizableAreaElement extends Element { - /** @type {CustomizableController} */ - customizable = null; -} - -class CustomizableController { - /** - * Internal data for this area - * @type {object} - */ - _internal = {}; - - /** - * The name of this customizable area - */ - get name() { - return this.areaEl.getAttribute("customizablename"); - } - - /** - * The display mode for this area - */ - get mode() { - const mode = this.areaEl.getAttribute("mode"); - - return mode == "icons" - ? "icons" - : mode == "text" - ? "text" - : "icons_text"; - } - - set mode(newMode) { - this.areaEl.setAttribute("mode", newMode); - } - - /** - * @param {Element} areaEl - */ - constructor(areaEl) { - this.areaEl = areaEl; - - this.update(); - - if (!this.areaEl.hasAttribute("mode")) { - this.mode = this._internal.mode || "icons"; - } - } - - update() { - const json = Services.prefs.getStringPref( - `${kCustomizableAreasPrefId}${this.name}`, - "{}" - ); - - let parsed = {}; - - try { - parsed = { ...(JSON.parse(json) || {}) }; - } catch (e) { - console.error( - `Failed to parse JSON data for area '${this.name}'.`, - e - ); - return; - } - - this._internal = parsed; - } - - /** - * Adds a new widget to the customizable area using a widget's ID - * @param {string} widgetId - */ - addWidget(widgetId) { - console.log("addWidget", widgetId); - } -} - -export const DotCustomizableUI = { - CustomizableAreaElement: _CustomizableAreaElement, - - /** @type {Window} */ - win: null, - - /** - * Initialises the singleton - * @param {Window} win - */ - init(win) { - console.time("DotCustomizableUI: init"); - - this.win = win; - - Services.prefs.addObserver(kCustomizableAreasPrefId, this.observePrefs); - - console.timeEnd("DotCustomizableUI: init"); - }, - - observePrefs(subject, topic, data) { - if (data.startsWith(kCustomizableAreasPrefId)) { - const areaId = data.split(kCustomizableAreasPrefId)[1]; - - if (this.areas.has(areaId)) { - const area = this.areas.get(areaId); - - area.update(); - } else { - console.warn(`Area for preference '${data}' does not exist!`); - } - } - }, - - /** - * Updates the mode attribute for an area's descendants - * @param {Element} areaEl - */ - _setModeForDescendants(areaEl) { - // const areaController = this.areas.get( - // areaEl.getAttribute("customizablename") - // ); - // const modeDependentEls = [ - // ...Array.from(areaEl.querySelectorAll(".toolbar-button")) - // ]; - // for (const dep of modeDependentEls) { - // dep.setAttribute("mode", areaController.mode); - // } - }, - - /** @type {MutationCallback} */ - observeAreaMutations(mutations) { - for (const mut of mutations) { - const area = /** @type {Element} */ (mut.target); - - if (mut.attributeName == "mode") { - this._setModeForDescendants(area); - } - } - }, - - /** - * A map of area IDs to their respective customizable area controller - * @type {Map} - */ - areas: new Map(), - - /** - * A map of area IDs to their mutation observers - * @type {Map} - */ - areaMutationObservers: new Map(), - - /** - * Initialises an Element as a customizable area - * @param {Element} area - * @param {string} name - * @param {object} [options] - * @param {boolean} [options.many] - Marks this area as one of many areas with the same name - * @param {boolean} [options.showKeybindings] - Determines whether to show keybindings on tooltips - */ - initCustomizableArea(area, name, options) { - area.classList.add("customizable-area"); - - area.setAttribute("customizablename", name); - area.toggleAttribute("customizablemany", options && !!options.many); - - area.attachShadow({ mode: "open" }); - - if ( - options && - "showKeybindings" in options && - options.showKeybindings == false - ) { - area.toggleAttribute("customizablenokeybindings", true); - } - - this.areas.set(name, new CustomizableController(area)); - } -}; diff --git a/components/customizableui/components/customizable-element.js b/components/customizableui/components/customizable-element.js new file mode 100644 index 0000000000..74358269fe --- /dev/null +++ b/components/customizableui/components/customizable-element.js @@ -0,0 +1,47 @@ +/* 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/. */ + +class BrowserCustomizableElement extends MozHTMLElement { + /** + * The schema used to validate attributes + */ + attributesSchema = {}; + + constructor() { + super(); + } + + get customizable() { + return gDot.customizable; + } + + connectedCallback() { + if (this.delayConnectedCallback()) return; + + this.classList.add("customizable-element"); + + if (this.shadowRoot) { + this.shadowRoot.prepend( + html("link", { + rel: "stylesheet", + href: "chrome://dot/skin/browser.css" + }) + ); + } + } + + /** + * Appends a component to the customizable element + * @param {Node} node + */ + appendComponent(node) { + this.appendChild(node); + } + + disconnectedCallback() { + if (this.delayConnectedCallback()) return; + } +} + +customElements.define("customizable-element", BrowserCustomizableElement); diff --git a/components/customizableui/components/customizable-root.js b/components/customizableui/components/customizable-root.js new file mode 100644 index 0000000000..5368bc2926 --- /dev/null +++ b/components/customizableui/components/customizable-root.js @@ -0,0 +1,17 @@ +/* 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/. */ + +class BrowserCustomizableRoot extends BrowserCustomizableElement { + constructor() { + super(); + } + + connectedCallback() { + super.connectedCallback(); + + this.classList.add("customizable-stack"); + } +} + +customElements.define("customizable-root", BrowserCustomizableRoot); diff --git a/components/customizableui/components/customizable-web-contents.js b/components/customizableui/components/customizable-web-contents.js new file mode 100644 index 0000000000..93ebfa1ea4 --- /dev/null +++ b/components/customizableui/components/customizable-web-contents.js @@ -0,0 +1,25 @@ +/* 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/. */ + +class BrowserCustomizableWebContents extends BrowserCustomizableElement { + constructor() { + super(); + } + + connectedCallback() { + super.connectedCallback(); + + if (this.customizable.root.querySelector("slot[name=web-contents]")) { + this.remove(); + return; + } + + this.appendChild(html("slot", { name: "web-contents" })); + } +} + +customElements.define( + "customizable-web-contents", + BrowserCustomizableWebContents +); diff --git a/components/customizableui/components/customizable.css b/components/customizableui/components/customizable.css new file mode 100644 index 0000000000..6c43622639 --- /dev/null +++ b/components/customizableui/components/customizable.css @@ -0,0 +1,48 @@ +/* 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/. */ + +customizable-root { + display: flex; + width: 100%; + height: 100%; +} + +.customizable-stack, +.customizable-shelf { + display: flex; + gap: 4px; +} + +.customizable-stack { + flex-direction: column; +} + +.customizable-shelf { + flex-direction: row; +} + +.customizable-element { + width: var(--width); + height: var(--height); +} + +.customizable-element:not([width]):not([height]) { + flex: 1; +} + +.customizable-element[width="fill"] { + --width: 100%; +} + +.customizable-element[width="hug"] { + --width: max-content; +} + +.customizable-element[height="fill"] { + --height: 100%; +} + +.customizable-element[height="hug"] { + --height: max-content; +} \ No newline at end of file diff --git a/components/customizableui/content/browser-customizable-area.js b/components/customizableui/content/browser-customizable-area.js index b7a19ecdd2..57249fa019 100644 --- a/components/customizableui/content/browser-customizable-area.js +++ b/components/customizableui/content/browser-customizable-area.js @@ -8,8 +8,7 @@ class BrowserCustomizableArea extends MozHTMLElement { this.attachShadow({ mode: "open" }); - this.shadowRoot.appendChild(this.areaElements.slot); - this.shadowRoot.appendChild(this.areaElements.container); + this.shadowRoot.appendChild(this.customizableContainer); this.shadowRoot.append( html("link", { @@ -141,119 +140,16 @@ class BrowserCustomizableArea extends MozHTMLElement { this.toggleAttribute("accent", toggled); } - /** - * The anatomy of the customizable area - * - * @typedef {Object} CustomizableAreaElements - * @property {HTMLDivElement} container - The customizable area's container - * @property {HTMLSlotElement} slot - The customizable area's non-customizable slot - * - * @returns {CustomizableAreaElements} - */ - get areaElements() { - return { - container: /** @type {HTMLDivElement} */ ( - this.shadowRoot.querySelector(".customizable-container") || - html("div", { - class: "customizable-container", - part: "customizable" - }) - ), - slot: /** @type {HTMLSlotElement} */ ( - this.shadowRoot.querySelector("slot") || - html("slot", { part: "content" }) - ) - }; - } - - /** - * Adds a widget to the customizable area - * @param {string} widgetId - * @param {object} [options] - * @param {number} [options.position] - The position to place the widget, 0 for beginning, null for end - * @param {object} [options.widgetArgs] - Arguments to pass to the widget - */ - addWidget(widgetId, options) { - if (!options) options = {}; - - let widget = null; - - switch (widgetId) { - case "custom-button": - case "back-button": - case "forward-button": - case "reload-button": - case "identity-button": - case "add-tab-button": - case "close-tab-button": - widget = /** @type {BrowserToolbarButton} */ ( - document.createElement("button", { is: widgetId }) - ); - break; - case "spring": - widget = document.createElement("browser-spring"); - widget.style.setProperty( - "--spring-grip", - options.widgetArgs.grip ?? "" - ); - - break; - case "tabs-list": - widget = /** @type {BrowserTabsElement} */ ( - document.createElement("browser-tabs") - ); - break; - case "addressbar": - widget = /** @type {BrowserAddressBar} */ ( - document.createElement("browser-addressbar") - ); - break; - case "tab-title": - widget = /** @type {BrowserTabLabel} */ ( - document.createElement("browser-tab-label") - ); - break; - case "tab-icon": - widget = /** @type {BrowserTabIcon} */ ( - document.createElement("browser-tab-icon") - ); - break; - } - - if (!widget) { - console.warn( - `Unknown widget with ID '${widgetId}' for area '${this.name}'.` - ); - return; - } - - for (const [key, value] of Object.entries(options.widgetArgs || {})) { - widget.setAttribute(`customizablearg-${key}`, value); - } - - const position = - options.position ?? this.areaElements.container.childNodes.length; - - this.areaElements.container.insertBefore( - widget, - this.areaElements.container.childNodes[position] + get customizableContainer() { + return ( + this.shadowRoot.querySelector(".customizable-container") || + html("div", { + class: "customizable-container", + part: "customizable" + }) ); } - /** - * Handles incoming preference updates - * @param {nsIPrefBranch} subject - * @param {string} topic - * @param {string} data - */ - _observePrefs(subject, topic, data) { - console.log(subject, topic, data); - if (data == this.prefId) { - console.log("observe prefs"); - this.render({ partial: true }); - } - } - /** * Connect this customizable area to a configuration * @param {object} options @@ -266,193 +162,12 @@ class BrowserCustomizableArea extends MozHTMLElement { this.layout = options.layout; this.showKeybindings = options.showKeybindings; - Services.prefs.addObserver(this.prefId, this._observePrefs.bind(this)); - - this.render(); - this.classList.add("customizable-area"); } - /** - * The raw stored data for this area - * @returns - */ - _getData() { - const data = Services.prefs.getStringPref(this.prefId, "{}"); - - let parsed = { - mode: "icons", - orientation: "horizontal", - widgets: [] - }; - - return { - ...parsed, - ...JSON.parse(data) - }; - } - - _createErrorBarrier() { - const error = document.createElement("div"); - error.classList.add("customizable-area-error"); - - const stylesheet = /** @type {HTMLLinkElement} */ ( - document.createElement("link") - ); - stylesheet.setAttribute("rel", "stylesheet"); - stylesheet.href = - "chrome://dot/content/widgets/browser-customizable-area-error.css"; - - error.attachShadow({ mode: "open" }); - error.shadowRoot.appendChild(stylesheet); - - const errorShadowContainer = document.createElement("div"); - errorShadowContainer.classList.add("customizable-area-error-container"); - - const errorMessage = document.createElement("span"); - errorMessage.textContent = "We're unable to display this area."; - - const errorButtonContainer = document.createElement("div"); - errorButtonContainer.classList.add( - "customizable-area-error-button-container" - ); - - const retryButton = document.createElement("button"); - retryButton.textContent = "Retry"; - - const resetButton = document.createElement("button"); - resetButton.textContent = "Reset to default"; - - retryButton.addEventListener("click", () => { - error.hidden = true; - - setTimeout(() => { - this.render(); - }, 100); - }); - - resetButton.addEventListener("click", () => { - console.log("todo: reset area"); - }); - - errorButtonContainer.appendChild(retryButton); - errorButtonContainer.appendChild(resetButton); - - errorShadowContainer.appendChild(errorMessage); - errorShadowContainer.appendChild(errorButtonContainer); - - error.shadowRoot.appendChild(errorShadowContainer); - - return error; - } - - /** - * Renders all registered widgets and gizmos for this area - * @param {object} [options] - * @param {boolean} [options.partial] - Determines whether to perform a partial render rather than a full render - */ - render(options) { - const { partial } = options || {}; - - // If we're not doing a partial render, clear out the container's children - if (true) { - this.areaElements.container.replaceChildren(""); - } - - const errorPanel = this.shadowRoot.querySelector( - ".customizable-area-error" - ); - if (errorPanel) { - errorPanel.remove(); - } - - let data = {}; - - try { - data = this._getData(); - } catch (e) { - console.error(`Failed to render area '${this.name}'!`, e); - - try { - const errorPanel = this.shadowRoot.querySelector( - ".customizable-area-error" - ); - if (errorPanel) { - errorPanel.remove(); - } - - this.areaElements.container.replaceChildren(""); - this.shadowRoot.appendChild(this._createErrorBarrier()); - } catch (e) { - console.error( - "CATASTROPHIC ERROR! Unable to render error element for failed error!", - e - ); - return; - } - - return; - } - - this.mode = data.mode; - this.orientation = - data.orientation == "vertical" ? "vertical" : "horizontal"; - this.accent = !!data.accent; - - for (let i = 0; i < data.widgets.length; i++) { - const widget = data.widgets[i]; - let widgetId = null; - let widgetArgs = {}; - - if (Array.isArray(widget)) { - widgetId = widget[0]; - widgetArgs = { ...(widget[1] || {}) }; - } else { - widgetId = widget.toString(); - } - - if (!widgetId || !widgetId.length) return; - - this.addWidget(widgetId, { position: i, widgetArgs }); - } - } - - /** - * Saves an attribute's value to the area config preference - * @param {string} name - */ - storeAttribute(name) { - const data = this._getData(); - - const attributeData = this[name] ?? this.getAttribute(name); - - data[name] = attributeData; - - try { - const serialized = JSON.stringify(data); - - Services.prefs.setStringPref(this.prefId, serialized); - } catch (e) { - console.error(`Failed to store value of attribute '${name}'!`, e); - return; - } - } - connectedCallback() { if (this.delayConnectedCallback()) return; } - - /** - * Fired when an attribute is changed on this area - * @param {string} name - * @param {string} oldValue - * @param {string} newValue - */ - attributeChangedCallback(name, oldValue, newValue) { - if (!this.isConnectedAndReady) return; - - this.storeAttribute(name); - } } customElements.define("browser-customizable-area", BrowserCustomizableArea); diff --git a/components/customizableui/jar.mn b/components/customizableui/jar.mn index 01d75f6be9..a293e9be23 100644 --- a/components/customizableui/jar.mn +++ b/components/customizableui/jar.mn @@ -6,7 +6,11 @@ dot.jar: % content dot %content/ contentaccessible=yes - content/widgets/browser-customizable-area.js (content/browser-customizable-area.js) - content/widgets/browser-customizable-area.css (content/browser-customizable-area.css) + content/widgets/browser-customizable-area.js (content/browser-customizable-area.js) + content/widgets/browser-customizable-area.css (content/browser-customizable-area.css) - content/widgets/browser-customizable-area-error.css (content/browser-customizable-area-error.css) + content/widgets/browser-customizable-area-error.css (content/browser-customizable-area-error.css) + + content/customizableui/components/ (components/*.*) + + content/customizableui/schemas/customizable_state.schema.json (schemas/customizable_state.schema.json) \ No newline at end of file diff --git a/components/customizableui/moz.build b/components/customizableui/moz.build index 7913e6c7ba..9baa91394d 100644 --- a/components/customizableui/moz.build +++ b/components/customizableui/moz.build @@ -7,5 +7,7 @@ JAR_MANIFESTS += ["jar.mn"] EXTRA_JS_MODULES += [ - "DotCustomizableUI.sys.mjs", + "BrowserCustomizable.sys.mjs", + "BrowserCustomizableInternal.sys.mjs", + "BrowserCustomizableShared.sys.mjs", ] \ No newline at end of file diff --git a/components/customizableui/schemas/customizable_state.schema.json b/components/customizableui/schemas/customizable_state.schema.json new file mode 100644 index 0000000000..9c312e1c2f --- /dev/null +++ b/components/customizableui/schemas/customizable_state.schema.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "chrome://dot/content/customizableui/schemas/customizable_state.schema.json", + "title": "Customizable UI State", + "type": "object", + "properties": { + "version": { + "type": "integer" + }, + "templates": { + "type": "object", + "patternProperties": { + "^.*$": { + "$ref": "#/$defs/component" + } + } + }, + "root": { + "$ref": "#/$defs/component" + } + }, + "required": [ + "version", + "root" + ], + "$defs": { + "component": { + "type": "array", + "minItems": 1, + "maxItems": 3, + "items": [ + { + "type": "string" + }, + { + "type": "object" + }, + { + "anyOf": [ + { + "$ref": "#/$defs/components-object" + }, + { + "$ref": "#/$defs/components-array" + } + ] + } + ], + "additionalItems": false + }, + "components-array": { + "type": "array", + "items": { + "$ref": "#/$defs/component" + } + }, + "components-object": { + "type": "object", + "patternProperties": { + "^.*$": { + "$ref": "#/$defs/components-array" + } + } + } + } +} \ No newline at end of file diff --git a/components/customizableui/test.json b/components/customizableui/test.json new file mode 100644 index 0000000000..29a1cd0889 --- /dev/null +++ b/components/customizableui/test.json @@ -0,0 +1,56 @@ + + +{ + "version": 1, + "templates": { + "tabs-list": ["shelf", {}, [ + ["tab-strip", { "width": "fill", "height": "fill" }, { + "tab": [["@tab"]] + }], + ["toolbar-button", { "is": "add-tab-button" }] + ]], + "addressbar": ["addressbar", { "background": "accent" }, { + "before": [ + ["toolbar-button", { "is": "shield-button" }], + ["toolbar-button", { "is": "identity-button" }] + ], + "content": [ + ["addressbar-input"] + ], + "after": [ + ["toolbar-button", { "is": "identity-button" }] + ] + }] + }, + "root": ["stack", { "width": "fill", "height": "fit" }, [ + ["stack", { "width": "fill", "height": "hug" }, [ + ["toolbar", { "name": "menubar", "mode": "text", "background": "accent" }, [ + ["toolbar-button", { "is": "file-menu-button" }], + ["toolbar-button", { "is": "edit-menu-button" }], + ["toolbar-button", { "is": "view-menu-button" }], + ["toolbar-button", { "is": "history-menu-button" }], + ["toolbar-button", { "is": "bookmarks-menu-button" }], + ["toolbar-button", { "is": "tools-menu-button" }], + ["toolbar-button", { "is": "help-menu-button" }] + ]], + ["toolbar", { "name": "toolbar", "mode": "icons", "background": "accent" }, [ + ["spring", { "width": 48 }], + ["@tabs-list"], + ["spring"], + ["toolbar-button", { "is": "list-tabs-button" }] + ]], + ["toolbar", { "name": "navbar", "mode": "icons" }, [ + ["toolbar-button", { "is": "back-button" }], + ["toolbar-button", { "is": "forward-button" }], + ["toolbar-button", { "is": "reload-button" }], + ["spring"], + ["@addressbar"], + ["spring"] + ]] + ]], + ["stack", { "width": "fill", "height": "fill" }, [ + ["sidebar", { "mode": "icons" }, []], + ["web-contents"] + ]] + ]] +} \ No newline at end of file diff --git a/components/customizableui/types.d.ts b/components/customizableui/types.d.ts new file mode 100644 index 0000000000..8102cc0160 --- /dev/null +++ b/components/customizableui/types.d.ts @@ -0,0 +1,298 @@ +/* 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/. */ + +export interface CustomizableConfig extends CustomizableAreaStack { + version: number; +} + +export type CustomizableAreaType = string; +export type CustomizableAreaOrientation = string; +export type CustomizableAreaMode = string; + +export type CustomizableAreas = Record< + CustomizableAreaWidgetReference, + CustomizableArea +>; + +export type CustomizableArea = CustomizableAreaStack | CustomizableAreaToolbar | CustomizableAreaSidebar | CustomizableAreaAddressbar | CustomizableAreaTab; + +export interface CustomizableAreaBase { + type: CustomizableAreaType; + with?: { + orientation?: CustomizableAreaOrientation; + layout?: CustomizableLayout; + components?: CustomizableAreas; + mode?: CustomizableAreaMode; + } +} + +export interface CustomizableAreaStack extends CustomizableAreaBase { + type: "stack"; +} + +export interface CustomizableAreaToolbar extends CustomizableAreaBase { + type: "toolbar"; +} + +export interface CustomizableAreaSidebar extends CustomizableAreaBase { + type: "sidebar"; + with?: { + type?: "drawer"; + } & CustomizableAreaBase["with"]; +} + +export interface CustomizableAreaAddressbar extends CustomizableAreaBase { + type: "addressbar"; +} + +export interface CustomizableAreaTab extends CustomizableAreaBase { + type: "tab"; + with?: { + hideActions?: boolean; + } & CustomizableAreaBase["with"]; +} + +export type CustomizableAreaWidgetReference = `@${string}` | string; + +export type CustomizableAreaWidgetOptions = Record; +export type CustomizableAreaWidget = + | CustomizableAreaWidgetReference + | [CustomizableAreaWidgetReference] + | [CustomizableAreaWidgetReference, CustomizableAreaWidgetOptions]; + +export type CustomizableLayout = CustomizableAreaWidget[]; + +// const config: CustomizableConfig = { +// version: 1, +// type: "stack", +// with: {} +// }; + +// const config: CustomizableConfig = { +// version: 1, +// type: "stack", +// with: { +// layout: ["@chrome", "@chrome", "@chrome"], +// orientation: "vertical", +// areas: { +// "@chrome": { +// type: "stack", +// with: { +// layout: ["@toolbars", "@web-contents"], +// orientation: "horizontal" +// }, +// }, + +// "@toolbars": { +// type: "stack", +// with: { +// layout: ["@menubar", "@toolbar", "@navbar"], +// orientation: "horizontal" +// } +// }, +// "@menubar": { +// type: "toolbar", +// with: { +// mode: "text", +// layout: [ +// [ +// "ToolbarButtton", +// { +// is: "main-menu-button", +// icon: "brand32", +// mode: "icons" +// } +// ], +// ["ToolbarButton", { is: "file-menu-button" }], +// ["ToolbarButton", { is: "edit-menu-button" }], +// ["ToolbarButton", { is: "view-menu-button" }], +// ["ToolbarButton", { is: "history-menu-button" }], +// ["ToolbarButton", { is: "bookmarks-menu-button" }], +// ["ToolbarButton", { is: "tools-menu-button" }], +// ["ToolbarButton", { is: "help-menu-button" }] +// ] +// } +// }, +// "@toolbar": { +// type: "toolbar", +// with: { +// mode: "icons", +// layout: [ +// ["FlexibleSpring", { width: 42 }], +// "@tab-strip", +// ["ToolbarButton", { is: "add-tab-button" }], +// ["FlexibleSpring"], +// ["ToolbarButton", { is: "list-tabs-button" }] +// ] +// } +// }, +// "@navbar": { +// type: "toolbar", +// with: { +// mode: "icons", +// layout: [ +// ["ToolbarButton", { is: "back-button" }], +// ["ToolbarButton", { is: "forward-button" }], +// ["ToolbarButton", { is: "reload-button" }], +// ["FlexibleSpring"], +// "@addressbar", +// ["FlexibleSpring"], +// ["ToolbarButton", { is: "downloads-button" }], +// ["ToolbarButton", { is: "profile-button" }], +// "@extensions-strip", +// ["ToolbarButton", { is: "main-menu-button" }] +// ] +// } +// }, +// "@personalbar": { +// type: "toolbar", +// with: { +// mode: "icons", +// layout: ["@bookmarks-strip"] +// } +// }, + +// "@tab-strip": { +// type: "stack", +// with: { +// mode: "icons", +// layout: [["TabStrip", { layout: ["@tab"] }]], +// orientation: "horizontal" +// } +// }, +// "@tab": { +// type: "tab", +// with: { +// mode: "icons", +// layout: [ +// ["TabIcon"], +// ["TabLabel"], +// ["ToolbarButton", { is: "close-tab-button" }] +// ] +// } +// }, + +// "@addressbar": { +// type: "stack", +// with: { +// mode: "icons", +// layout: [ +// ["ToolbarButton", { is: "shield-button" }], +// ["ToolbarButton", { is: "identity-button" }], +// ["AddressbarInput"], +// ["ExtensionsStrip", { type: "page_action" }], +// ["ToolbarButton", { is: "bookmark-page-button" }] +// ], +// orientation: "horizontal" +// } +// }, + +// "@extensions-strip": { +// type: "stack", +// with: { +// mode: "icons", +// layout: [ +// [ +// "ExtensionsStrip", +// { +// type: "browser_action", +// filter: "ext.badgeCount >= 1", +// overflow_rest: true +// } +// ] +// ], +// orientation: "horizontal" +// } +// }, + +// "@bookmarks-strip": { +// type: "stack", +// with: { +// mode: "icons", +// layout: [ +// [ +// "ToolbarButton", +// { +// is: "places-button", +// filter: "entry.type == 'bookmarks' && entry.id == 'f39flhbag96'" +// } +// ], +// ["PlacesStrip", { type: "bookmarks" }] +// ], +// orientation: "horizontal" +// } +// }, + +// "@web-contents": { +// type: "stack", +// with: { +// layout: [ +// "@web-contents-sidebar", +// ["WebContents", { flex: 1 }] +// ], +// orientation: "vertical" +// } +// }, + +// "@web-contents-sidebar": { +// type: "sidebar", +// with: { +// mode: "icons", +// type: "drawer", +// layout: [ +// [ +// "ToolbarButton", +// { +// is: "places-button", +// filter: "entry.type == 'bookmarks'" +// } +// ], +// [ +// "ToolbarButton", +// { +// is: "places-button", +// filter: "entry.type == 'history'" +// } +// ], +// ["ToolbarButton", { is: "downloads-button" }], +// ["Divider"], +// [ +// "ToolbarButton", +// { +// is: "places-button", +// filter: "entry.type == 'saved_site' && entry.url == 'https://app.element.io'" +// } +// ], +// [ +// "ToolbarButton", +// { +// is: "places-button", +// filter: "entry.type == 'saved_site' && entry.url == 'https://web.whatsapp.com'" +// } +// ], +// [ +// "ToolbarButton", +// { +// is: "places-button", +// filter: "entry.type == 'saved_site' && entry.url == 'https://discord.com/app'" +// } +// ], +// [ +// "ToolbarButton", +// { +// is: "places-button", +// filter: "entry.type == 'saved_site' && entry.url == 'https://en.wikipedia.org/wiki/Main_Page'" +// } +// ], +// ["FlexibleSpring"], +// ["ToolbarButton", { is: "profile-button" }], +// ["ToolbarButton", { is: "settings-button" }], +// ["ToolbarButton", { is: "collapse-button" }] +// ], +// orientation: "vertical" +// } +// } +// } +// } +// }; diff --git a/components/dev/content/dev-debug-panel.css b/components/dev/content/dev-debug-panel.css index 39ac14d30f..c9bf68b2dc 100644 --- a/components/dev/content/dev-debug-panel.css +++ b/components/dev/content/dev-debug-panel.css @@ -68,7 +68,8 @@ dev-debug-panel .dev-branding-lockup { gap: 0.75rem; } -dev-debug-panel .dev-active-theme-container { +dev-debug-panel .dev-active-theme-container, +dev-debug-panel .dev-customizable-ui-container { display: flex; flex-direction: column; gap: 0.5rem; diff --git a/components/dev/content/dev-debug-panel.js b/components/dev/content/dev-debug-panel.js index dcf18ecebe..6820f85a4c 100644 --- a/components/dev/content/dev-debug-panel.js +++ b/components/dev/content/dev-debug-panel.js @@ -2,16 +2,20 @@ * 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/. */ -var { AddonManager } = ChromeUtils.importESModule("resource://gre/modules/AddonManager.sys.mjs"); +var { AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); -var { AppConstants } = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs"); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); var { DotAppConstants } = ChromeUtils.importESModule( - "resource://gre/modules/DotAppConstants.sys.mjs" + "resource://gre/modules/DotAppConstants.sys.mjs" ); var { NativeTitlebar } = ChromeUtils.importESModule( - "resource:///modules/NativeTitlebar.sys.mjs" + "resource:///modules/NativeTitlebar.sys.mjs" ); /** @@ -21,14 +25,14 @@ var { NativeTitlebar } = ChromeUtils.importESModule( * @returns {string} */ function formatBytes(bytes, decimals = 2, k = 1024) { - if (!+bytes) return "0 Bytes"; + if (!+bytes) return "0 Bytes"; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ["Bytes", "kB", "MB", "GB", "TB"]; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ["Bytes", "kB", "MB", "GB", "TB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); + const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; } /** @@ -38,225 +42,297 @@ function formatBytes(bytes, decimals = 2, k = 1024) { * @returns */ function perDiff(val, max) { - return ((1.0 - (max - val) / max) * 100).toFixed(2); + return ((1.0 - (max - val) / max) * 100).toFixed(2); } class DeveloperDebugPanel extends MozHTMLElement { - constructor() { - super(); - } - - elements = { - app_info: html("span"), - proc_info: html("div"), - user_agent: html("span"), - - graph: /** @type {DeveloperDebugGraph} */ (html("dev-debug-graph")), - - active_theme: /** @type {HTMLSelectElement} */ ( - html("select", { class: "dev-active-theme" }) - ), - - native_titlebar: /** @type {HTMLInputElement} */ ( - html("input", { type: "checkbox", id: "dev-native-theme-enabled" }) - ) - }; - - resourceUsageInt = null; - - onAddonEnabled(addon) { - if (!addon || addon.type != "theme") return; - - this.renderThemes().then((_) => { - this.elements.active_theme.value = addon.id; - }); - } - - async calculateResourceUsage() { - const procInfo = await ChromeUtils.requestProcInfo(); - - /** @type {any[]} */ - let data = [ - html("span", {}, `PID: ${procInfo.pid}`), - html("span", {}, `Memory: ${formatBytes(procInfo.memory)}`), - html("span", {}, `Processes: ${procInfo.children.length}`), - html("span", {}, `Threads: ${procInfo.threads.length}`) - ]; - - if (procInfo.memory >= Math.max(...(this.elements.graph.points.default || []))) { - this.elements.graph.max = - Math.ceil((procInfo.memory + 50000000) /* 50mb */ / 50000000) * 50000000; - } - this.elements.graph.addPoint(procInfo.memory); - - if (procInfo.children.length) { - data.push(html("br")); - - for (const child of procInfo.children.sort((a, b) => b.memory - a.memory)) { - this.elements.graph.addPoint(child.memory, child.pid.toString()); - - const groupColour = this.elements.graph.pointGroupColours[child.pid.toString()]; - - const groupDot = /** @type {HTMLSpanElement} */ ( - html("div", { class: "dev-graph-group-dot" }) - ); - groupDot.style.setProperty("--color", groupColour); - - data.push( - html( - "div", - { class: "dev-graph-group" }, - groupDot, - html( - "span", - {}, - `${child.type} (id=${child.childID} pid=${child.pid}, mem=${formatBytes( - child.memory - )}, thds=${child.threads.length}, wins=${child.windows.length})` - ) - ) - ); - } - } - - this.elements.proc_info.textContent = ""; - this.elements.proc_info.append(...data); - - // Lazy way of updating this value - this.elements.native_titlebar.checked = NativeTitlebar.enabled; - } - - async renderThemes() { - const allThemes = await AddonManager.getAddonsByTypes(["theme"]); - - // Clear children - this.elements.active_theme.replaceChildren(); - - for (const theme of allThemes.sort((a, b) => a.id.localeCompare(b.id))) { - const option = html("option", { value: theme.id }, `${theme.name} (${theme.id})`); - - this.elements.active_theme.appendChild(option); - } - } - - async init() { - const activeTheme = (await AddonManager.getAddonsByTypes(["theme"])).find( - (ext) => ext.isActive - ); - - AddonManager.addAddonListener({ - onEnabled: this.onAddonEnabled.bind(this) - }); - - this.resourceUsageInt = setInterval(() => { - this.calculateResourceUsage(); - }, 1000); - - this.onAddonEnabled(activeTheme); - this.calculateResourceUsage(); - - const dotVersion = document.createElement("strong"); - dotVersion.textContent = `Dot Browser v${DotAppConstants.DOT_APP_VERSION} (${AppConstants.MOZ_BUILDID})`; - - this.elements.app_info.append( - html( - "div", - { class: "dev-branding-lockup" }, - html("img", { src: "chrome://branding/content/icon32.png" }), - html("img", { src: "chrome://branding/content/about-wordmark.svg", height: "48" }) - ), - html("br"), - dotVersion, - html("br"), - `Firefox v${AppConstants.MOZ_APP_VERSION}` - ); - - this.elements.user_agent.textContent = `user_agent = ${Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler) - .userAgent - }`; - - this.elements.active_theme.addEventListener("change", async (event) => { - const { value } = /** @type {HTMLSelectElement} */ (event.target); - - const addon = await AddonManager.getAddonByID(value); - - if (addon) { - addon.enable(); - } - }); - - this.elements.native_titlebar.addEventListener("change", async (event) => { - const { checked } = /** @type {HTMLInputElement} */ (event.target); - - NativeTitlebar.set(checked, true); - }) - } - - insertStylesheet() { - const sheet = document.createProcessingInstruction( - "xml-stylesheet", - `href="chrome://dot/content/widgets/dev-debug-panel.css" type="text/css"` - ); - - document.insertBefore(sheet, document.documentElement); - } - - connectedCallback() { - if (this.delayConnectedCallback()) return; - this.classList.add("dev-panel"); - - this.appendChild(this.elements.app_info); - this.appendChild(this.elements.proc_info); - this.appendChild(this.elements.user_agent); - this.appendChild( - html( - "div", - { class: "dev-active-theme-container" }, - html("label", {}, "Active Theme:"), - this.elements.active_theme - ) - ); - - this.appendChild( - html( - "div", - { class: "dev-native-titlebar-container" }, - html("label", { for: "dev-native-theme-enabled" }, "Native Titlebar:"), - this.elements.native_titlebar - ) - ); - this.elements.native_titlebar.checked = NativeTitlebar.enabled; - - this.appendChild(this.elements.graph); - - this.insertStylesheet(); - - if (window.location.href == "chrome://dot/content/dev-debug-popout.xhtml") { - new ResizeObserver(() => { - window.document.documentElement.style.setProperty( - "--height", - this.getBoundingClientRect().height + "px" - ); - }).observe(this); - - const devtoolsButton = html("button", {}, "Open Browser Toolbox"); - devtoolsButton.addEventListener("click", () => { - var { BrowserToolboxLauncher } = ChromeUtils.importESModule( - "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs" - ); - - BrowserToolboxLauncher.init(); - }); - - this.appendChild(devtoolsButton); - } - } - - disconnectedCallback() { - if (this.delayConnectedCallback()) return; - - clearInterval(this.resourceUsageInt); - } + constructor() { + super(); + } + + elements = { + app_info: html("span"), + proc_info: html("div"), + user_agent: html("span"), + + graph: /** @type {DeveloperDebugGraph} */ (html("dev-debug-graph")), + + active_theme: /** @type {HTMLSelectElement} */ ( + html("select", { class: "dev-active-theme" }) + ), + + native_titlebar: /** @type {HTMLInputElement} */ ( + html("input", { type: "checkbox", id: "dev-native-theme-enabled" }) + ), + + customizableui_data: /** @type {HTMLTextAreaElement} */ ( + html("textarea", { readonly: "", rows: 5 }) + ) + }; + + resourceUsageInt = null; + + onAddonEnabled(addon) { + if (!addon || addon.type != "theme") return; + + this.renderThemes().then((_) => { + this.elements.active_theme.value = addon.id; + }); + } + + async calculateResourceUsage() { + const procInfo = await ChromeUtils.requestProcInfo(); + + /** @type {any[]} */ + let data = [ + html("span", {}, `PID: ${procInfo.pid}`), + html("span", {}, `Memory: ${formatBytes(procInfo.memory)}`), + html("span", {}, `Processes: ${procInfo.children.length}`), + html("span", {}, `Threads: ${procInfo.threads.length}`) + ]; + + if ( + procInfo.memory >= + Math.max(...(this.elements.graph.points.default || [])) + ) { + this.elements.graph.max = + Math.ceil((procInfo.memory + 50000000) /* 50mb */ / 50000000) * + 50000000; + } + this.elements.graph.addPoint(procInfo.memory); + + if (procInfo.children.length) { + data.push(html("br")); + + for (const child of procInfo.children.sort( + (a, b) => b.memory - a.memory + )) { + this.elements.graph.addPoint( + child.memory, + child.pid.toString() + ); + + const groupColour = + this.elements.graph.pointGroupColours[child.pid.toString()]; + + const groupDot = /** @type {HTMLSpanElement} */ ( + html("div", { class: "dev-graph-group-dot" }) + ); + groupDot.style.setProperty("--color", groupColour); + + data.push( + html( + "div", + { class: "dev-graph-group" }, + groupDot, + html( + "span", + {}, + `${child.type} (id=${child.childID} pid=${ + child.pid + }, mem=${formatBytes(child.memory)}, thds=${ + child.threads.length + }, wins=${child.windows.length})` + ) + ) + ); + } + } + + this.elements.proc_info.textContent = ""; + this.elements.proc_info.append(...data); + + // Lazy way of updating this value + this.elements.native_titlebar.checked = NativeTitlebar.enabled; + } + + async renderThemes() { + const allThemes = await AddonManager.getAddonsByTypes(["theme"]); + + // Clear children + this.elements.active_theme.replaceChildren(); + + for (const theme of allThemes.sort((a, b) => + a.id.localeCompare(b.id) + )) { + const option = html( + "option", + { value: theme.id }, + `${theme.name} (${theme.id})` + ); + + this.elements.active_theme.appendChild(option); + } + } + + // https://stackoverflow.com/a/54931396 + prettyStringify(obj) { + return JSON.stringify( + obj, + function (k, v) { + if (v instanceof Array) return JSON.stringify(v); + return v; + }, + 2 + ); + } + + getCustomizableUIData() { + this.elements.customizableui_data.value = JSON.stringify( + JSON.parse( + Services.prefs.getStringPref("dot.customizable.state", "{}") + ) + ); + } + + async init() { + const activeTheme = ( + await AddonManager.getAddonsByTypes(["theme"]) + ).find((ext) => ext.isActive); + + AddonManager.addAddonListener({ + onEnabled: this.onAddonEnabled.bind(this) + }); + + this.resourceUsageInt = setInterval(() => { + this.calculateResourceUsage(); + }, 1000); + + this.onAddonEnabled(activeTheme); + this.calculateResourceUsage(); + + setInterval(() => { + this.getCustomizableUIData(); + }, 500); + + const dotVersion = document.createElement("strong"); + dotVersion.textContent = `Dot Browser v${DotAppConstants.DOT_APP_VERSION} (${AppConstants.MOZ_BUILDID})`; + + this.elements.app_info.append( + html( + "div", + { class: "dev-branding-lockup" }, + html("img", { src: "chrome://branding/content/icon32.png" }), + html("img", { + src: "chrome://branding/content/about-wordmark.svg", + height: "48" + }) + ), + html("br"), + dotVersion, + html("br"), + `Firefox v${AppConstants.MOZ_APP_VERSION}` + ); + + this.elements.user_agent.textContent = `user_agent = ${ + Cc["@mozilla.org/network/protocol;1?name=http"].getService( + Ci.nsIHttpProtocolHandler + ).userAgent + }`; + + this.elements.active_theme.addEventListener("change", async (event) => { + const { value } = /** @type {HTMLSelectElement} */ (event.target); + + const addon = await AddonManager.getAddonByID(value); + + if (addon) { + addon.enable(); + } + }); + + this.elements.native_titlebar.addEventListener( + "change", + async (event) => { + const { checked } = /** @type {HTMLInputElement} */ ( + event.target + ); + + NativeTitlebar.set(checked, true); + } + ); + } + + insertStylesheet() { + const sheet = document.createProcessingInstruction( + "xml-stylesheet", + `href="chrome://dot/content/widgets/dev-debug-panel.css" type="text/css"` + ); + + document.insertBefore(sheet, document.documentElement); + } + + connectedCallback() { + if (this.delayConnectedCallback()) return; + this.classList.add("dev-panel"); + + this.appendChild(this.elements.app_info); + this.appendChild(this.elements.proc_info); + this.appendChild(this.elements.user_agent); + this.appendChild( + html( + "div", + { class: "dev-active-theme-container" }, + html("label", {}, "Active Theme:"), + this.elements.active_theme + ) + ); + + this.appendChild( + html( + "div", + { class: "dev-native-titlebar-container" }, + html( + "label", + { for: "dev-native-theme-enabled" }, + "Native Titlebar:" + ), + this.elements.native_titlebar + ) + ); + this.elements.native_titlebar.checked = NativeTitlebar.enabled; + + this.appendChild( + html( + "div", + { class: "dev-customizable-ui-container" }, + html("label", {}, "Customizable UI State:"), + this.elements.customizableui_data + ) + ); + + this.appendChild(this.elements.graph); + + this.insertStylesheet(); + + if ( + window.location.href == + "chrome://dot/content/dev-debug-popout.xhtml" + ) { + new ResizeObserver(() => { + window.document.documentElement.style.setProperty( + "--height", + this.getBoundingClientRect().height + "px" + ); + }).observe(this); + + const devtoolsButton = html("button", {}, "Open Browser Toolbox"); + devtoolsButton.addEventListener("click", () => { + var { BrowserToolboxLauncher } = ChromeUtils.importESModule( + "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs" + ); + + BrowserToolboxLauncher.init(); + }); + + this.appendChild(devtoolsButton); + } + } + + disconnectedCallback() { + if (this.delayConnectedCallback()) return; + + clearInterval(this.resourceUsageInt); + } } customElements.define("dev-debug-panel", DeveloperDebugPanel); diff --git a/components/search/content/browser-addressbar.js b/components/search/content/browser-addressbar.js index 26cf874c7d..e53a26194c 100644 --- a/components/search/content/browser-addressbar.js +++ b/components/search/content/browser-addressbar.js @@ -2,243 +2,11 @@ * 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/. */ -class BrowserAddressBarIdentityBox extends BrowserContextualMixin( - MozHTMLElement -) { - constructor() { - super(); - } - - /** - * Handles incoming events - * @param {Event} event - */ - handleEvent(event) { - const target = /** @type {HTMLElement} */ (event.target); - - switch (event.type) { - case "mouseover": - case "mouseout": { - const closestToolbarButton = target.closest(".toolbar-button"); - - if (closestToolbarButton?.previousElementSibling) { - if ( - closestToolbarButton?.hasAttribute("precedes-hover") && - event.type == "mouseover" - ) { - return; - } - - closestToolbarButton?.previousElementSibling.toggleAttribute( - "precedes-hover", - event.type == "mouseover" && - !closestToolbarButton?.hasAttribute("disabled") - ); - } - - break; - } - } - } - - /** @type {MutationCallback} */ - _observeMutations(mutations) { - this.toggleAttribute( - "onlychild", - this.children.length <= 1 || - (this.children.length <= 1 && - this.children[0]?.getAttribute("mode") == "icons") - ); - } - - connectedCallback() { - if (this.delayConnectedCallback()) return; - - this.classList.add("addressbar-identity-box"); - - const identityButton = document.createElement("button", { - is: "identity-button" - }); - identityButton.classList.add("addressbar-identity-button"); - - this.append( - document.createElement("button", { - is: "reload-button" - }) - ); - this.appendChild(identityButton); - - this.addEventListener("mouseover", this); - this.addEventListener("mouseout", this); - - this._mutationObserver = new MutationObserver( - this._observeMutations.bind(this) - ); - this._mutationObserver.observe(this, { - childList: true, - subtree: true - }); - } - - disconnectedCallback() { - this.removeEventListener("mouseover", this); - this.removeEventListener("mouseout", this); - } -} - -customElements.define( - "browser-addressbar-identity-box", - BrowserAddressBarIdentityBox -); - -class BrowserAddressBarInput extends HTMLInputElement { - /** - * Handles incoming events - * @param {Event} event - */ - handleEvent(event) { - switch (event.type) { - case "focusin": { - this.parentElement.toggleAttribute("focuswithin", true); - - this.addEventListener( - "mouseup", - () => { - let start = this.selectionStart; - let end = this.selectionEnd; - - if (start == end) { - start = 0; - end = this.value.length; - } - - this.setSelectionRange(start, end); - }, - { once: true } - ); - break; - } - case "focusout": { - this.parentElement.toggleAttribute("focuswithin", false); - - this.setSelectionRange(-1, -1); - - break; - } - } - } - - async connectedCallback() { - this.type = "text"; - this.classList.add("addressbar-input"); - - document.l10n.setAttributes(this, "addressbar-input-field"); - this.setAttribute("data-l10n-attrs", "placeholder"); - - this.addEventListener("focusin", this); - this.addEventListener("focusout", this); - } -} - -customElements.define("browser-addressbar-input", BrowserAddressBarInput, { - extends: "input" -}); - class BrowserAddressBar extends BrowserCustomizableArea { constructor() { super(); } - /** - * The browser window's selectedTab - */ - get tab() { - return gDot.tabs.selectedTab; - } - - /** - * The selected browser element - * @returns {ChromeBrowser | null} - */ - get browser() { - if (!gDot?.tabs?._isWebContentsBrowserElement(this.tab.webContents)) { - return null; - } - return /** @type {ChromeBrowser} */ (this.tab.webContents); - } - - /** - * The anatomy of the address bar - * - * @typedef {Object} AddressBarElements - * @property {BrowserAddressBarIdentityBox} identityBox - The addressbar's identity box - * @property {BrowserAddressBarInput} input - The addressbar's input element - * - * @returns {AddressBarElements} - */ - get elements() { - return { - identityBox: /** @type {BrowserAddressBarIdentityBox} */ ( - this.querySelector(".addressbar-identity-box") || - html("browser-addressbar-identity-box") - ), - input: /** @type {BrowserAddressBarInput} */ ( - this.querySelector(".addressbar-input") || - document.createElement("input", { - is: "browser-addressbar-input" - }) - ) - }; - } - - /** - * Updates the input field of the addressbar - */ - async updateInput() { - const { hasInvalidPageProxyState, isInternalPage } = - this.tab.siteIdentity; - - this.elements.input.value = - hasInvalidPageProxyState || isInternalPage - ? "" - : this.browser.currentURI.spec; - - document.l10n.setAttributes( - this.elements.input, - "addressbar-input-field-with-engine", - { - engine: (await gDot.search.defaultEngine).name - } - ); - } - - /** - * Handles incoming events - * @param {CustomEvent} event - */ - handleEvent(event) { - switch (event.type) { - case "BrowserTabs::TabSelect": { - if (!gDot?.tabs || event.detail !== this.tab) return; - - this.updateInput(); - break; - } - case "BrowserTabs::BrowserLocationChange": { - if (event.detail.browser !== this.browser) return; - - this.updateInput(); - break; - } - case "BrowserTabs::TabIdentityChanged": { - if (event.detail.tab !== this.tab) return; - - this.updateInput(); - break; - } - } - } - connectedCallback() { super.connect({ name: "addressbar", diff --git a/components/tabs/content/browser-tab.js b/components/tabs/content/browser-tab.js index b262848610..66e3c5015a 100644 --- a/components/tabs/content/browser-tab.js +++ b/components/tabs/content/browser-tab.js @@ -386,7 +386,6 @@ class BrowserRenderedTab extends BrowserCustomizableArea { ); setInterval(() => { - this.querySelector("#tab-debug").hidden = true; this.querySelector("#tab-debug").innerHTML = "" + [ diff --git a/components/widgets/content/browser-toolbar-button.js b/components/widgets/content/browser-toolbar-button.js index d40798f7df..23e43c872a 100644 --- a/components/widgets/content/browser-toolbar-button.js +++ b/components/widgets/content/browser-toolbar-button.js @@ -155,7 +155,7 @@ class BrowserToolbarButton extends BrowserContextualMixin(HTMLButtonElement) { * The mode of the toolbar button */ get mode() { - return this.getAttribute("mode") || this.toolbar.mode; + return this.getAttribute("mode"); } /** diff --git a/components/widgets/content/browser-toolbar.css b/components/widgets/content/browser-toolbar.css index 129a843518..0ad51108a1 100644 --- a/components/widgets/content/browser-toolbar.css +++ b/components/widgets/content/browser-toolbar.css @@ -11,8 +11,6 @@ browser-toolbar { --padding-x: 4px; - padding-inline: var(--padding-x); - -moz-window-dragging: drag; min-height: 40px; @@ -59,10 +57,16 @@ browser-toolbar[collapse] { } } -browser-toolbar.customizable-area>.toolbar-button { - align-self: center; +:host(browser-toolbar) .toolbar-container { + display: flex; + flex: 1; + padding-inline: var(--padding-x); +} + +:host(browser-toolbar) .toolbar-container { + align-items: center; } -:host(browser-toolbar[initial]) .customizable-container { +/* :host(browser-toolbar[initial]) .toolbar-container { padding-block-start: calc(var(--browser-ui-density) / 3); -} \ No newline at end of file +} */ \ No newline at end of file diff --git a/components/widgets/content/browser-toolbar.js b/components/widgets/content/browser-toolbar.js index 2f477cd03d..2c78256da5 100644 --- a/components/widgets/content/browser-toolbar.js +++ b/components/widgets/content/browser-toolbar.js @@ -2,19 +2,41 @@ * 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/. */ -class BrowserToolbar extends BrowserCustomizableArea { +class BrowserToolbar extends BrowserCustomizableElement { constructor() { super(); + + this.attachShadow({ mode: "open" }); } + attributesSchema = { + properties: { + width: { + $ref: "#/$defs/length" + }, + height: { + $ref: "#/$defs/length" + }, + mode: { + $ref: "#/$defs/mode" + } + }, + required: ["mode", "width", "height"] + }; + /** * The anatomy of the toolbar's shadow DOM */ get shadowElements() { return { slot: /** @type {HTMLSlotElement} */ ( - this.shadowRoot.querySelector("slot") || - html("slot", { part: "content" }) + this.shadowRoot.querySelector("slot") || html("slot") + ), + container: /** @type {HTMLDivElement} */ ( + this.shadowRoot.querySelector(".toolbar-container") || + html("div", { + class: "toolbar-container customizable-shelf" + }) ), csd: /** @type {BrowserWindowControls} */ ( this.shadowRoot.querySelector("browser-window-controls") || @@ -77,12 +99,19 @@ class BrowserToolbar extends BrowserCustomizableArea { } } + /** + * Appends a component to the shadow root of the toolbar + * @param {Node} node + */ + appendComponent(node) { + this.shadowRoot.appendChild(this.shadowElements.container); + this.shadowElements.container.appendChild(node); + } + connectedCallback() { - super.connect({ - name: this.getAttribute("slot") || this.getAttribute("name"), + super.connectedCallback(); - layout: "toolbar" - }); + if (this.delayConnectedCallback()) return; this.shadowRoot.appendChild(this.shadowElements.csd); diff --git a/modules_registry.mjs b/modules_registry.mjs index 3e18dd01cb..a5f134d994 100644 --- a/modules_registry.mjs +++ b/modules_registry.mjs @@ -8,12 +8,17 @@ const registry = { AccentColorManager: "themes/AccentColorManager.sys.mjs", - DotCustomizableUI: "components/customizableui/DotCustomizableUI.sys.mjs", - DotWindowTracker: "modules/DotWindowTracker.sys.mjs", BrowserCompatibility: "components/compat/BrowserCompatibility.sys.mjs", + BrowserCustomizable: + "components/customizableui/BrowserCustomizable.sys.mjs", + BrowserCustomizableInternal: + "components/customizableui/BrowserCustomizableInternal.sys.mjs", + BrowserCustomizableShared: + "components/customizableui/BrowserCustomizableShared.sys.mjs", + BrowserTabs: "components/tabs/BrowserTabs.sys.mjs", BrowserTabsUtils: "components/tabs/BrowserTabsUtils.sys.mjs", diff --git a/package.json b/package.json index 3d57e4980c..e9863ab474 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,5 @@ "glob": "^10.2.1", "prettier": "^3.0.3", "typescript": "^4.8.2" - }, - "dependencies": {} -} + } +} \ No newline at end of file diff --git a/themes/shared/browser-shared.css b/themes/shared/browser-shared.css index 2b77ef2a4b..14b6a3652f 100644 --- a/themes/shared/browser-shared.css +++ b/themes/shared/browser-shared.css @@ -26,6 +26,9 @@ @import url("chrome://dot/content/widgets/browser-tab-label.css"); @import url("chrome://dot/content/widgets/browser-tab-icon.css"); +/* Customizable UI */ +@import url("chrome://dot/content/customizableui/components/customizable.css"); + /* XUL */ @import url("chrome://dot/skin/input.css"); diff --git a/types.d.ts b/types.d.ts index d62124faf2..a0c559adaf 100644 --- a/types.d.ts +++ b/types.d.ts @@ -97,11 +97,57 @@ declare global { interface Document { l10n: Gecko.LocalizationInstance; - commandDispatcher: Gecko.nsIDOMXULCommandDispatcher; + commandDispatcher: Gecko.nsIDOMXULCommandDispatcher; } - interface Event { - defaultCancelled: boolean; - defaultPreventedByChrome: boolean; - } + interface Event { + defaultCancelled: boolean; + defaultPreventedByChrome: boolean; + } + + interface Console { + createInstance: (options: { + // An optional function to intercept all strings written to stdout. + dump?: (message: string) => void; + + // An optional prefix string to be printed before the actual logged message. + prefix?: string; + + // An ID representing the source of the message. Normally the inner ID of a + // DOM window. + innerID?: string; + + // String identified for the console, this will be passed through the console + // notifications. + consoleID?: string; + + // Identifier that allows to filter which messages are logged based on their + // log level. + maxLogLevel?: + | "All" + | "Debug" + | "Log" + | "Info" + | "Clear" + | "Trace" + | "TimeLog" + | "TimeEnd" + | "Time" + | "Group" + | "GroupEnd" + | "Profile" + | "ProfileEnd" + | "Dir" + | "Dirxml" + | "Warn" + | "Error" + | "Off"; + + // String pref name which contains the level to use for maxLogLevel. If the + // pref doesn't exist, gets removed or it is used in workers, the maxLogLevel + // will default to the value passed to this constructor (or "all" if it wasn't + // specified). + maxLogLevelPref?: string; + }) => Console; + } }