as-toggle.js

import {Component_A} from './basic/as-component.js';
import {FP_Toggle_A} from './fp-ext/fp-toggle-ext.js';
import {formatOptionsString, initDynamicOptions, generateOptionsString} from './utils/utils.js';
import {ErrorCode, ExceptionIdMap} from '../exception/exceptionDesc.js';

/**
 * @typedef TComponents.ToggleProps
 * @prop {object} [options] Additional options for the toggle component.
 * Items in this object include:
 * - **responsive** (boolean, default: false): Whether the toggle group should be responsive.
 * @prop {string} [tips] Tooltip text for the component.
 * @prop {Function} [onCreated] Function to be called when the toggle component is created.
 * @prop {Function} [onMounted] Function to be called when the toggle component is mounted.
 * @prop {Function} [onChange] Function to be called when the toggle state changes.
 * @prop {string} [position] CSS position property.
 * @prop {number} [width] Width of the component.
 * @prop {number} [height] Height of the component.
 * @prop {number} [top] Top position of the component.
 * @prop {number} [left] Left position of the component.
 * @prop {number} [rotation] Rotation angle of the component.
 * @prop {number} [zIndex] Z-index of the component.
 * @prop {string} [color] Text color of the toggle items.
 * @prop {object} [font] Font configuration for the toggle labels.
 * This object controls text appearance:
 * - **fontSize** (number, default: 12): Font size in pixels.
 * - **fontFamily** (string, default: 'Segoe UI'): Font family name.
 * - **style** (object): Font style configuration, containing `fontStyle`, `fontWeight`, `textDecoration`.
 * @prop {string} [toggledColor] Background color of the toggled items.
 * @prop {boolean} [multi] Whether to allow multiple selections.
 * @prop {string} [optionItems] Formatted string representing the toggle options.
 * @prop {object} [optionConfig] Configuration for dynamic options.
 * This object controls how options are provided:
 * - **mode** (string, default: 'fixed'): Options mode, e.g. 'fixed', 'sync', 'initialize'.
 * - **type** (string): Type of the options data source.
 * - **isHidden** (boolean, default: false): Whether this option configuration is hidden.
 * - **variablePath** (string): Variable path used when options are dynamic.
 * @prop {Array<Object>} [optionIcons] Icon configuration for each option.
 * @prop {string[]} [optionActions] Action configuration for each option.
 * @prop {string} [defaultState] Default state of the component.
 * @memberof TComponents
 */

/**
 * @ignore
 */
const logModule = 'as-toggle-a';

/**
 * @description Toggle_A element.
 * This class focuses on the specific properties of the Toggle_A component.
 * Since it inherits from Accessor_A, all basic properties (e.g., height, width) are available but documented in the Accessor_A part.
 * @class TComponents.Toggle_A
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} parent - HTML element that is going to be the parent of the component
 * @param {TComponents.ToggleProps} props
 * @example
 * const toggle = new TComponents.Toggle_A(document.body, {
 *   position: 'absolute',
 *   zIndex: 1000,
 * });
 *
 * // Render the component.
 * await toggle.render();
 */
export class Toggle_A extends Component_A {
  constructor(parent, props = {}) {
    super(parent, props);

    /**
     * @instance
     * @private
     * @type {TComponents.ToggleProps}
     */
    this._props;
    this._toggle = new FP_Toggle_A();
    this._changed = [];
  }

  /**
   * @description Returns the default values of class properties (excluding parent properties).
   * @member TComponents.Toggle_A#defaultProps
   * @method
   * @protected
   * @returns {TComponents.ToggleProps}
   */
  defaultProps() {
    return {
      options: {
        responsive: false,
      },
      tips: '',
      // life cycle
      onCreated: '',
      onMounted: '',
      onDispose: '',
      onChange: '',
      // X/Y/W/H/B/R
      position: 'static',
      width: 320,
      height: 32,
      top: 0,
      left: 0,
      rotation: 0,
      zIndex: 0,
      // font color
      color: '#000000',
      // font style
      font: {
        fontSize: 12,
        fontFamily: 'Segoe UI',
        style: {
          fontStyle: 'normal',
          fontWeight: 'normal',
          textDecoration: 'none',
        },
      },
      // toggled background color
      toggledColor: '#3366ff',
      // Data
      multi: false, // if allow multi selection
      optionItems: `text1|value1;text2|value2;text3|value3`,
      optionConfig: {
        mode: 'fixed', //fixed, sync,initialize
        type: '',
        isHidden: false,
        variablePath: '',
      },
      optionIcons: [
        {icon: '', toggledIcon: ''},
        {icon: '', toggledIcon: ''},
        {icon: '', toggledIcon: ''},
      ],
      optionActions: ['', '', ''],
      defaultState: 'show_enable',
    };
  }

  /**
   * @description  Initializes the toggle component.
   * @member TComponents.Toggle_A#onInit
   * @method
   * @async
   * @returns {Promise<void>}
   */
  async onInit() {}

