Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
"use strict";
// This is loaded into all XUL windows. Wrap in a block to prevent
// leaking to window scope.
{
let imports = {};
ChromeUtils.defineESModuleGetters(imports, {
ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
});
const MozMenuItemBaseMixin = Base => {
class MozMenuItemBase extends MozElements.BaseTextMixin(Base) {
// nsIDOMXULSelectControlItemElement
set value(val) {
this.setAttribute("value", val);
}
get value() {
return this.getAttribute("value") || "";
}
// nsIDOMXULSelectControlItemElement
get selected() {
return this.getAttribute("selected") == "true";
}
// nsIDOMXULSelectControlItemElement
get control() {
var parent = this.parentNode;
// Return the parent if it is a menu or menulist.
if (parent && XULMenuElement.isInstance(parent.parentNode)) {
return parent.parentNode;
}
return null;
}
// nsIDOMXULContainerItemElement
get parentContainer() {
for (var parent = this.parentNode; parent; parent = parent.parentNode) {
if (XULMenuElement.isInstance(parent)) {
return parent;
}
}
return null;
}
}
MozXULElement.implementCustomInterface(MozMenuItemBase, [
Ci.nsIDOMXULSelectControlItemElement,
Ci.nsIDOMXULContainerItemElement,
]);
return MozMenuItemBase;
};
const MozMenuBaseMixin = Base => {
class MozMenuBase extends MozMenuItemBaseMixin(Base) {
set open(val) {
this.openMenu(val);
}
get open() {
return this.hasAttribute("open");
}
get itemCount() {
var menupopup = this.menupopup;
return menupopup ? menupopup.children.length : 0;
}
get menupopup() {
const XUL_NS =
for (
var child = this.firstElementChild;
child;
child = child.nextElementSibling
) {
if (child.namespaceURI == XUL_NS && child.localName == "menupopup") {
return child;
}
}
return null;
}
appendItem(aLabel, aValue) {
var menupopup = this.menupopup;
if (!menupopup) {
menupopup = this.ownerDocument.createXULElement("menupopup");
this.appendChild(menupopup);
}
var menuitem = this.ownerDocument.createXULElement("menuitem");
menuitem.setAttribute("label", aLabel);
menuitem.setAttribute("value", aValue);
return menupopup.appendChild(menuitem);
}
getIndexOfItem(aItem) {
var menupopup = this.menupopup;
if (menupopup) {
var items = menupopup.children;
var length = items.length;
for (var index = 0; index < length; ++index) {
if (items[index] == aItem) {
return index;
}
}
}
return -1;
}
getItemAtIndex(aIndex) {
var menupopup = this.menupopup;
if (!menupopup || aIndex < 0 || aIndex >= menupopup.children.length) {
return null;
}
return menupopup.children[aIndex];
}
}
MozXULElement.implementCustomInterface(MozMenuBase, [
Ci.nsIDOMXULContainerElement,
]);
return MozMenuBase;
};
// The <menucaption> element is used for rendering <html:optgroup> inside of <html:select>,
// See SelectParentHelper.sys.mjs.
class MozMenuCaption extends MozMenuBaseMixin(MozXULElement) {
static get inheritedAttributes() {
return {
".menu-iconic-left": "selected,disabled,checked",
".menu-iconic-icon": "src=image,validate,src",
".menu-iconic-text": "value=label,crop,highlightable",
".menu-iconic-highlightable-text": "text=label,crop,highlightable",
};
}
connectedCallback() {
this.textContent = "";
this.appendChild(
MozXULElement.parseXULToFragment(`
<hbox class="menu-iconic-left" align="center" pack="center" aria-hidden="true">
<image class="menu-iconic-icon" aria-hidden="true"></image>
</hbox>
<label class="menu-iconic-text" flex="1" crop="end" aria-hidden="true"></label>
<label class="menu-iconic-highlightable-text" crop="end" aria-hidden="true"></label>
`)
);
this.initializeAttributeInheritance();
}
}
customElements.define("menucaption", MozMenuCaption);
// In general, wait to render menus and menuitems inside menupopups
// until they are going to be visible:
window.addEventListener(
"popupshowing",
e => {
if (e.originalTarget.ownerDocument != document) {
return;
}
e.originalTarget.setAttribute("hasbeenopened", "true");
for (let el of e.originalTarget.querySelectorAll("menuitem, menu")) {
el.render();
}
},
{ capture: true }
);
class MozMenuItem extends MozMenuItemBaseMixin(MozXULElement) {
static get observedAttributes() {
return super.observedAttributes.concat("acceltext", "key");
}
attributeChangedCallback(name, oldValue, newValue) {
if (name == "acceltext") {
if (this._ignoreAccelTextChange) {
this._ignoreAccelTextChange = false;
} else {
this._accelTextIsDerived = false;
this._computeAccelTextFromKeyIfNeeded();
}
}
if (name == "key") {
this._computeAccelTextFromKeyIfNeeded();
}
super.attributeChangedCallback(name, oldValue, newValue);
}
static get inheritedAttributes() {
return {
".menu-iconic-text": "value=label,crop,accesskey,highlightable",
".menu-text": "value=label,crop,accesskey,highlightable",
".menu-iconic-highlightable-text":
"text=label,crop,accesskey,highlightable",
".menu-iconic-left": "selected,_moz-menuactive,disabled,checked",
".menu-iconic-icon":
"src=image,validate,triggeringprincipal=iconloadingprincipal",
".menu-iconic-accel": "value=acceltext",
".menu-accel": "value=acceltext",
};
}
static get iconicNoAccelFragment() {
// Add aria-hidden="true" on all DOM, since XULMenuAccessible handles accessibility here.
let frag = document.importNode(
MozXULElement.parseXULToFragment(`
<hbox class="menu-iconic-left" align="center" pack="center" aria-hidden="true">
<image class="menu-iconic-icon"/>
</hbox>
<label class="menu-iconic-text" flex="1" crop="end" aria-hidden="true"/>
<label class="menu-iconic-highlightable-text" crop="end" aria-hidden="true"/>
`),
true
);
Object.defineProperty(this, "iconicNoAccelFragment", { value: frag });
return frag;
}
static get iconicFragment() {
let frag = document.importNode(
MozXULElement.parseXULToFragment(`
<hbox class="menu-iconic-left" align="center" pack="center" aria-hidden="true">
<image class="menu-iconic-icon"/>
</hbox>
<label class="menu-iconic-text" flex="1" crop="end" aria-hidden="true"/>
<label class="menu-iconic-highlightable-text" crop="end" aria-hidden="true"/>
<hbox class="menu-accel-container" aria-hidden="true">
<label class="menu-iconic-accel"/>
</hbox>
`),
true
);
Object.defineProperty(this, "iconicFragment", { value: frag });
return frag;
}
static get plainFragment() {
let frag = document.importNode(
MozXULElement.parseXULToFragment(`
<label class="menu-text" crop="end" aria-hidden="true"/>
<hbox class="menu-accel-container" aria-hidden="true">
<label class="menu-accel"/>
</hbox>
`),
true
);
Object.defineProperty(this, "plainFragment", { value: frag });
return frag;
}
get isIconic() {
let type = this.getAttribute("type");
return (
type == "checkbox" ||
type == "radio" ||
this.classList.contains("menuitem-iconic")
);
}
get isMenulistChild() {
return this.matches("menulist > menupopup > menuitem");
}
get isInHiddenMenupopup() {
return this.matches("menupopup:not([hasbeenopened]) menuitem");
}
_computeAccelTextFromKeyIfNeeded() {
if (!this._accelTextIsDerived && this.getAttribute("acceltext")) {
return;
}
let accelText = (() => {
if (!document.contains(this)) {
return null;
}
let keyId = this.getAttribute("key");
if (!keyId) {
return null;
}
let key = document.getElementById(keyId);
if (!key) {
let msg =
`Key ${keyId} of menuitem ${this.getAttribute("label")} ` +
`could not be found`;
if (keyId.startsWith("ext-key-id-")) {
console.info(msg);
} else {
console.error(msg);
}
return null;
}
return imports.ShortcutUtils.prettifyShortcut(key);
})();
this._accelTextIsDerived = true;
// We need to ignore the next attribute change callback for acceltext, in
// order to not reenter here.
this._ignoreAccelTextChange = true;
if (accelText) {
this.setAttribute("acceltext", accelText);
} else {
this.removeAttribute("acceltext");
}
}
render() {
if (this.renderedOnce) {
return;
}
this.renderedOnce = true;
this.textContent = "";
if (this.isMenulistChild) {
this.append(this.constructor.iconicNoAccelFragment.cloneNode(true));
} else if (this.isIconic) {
this.append(this.constructor.iconicFragment.cloneNode(true));
} else {
this.append(this.constructor.plainFragment.cloneNode(true));
}
this._computeAccelTextFromKeyIfNeeded();
this.initializeAttributeInheritance();
}
connectedCallback() {
if (this.renderedOnce) {
this._computeAccelTextFromKeyIfNeeded();
}
// Eagerly render if we are being inserted into a menulist (since we likely need to
// size it), or into an already-opened menupopup (since we are already visible).
// Checking isConnectedAndReady is an optimization that will let us quickly skip
// non-menulists that are being connected during parse.
if (
this.isMenulistChild ||
(this.isConnectedAndReady && !this.isInHiddenMenupopup)
) {
this.render();
}
}
}
customElements.define("menuitem", MozMenuItem);
const isHiddenWindow =
document.documentURI == "chrome://browser/content/hiddenWindowMac.xhtml";
class MozMenu extends MozMenuBaseMixin(
MozElements.MozElementMixin(XULMenuElement)
) {
static get inheritedAttributes() {
return {
".menubar-text": "value=label,accesskey,crop",
".menu-iconic-text": "value=label,accesskey,crop,highlightable",
".menu-text": "value=label,accesskey,crop",
".menu-iconic-highlightable-text":
"text=label,crop,accesskey,highlightable",
".menubar-left": "src=image",
".menu-iconic-icon":
"src=image,triggeringprincipal=iconloadingprincipal,validate",
".menu-iconic-accel": "value=acceltext",
".menu-right": "_moz-menuactive,disabled",
".menu-accel": "value=acceltext",
};
}
get needsEagerRender() {
return (
this.isMenubarChild || this.isMenulistChild || !this.isInHiddenMenupopup
);
}
get isMenubarChild() {
return this.matches("menubar > menu");
}
get isMenulistChild() {
return this.matches("menulist > menupopup > menu");
}
get isInHiddenMenupopup() {
return this.matches("menupopup:not([hasbeenopened]) menu");
}
get isIconic() {
return this.classList.contains("menu-iconic");
}
get fragment() {
let { isMenubarChild, isIconic } = this;
let fragment = null;
// Add aria-hidden="true" on all DOM, since XULMenuAccessible handles accessibility here.
if (isMenubarChild && isIconic) {
if (!MozMenu.menubarIconicFrag) {
MozMenu.menubarIconicFrag = MozXULElement.parseXULToFragment(`
<image class="menubar-left" aria-hidden="true"/>
<label class="menubar-text" crop="end" aria-hidden="true"/>
`);
}
fragment = document.importNode(MozMenu.menubarIconicFrag, true);
}
if (isMenubarChild && !isIconic) {
if (!MozMenu.menubarFrag) {
MozMenu.menubarFrag = MozXULElement.parseXULToFragment(`
<label class="menubar-text" crop="end" aria-hidden="true"/>
`);
}
fragment = document.importNode(MozMenu.menubarFrag, true);
}
if (!isMenubarChild && isIconic) {
if (!MozMenu.normalIconicFrag) {
MozMenu.normalIconicFrag = MozXULElement.parseXULToFragment(`
<hbox class="menu-iconic-left" align="center" pack="center" aria-hidden="true">
<image class="menu-iconic-icon"/>
</hbox>
<label class="menu-iconic-text" flex="1" crop="end" aria-hidden="true"/>
<label class="menu-iconic-highlightable-text" crop="end" aria-hidden="true"/>
<hbox class="menu-accel-container" anonid="accel" aria-hidden="true">
<label class="menu-iconic-accel"/>
</hbox>
<hbox align="center" class="menu-right" aria-hidden="true">
<image/>
</hbox>
`);
}
fragment = document.importNode(MozMenu.normalIconicFrag, true);
}
if (!isMenubarChild && !isIconic) {
if (!MozMenu.normalFrag) {
MozMenu.normalFrag = MozXULElement.parseXULToFragment(`
<label class="menu-text" crop="end" aria-hidden="true"/>
<hbox class="menu-accel-container" anonid="accel" aria-hidden="true">
<label class="menu-accel"/>
</hbox>
<hbox align="center" class="menu-right" aria-hidden="true">
<image/>
</hbox>
`);
}
fragment = document.importNode(MozMenu.normalFrag, true);
}
return fragment;
}
render() {
// There are 2 main types of menus:
// (1) direct descendant of a menubar
// (2) all other menus
// There is also an "iconic" variation of (1) and (2) based on the class.
// To make this as simple as possible, we don't support menus being changed from one
// of these types to another after the initial DOM connection. It'd be possible to make
// this work by keeping track of the markup we prepend and then removing / re-prepending
// during a change, but it's not a feature we use anywhere currently.
if (this.renderedOnce) {
return;
}
this.renderedOnce = true;
// There will be a <menupopup /> already. Don't clear it out, just put our markup before it.
this.prepend(this.fragment);
this.initializeAttributeInheritance();
}
connectedCallback() {
// On OSX we will have a bunch of menus in the hidden window. They get converted
// into native menus based on the host attributes, so the inner DOM doesn't need
// to be created.
if (isHiddenWindow) {
return;
}
if (this.delayConnectedCallback()) {
return;
}
// Wait until we are going to be visible or required for sizing a popup.
if (!this.needsEagerRender) {
return;
}
this.render();
}
}
customElements.define("menu", MozMenu);
}