430 lines
11 KiB
JavaScript
430 lines
11 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/. */
|
|
|
|
"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);
|
|
}
|