forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			567 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			567 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.
 | |
| (() => {
 | |
|   class MozRadiogroup extends MozElements.BaseControl {
 | |
|     constructor() {
 | |
|       super();
 | |
| 
 | |
|       this.addEventListener("mousedown", event => {
 | |
|         if (this.disabled) {
 | |
|           event.preventDefault();
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       /**
 | |
|        * keyboard navigation  Here's how keyboard navigation works in radio groups on Windows:
 | |
|        * The group takes 'focus'
 | |
|        * The user is then free to navigate around inside the group
 | |
|        * using the arrow keys. Accessing previous or following radio buttons
 | |
|        * is done solely through the arrow keys and not the tab button. Tab
 | |
|        * takes you to the next widget in the tab order
 | |
|        */
 | |
|       this.addEventListener("keypress", event => {
 | |
|         if (event.key != " " || event.originalTarget != this) {
 | |
|           return;
 | |
|         }
 | |
|         this.selectedItem = this.focusedItem;
 | |
|         this.selectedItem.doCommand();
 | |
|         // Prevent page from scrolling on the space key.
 | |
|         event.preventDefault();
 | |
|       });
 | |
| 
 | |
|       this.addEventListener("keypress", event => {
 | |
|         if (
 | |
|           event.keyCode != KeyEvent.DOM_VK_UP ||
 | |
|           event.originalTarget != this
 | |
|         ) {
 | |
|           return;
 | |
|         }
 | |
|         this.checkAdjacentElement(false);
 | |
|         event.stopPropagation();
 | |
|         event.preventDefault();
 | |
|       });
 | |
| 
 | |
|       this.addEventListener("keypress", event => {
 | |
|         if (
 | |
|           event.keyCode != KeyEvent.DOM_VK_LEFT ||
 | |
|           event.originalTarget != this
 | |
|         ) {
 | |
|           return;
 | |
|         }
 | |
|         // left arrow goes back when we are ltr, forward when we are rtl
 | |
|         this.checkAdjacentElement(
 | |
|           document.defaultView.getComputedStyle(this).direction == "rtl"
 | |
|         );
 | |
|         event.stopPropagation();
 | |
|         event.preventDefault();
 | |
|       });
 | |
| 
 | |
|       this.addEventListener("keypress", event => {
 | |
|         if (
 | |
|           event.keyCode != KeyEvent.DOM_VK_DOWN ||
 | |
|           event.originalTarget != this
 | |
|         ) {
 | |
|           return;
 | |
|         }
 | |
|         this.checkAdjacentElement(true);
 | |
|         event.stopPropagation();
 | |
|         event.preventDefault();
 | |
|       });
 | |
| 
 | |
|       this.addEventListener("keypress", event => {
 | |
|         if (
 | |
|           event.keyCode != KeyEvent.DOM_VK_RIGHT ||
 | |
|           event.originalTarget != this
 | |
|         ) {
 | |
|           return;
 | |
|         }
 | |
|         // right arrow goes forward when we are ltr, back when we are rtl
 | |
|         this.checkAdjacentElement(
 | |
|           document.defaultView.getComputedStyle(this).direction == "ltr"
 | |
|         );
 | |
|         event.stopPropagation();
 | |
|         event.preventDefault();
 | |
|       });
 | |
| 
 | |
|       /**
 | |
|        * set a focused attribute on the selected item when the group
 | |
|        * receives focus so that we can style it as if it were focused even though
 | |
|        * it is not (Windows platform behaviour is for the group to receive focus,
 | |
|        * not the item
 | |
|        */
 | |
|       this.addEventListener("focus", event => {
 | |
|         if (event.originalTarget != this) {
 | |
|           return;
 | |
|         }
 | |
|         this.setAttribute("focused", "true");
 | |
|         if (this.focusedItem) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         var val = this.selectedItem;
 | |
|         if (!val || val.disabled || val.hidden || val.collapsed) {
 | |
|           var children = this._getRadioChildren();
 | |
|           for (var i = 0; i < children.length; ++i) {
 | |
|             if (
 | |
|               !children[i].hidden &&
 | |
|               !children[i].collapsed &&
 | |
|               !children[i].disabled
 | |
|             ) {
 | |
|               val = children[i];
 | |
|               break;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         this.focusedItem = val;
 | |
|       });
 | |
| 
 | |
|       this.addEventListener("blur", event => {
 | |
|         if (event.originalTarget != this) {
 | |
|           return;
 | |
|         }
 | |
|         this.removeAttribute("focused");
 | |
|         this.focusedItem = null;
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     connectedCallback() {
 | |
|       if (this.delayConnectedCallback()) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // When this is called via `connectedCallback` there are two main variations:
 | |
|       //   1) The radiogroup and radio children are defined in markup.
 | |
|       //   2) We are appending a DocumentFragment
 | |
|       // In both cases, the <radiogroup> connectedCallback fires first. But in (2),
 | |
|       // the children <radio>s won't be upgraded yet, so r.control will be undefined.
 | |
|       // To avoid churn in this case where we would have to reinitialize the list as each
 | |
|       // child radio gets upgraded as a result of init(), ignore the resulting calls
 | |
|       // to radioAttached.
 | |
|       this.ignoreRadioChildConstruction = true;
 | |
|       this.init();
 | |
|       this.ignoreRadioChildConstruction = false;
 | |
|       if (!this.value) {
 | |
|         this.selectedIndex = 0;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     init() {
 | |
|       this._radioChildren = null;
 | |
| 
 | |
|       if (this.getAttribute("disabled") == "true") {
 | |
|         this.disabled = true;
 | |
|       }
 | |
| 
 | |
|       var children = this._getRadioChildren();
 | |
|       var length = children.length;
 | |
|       for (var i = 0; i < length; i++) {
 | |
|         if (children[i].getAttribute("selected") == "true") {
 | |
|           this.selectedIndex = i;
 | |
|           return;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       var value = this.value;
 | |
|       if (value) {
 | |
|         this.value = value;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Called when a new <radio> gets added to an already connected radiogroup.
 | |
|      * This can happen due to DOM getting appended after the <radiogroup> is created.
 | |
|      * When this happens, reinitialize the UI if necessary to make sure the state is
 | |
|      * consistent.
 | |
|      *
 | |
|      * @param {DOMNode} child
 | |
|      *                  The <radio> element that got added
 | |
|      */
 | |
|     radioAttached(child) {
 | |
|       if (this.ignoreRadioChildConstruction) {
 | |
|         return;
 | |
|       }
 | |
|       if (!this._radioChildren || !this._radioChildren.includes(child)) {
 | |
|         this.init();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Called when a new <radio> gets removed from a radio group.
 | |
|      *
 | |
|      * @param {DOMNode} child
 | |
|      *                  The <radio> element that got removed
 | |
|      */
 | |
|     radioUnattached(child) {
 | |
|       // Just invalidate the cache, next time it's fetched it'll get rebuilt.
 | |
|       this._radioChildren = null;
 | |
|     }
 | |
| 
 | |
|     set value(val) {
 | |
|       this.setAttribute("value", val);
 | |
|       var children = this._getRadioChildren();
 | |
|       for (var i = 0; i < children.length; i++) {
 | |
|         if (String(children[i].value) == String(val)) {
 | |
|           this.selectedItem = children[i];
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     get value() {
 | |
|       return this.getAttribute("value");
 | |
|     }
 | |
| 
 | |
|     set disabled(val) {
 | |
|       if (val) {
 | |
|         this.setAttribute("disabled", "true");
 | |
|       } else {
 | |
|         this.removeAttribute("disabled");
 | |
|       }
 | |
|       var children = this._getRadioChildren();
 | |
|       for (var i = 0; i < children.length; ++i) {
 | |
|         children[i].disabled = val;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     get disabled() {
 | |
|       if (this.getAttribute("disabled") == "true") {
 | |
|         return true;
 | |
|       }
 | |
|       var children = this._getRadioChildren();
 | |
|       for (var i = 0; i < children.length; ++i) {
 | |
|         if (
 | |
|           !children[i].hidden &&
 | |
|           !children[i].collapsed &&
 | |
|           !children[i].disabled
 | |
|         ) {
 | |
|           return false;
 | |
|         }
 | |
|       }
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     get itemCount() {
 | |
|       return this._getRadioChildren().length;
 | |
|     }
 | |
| 
 | |
|     set selectedIndex(val) {
 | |
|       this.selectedItem = this._getRadioChildren()[val];
 | |
|     }
 | |
| 
 | |
|     get selectedIndex() {
 | |
|       var children = this._getRadioChildren();
 | |
|       for (var i = 0; i < children.length; ++i) {
 | |
|         if (children[i].selected) {
 | |
|           return i;
 | |
|         }
 | |
|       }
 | |
|       return -1;
 | |
|     }
 | |
| 
 | |
|     set selectedItem(val) {
 | |
|       var focused = this.getAttribute("focused") == "true";
 | |
|       var alreadySelected = false;
 | |
| 
 | |
|       if (val) {
 | |
|         alreadySelected = val.getAttribute("selected") == "true";
 | |
|         val.setAttribute("focused", focused);
 | |
|         val.setAttribute("selected", "true");
 | |
|         this.setAttribute("value", val.value);
 | |
|       } else {
 | |
|         this.removeAttribute("value");
 | |
|       }
 | |
| 
 | |
|       // uncheck all other group nodes
 | |
|       var children = this._getRadioChildren();
 | |
|       var previousItem = null;
 | |
|       for (var i = 0; i < children.length; ++i) {
 | |
|         if (children[i] != val) {
 | |
|           if (children[i].getAttribute("selected") == "true") {
 | |
|             previousItem = children[i];
 | |
|           }
 | |
| 
 | |
|           children[i].removeAttribute("selected");
 | |
|           children[i].removeAttribute("focused");
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       var event = document.createEvent("Events");
 | |
|       event.initEvent("select", false, true);
 | |
|       this.dispatchEvent(event);
 | |
| 
 | |
|       if (focused) {
 | |
|         if (alreadySelected) {
 | |
|           // Notify accessibility that this item got focus.
 | |
|           event = document.createEvent("Events");
 | |
|           event.initEvent("DOMMenuItemActive", true, true);
 | |
|           val.dispatchEvent(event);
 | |
|         } else {
 | |
|           // Only report if actual change
 | |
|           if (val) {
 | |
|             // Accessibility will fire focus for this.
 | |
|             event = document.createEvent("Events");
 | |
|             event.initEvent("RadioStateChange", true, true);
 | |
|             val.dispatchEvent(event);
 | |
|           }
 | |
| 
 | |
|           if (previousItem) {
 | |
|             event = document.createEvent("Events");
 | |
|             event.initEvent("RadioStateChange", true, true);
 | |
|             previousItem.dispatchEvent(event);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     get selectedItem() {
 | |
|       var children = this._getRadioChildren();
 | |
|       for (var i = 0; i < children.length; ++i) {
 | |
|         if (children[i].selected) {
 | |
|           return children[i];
 | |
|         }
 | |
|       }
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     set focusedItem(val) {
 | |
|       if (val) {
 | |
|         val.setAttribute("focused", "true");
 | |
|         // Notify accessibility that this item got focus.
 | |
|         let event = document.createEvent("Events");
 | |
|         event.initEvent("DOMMenuItemActive", true, true);
 | |
|         val.dispatchEvent(event);
 | |
|       }
 | |
| 
 | |
|       // unfocus all other group nodes
 | |
|       var children = this._getRadioChildren();
 | |
|       for (var i = 0; i < children.length; ++i) {
 | |
|         if (children[i] != val) {
 | |
|           children[i].removeAttribute("focused");
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     get focusedItem() {
 | |
|       var children = this._getRadioChildren();
 | |
|       for (var i = 0; i < children.length; ++i) {
 | |
|         if (children[i].getAttribute("focused") == "true") {
 | |
|           return children[i];
 | |
|         }
 | |
|       }
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     checkAdjacentElement(aNextFlag) {
 | |
|       var currentElement = this.focusedItem || this.selectedItem;
 | |
|       var i;
 | |
|       var children = this._getRadioChildren();
 | |
|       for (i = 0; i < children.length; ++i) {
 | |
|         if (children[i] == currentElement) {
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|       var index = i;
 | |
| 
 | |
|       if (aNextFlag) {
 | |
|         do {
 | |
|           if (++i == children.length) {
 | |
|             i = 0;
 | |
|           }
 | |
|           if (i == index) {
 | |
|             break;
 | |
|           }
 | |
|         } while (
 | |
|           children[i].hidden ||
 | |
|           children[i].collapsed ||
 | |
|           children[i].disabled
 | |
|         );
 | |
|         // XXX check for display/visibility props too
 | |
| 
 | |
|         this.selectedItem = children[i];
 | |
|         children[i].doCommand();
 | |
|       } else {
 | |
|         do {
 | |
|           if (i == 0) {
 | |
|             i = children.length;
 | |
|           }
 | |
|           if (--i == index) {
 | |
|             break;
 | |
|           }
 | |
|         } while (
 | |
|           children[i].hidden ||
 | |
|           children[i].collapsed ||
 | |
|           children[i].disabled
 | |
|         );
 | |
|         // XXX check for display/visibility props too
 | |
| 
 | |
|         this.selectedItem = children[i];
 | |
|         children[i].doCommand();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _getRadioChildren() {
 | |
|       if (this._radioChildren) {
 | |
|         return this._radioChildren;
 | |
|       }
 | |
| 
 | |
|       let radioChildren = [];
 | |
|       if (this.hasChildNodes()) {
 | |
|         for (let radio of this.querySelectorAll("radio")) {
 | |
|           customElements.upgrade(radio);
 | |
|           if (radio.control == this) {
 | |
|             radioChildren.push(radio);
 | |
|           }
 | |
|         }
 | |
|       } else {
 | |
|         const XUL_NS =
 | |
|           "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 | |
|         for (let radio of this.ownerDocument.getElementsByAttribute(
 | |
|           "group",
 | |
|           this.id
 | |
|         )) {
 | |
|           if (radio.namespaceURI == XUL_NS && radio.localName == "radio") {
 | |
|             customElements.upgrade(radio);
 | |
|             radioChildren.push(radio);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       return (this._radioChildren = radioChildren);
 | |
|     }
 | |
| 
 | |
|     getIndexOfItem(item) {
 | |
|       return this._getRadioChildren().indexOf(item);
 | |
|     }
 | |
| 
 | |
|     getItemAtIndex(index) {
 | |
|       var children = this._getRadioChildren();
 | |
|       return index >= 0 && index < children.length ? children[index] : null;
 | |
|     }
 | |
| 
 | |
|     appendItem(label, value) {
 | |
|       var radio = document.createXULElement("radio");
 | |
|       radio.setAttribute("label", label);
 | |
|       radio.setAttribute("value", value);
 | |
|       this.appendChild(radio);
 | |
|       return radio;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   MozXULElement.implementCustomInterface(MozRadiogroup, [
 | |
|     Ci.nsIDOMXULSelectControlElement,
 | |
|     Ci.nsIDOMXULRadioGroupElement,
 | |
|   ]);
 | |
| 
 | |
|   customElements.define("radiogroup", MozRadiogroup);
 | |
| 
 | |
|   class MozRadio extends MozElements.BaseText {
 | |
|     static get markup() {
 | |
|       return `
 | |
|       <image class="radio-check"></image>
 | |
|       <hbox class="radio-label-box" align="center" flex="1">
 | |
|         <image class="radio-icon"></image>
 | |
|         <label class="radio-label" flex="1"></label>
 | |
|       </hbox>
 | |
|       `;
 | |
|     }
 | |
| 
 | |
|     static get inheritedAttributes() {
 | |
|       return {
 | |
|         ".radio-check": "disabled,selected",
 | |
|         ".radio-label": "text=label,accesskey,crop",
 | |
|         ".radio-icon": "src",
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     constructor() {
 | |
|       super();
 | |
|       this.addEventListener("click", event => {
 | |
|         if (!this.disabled) {
 | |
|           this.control.selectedItem = this;
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       this.addEventListener("mousedown", event => {
 | |
|         if (!this.disabled) {
 | |
|           this.control.focusedItem = this;
 | |
|         }
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     connectedCallback() {
 | |
|       if (this.delayConnectedCallback()) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (!this.connectedOnce) {
 | |
|         this.connectedOnce = true;
 | |
|         // If the caller didn't provide custom content then append the default:
 | |
|         if (!this.firstElementChild) {
 | |
|           this.appendChild(this.constructor.fragment);
 | |
|           this.initializeAttributeInheritance();
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       var control = (this._control = this.control);
 | |
|       if (control) {
 | |
|         control.radioAttached(this);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     disconnectedCallback() {
 | |
|       if (this.control) {
 | |
|         this.control.radioUnattached(this);
 | |
|       }
 | |
|       this._control = null;
 | |
|     }
 | |
| 
 | |
|     set value(val) {
 | |
|       this.setAttribute("value", val);
 | |
|     }
 | |
| 
 | |
|     get value() {
 | |
|       return this.getAttribute("value");
 | |
|     }
 | |
| 
 | |
|     get selected() {
 | |
|       return this.hasAttribute("selected");
 | |
|     }
 | |
| 
 | |
|     get radioGroup() {
 | |
|       return this.control;
 | |
|     }
 | |
| 
 | |
|     get control() {
 | |
|       if (this._control) {
 | |
|         return this._control;
 | |
|       }
 | |
| 
 | |
|       var radiogroup = this.closest("radiogroup");
 | |
|       if (radiogroup) {
 | |
|         return radiogroup;
 | |
|       }
 | |
| 
 | |
|       var group = this.getAttribute("group");
 | |
|       if (!group) {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       var parent = this.ownerDocument.getElementById(group);
 | |
|       if (!parent || parent.localName != "radiogroup") {
 | |
|         parent = null;
 | |
|       }
 | |
|       return parent;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   MozXULElement.implementCustomInterface(MozRadio, [
 | |
|     Ci.nsIDOMXULSelectControlItemElement,
 | |
|   ]);
 | |
|   customElements.define("radio", MozRadio);
 | |
| })();
 | 
