From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../content/unified-toolbar-customization.mjs | 414 +++++++++++++++++++++ 1 file changed, 414 insertions(+) create mode 100644 comm/mail/components/unifiedtoolbar/content/unified-toolbar-customization.mjs (limited to 'comm/mail/components/unifiedtoolbar/content/unified-toolbar-customization.mjs') diff --git a/comm/mail/components/unifiedtoolbar/content/unified-toolbar-customization.mjs b/comm/mail/components/unifiedtoolbar/content/unified-toolbar-customization.mjs new file mode 100644 index 0000000000..1acdf85b57 --- /dev/null +++ b/comm/mail/components/unifiedtoolbar/content/unified-toolbar-customization.mjs @@ -0,0 +1,414 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../../../base/content/spacesToolbar.js */ +/* import-globals-from ../../../base/content/utilityOverlay.js */ + +import { + storeState, + getState, +} from "resource:///modules/CustomizationState.mjs"; +import "./unified-toolbar-tab.mjs"; // eslint-disable-line import/no-unassigned-import +import "./unified-toolbar-customization-pane.mjs"; // eslint-disable-line import/no-unassigned-import +import { + BUTTON_STYLE_MAP, + BUTTON_STYLE_PREF, +} from "resource:///modules/ButtonStyle.mjs"; + +/** + * Set of names of the built in spaces. + * + * @type {Set} + */ +const BUILTIN_SPACES = new Set([ + "mail", + "addressbook", + "calendar", + "tasks", + "chat", + "settings", +]); + +/** + * Customization palette container for the unified toolbar. Contained in a + * custom element for state management. When visible, the document should have + * the customizingUnifiedToolbar class. + * Template: #unifiedToolbarCustomizationTemplate. + */ +class UnifiedToolbarCustomization extends HTMLElement { + /** + * Reference to the container where the space tabs go in. The tab panels will + * be placed after this element. + * + * @type {?HTMLDivElement} + */ + #tabList = null; + + #buttonStyle = null; + + connectedCallback() { + if (this.hasConnected) { + return; + } + this.hasConnected = true; + + const template = document + .getElementById("unifiedToolbarCustomizationTemplate") + .content.cloneNode(true); + const form = template.querySelector("form"); + form.addEventListener( + "submit", + event => { + event.preventDefault(); + this.#save(); + }, + { + passive: false, + } + ); + form.addEventListener("reset", event => { + this.#reset(); + }); + template + .querySelector("#unifiedToolbarCustomizationCancel") + .addEventListener("click", () => { + this.toggle(false); + }); + this.#buttonStyle = template.querySelector("#buttonStyle"); + this.#buttonStyle.addEventListener("change", this.#handleButtonStyleChange); + this.addEventListener("itemchange", this.#handleItemChange, { + capture: true, + }); + this.addEventListener("additem", this.#handleAddItem, { + capture: true, + }); + this.addEventListener("removeitem", this.#handleRemoveItem, { + capture: true, + }); + this.#tabList = template.querySelector("#customizationTabs"); + this.#tabList.addEventListener("tabswitch", this.#handleTabSwitch, { + capture: true, + }); + template + .querySelector("#customizationToSettingsButton") + .addEventListener("click", this.#handleSettingsButton); + this.initialize(); + this.append(template); + this.#updateResetToDefault(); + this.addEventListener("keyup", this.#handleKeyboard); + this.addEventListener("keyup", this.#closeByKeyboard); + this.addEventListener("keypress", this.#handleKeyboard); + this.addEventListener("keydown", this.#handleKeyboard); + } + + #handleItemChange = event => { + event.stopPropagation(); + this.#updateResetToDefault(); + this.#updateUnsavedChangesState(); + }; + + #handleTabSwitch = event => { + event.stopPropagation(); + this.#updateUnsavedChangesState(); + }; + + #handleButtonStyleChange = event => { + for (const pane of this.querySelectorAll( + "unified-toolbar-customization-pane" + )) { + pane.updateButtonStyle(event.target.value); + } + this.#updateUnsavedChangesState(); + }; + + #handleSettingsButton = event => { + event.preventDefault(); + openPreferencesTab("paneGeneral", "layoutGroup"); + this.toggle(false); + }; + + #handleAddItem = event => { + event.stopPropagation(); + const tabPanes = Array.from( + this.querySelectorAll("unified-toolbar-customization-pane") + ); + for (const pane of tabPanes) { + pane.addItem(event.detail.itemId); + } + }; + + #handleRemoveItem = event => { + event.stopPropagation(); + const tabPanes = Array.from( + this.querySelectorAll("unified-toolbar-customization-pane") + ); + for (const pane of tabPanes) { + pane.removeItem(event.detail.itemId); + } + }; + + /** + * Close the customisation pane when Escape is released + * + * @param {KeyboardEvent} event - The keyboard event + */ + #closeByKeyboard = event => { + if (event.key == "Escape") { + event.preventDefault(); + this.toggle(false); + } + }; + + /** + * Ensure keyboard events are not propagated outside the customization dialog. + * + * @param {KeyboardEvent} event - The keyboard event. + */ + #handleKeyboard = event => { + event.stopPropagation(); + }; + + /** + * Update state of reset to default button. + */ + #updateResetToDefault() { + const tabPanes = Array.from( + this.querySelectorAll("unified-toolbar-customization-pane") + ); + const isDefault = tabPanes.every(pane => pane.matchesDefaultState); + this.querySelector('button[type="reset"]').disabled = isDefault; + } + + #updateUnsavedChangesState() { + const tabPanes = Array.from( + this.querySelectorAll("unified-toolbar-customization-pane") + ); + const unsavedChanges = + tabPanes.some(tabPane => tabPane.hasChanges) || + this.#buttonStyle.value != + BUTTON_STYLE_MAP[Services.prefs.getIntPref(BUTTON_STYLE_PREF, 0)]; + const otherSpacesHaveUnsavedChanges = + unsavedChanges && + tabPanes.some(tabPane => tabPane.hidden && tabPane.hasChanges); + this.querySelector('button[type="submit"]').disabled = !unsavedChanges; + document.getElementById( + "unifiedToolbarCustomizationUnsavedChanges" + ).hidden = !otherSpacesHaveUnsavedChanges; + } + + /** + * Generate a tab and tab pane that are linked together for the given space. + * If the space is the current space, the tab is marked as active. + * + * @param {SpaceInfo} space + * @returns {{tab: UnifiedToolbarTab, tabPane: UnifiedToolbarCustomizationPane}} + */ + #makeSpaceTab(space) { + const activeSpace = space === gSpacesToolbar.currentSpace; + const tabId = `unified-toolbar-customization-tab-${space.name}`; + const paneId = `unified-toolbar-customization-pane-${space.name}`; + const tab = document.createElement("unified-toolbar-tab"); + tab.id = tabId; + tab.setAttribute("aria-controls", paneId); + if (activeSpace) { + tab.setAttribute("selected", true); + } + const isBuiltinSpace = BUILTIN_SPACES.has(space.name); + if (isBuiltinSpace) { + document.l10n.setAttributes(tab, `customize-space-tab-${space.name}`); + } else { + const title = space.button.title; + tab.textContent = title; + tab.title = title; + tab.style = space.button.querySelector("img").style.cssText; + } + const tabPane = document.createElement( + "unified-toolbar-customization-pane" + ); + tabPane.id = paneId; + tabPane.setAttribute("space", space.name); + tabPane.setAttribute("aria-labelledby", tabId); + tabPane.toggleAttribute("builtin-space", isBuiltinSpace); + tabPane.hidden = !activeSpace; + return { tab, tabPane }; + } + + /** + * Reset all the spaces to their default customization state. + */ + #reset() { + const tabPanes = Array.from( + this.querySelectorAll("unified-toolbar-customization-pane") + ); + for (const pane of tabPanes) { + pane.reset(); + } + } + + /** + * Save the current state of the toolbar and hide the customization. + */ + #save() { + const tabPanes = Array.from( + this.querySelectorAll("unified-toolbar-customization-pane") + ); + const state = Object.fromEntries( + tabPanes + .filter(pane => !pane.matchesDefaultState) + .map(pane => [pane.getAttribute("space"), pane.itemIds]) + ); + Services.prefs.setIntPref( + BUTTON_STYLE_PREF, + BUTTON_STYLE_MAP.indexOf(this.#buttonStyle.value) + ); + // Toggle happens before saving, so the newly restored buttons don't have to + // be updated when the globalOverlay flag on tabmail goes away. + this.toggle(false); + storeState(state); + } + + /** + * Initialize the contents of this from the current state. Specifically makes + * sure all the spaces have a tab, and all tabs still have a space. + * + * @param {boolean} [deep = false] - If true calls initialize on all tab + * panes. + */ + initialize(deep = false) { + const state = getState(); + const existingTabs = Array.from(this.#tabList.children); + const tabSpaces = existingTabs.map(tab => tab.id.split("-").pop()); + const spaceNames = new Set(gSpacesToolbar.spaces.map(space => space.name)); + const removedTabs = existingTabs.filter( + (tab, index) => !spaceNames.has(tabSpaces[index]) + ); + for (const tab of removedTabs) { + tab.pane.remove(); + tab.remove(); + } + const newTabs = gSpacesToolbar.spaces.map(space => { + if (tabSpaces.includes(space.name)) { + const tab = existingTabs[tabSpaces.indexOf(space.name)]; + if (!BUILTIN_SPACES.has(space.name)) { + const title = space.button.title; + tab.textContent = title; + tab.title = title; + tab.style = space.button.querySelector("img").style.cssText; + } + return [tab, tab.pane]; + } + const { tab, tabPane } = this.#makeSpaceTab(space); + return [tab, tabPane]; + }); + this.#tabList.replaceChildren(...newTabs.map(([tab]) => tab)); + let previousNode = this.#tabList; + for (const [, tabPane] of newTabs) { + previousNode.after(tabPane); + const space = tabPane.getAttribute("space"); + if (state.hasOwnProperty(space)) { + tabPane.setAttribute("current-items", state[space].join(",")); + } else { + tabPane.removeAttribute("current-items"); + } + previousNode = tabPane; + if (deep) { + tabPane.initialize(deep); + } + } + this.#buttonStyle.value = + BUTTON_STYLE_MAP[Services.prefs.getIntPref(BUTTON_STYLE_PREF, 0)]; + // Update state of reset to default button only when updating tab panes too. + if (deep) { + this.#updateResetToDefault(); + this.#updateUnsavedChangesState(); + } + } + + /** + * Toggle unified toolbar customization. + * + * @param {boolean} [visible] - If passed, defines if customization should + * be active. + */ + toggle(visible) { + if (visible) { + this.initialize(true); + let tabToSelect; + if (gSpacesToolbar.currentSpace) { + tabToSelect = document.getElementById( + `unified-toolbar-customization-tab-${gSpacesToolbar.currentSpace.name}` + ); + } + if ( + !tabToSelect && + !this.querySelector(`unified-toolbar-tab[selected="true"]`) + ) { + tabToSelect = this.querySelector("unified-toolbar-tab"); + } + if (tabToSelect) { + tabToSelect.select(); + } + } + + document.getElementById("tabmail").globalOverlay = visible; + document.documentElement.classList.toggle( + "customizingUnifiedToolbar", + visible + ); + + // Make sure focus is where it belongs. + if (visible) { + if ( + document.activeElement !== this && + !this.contains(document.activeElement) + ) { + Services.focus.moveFocus( + window, + this, + Services.focus.MOVEFOCUS_FIRST, + 0 + ); + } + } else { + Services.focus.moveFocus( + window, + document.body, + Services.focus.MOVEFOCUS_ROOT, + 0 + ); + } + } + + /** + * Check if an item is active in all spaces. + * + * @param {string} itemId - Item ID of the item to check for. + * @returns {boolean} If the given item is found active in all spaces. + */ + activeInAllSpaces(itemId) { + return Array.from( + this.querySelectorAll("unified-toolbar-customization-pane"), + pane => pane.hasItem(itemId) + ).every(hasItem => hasItem); + } + + /** + * Check if an item is active in two or more spaces. + * + * @param {string} itemId - Item ID of the item to check for. + * @returns {boolean} If the given item is active in at least two spaces. + */ + activeInMultipleSpaces(itemId) { + return ( + Array.from( + this.querySelectorAll("unified-toolbar-customization-pane"), + pane => pane.hasItem(itemId) + ).filter(Boolean).length > 1 + ); + } +} +customElements.define( + "unified-toolbar-customization", + UnifiedToolbarCustomization +); -- cgit v1.2.3