Files
Dão Gottwald b03a1a7a10 Bug 1586591 - Remove moz-input-box. r=desktop-theme-reviewers,tgiles,emilio
Thunderbird's compose window was the last consumer outside this repo; it moved off in bug 2066237, which has landed in comm-central.

The urlbar's a11y-checks exemption goes with it, since the wrapper no longer carries the combobox role that the check keyed on; the old searchbar still does.

Differential Revision: https://phabricator.services.mozilla.com/D321477
2026-09-11 14:01:21 +00:00

287 lines
9.9 KiB
JavaScript

/* 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/. */
// update menu items that rely on focus or on the current selection
function goUpdateGlobalEditMenuItems(force) {
// Don't bother updating the edit commands if they aren't visible in any way
// (i.e. the Edit menu isn't open, nor is the context menu open, nor have the
// cut, copy, and paste buttons been added to the toolbars) for performance.
// This only works in applications/on platforms that set the gEditUIVisible
// flag, so we check to see if that flag is defined before using it.
if (!force && typeof gEditUIVisible != "undefined" && !gEditUIVisible) {
return;
}
goUpdateUndoEditMenuItems();
goUpdateCommand("cmd_cut");
goUpdateCommand("cmd_copy");
goUpdatePasteMenuItems();
goUpdateCommand("cmd_selectAll");
goUpdateCommand("cmd_delete");
goUpdateCommand("cmd_switchTextDirection");
}
// update menu items that relate to undo/redo
function goUpdateUndoEditMenuItems() {
goUpdateCommand("cmd_undo");
goUpdateCommand("cmd_redo");
}
// update menu items that depend on clipboard contents
function goUpdatePasteMenuItems() {
goUpdateCommand("cmd_paste");
goUpdateCommand("cmd_pasteNoFormatting");
}
// Inject the commandset here instead of relying on preprocessor to share this across documents.
window.addEventListener(
"DOMContentLoaded",
() => {
let container =
document.querySelector("commandset") || document.documentElement;
let fragment = MozXULElement.parseXULToFragment(`
<commandset id="editMenuCommands">
<commandset id="editMenuCommandSetAll" commandupdater="true" events="focus,select" />
<commandset id="editMenuCommandSetUndo" commandupdater="true" events="undo" />
<commandset id="editMenuCommandSetPaste" commandupdater="true" events="clipboard" />
<command id="cmd_undo" internal="true"/>
<command id="cmd_redo" internal="true" />
<command id="cmd_cut" internal="true" />
<command id="cmd_copy" internal="true" />
<command id="cmd_paste" internal="true" />
<command id="cmd_pasteNoFormatting" internal="true" />
<command id="cmd_delete" />
<command id="cmd_selectAll" internal="true" />
<command id="cmd_switchTextDirection" />
</commandset>
`);
let editMenuCommandSetAll = fragment.querySelector(
"#editMenuCommandSetAll"
);
editMenuCommandSetAll.addEventListener("commandupdate", function () {
goUpdateGlobalEditMenuItems();
});
let editMenuCommandSetUndo = fragment.querySelector(
"#editMenuCommandSetUndo"
);
editMenuCommandSetUndo.addEventListener("commandupdate", function () {
goUpdateUndoEditMenuItems();
});
let editMenuCommandSetPaste = fragment.querySelector(
"#editMenuCommandSetPaste"
);
editMenuCommandSetPaste.addEventListener("commandupdate", function () {
goUpdatePasteMenuItems();
});
fragment.firstElementChild.addEventListener("command", event => {
let commandID = event.target.id;
goDoCommand(commandID);
});
container.appendChild(fragment);
},
{ once: true }
);
/**
* The context menu for HTML inputs and textareas in chrome documents, holding
* the usual undo / cut / copy / paste / delete / select all items. It's created
* on first use and shared by every input in the document, so consumers
* contribute their own items to it with addItems().
*/
var EditContextMenu = {
_itemSets: [],
/**
* The input the menu was last opened on, for consumers that need to tell
* their own input's items and commands from another's.
*
* @type {Element}
*/
input: null,
/**
* The menu, built on first access.
*
* @type {Element}
*/
get popup() {
return this._ensurePopup();
},
/**
* Adds items to the menu.
*
* @param {object} itemSet
* @param {(input: Element) => boolean} itemSet.matches
* Whether the items apply to the input the menu is being opened on. They
* are hidden when it returns false.
* @param {() => DocumentFragment} itemSet.createItems
* Creates the items. Called once per menu, which may be built more than
* once in the lifetime of a document.
* @param {(input: Element, items: Element[], event: MouseEvent) => void} [itemSet.onShowing]
* Called before the menu opens on a matching input, to update the items'
* state. `items` is the set's live membership: amend it to claim items
* created outside createItems(), so they're hidden along with the rest when
* the menu opens on another input. `event` is the contextmenu event, which
* carries the position a spellchecker resolves the clicked word from.
* @param {string} [itemSet.after]
* Id of the item to insert the items after. They go last by default.
* @param {string} [itemSet.before]
* Id of the item to insert the items before, for a set that belongs above
* the standard items. Ignored when `after` is given.
* @returns {object}
* The set, to pass to removeItems().
*/
addItems({ matches, createItems, onShowing, after, before }) {
let itemSet = { matches, createItems, onShowing, after, before, items: [] };
this._itemSets.push(itemSet);
let popup = document.getElementById("textbox-contextmenu");
if (popup) {
this._insertItems(popup, itemSet);
}
return itemSet;
},
/**
* Removes items from the menu.
*
* @param {object} itemSet
* The set addItems() returned.
*/
removeItems(itemSet) {
this._itemSets = this._itemSets.filter(set => set != itemSet);
for (let item of itemSet.items) {
item.remove();
}
itemSet.items = [];
},
/**
* Opens the menu for an input.
*
* @param {Element} input
* The input or textarea to open the menu for.
* @param {MouseEvent} event
* The contextmenu event that asked for the menu.
* @param {object} [options]
* @param {Element} [options.anchor]
* Anchors the menu below this element instead of opening it at the event's
* screen position, for an event that carries no useful position.
*/
open(input, event, { anchor } = {}) {
let popup = this._ensurePopup();
this.input = input;
// Commands are enabled for whatever has focus, so the items would otherwise
// reflect a different element than the one the menu was opened on.
if (document.commandDispatcher.focusedElement != input) {
input.focus();
}
goUpdateGlobalEditMenuItems(true);
for (let itemSet of this._itemSets) {
let matches = itemSet.matches(input);
for (let item of itemSet.items) {
item.hidden = !matches;
}
if (matches) {
itemSet.onShowing?.(input, itemSet.items, event);
}
}
if (anchor) {
popup.openPopup(anchor, "after_start", 0, 0, true, false, event);
} else {
popup.openPopupAtScreen(event.screenX, event.screenY, true, event);
}
},
_ensurePopup() {
let popup = document.getElementById("textbox-contextmenu");
if (popup) {
return popup;
}
MozXULElement.insertFTLIfNeeded("toolkit/global/textActions.ftl");
// showservicesmenu is read by macOS cocoa code to enable the "services"
// menu here.
document.documentElement.appendChild(
MozXULElement.parseXULToFragment(`
<menupopup id="textbox-contextmenu" class="textbox-contextmenu"
showservicesmenu="true">
<menuitem id="edit-contextmenu-undo" data-l10n-id="text-action-undo" command="cmd_undo"></menuitem>
<menuitem id="edit-contextmenu-redo" data-l10n-id="text-action-redo" command="cmd_redo"></menuitem>
<menuseparator></menuseparator>
<menuitem id="edit-contextmenu-cut" data-l10n-id="text-action-cut" command="cmd_cut"></menuitem>
<menuitem id="edit-contextmenu-copy" data-l10n-id="text-action-copy" command="cmd_copy"></menuitem>
<menuitem id="edit-contextmenu-paste" data-l10n-id="text-action-paste" command="cmd_paste"></menuitem>
<menuitem id="edit-contextmenu-delete" data-l10n-id="text-action-delete" command="cmd_delete"></menuitem>
<menuitem id="edit-contextmenu-select-all" data-l10n-id="text-action-select-all" command="cmd_selectAll"></menuitem>
</menupopup>
`)
);
popup = document.documentElement.lastElementChild;
for (let itemSet of this._itemSets) {
this._insertItems(popup, itemSet);
}
return popup;
},
_insertItems(popup, itemSet) {
let fragment = itemSet.createItems();
itemSet.items = [...fragment.children];
if (itemSet.after) {
popup.querySelector(`#${itemSet.after}`).after(fragment);
} else if (itemSet.before) {
popup.querySelector(`#${itemSet.before}`).before(fragment);
} else {
popup.appendChild(fragment);
}
},
};
EditContextMenu.addItems({
matches: input => input.type == "password",
createItems() {
return MozXULElement.parseXULToFragment(`
<menuitem id="edit-contextmenu-reveal-password"
data-l10n-id="text-action-reveal-password" type="checkbox"/>
`);
},
onShowing(input, [item]) {
// Reassigned on every contextmenu so it acts on the current input.
item.oncommand = () => {
input.revealPassword = !input.revealPassword;
};
item.toggleAttribute("checked", input.revealPassword);
},
});
// Support context menus on html textareas in the parent process:
window.addEventListener("contextmenu", e => {
const HTML_NS = "http://www.w3.org/1999/xhtml";
let target = e.composedTarget;
let needsContextMenu =
target.ownerDocument == document &&
!e.defaultPrevented &&
["textarea", "input"].includes(target.localName) &&
target.namespaceURI == HTML_NS;
if (!needsContextMenu) {
return;
}
EditContextMenu.open(target, e);
// Don't show any other context menu at the same time. There can be a
// context menu from an ancestor too but we only want to show this one.
e.preventDefault();
});