forked from mirrors/gecko-dev
		
	Differential Revision: https://phabricator.services.mozilla.com/D17526 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			396 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			396 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.
 | 
						|
{
 | 
						|
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;
 | 
						|
    }
 | 
						|
 | 
						|
    this.init();
 | 
						|
    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 and XBL construction happens on
 | 
						|
   * it. Sometimes the XBL construction happens after the <radiogroup> has
 | 
						|
   * already been added to the DOM. This can happen due to asynchronous XBL
 | 
						|
   * construction (see Bug 1496137), or just due to normal DOM appending 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
 | 
						|
   */
 | 
						|
  radioChildConstructed(child) {
 | 
						|
    if (!this._radioChildren || !this._radioChildren.includes(child)) {
 | 
						|
      this.init();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  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;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return val;
 | 
						|
  }
 | 
						|
 | 
						|
  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;
 | 
						|
    }
 | 
						|
    return 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];
 | 
						|
    return 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);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return val;
 | 
						|
  }
 | 
						|
 | 
						|
  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");
 | 
						|
    }
 | 
						|
    return val;
 | 
						|
  }
 | 
						|
 | 
						|
  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;
 | 
						|
 | 
						|
    var radioChildren = [];
 | 
						|
 | 
						|
    if (this.hasChildNodes()) {
 | 
						|
      return this._radioChildren = [...this.querySelectorAll("radio")]
 | 
						|
        .filter(r => r.control == this);
 | 
						|
    }
 | 
						|
 | 
						|
    // We don't have child nodes.
 | 
						|
    const XUL_NS = "http://www.mozilla.org/keymaster/" +
 | 
						|
      "gatekeeper/there.is.only.xul";
 | 
						|
 | 
						|
    var elems = this.ownerDocument.getElementsByAttribute("group", this.id);
 | 
						|
    for (var i = 0; i < elems.length; i++) {
 | 
						|
      if ((elems[i].namespaceURI == XUL_NS) &&
 | 
						|
        (elems[i].localName == "radio")) {
 | 
						|
        radioChildren.push(elems[i]);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    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);
 | 
						|
}
 |