toggle.js

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

/**
 * Creates a clickable toggle that can be switched off and on.
 * <div><img src="https://www.minicomps.org/images/toggle.png"/></div>
 * @example
 * const panel = new Panel(document.body, 20, 20, 200, 200);
 * new Toggle(panel, 20, 20, "Toggle", false, event => console.log(event.target.toggled));
 * @extends Component
 */
export class Toggle extends Component {
  /**
   * Constructor
   * @param {HTMLElement} parent - The element to add this toggle to.
   * @param {number} x - The x position of the toggle. Default 0.
   * @param {number} y - The y position of the toggle. Default 0.
   * @param {string} label - The text for the toggle's label. Default empty string.
   * @param {boolean} toggled - The initial toggled state of the toggle. Default false.
   * @param {function} defaultHandler - A function that will handle the "click" event.
   */
  constructor(parent, x, y, label, toggled, defaultHandler) {
    super(parent, x, y);
    this._label = label;
    this._labelPosition = Toggle.labelPosition;
    this._onLabel = Toggle.onLabel;
    this._offLabel = Toggle.offLabel;

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

    this.setShowStateLabels(Toggle.showStateLabels);
    this.setSize(Toggle.width, Toggle.height);
    this.setToggled(toggled || false);
    this._updateLabel();
    this.addEventListener("click", defaultHandler);
    this._addToParent();
  }

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

  _createChildren() {
    this._setWrapperClass("MinimalToggle");
    this._wrapper.tabIndex = 0;
    this._textLabel = new Label(this._wrapper, 0, -15, this._label);
    this._stateLabel = new Label(this._wrapper, 5, 0, this._onLabel)
      .setAutosize(false);
    this._handle = this._createDiv(this._wrapper, "MinimalToggleHandle");
  }

  _createStyle() {
    const style = document.createElement("style");
    style.textContent = Style.toggle;
    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("keypress", this._onKeyPress);
  }

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

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

  _onKeyPress(event) {
    if (event.keyCode === 13 && this._enabled) {
      this._wrapper.click();
    }
  }

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

  _updateLabel() {
    if (this._labelPosition === "left") {
      this._textLabel.x = -this._textLabel.width - 5;
      this._textLabel.y = (this._height - this._textLabel.height) / 2;
    } else if (this._labelPosition === "top") {
      this._textLabel.x = 0;
      this._textLabel.y = -this._textLabel.height - 5;
    } else if (this._labelPosition === "right") {
      this._textLabel.x = this._width + 5;
      this._textLabel.y = (this._height - this._textLabel.height) / 2;
    } else {
      this._textLabel.x = 0;
      this._textLabel.y = this._height + 5;
    }
  }

  _updateToggle() {
    this._stateLabel.align = "center";
    this._stateLabel.width = this._width / 2;
    if (this._toggled) {
      this._stateLabel.x = 0;
      this._stateLabel.text = this._onLabel;
      this._handle.style.left = "50%";
    } else {
      this._stateLabel.x = this._width / 2;
      this._stateLabel.text = this._offLabel;
      this._handle.style.left = 0;
    }
  }

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

  /**
   * Adds a handler function for the "click" event on this toggle.
   * @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 current text of the label.
   */
  getLabel() {
    return this._label;
  }

  /**
   * @returns the position of the text label (left, right, top or bottom);
   */
  getLabelPosition() {
    return this._labelPosition;
  }

  /**
   * @returns the value of the state label when the toggle is toggled off.
   */
  getOffLabel() {
    return this._offLabel;
  }

  /**
   * @returns the value of the state label when the toggle is toggled on.
   */
  getOnLabel() {
    return this._onLabel;
  }

  /**
   * @returns whether or not the state labels will be shown.
   */
  getShowStateLabels() {
    return this._showStateLabels;
  }

  /**
   * @returns whether or not this toggle is toggled on.
   */
  getToggled() {
    return this._toggled;
  }

  setEnabled(enabled) {
    if (this._enabled === enabled) {
      return this;
    }
    super.setEnabled(enabled);
    this._textLabel.enable = enabled;
    if (this._enabled) {
      this._setWrapperClass("MinimalToggle");
      this._wrapper.tabIndex = 0;
    } else {
      this._setWrapperClass("MinimalToggleDisabled");
      this._wrapper.tabIndex = -1;
    }
    return this;
  }

