forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			491 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			491 lines
		
	
	
	
		
			15 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.
 | |
| {
 | |
|   let imports = {};
 | |
|   ChromeUtils.defineModuleGetter(
 | |
|     imports,
 | |
|     "ShortcutUtils",
 | |
|     "resource://gre/modules/ShortcutUtils.jsm"
 | |
|   );
 | |
| 
 | |
|   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 && parent.parentNode instanceof XULMenuElement) {
 | |
|           return parent.parentNode;
 | |
|         }
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       // nsIDOMXULContainerItemElement
 | |
|       get parentContainer() {
 | |
|         for (var parent = this.parentNode; parent; parent = parent.parentNode) {
 | |
|           if (parent instanceof XULMenuElement) {
 | |
|             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 =
 | |
|           "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 | |
| 
 | |
|         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.jsm.
 | |
|   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="right" aria-hidden="true"></label>
 | |
|       <label class="menu-iconic-highlightable-text" crop="right" 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="right" aria-hidden="true"/>
 | |
|       <label class="menu-iconic-highlightable-text" crop="right" 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="right" aria-hidden="true"/>
 | |
|       <label class="menu-iconic-highlightable-text" crop="right" 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="right" 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) {
 | |
|           Cu.reportError(
 | |
|             `Key ${keyId} of menuitem ${this.getAttribute("label")} ` +
 | |
|               `could not be found`
 | |
|           );
 | |
|           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="right" aria-hidden="true"/>
 | |
|         `);
 | |
|         }
 | |
|         fragment = document.importNode(MozMenu.menubarIconicFrag, true);
 | |
|       }
 | |
|       if (isMenubarChild && !isIconic) {
 | |
|         if (!MozMenu.menubarFrag) {
 | |
|           MozMenu.menubarFrag = MozXULElement.parseXULToFragment(`
 | |
|           <label class="menubar-text" crop="right" 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="right" aria-hidden="true"/>
 | |
|           <label class="menu-iconic-highlightable-text" crop="right" 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="right" 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);
 | |
| }
 | 
