as-checkboxgroup-a.js

import {Component_A} from './basic/as-component.js';
import {formatOptionsString, initDynamicOptions, generateOptionsString} from './utils/utils.js';
import {Checkbox_A} from './as-checkbox-a.js';
import {ErrorCode} from './../exception/exceptionDesc.js';

/**
 * @typedef TComponents.CheckboxGroupProps
 * @prop {object} [options] Additional options for the component
 * Items in this object include:
 * - **responsive** (boolean, default: false): Whether the checkbox group should be responsive
 * @prop {string} [tips] Tooltip text for the component
 * @prop {Function} [onCreated] Function to be called when the component is created
 * @prop {Function} [onMounted] Function to be called when the component is mounted
 * @prop {Function} [onChange] Function to be called when checkbox 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 {number} [borderRadius] <span style="color:red; font-weight: bold;"> Invalid attribute. Border radius of the component. </span>
 * @prop {string} [color] Text color of the component
 * @prop {object} [font] Font properties of the component
 * 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} [optionItems] Formatted string representing the checkbox 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): Data source type
 * - **isHidden** (boolean, default: false): Whether the option configuration is hidden
 * - **variablePath** (string): Path of the variable used for dynamic options
 * @prop {string} [defaultState] Default state of the component
 * @prop {string} [dataStruct] Data structure associated with the component
 * @memberof TComponents
 */

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

/**
 * @description Checkbox group component that allows multiple selections. Each checkbox can be seperately checked or unchecked.
 * This class focuses on the specific properties of the Checkbox group 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.CheckboxGroup_A
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} parent - HTML element that is going to be the parent of the component
 * @param {TComponents.CheckboxGroupProps} props - Properties of the checkbox group component.
 * @example
 * const checkboxGroup = new TComponents.CheckboxGroup_A(document.body, {
 *   position: 'absolute',
 *   zIndex: 1000,
 *   optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`
 * });
 *
 * await checkboxGroup.render();
 */
export class CheckboxGroup_A extends Component_A {
  constructor(parent, props = {}) {
    super(parent, props);
    this._props;
    // this.initPropsDep(['optionItems']);
  }

