as-dropdown.js

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

/**
 * @ignore
 */
const logModule = 'as-dropdown';

/**
 * @typedef TComponents.DropdownProps
 * @prop {object} [options] Additional options for the dropdown component
 * Items in this object include:
 * - **responsive** (boolean, default: false): Whether the dropdown 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 dropdown value is changed
 * @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} [borderRadius] Border radius of the component
 * @prop {number} [rotation] Rotation angle of the component
 * @prop {number} [zIndex] Z-index of the component
 * @prop {string|null} [icon] Prefix icon for the dropdown
 * @prop {string} [text] Dropdown text
 * @prop {string} [color] Text color of the dropdown
 * @prop {string} [backgroundColor] Background color of the dropdown
 * @prop {object} [font] Font settings for the dropdown text
 * This object controls text appearance:
 * - **fontSize** (number, default: 16): Font size of the displayed text
 * - **fontFamily** (string, default: 'Arial'): Font family of the displayed text
 * - **style** (object): Font style configuration, containing `fontStyle`, `fontWeight`, `textDecoration`
 * - **textAlign** (string, default: 'center'): Text alignment inside the dropdown
 * @prop {string} [optionItems] Options for the dropdown in the format `text1|value1;\\ntext2|value2;\\ntext3|value3`
 * @prop {object} [optionConfig] Configuration for dynamic options
 * This object controls how options are provided:
 * - **mode** (string, default: 'fixed'): Options mode, e.g. 'fixed' or 'variable'
 * - **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 {Array<Object>} [optionIcons] Icon configuration for each option
 * @prop {string[]} [optionActions] Action configuration for each option
 * @prop {string} [defaultState] Default state of the component
 * @prop {boolean} [showArrow] Whether to show the dropdown arrow
 * @prop {string} [dataStruct] Data structure associated with the component
 * @memberof TComponents
 */

/**
 * The Dropdown component allows users to choose from a list of options. Additional callbacks can be added with the {@link TComponents.DropDown#onChange|onChange} method.
 * This class focuses on the specific properties of the DropDown 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.DropDown
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} parent - HTML element that is going to be the parent of the component
 * @param {TComponents.DropdownProps} props - The properties of the dropdown component
 * @example
 * const dropDown = new TComponents.DropDown(document.body, {
 *   position: 'absolute',
 *   zIndex: 1000,
 *   optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`,
 * });
 *
 * await dropDown.render();
 */
export class DropDown extends Component_A {
  constructor(parent, props = {}) {
    super(parent, props);

    /**
     * @instance
     * @private
     * @type {TComponents.SelectProps}
     */
    this._props;

    this._dropdown = new FP_DropDown_A();
  }