  /**
   * @description there are something need to do after render once
   * @member TComponents.Toggle_A#afterRenderOnce
   * @method
   * @protected
   * @returns {void}
   */
  afterRenderOnce() {
    initDynamicOptions(this._props.optionConfig, (data) => {
      this.setProps({
        optionItems: generateOptionsString(data),
      });
    });
  }

  /**
   * @description  Renders the toggle component.
   * @member TComponents.Toggle_A#onRender
   * @method
   * @async
   * @throws {Error} If an error occurs during rendering.
   * @returns {Promise<void>}
   */
  async onRender() {
    try {
      this.removeAllEventListeners();

      this._toggle._multi = this._props.multi;
      this._toggle._model = this.generateModel();
      this._toggle._color = this._props.color;
      this._toggle._font = this._props.font;
      this._toggle._toggledColor = this._props.toggledColor;
      this._toggle._enabled = this.enabled;
      this._toggle._onselection = this._cbOnChange.bind(this);

      // update toggled color class
      document.documentElement.style.setProperty('--toggled-bg-color', this._props.toggledColor);

      const toggleContainer = this.find('.tc-toggle');
      if (toggleContainer) this._toggle.attachToElement(toggleContainer);
      this._addTips();
    } catch (e) {
      // Runtime errors: write specific content to the log, throw error code
      Logger.e(
        logModule,
        ErrorCode.FailedToRunOnRender,
        `Error happens on onRender of Toggle_A component ${this.compId}.`,
        e,
      );
      throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
    }
  }

  /**
   * @description Handles the change event for the toggle element.
   * @member TComponents.Toggle_A~_cbOnChange
   * @method
   * @private
   * @async
   * @param {object} e - The change parameters.
   * @returns {void}
   */
  async _cbOnChange({changed = [], all = [], item = {}, index = -1}) {
    if (this.enabled) {
      this._changed = changed;
      if (item.action) {
        try {
          var action = Component_A.genFuncTemplate(item.action, this);
          action && (await action());
        } catch (e) {
          Component_A.popupEventError(e, `Item ${index} Action`, logModule);
          return;
        }
      }

      if (this._props.onChange) {
        try {
          var fn = Component_A.genFuncTemplate(this._props.onChange, this);
          fn && (await fn());
        } catch (e) {
          Component_A.popupEventError(e, 'onChange', logModule);
        }
      }
    }
  }

  /**
   * @description Generate toggle button model with value parameter
   * @member TComponents.Toggle_A#generateModel
   * @method
   * @returns {any[]} The generated model array.
   */
  generateModel() {
    let model = [];
    for (let i = 0; i < this.optionItems.length; i++) {
      model.push({
        text: this.optionItems[i].text,
        value: this.optionItems[i].value,
        icon: this._props.optionIcons[i] && this._props.optionIcons[i].icon ? this._props.optionIcons[i].icon : '',
        toggledIcon:
          this._props.optionIcons[i] && this._props.optionIcons[i].toggledIcon
            ? this._props.optionIcons[i].toggledIcon
            : '',
        action: this._props.optionActions[i] ? this._props.optionActions[i] : '',
      });
    }
    return model;
  }

  /**
   * @description Set the toggle status of the specifc index button.
   * @member {number} TComponents.Toggle_A#setToggled
   * @method
   * @param {number} index the index of target button
   * @param {boolean} toggled null: invert the status of target button; true/false: set target button to true/false
   * @param {boolean} fireCallback True if expecting to trigger onClick event
   * @returns {void}
   */
  setToggled(index, toggled = null, fireCallback = false) {
    if (!this.enabled) return;
    if (index < 0 || index >= this._toggle._toggleState.length) {
      Logger.w(logModule, ErrorCode.FailedToSetNum, `Invalid index ${index} for toggle component ${this.compId}.`);
      throw new Error(ErrorCode.FailedToSetNum, {
        cause: `Invalid index ${index} for toggle component ${this.compId}.`,
      });
    }
    this._toggle._setToggled(index, toggled, fireCallback);
  }

  /**
   * @description Returns the markup for the toggle component.
   * @member TComponents.Toggle_A#markup
   * @method
   * @returns {string} HTML markup string.
   */
  markup() {
    return /*html*/ `
    <div class="tc-toggle"></div>
    `;
  }

  /**
   * @returns {boolean[]}
   */
  get toggleState() {
    return this._toggle._toggleState;
  }

  /**
   * @returns {any[]}
   */
  get changed() {
    return this._changed;
  }

  /**
   * @returns {boolean}
   */
  get multi() {
    return this._toggle._multi;
  }

  /**
   * @description Set if the toggle button allows multi toggled button.
   * @member {boolean} TComponents.Toggle_A#multi
   * @instance
   * @param {boolean} v
   * @example
   * const toggle = new TComponents.Toggle_A(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   * });
   *
   * // Render the component.
   * await toggle.render();
   *
   * toggle.multi = true;
   */
  set multi(v) {
    this.setProps({
      multi: v,
    });
  }