  /**
   * @description Returns the default values of class properties (excluding parent properties).
   * @member TComponents.CheckboxGroup_A#defaultProps
   * @method
   * @protected
   * @returns {TComponents.CheckboxGroupProps} The default properties of the checkbox group component.
   */
  defaultProps() {
    return {
      options: {
        responsive: false,
      },
      tips: '',
      // lifecycle
      onCreated: '',
      onMounted: '',
      onDispose: '',
      onChange: '',
      // X/Y/W/H/B/R
      position: 'static',
      width: 320,
      height: 32,
      top: 0,
      left: 0,
      rotation: 0,
      zIndex: 0,
      borderRadius: 0, // Invalid attribute
      // font and color
      color: '#000000',
      font: {
        fontSize: 12,
        fontFamily: 'Segoe UI',
        style: {
          fontStyle: 'normal',
          fontWeight: 'normal',
          textDecoration: 'none',
        },
      },
      // The properties.
      optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`,
      optionConfig: {
        mode: 'fixed', //fixed, sync,initialize
        type: '',
        isHidden: false,
        variablePath: '',
      },
      defaultState: 'show_enable',
      dataStruct: '',
    };
  }

  /**
   * @description Initializes the component and sets up click event handling.
   * @member TComponents.CheckboxGroup_A#onInit
   * @method
   * @async
   * @throws {Error} If an error occurs during initialization.
   * @returns {Promise<void>} A promise that resolves when the initialization is complete.
   */
  async onInit() {}

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

  /**
   * @description Maps the internal components.
   * @member TComponents.CheckboxGroup_A~groupComponents
   * @method
   * @private
   * @returns {object} The mapped components
   */
  groupComponents() {
    // Special case, the `this.child` within the lifecycle is only initialized once during construction and will not be dynamically added or removed afterward.
    // Here, we will use external properties to control the dynamic addition and removal of `child`. Therefore, to ensure that it is assigned each time, `this.child` must be set to null once.
    // This code is reflected in the `initChildrenComponents` function of `Component_A`.

    const child = [];

    for (let i = 0; i < this.optionItems.length; i++) {
      const ctext = this.optionItems[i].text;
      const cvalue = this.optionItems[i].value;

      const checkbox = new Checkbox_A(null, {
        text: ctext,
        font: this._props.font,
        color: this._props.color,
        checked: ctext == this.text,
        enabled: this.enabled,
        onChange: this._onChange.bind(this, i),
        selectedIndex: i,
        value: cvalue,
      });
      child.push(checkbox);
    }
    return child;
  }

  /**
   * @description Returns an object containing the components mapped to their identifiers.
   * As one of the methods of component lifecycle,
   * **we do not recommend that users call this function manually.**
   * @member TComponents.CheckboxGroup_A#mapComponents
   * @method
   * @returns {object} An object mapping identifiers to components.
   */
  mapComponents() {
    return {};
  }

  /**
   * @description Returns an object containing the components mapped to their identifiers.
   * @member TComponents.CheckboxGroup_A~mapComponents
   * @method
   * @private
   * @returns {object} An object mapping identifiers to components.
   */
  _mapComponents() {
    // Special case, the `this.child` within the lifecycle is only initialized once during construction and will not be dynamically added or removed afterward.
    // Here, we will use external properties to control the dynamic addition and removal of `child`. Therefore, to ensure that it is assigned each time, `this.child` must be set to null once.
    // This code is reflected in the `initChildrenComponents` function of `Component_A`.

    const child = [];

    this._optionItemList = formatOptionsString(this._props.optionItems);

    for (let i = 0; i < this._optionItemList.length; i++) {
      let ischecked = false;
      const ctext = this._optionItemList[i].text;
      const cvalue = this._optionItemList[i].value;

      // Get specifical child item previous checked state.
      if (Array.isArray(this.child)) {
        const index = this.child.findIndex((c) => c._props.text == ctext);
        if (index >= 0) ischecked = this.child[index]._props.checked;
      }
      const checkbox = new Checkbox_A(null, {
        text: ctext,
        font: this._props.font,
        color: this._props.color,
        checked: ischecked,
        enabled: this.enabled,
        onChange: this._props.onChange,
        selectedIndex: i,
        value: cvalue,
      });
      child.push(checkbox);
    }

    this.child = [];
    return child;
  }

  /**
   * @description Renders the component on the screen and applies the necessary styles and event listeners.
   * @member TComponents.CheckboxGroup_A#onRender
   * @method
   * @async
   * @throws {Error} If an error occurs during rendering.
   * @returns {Promise<void>} A promise that resolves when the rendering is complete.
   */
  async onRender() {
    try {
      this.removeAllEventListeners();
      const checkBoxGroupContainer = this.find('.tc-checkbox-group-a-container');

      this.child = this.groupComponents();
      for (let i = 0; i < this.child.length; i++) {
        this.child[i].attachToElement(checkBoxGroupContainer);
        this.child[i].render();
      }
      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 button component ${this.compId}.`,
        e,
      );
      throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
    }
  }

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

  /**
   * @description Callback function which is called when one of the checkboxes is clicked.
   * @member TComponents.CheckboxGroup_A~_onChange
   * @method
   * @private
   * @async
   * @returns {Promise<void>} A promise that resolves when the change event is processed.
   */
  async _onChange(index) {
    if (this.enabled) {
      try {
        var fn = Component_A.genFuncTemplate(this._props.onChange, this);
        if (typeof fn == 'function') {
          await fn(index);
        }
      } catch (e) {
        Component_A.popupEventError(e, 'onChange', logModule);
      }
    }
  }

  /**
   * @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 checkbox group 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 checkbox group 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.CheckboxGroup_A#onChange
   * @instance
   * @param {Function} t - A function to be executed or a function itself to handle the `onChange` event.
   * @example
   * const checkboxGroup = new TComponents.CheckboxGroup_A(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`
   * });
   *
   * await checkboxGroup.render();
   *
   * // example 1.
   * checkboxGroup.onChange = async function() {
   *   console.log('Checkbox group changed.', this.color);
   * };
   * @example
   * // example 2.
   * checkboxGroup.onChange = "console.log('Checkbox group changed.', this.color);";
   * @example
   * // example 3.
   * // Note that the `this` context will not refer to the checkbox group object
   * checkboxGroup.onChange = () => {
   *   console.log('Checkbox group changed: ', checkboxGroup.color);
   * };
   */
  set onChange(t) {
    this.setProps({
      onChange: t,
    });
  }

  /**
   * @returns {object[]}
   */
  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.CheckboxGroup_A#optionItems
   * @instance
   * @param {string} itemsString - The formatted string of option items.
   * @throws {Error} If the optionsConfig is not in fixed mode.
   * @example
   * const checkboxGroup = new TComponents.CheckboxGroup_A(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`
   * });
   *
   * await checkboxGroup.render();
   * // Set the option items using a formatted string
   * checkboxGroup.optionItems = `text4|value4;\ntext5|value5;\ntext6|value6`;
   */
  set optionItems(itemsString) {
    const optionConfig = this._props.optionConfig;
    if (optionConfig && optionConfig.mode == 'fixed') {
      this.setProps({
        optionItems: itemsString,
      });
    } else {
      throw new Error(ErrorCode.FailedToUpdateComponentWithSetter, {
        cause: 'The optionConfig is not in fixed mode, can not set optionItems',
      });
    }
  }

  /**
   * @returns {number[]}
   */
  get checkedIndexes() {
    const checkedIndexes = [];
    for (let i = 0; i < this.child.length; i++) {
      if (this.child[i].checked) {
        checkedIndexes.push(i);
      }
    }
    return checkedIndexes;
  }

  /**
   * @description Sets the selected indexes.
   * @member {number[]} TComponents.CheckboxGroup_A#checkedIndexes
   * @instance
   * @param {number[]} indexes - The Array of indexes to be selected.
   * @example
   * const checkboxGroup = new TComponents.CheckboxGroup_A(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`
   * });
   *
   *await checkboxGroup.render();
   * // Set the checked indexes
   * checkboxGroup.checkedIndexes = [0, 1];
   */
  set checkedIndexes(indexes) {
    for (let i = 0; i < this.child.length; i++) {
      this.child[i].checked = indexes.includes(i);
    }
  }

  /**
   * @deprecated Use {@link TComponents.CheckboxGroup_A#selectedIndexes} instead.
   * @returns {number[]}
   */
  get selectedIndexs() {
    return this.checkedIndexes;
  }

  /**
   * @returns {number[]}
   */
  get selectedIndexes() {
    return this.checkedIndexes;
  }

  /**
   * @description Sets the selected indexes. (Alias for checkedIndexes)
   * @member {number[]} TComponents.CheckboxGroup_A#selectedIndexes
   * @instance
   * @param {number[]} indexes - The Array of indexes to be selected.
   * @example
   * const checkboxGroup = new TComponents.CheckboxGroup_A(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`
   * });
   *
   * await checkboxGroup.render();
   * // Set the selected indexes
   * checkboxGroup.selectedIndexes = [0, 1];
   */
  set selectedIndexes(indexes) {
    this.checkedIndexes = indexes;
  }

  /**
   * @deprecated Use {@link TComponents.CheckboxGroup_A#selectedIndexes} instead.
   * @description Sets the selected indexes. (Alias for checkedIndexes)
   * @member {number[]} TComponents.CheckboxGroup_A#selectedIndexs
   * @instance
   * @param {number[]} indexes - The Array of indexes to be selected.
   * @example
   * const checkboxGroup = new TComponents.CheckboxGroup_A(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`
   * });
   *
   * await checkboxGroup.render();
   * // Set the selected indexes
   * checkboxGroup.selectedIndexs = [0, 1];
   */

  set selectedIndexs(indexes) {
    this.checkedIndexes = indexes;
  }

  /**
   * @returns {string}
   */
  get color() {
    return this.props.color;
  }

  /**
   * @description Sets the checkboxgroup's text color
   * @member {string} TComponents.CheckboxGroup_A#color
   * @instance
   * @param {string} a For example, '#ffffff'
   * @example
   * const checkboxGroup = new TComponents.CheckboxGroup_A(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`
   * });
   *
   * await checkboxGroup.render();
   *
   * checkboxGroup.color = '#ff0000'; // Sets the text color to red
   */
  set color(a) {
    this.setProps({color: a});
  }
}

/**
 * @description Loads the CSS styles for the CheckboxGroup_A component.
 * @alias loadCssClassFromString
 * @member TComponents.CheckboxGroup_A.loadCssClassFromString
 * @method
 * @static
 * @returns {void}
 * @example
 * TComponents.CheckboxGroup_A.loadCssClassFromString(`
 *   .tc-checkbox-group-a {
 *     height: inherit;
 *   }`
 * );
 */
CheckboxGroup_A.loadCssClassFromString(/*css*/ `
.tc-checkbox-group-a {
  height: inherit;
  width: 100%;
  min-width: 0px;
  min-height: 0px;
  padding: 0px;
  margin: 0px;
}

.tc-checkbox-group-a-container {
  height: 100%;
  width: 100%;
  overflow: hidden;
  display: flex;
  flex-wrap: wrap;
  align-content: space-around;
  justify-content: space-between;


}
.tc-checkbox-group-a-container > * {
  margin-right: 10px;
}
.tc-checkbox-group-a-container > *:last-child {
  margin-right: 0px;
}
`);