  /**
   * @description Returns the default values of class properties (excluding parent properties).
   * @member TComponents.DropDown#defaultProps
   * @method
   * @protected
   * @returns {TComponents.DropDownProps} The default properties object.
   */
  defaultProps() {
    return {
      options: {
        responsive: false,
      },
      tips: '',
      // life cycle
      onCreated: '',
      onMounted: '',
      onDispose: '',

      onChange: '',

      // ⭐ W/H/X/Y/B/R/Z: Component required attributes.
      position: 'static',
      width: 140,
      height: 32,
      top: 0,
      left: 0,
      borderRadius: 4,
      rotation: 0,
      zIndex: 0,

      icon: null,
      text: 'Dropdown',

      // font style
      color: '#000000',
      backgroundColor: 'rgba(255, 255, 255, 0)',
      font: {
        fontSize: 16,
        fontFamily: 'Arial',
        style: {
          fontStyle: 'normal',
          fontWeight: 'normal',
          textDecoration: 'none',
        },
        textAlign: 'center',
      },
      optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`,
      optionConfig: {
        mode: 'fixed', //fixed, variable
        type: '',
        isHidden: false,
        variablePath: '',
      },
      optionIcons: [
        {icon: '', toggledIcon: ''},
        {icon: '', toggledIcon: ''},
        {icon: '', toggledIcon: ''},
      ],
      optionActions: ['', '', ''],
      defaultState: 'show_enable',
      showArrow: true,
      dataStruct: '',
    };
  }

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

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

  /**
   * @description This attribute represents the text content of the input field.
   * It can be read to get the current content. Setting this attribute will programmatically
   * update the content and will not trigger the onchange callback function
   * to be called.
   * @member {string} TComponents.DropDown#text
   * @instance
   * @param {string} text
   * @example
   *
   *  const dropDown = new TComponents.DropDown(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: 'Dropdown title'
   * });
   *
   * // Render the component.
   * dropDown.render();
   *
   * dropDown.text = 'title updated';
   */
  set text(text) {
    this.commitProps({text: text});
    this._dropdown.text = text;
  }

  /**
   * @description return the display label of the dropdown
   * @member {string} TComponents.DropDown#text
   * @instance
   * @param {string} text
   * @example
   *
   *  const dropDown = new TComponents.DropDown(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: 'Dropdown title'
   * });
   *
   * // Render the component.
   * dropDown.render();
   *
   * console.log(dropDown.text); // Output: 'Dropdown title'
   */
  get text() {
    return this._props.text;
  }

  /**
   * @description Renders the select component.
   * @member TComponents.DropDown#onRender
   * @method
   * @async
   * @returns {Promise<void>}
   */
  async onRender() {
    try {
      this._dropdown.text = this._props.text;
      this._dropdown.icon = this._props.icon;
      this._dropdown.borderRadius = this._props.borderRadius;
      this._dropdown.color = this._props.color;
      this._dropdown.backgroundColor = this._props.backgroundColor;
      this._dropdown.font = this._props.font;
      this._dropdown.showArrow = this._props.showArrow;
      this._dropdown._onselection = this._cbOnChange.bind(this);
      this._dropdown.props = this._props;
      this._dropdown._model = this.generateModel();

      const dropdownContainer = this.find('.tc-dropdown');
      if (dropdownContainer) this._dropdown.attachToElement(dropdownContainer);
      this._addTips();
      Component_A.resolveBindingExpression(this._props.text, this);
    } catch (e) {
      // Runtime errors: write specific content to the log, throw error code
      Logger.e(
        logModule,
        ErrorCode.FailedToRunOnRender,
        `Error happens on onRender of Dropdown component ${this.compId}.`,
        e,
      );
      throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
    }
  }

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

  /**
   * @description Generate dropdown model with value parameter
   * @member TComponents.DropDown~generateModel
   * @method
   * @private
   * @returns {object[]} The generated dropdown model.
   */
  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 Handles the change event for the dropdown element.
   * @member TComponents.DropDown~_cbOnChange
   * @method
   * @private
   * @async
   * @param {Event} e - The change event.
   * @returns {Promise<void>}
   */
  async _cbOnChange(item, index) {
    if (this.enabled) {
      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;
        }
      }
      this.selectedIndex = index;
      try {
        var fn = Component_A.genFuncTemplate(this._props.onChange, this);
        fn && (await fn());
      } catch (e) {
        Component_A.popupEventError(e, `onChange`, logModule);
      }
    }
  }

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

  /**
   * @description Set the `onChange` handler for the dropdown.
   * 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 dropdown 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.onClick = "console.log('Action done.');"`
   * - Incorrect (Function Declaration): `xx.onClick = "function() { console.log('Action done.'); }"`
   * @member {Function} TComponents.DropDown#onChange
   * @instance
   * @param {Function} t - The new change handler function.
   * @example
   * const dropDown = new TComponents.DropDown(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`,
   * });
   *
   * await dropDown.render();
   *
   * // Example 1: Using a string as the handler:
   * dropDown.onChange = "console.log('hello', this.selectedValue);"
   * @example
   * // Example 2: Using a arrow function as the handler:
   * // Note that the `this` context will not refer to the dropDown object
   * dropDown.onChange = () => { console.log(dropDown.selectedValue); }
   * @example
   * // Example 3: Using function with async operation:
   * dropDown.onChange = async function () {
   *  console.log('hello', this.selectedValue);
   * }
   */
  set onChange(t) {
    this.setProps({onChange: t});
  }

  /**
   * @description The selected index of the dropdown.
   * The obtained value should be treated as a read-only property and should not be used for manipulation.
   * @member {number} TComponents.DropDown#selectedIndex
   * @instance
   */
  get selectedValue() {
    if (typeof this.selectedIndex == 'undefined') return '';
    return this.optionItems[this.selectedIndex].value;
  }

  /**
   * @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.DropDown#optionItems
   * @instance
   * @param {string} itemsString - The formatted string of option items.
   * @example
   * const dropDown = new TComponents.DropDown(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   optionItems: `text1|value1;\ntext2|value2;\ntext3|value3`,
   * });
   *
   * await dropDown.render();
   *
   * dropDown.optionItems = `newText1|newValue1;\nnewText2|newValue2;`;
   */
  set optionItems(itemsString) {
    const optionConfig = this._props.optionConfig;
    if (optionConfig && optionConfig.mode == 'fixed') {
      this.setProps({
        optionItems: itemsString,
      });
    } else {
      throw new Error(ErrorCode.FailedToUpdateComponentWithSetter, {
        cause: 'Cannot set prop optionItems when optionConfig is not in fixed mode.',
      });
    }
  }
}

/**
 * @description Add css properties to the component
 * @alias loadCssClassFromString
 * @member TComponents.DropDown.loadCssClassFromString
 * @method
 * @static
 * @param {string} css - The css string to be loaded into style tag
 * @returns {void}
 * @example
 * TComponents.DropDown.loadCssClassFromString(`.tc-dropdown { background-color: red; }`);
 */
DropDown.loadCssClassFromString(/*css*/ `
.tc-dropdown {
  height: inherit;
  width: 100%;
  min-width: 0px;
  min-height: 0px;
  padding: 0px;
  margin: 0px;
}
.tc-dropdown .fp-components-dropdown-container{
  height:100%;
}

.tc-dropdown .fp-components-dropdown {
  border:none;
  padding:0 8px;
  display:flex;
  flex-direction: row;
  align-items: center;
  align-content: center;
  background:transparent;
}


.fp-components-dropdown-menu-hide{
  display:none !important;
}

.tc-dropdown .fp-components-dropdown >i,
.tc-dropdown .fp-components-dropdown >span,
.tc-dropdown .fp-components-dropdown >canvas{
  display:flex;
}

.tc-dropdown .fp-components-dropdown .fp-components-dropdown-arrow-icon {
  margin-left: 8px;
}

.tc-dropdown .fp-components-dropdown-icon-font {
  margin-right: 8px;
}

.tc-dropdown .fp-components-dropdown-menu .fp-components-dropdown-overlay{
  background:red;
}

.tc-dropdown .fp-components-dropdown-disabled,
.tc-dropdown .fp-components-dropdown  {
  width: 100%;
  height: 100%;
}

.tc-dropdown .fp-components-dropdown-disabled {
  cursor:not-allowed !important;
  user-select: none;
}
.tc-dropdown .fp-components-dropdown:hover {
  opacity:0.7;
}

.custom-dropdown-option.selected,.custom-dropdown-option:hover{
  background-color: #f0f0f0;
}

`);