radiobutton.js

import { Component } from "./component.js";
import { Label } from "./label.js";
import { RadioButtonGroup } from "./radiobuttongroup.js";
import { Style } from "./style.js";

/**
 * Creates a clickable radio button with a label that can be selected by clicking. Radio buttons are assigned to a group and only one radio button in a group will be selected at any one time.
 * You can get the text of the currently checked radio button in a group by calling RadioButtonGroup.getValueForGroup(group).
 * <div><img src="https://www.minicomps.org/images/radiobutton.png"/></div>
 * @example
 * const panel = new Panel(document.body, 20, 20, 200, 200);
 * const vbox = new VBox(panel, 20, 20, 10);
 * new RadioButton(vbox, 0, 0, "group", "Option 1", true, update);
 * new RadioButton(vbox, 0, 0, "group", "Option 2", false, update);
 * new RadioButton(vbox, 0, 0, "group", "Option 3", false, update);
 * function update() {
 *   console.log(RadioButtonGroup.getValueForGroup("group"));
 * }
 * @extends Component
 */
export class RadioButton extends Component {
  /**
   * Constructor
   * @param {HTMLElement} parent - The element to add this radio button to.
   * @param {number} x - The x position of the radio button. Default 0.
   * @param {number} y - The y position of the radio button. Default 0.
   * @param {string} group - The group this radio button belongs to. Default "group".
   * @param {string} label - The label of the radio button. Default empty string.
   * @param {boolean} checked - The initial checked state of the radio button. Default false.
   * @param {function} defaultHandler - A function that will handle the "click" event.
   */
  constructor(parent, x, y, group, label, checked, defaultHandler) {
    super(parent, x, y);
    RadioButtonGroup._addToGroup(group, this);
    this._group = group || "group";
    this._label = label || "";

    this._createStyle();
    this._createChildren();
    this._createListeners();

    this.setSize(100, 10);
    this.setChecked(checked || false);
    this.addEventListener("click", defaultHandler);
    this._addToParent();
    this._updateWidth();
  }

  //////////////////////////////////
  // Core
  //////////////////////////////////

  _createChildren() {
    this._setWrapperClass("MinimalRadioButton");
    this._wrapper.tabIndex = 0;
    this._check = this._createDiv(this._wrapper, "MinimalRadioButtonCheck");
    this._textLabel = new Label(this._wrapper, 15, 0, this._label);
  }

  _createStyle() {
    const style = document.createElement("style");
    style.textContent = Style.radiobutton;
    this.shadowRoot.append(style);
  }

  _createListeners() {
    this._onClick = this._onClick.bind(this);
    this._onKeyPress = this._onKeyPress.bind(this);
    this._wrapper.addEventListener("click", this._onClick);
    this._wrapper.addEventListener("keydown", this._onKeyPress);
  }

  //////////////////////////////////
  // Handlers
  //////////////////////////////////

  _onClick(event) {
    event.stopPropagation();
    if (this._enabled) {
      this.setChecked(true);
      this.dispatchEvent(new CustomEvent("click", { detail: this._label }));
    }
  }

  _onKeyPress(event) {
    if (event.keyCode === 13 && this._enabled) {
      // enter
      this._wrapper.click();
    } else if (event.keyCode === 40) {
      // down
      event.preventDefault();
      RadioButtonGroup._getNextInGroup(this._group, this).focus();
    } else if (event.keyCode === 38) {
      // up
      event.preventDefault();
      RadioButtonGroup._getPrevInGroup(this._group, this).focus();
    }
  }

  //////////////////////////////////
  // Private
  //////////////////////////////////

  focus() {
    if (this._enabled) {
      this._wrapper.focus();
    }
  }

  _updateCheckStyle() {
    let className = this._checked
      ? "MinimalRadioButtonCheckChecked "
      : "MinimalRadioButtonCheck ";

    if (!this._enabled) {
      className += "MinimalRadioButtonCheckDisabled";
    }
    this._check.setAttribute("class", className);
    this._check.setAttribute("class", className);
    if (this._enabled) {
      this._setWrapperClass("MinimalRadioButton");
    } else {
      this._setWrapperClass("MinimalRadioButtonDisabled");
    }
  }

  _updateWidth() {
    this.style.width = this._textLabel.x + this._textLabel.width + "px";
  }

  //////////////////////////////////
  // Public
  //////////////////////////////////

  /**
   * Adds a handler function for the "click" event on this radio button.
   * @param {function} handler - A function that will handle the "click" event.
   * @returns This instance, suitable for chaining.
   */
  addHandler(handler) {
    this.addEventListener("click", handler);
    return this;
  }

  /**
   * Automatically changes the value of a property on a target object with the main value of this component changes.
   * @param {object} target - The target object to change.
   * @param {string} prop - The string name of a property on the target object.
   * @return This instance, suitable for chaining.
   */
  bind(target, prop) {
    this.addEventListener("click", event => {
      target[prop] = event.detail;
    });
    return this;
  }

  /**
   * @returns the text in the label.
   */
  getLabel() {
    return this._label;
  }

  /**
   * @returns whether or not the radiobutton is checked.
   */
  getChecked() {
    return this._checked;
  }

  getWidth() {
    return this._textLabel.x + this._textLabel.width;
  }

  /**
   * Sets the checked state of this radio button.
   * @params {boolean} checked - Whether or not this radio button will be checked.
   * @returns This instance, suitable for chaining.
   */
  setChecked(checked) {
    if (checked) {
      RadioButtonGroup._clearGroup(this._group);
    }
    this._checked = checked;
    this._updateCheckStyle();
    return this;
  }

  setEnabled(enabled) {
    if (this._enabled === enabled) {
      return this;
    }
    super.super(enabled);
    this._updateCheckStyle();
    this._textLabel.enabled = enabled;
    if (this._enabled) {
      this._wrapper.tabIndex = 0;
    } else {
      this._wrapper.tabIndex = -1;
    }
    return this;
  }

  setHeight(height) {
    super.setHeight(height);
    this._textLabel.height = height;
    this._check.style.top = Math.round((this._height - 10) / 2) + "px";
    return this;
  }
  /**
   * Sets the label of this radio button.
   * @param {string} label - The label to set on this radio button.
   * @returns this instance, suitable for chaining.
   */
  setLabel(label) {
    this._label = label;
    this._textLabel.text = label;
    this._updateWidth();
    return this;
  }

  /**
   * Does nothing. Width is set automatically based on the size of the check and label.
   * @returns This instance.
   */
  setWidth() {
    return this;
  }

  //////////////////////////////////
  // Getters/Setters
  // alphabetical. getter first.
  //////////////////////////////////

  /**
   * Sets and gets the checked state of the radio button.
   */
  get checked() {
    return this.getChecked();
  }
  set checked(checked) {
    this.setChecked(checked);
  }

  /**
   * Sets and gets the label shown in the radio button's label.
   */
  get label() {
    return this.getLabel();
  }
  set label(label) {
    this.setLabel(label);
  }
}
customElements.define("minimal-radiobutton", RadioButton);