  /**
   * @returns {Function|undefined}
   */
  get onChange() {
    try {
      var fn = Component_A.genFuncTemplateWithPopup(this._props.onChange, this);
    } catch (e) {
      return undefined;
    }
    if (typeof fn == 'function') return fn;
    else return undefined;
  }

  /**
   * @description Sets the `onChange` event handler for the toggle component.
   * The handler can either be a string representing a function to be executed or a function itself.
   * 1. If you are using an arrow function, like `()=>{}`,
   * the `this` property of the scope may not refer to the toggle object.
   * 2. If you are using string assignment to define code execution,
   * the string should contain `only the body of the code (executable statements)`,
   * not a complete function declaration. Therefore, including function keywords like function or async function is incorrect.
   * - Correct (Statements Only): `xx.onChange = "console.log('Action done.');"`
   * - Incorrect (Function Declaration): `xx.onChange = "function() { console.log('Action done.'); }"`
   * @member {Function} TComponents.Toggle_A#onChange
   * @instance
   * @param {string|Function} t - A string representing a function to be executed or a function itself to handle the `onChange` event.
   * @example
   * const toggle = new TComponents.Toggle_A(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   * });
   *
   * // Render the component.
   * await toggle.render();
   * // Example 1: Using a string as the handler:
   * toggle.onChange = "console.log(this.multi);"
   * @example
   * // Example 2: Using a function as the handler:
   * toggle.onChange = function () {
   *   console.log(this.multi);
   * };
   * @example
   * // Example 3: Using an arrow function as the handler:
   * // Note that the `this` context will not refer to the toggle object
   * toggle.onChange = () => {
   *   console.log(toggle.multi);
   * };
   */
  set onChange(t) {
    this.setProps({onChange: t});
  }

  /**
   * @returns {any[]}
   */
  get optionItems() {
    const data = formatOptionsString(this._props.optionItems);
    for (let i = 0; i < data.length; i++) {
      data[i].text = Component_A.tParse(data[i].text.replace(/^\n+/, ''));
    }
    return data;
  }

  /**
   * @description Sets the option items from a formatted string.
   * @member {string} TComponents.Toggle_A#optionItems
   * @instance
   * @param {string} itemsString - The formatted string of option items.
   * @example
   * const toggle = new TComponents.Toggle_A(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   * });
   *
   * // Render the component.
   * await toggle.render();
   *
   * toggle.optionItems = `text4|value4;text5|value5;text6|value6`;
   */
  set optionItems(itemsString) {
    const optionsConfig = this._props.optionConfig;
    if (optionsConfig && optionsConfig.mode == 'fixed') {
      this.setProps({
        optionItems: itemsString,
      });
    } else {
      throw new Error(ErrorCode.FailedToUpdateComponentWithSetter, {
        cause: 'Cannot set prop optionItems when optionsConfig is not in fixed mode.',
      });
    }
  }
}

/**
 * @description Add css properties to the component
 * @alias loadCssClassFromString
 * @member TComponents.Toggle_A.loadCssClassFromString
 * @method
 * @static
 * @param {string} css - The css string to be loaded into style tag
 * @returns {void}
 * @example
 * TComponents.Toggle_A.loadCssClassFromString(`
 *   .tc-toggle {
 *     height: inherit;
 *    }`
 * );
 */
Toggle_A.loadCssClassFromString(/*css*/ `
.tc-toggle {
  height: 100%;
  width: 100%;
  min-width: 0px;
  min-height: 0px;
  padding: 0px;
  margin: 0px;
}
.tc-toggle .fp-components-toggle {
  width: 100%;
  height: 100%;
  min-width: 0px;
  min-height: 0px;
  display: flex;
  flex-wrap: wrap;
  overflow: hidden;
  flex-direction: row;
  border-radius: 8px 8px 8px 8px;
  border: 1px groove rgb(204,204,204);
  box-sizing: border-box;
}

.tc-toggle .fp-components-toggle-disabled{
  opacity:0.7;
  cursor: not-allowed;
}

.tc-toggle .fp-components-toggle-disabled > *{
  pointer-events: none;
}

.tc-toggle .fp-components-toggle > * {
  border-radius: 0 0 0 0;
  border: none;
  box-shadow: 5px 5px 0px 8px var(--fp-color-GRAY-10);
  justify-content: center;
  align-items: center;
  white-space: nowrap;
  display:flex;
  flex-direction:row;
  gap: 10px;   
  text-overflow: ellipsis;
}
.tc-toggle .fp-components-toggle-icon {
  width: auto;
  height: auto;
  background-size: 16px 16px;
  background-repeat: no-repeat;
  background-position: center;
}
.tc-toggle .fp-components-toggle > *:hover {
  background-color: var(--toggled-bg-color);
}
`);