/* 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.
{
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const MozXULMenuElement = MozElements.MozElementMixin(XULMenuElement);
const MenuBaseControl = MozElements.BaseControlMixin(MozXULMenuElement);
class MozMenuList
extends MenuBaseControl {
constructor() {
super();
this.addEventListener(
"command",
event => {
if (event.target.parentNode.parentNode ==
this) {
this.selectedItem = event.target;
}
},
true
);
this.addEventListener(
"popupshowing", event => {
if (event.target.parentNode ==
this) {
this.activeChild =
null;
if (
this.selectedItem) {
// Not ready for auto-setting the active child in hierarchies yet.
// For now, only do this when the outermost menupopup opens.
this.activeChild =
this.mSelectedInternal;
}
}
});
this.addEventListener(
"keypress",
event => {
if (
event.defaultPrevented ||
event.altKey ||
event.ctrlKey ||
event.metaKey
) {
return;
}
if (
AppConstants.platform ===
"macosx" &&
!
this.open &&
(event.keyCode == KeyEvent.DOM_VK_UP ||
event.keyCode == KeyEvent.DOM_VK_DOWN)
) {
// This should open the menulist on macOS, see
// XULButtonElement::PostHandleEvent.
return;
}
if (
event.keyCode == KeyEvent.DOM_VK_UP ||
event.keyCode == KeyEvent.DOM_VK_DOWN ||
event.keyCode == KeyEvent.DOM_VK_PAGE_UP ||
event.keyCode == KeyEvent.DOM_VK_PAGE_DOWN ||
event.keyCode == KeyEvent.DOM_VK_HOME ||
event.keyCode == KeyEvent.DOM_VK_END ||
event.keyCode == KeyEvent.DOM_VK_BACK_SPACE ||
event.charCode > 0
) {
// Moving relative to an item: start from the currently selected item
this.activeChild =
this.mSelectedInternal;
if (
this.handleKeyPress(event)) {
this.activeChild.doCommand();
event.preventDefault();
}
}
},
{ mozSystemGroup:
true }
);
this.attachShadow({ mode:
"open" });
}
static get inheritedAttributes() {
return {
image:
"src=image",
"#label":
"value=label,crop,accesskey",
"#highlightable-label":
"text=label,crop,accesskey",
dropmarker:
"disabled,open",
};
}
static get markup() {
// Accessibility information of these nodes will be presented
// on XULComboboxAccessible generated from <menulist>;
// hide these nodes from the accessibility tree.
return `
<html:link href=
"chrome://global/skin/menulist.css" rel="stylesheet"/>
<hbox id=
"label-box" part=
"label-box" flex=
"1" role=
"none">
<image part=
"icon" role=
"none"/>
<label id=
"label" part=
"label" crop=
"end" flex=
"1" role=
"none"/>
<label id=
"highlightable-label" part=
"label" crop=
"end" flex=
"1" role=
"none"/>
</hbox>
<dropmarker part=
"dropmarker" type=
"menu" role=
"none"/>
<html:slot/>
`;
}
connectedCallback() {
if (
this.delayConnectedCallback()) {
return;
}
// Append and track children so they can be removed in disconnectedCallback.
if (!
this.hasAttribute(
"popuponly")) {
this.shadowRoot.appendChild(
this.constructor.fragment);
}
else {
this.shadowRoot.appendChild(document.createElement(
"slot"));
}
this._managedNodes = [];
if (
this.shadowRoot.children) {
let childElements = Array.from(
this.shadowRoot.children);
childElements.forEach(child => {
this._managedNodes.push(child);
});
}
if (!
this.hasAttribute(
"popuponly")) {
this.initializeAttributeInheritance();
}
this.setInitialSelection();
}
// nsIDOMXULSelectControlElement
set value(val) {
// if the new value is null, we still need to remove the old value
if (val ==
null) {
this.selectedItem = val;
return;
}
var arr =
null;
var popup =
this.menupopup;
if (popup) {
arr = popup.getElementsByAttribute(
"value", val);
}
if (arr && arr.item(0)) {
this.selectedItem = arr[0];
}
else {
this.selectedItem =
null;
this.setAttribute(
"value", val);
}
}
// nsIDOMXULSelectControlElement
get value() {
return this.getAttribute(
"value") ||
"";
}
// nsIDOMXULMenuListElement
set image(val) {
this.setAttribute(
"image", val);
}
// nsIDOMXULMenuListElement
get image() {
return this.getAttribute(
"image") ||
"";
}
// nsIDOMXULMenuListElement
get label() {
return this.getAttribute(
"label") ||
"";
}
set description(val) {
this.setAttribute(
"description", val);
}
get description() {
return this.getAttribute(
"description") ||
"";
}
// nsIDOMXULMenuListElement
set open(val) {
this.openMenu(val);
}
// nsIDOMXULMenuListElement
get open() {
return this.hasAttribute(
"open");
}
// nsIDOMXULSelectControlElement
get itemCount() {
return this.menupopup ?
this.menupopup.children.length : 0;
}
get menupopup() {
var popup =
this.firstElementChild;
while (popup && popup.localName !=
"menupopup") {
popup = popup.nextElementSibling;
}
return popup;
}
// nsIDOMXULSelectControlElement
set selectedIndex(val) {
var popup =
this.menupopup;
if (popup && 0 <= val) {
if (val < popup.children.length) {
this.selectedItem = popup.children[val];
}
}
else {
this.selectedItem =
null;
}
}
// nsIDOMXULSelectControlElement
get selectedIndex() {
// Quick and dirty. We won't deal with hierarchical menulists yet.
if (
!
this.selectedItem ||
!
this.mSelectedInternal.parentNode ||
this.mSelectedInternal.parentNode.parentNode !=
this
) {
return -1;
}
var children =
this.mSelectedInternal.parentNode.children;
var i = children.length;
while (i--) {
if (children[i] ==
this.mSelectedInternal) {
break;
}
}
return i;
}
// nsIDOMXULSelectControlElement
set selectedItem(val) {
var oldval =
this.mSelectedInternal;
if (oldval == val) {
return;
}
if (val && !
this.contains(val)) {
return;
}
if (oldval) {
oldval.removeAttribute(
"selected");
this.mAttributeObserver.disconnect();
}
this.mSelectedInternal = val;
let attributeFilter = [
"value",
"label",
"image",
"description"];
if (val) {
val.setAttribute(
"selected",
"true");
for (let attr of attributeFilter) {
if (val.hasAttribute(attr)) {
this.setAttribute(attr, val.getAttribute(attr));
}
else {
this.removeAttribute(attr);
}
}
this.mAttributeObserver =
new MutationObserver(
this.handleMutation.bind(
this)
);
this.mAttributeObserver.observe(val, { attributeFilter });
}
else {
for (let attr of attributeFilter) {
this.removeAttribute(attr);
}
}
var event = document.createEvent(
"Events");
event.initEvent(
"select",
true,
true);
this.dispatchEvent(event);
event = document.createEvent(
"Events");
event.initEvent(
"ValueChange",
true,
true);
this.dispatchEvent(event);
}
// nsIDOMXULSelectControlElement
get selectedItem() {
return this.mSelectedInternal;
}
setInitialSelection() {
if (
this.getAttribute(
"noinitialselection") ===
"true") {
return;
}
this.mSelectedInternal =
null;
this.mAttributeObserver =
null;
var popup =
this.menupopup;
if (popup) {
var arr = popup.getElementsByAttribute(
"selected",
"true");
var editable =
this.editable;
var value =
this.value;
if (!arr.item(0) && value) {
arr = popup.getElementsByAttribute(
editable ?
"label" :
"value",
value
);
}
if (arr.item(0)) {
this.selectedItem = arr[0];
}
else if (!editable) {
this.selectedIndex = 0;
}
}
}
contains(item) {
if (!item) {
return false;
}
var parent = item.parentNode;
return parent && parent.parentNode ==
this;
}
handleMutation(aRecords) {
for (let record of aRecords) {
let t = record.target;
if (t ==
this.mSelectedInternal) {
let attrName = record.attributeName;
switch (attrName) {
case "value":
case "label":
case "image":
case "description":
if (t.hasAttribute(attrName)) {
this.setAttribute(attrName, t.getAttribute(attrName));
}
else {
this.removeAttribute(attrName);
}
}
}
}
}
// nsIDOMXULSelectControlElement
getIndexOfItem(item) {
var popup =
this.menupopup;
if (popup) {
var children = popup.children;
var i = children.length;
while (i--) {
if (children[i] == item) {
return i;
}
}
}
return -1;
}
// nsIDOMXULSelectControlElement
getItemAtIndex(index) {
var popup =
this.menupopup;
if (popup) {
var children = popup.children;
if (index >= 0 && index < children.length) {
return children[index];
}
}
return null;
}
appendItem(label, value, description) {
if (!
this.menupopup) {
this.appendChild(MozXULElement.parseXULToFragment(`<menupopup />`));
}
var popup =
this.menupopup;
popup.appendChild(MozXULElement.parseXULToFragment(`<menuitem />`));
var item = popup.lastElementChild;
if (label !== undefined) {
item.setAttribute(
"label", label);
}
item.setAttribute(
"value", value);
if (description) {
item.setAttribute(
"description", description);
}
return item;
}
removeAllItems() {
this.selectedItem =
null;
var popup =
this.menupopup;
if (popup) {
this.removeChild(popup);
}
}
disconnectedCallback() {
if (
this.mAttributeObserver) {
this.mAttributeObserver.disconnect();
}
if (
this._managedNodes) {
this._managedNodes.forEach(node => node.remove());
this._managedNodes =
null;
}
}
}
MenuBaseControl.implementCustomInterface(MozMenuList, [
Ci.nsIDOMXULMenuListElement,
Ci.nsIDOMXULSelectControlElement,
]);
customElements.define(
"menulist", MozMenuList);
}