  setHeight(height) {
    super.setHeight(height);
    this._stateLabel.height = height;
    return this;
  }

  /**
   * Sets the label of this toggle.
   * @param {string} label - The label to set on this toggle.
   * @returns this instance, suitable for chaining.
   */
  setLabel(label) {
    this._label = label;
    this._textLabel.text = label;
    this._updateLabel();
    return this;
  }

  /**
   * Sets the label position of the text label.
   * @param {string} position - The position to place the text lable: "top" (default), "left", "right" or "bottom".
   * @returns this instance, suitable for chaining.
   */
  setLabelPosition(position) {
    this._labelPosition = position;
    this._updateLabel();
    return this;
  }

  /**
   * Sets the value of the state label when the toggle is toggled off.
   * @param {string} label - the text of the label.
   * @returns This instance.
   */
  setOffLabel(label) {
    this._offLabel = label;
    if (!this._toggled) {
      this._stateLabel.text = label;
    }
    return this;
  }

  /**
   * Sets the value of the state label when the toggle is toggled on.
   * @param {string} label - the text of the label.
   * @returns This instance.
   */
  setOnLabel(label) {
    this._onLabel = label;
    if (this._toggled) {
      this._stateLabel.text = label;
    }
    return this;
  }

  /**
   * Sets whether or not the toggle will display state labels showing its on/off state.
   * @param {boolean} show - Whether or not to show the state labels.
   * @returns This instance.
   */
  setShowStateLabels(show) {
    this._showStateLabels = show;
    if (show) {
      this._stateLabel.style.visibility = "visible";
    } else {
      this._stateLabel.style.visibility = "hidden";
    }
    return this;
  }

  /**
   * Sets whether or not this toggle will be toggled (on).
   * @params {boolean} toggle - Whether this toggle will be toggled on or off.
   * @returns This instance, suitable for chaining.
   */
  setToggled(toggled) {
    this._toggled = toggled;
    this._updateToggle();
    return this;
  }

  setWidth(width) {
    super.setWidth(width);
    this._stateLabel.width = width - 10;
    return this;
  }

  /**
   * Toggles the state of the toggle between toggled and not toggled.
   * @returns This instance, suitable for chaining.
   */
  toggle() {
    this.setToggled(!this._toggled);
    return this;
  }

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

  /**
   * Gets and sets the text of the toggle's text label.
   */
  get label() {
    return this.getLabel();
  }
  set label(label) {
    this.setLabel(label);
  }

  /**
   * Gets and sets the position of the text label displayed on the toggle. Valid values are "top" (default), "left" and "bottom".
   */
  get labelPosition() {
    return this.getLabelPosition();
  }
  set labelPosition(pos) {
    this.setLabelPosition(pos);
  }

  /**
   * Gets and sets the text of the state label in the off position.
   */
  get offLabel() {
    return this.getOffLabel();
  }
  set offLabel(label) {
    this.setOffLabel(label);
  }

  /**
   * Gets and sets the text of the state label in the on position.
   */
  get onLabel() {
    return this.getOnLabel();
  }
  set onLabel(label) {
    this.setOnLabel(label);
  }

  /**
   * Gets and sets whether or not the state labels will be shown.
   */
  get showStateLabels() {
    return this.getShowStateLabels();
  }

  set showStateLabels(show) {
    this.setShowStateLabels(show);
  }

  /**
   * Sets and gets the toggled state of the toggle.
   */
  get toggled() {
    return this.getToggled();
  }
  set toggled(toggled) {
    this.setToggled(toggled);
  }
}

//////////////////////////////////
// DEFAULTS
//////////////////////////////////

/**
 * Default labelPosition value for all Toggles.
 */
Toggle.labelPosition = "top";
/**
 * Default showStateLabels value for all Toggles.
 */
Toggle.showStateLabels = true;
/**
 * Default onLabel value for all Toggles.
 */
Toggle.onLabel = "On";
/**
 * Default offLabel value for all Toggles.
 */
Toggle.offLabel = "Off";
/**
 * Default width for all Toggles.
 */
Toggle.width = 50;
/**
 * Default height for all Toggles.
 */
Toggle.height = 20;

customElements.define("minimal-toggle", Toggle);