as-digitalled.js

import {Component_A} from './basic/as-component.js';
import {FP_Digital_A} from './fp-ext/fp-digital-ext.js';
import {checkCssSupport} from './utils/utils.js';
import {ErrorCode} from './../exception/exceptionDesc.js';

/**
 * @typedef TComponents.SignalIndicatorProps
 * @prop {object} [options] Additional options for the digital LED component.
 * Items in this object include:
 * - **responsive** (boolean, default: false): Whether the component 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 {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] The color of the digital LED component.
 * @prop {string} [size] Size style template of the component.
 * @prop {object} [font] Font settings for the digital LED component.
 * This object controls text appearance:
 * - **fontSize** (number, default: 20): Font size of the displayed text.
 * - **fontFamily** (string, default: 'Segoe UI'): Font family of the displayed text.
 * @prop {string|number|boolean} [text] The text value displayed by the digital LED component.
 * @prop {object} [inputVar] Configuration object for input variable binding.
 * This object configures the bound input variable:
 * - **type** (string): Input variable type. Default is `Component_A.INPUTVAR_TYPE.BOOL`.
 * - **func** (string): Binding behavior. Default is `Component_A.INPUTVAR_FUNC.CUSTOM`.
 * - **value** (string): Initial bound value, default is `'0'`.
 * - **isHidden** (boolean, default: false): Whether the variable is hidden.
 * @prop {string} [onChange] Please use onClick, the change event will be deprecated soon. A string representing a function to be called when the signal changes its state.
 * @prop {string} [onClick] A string representing a function to be called when the component is clicked.
 * @prop {boolean} [readOnly] Indicates whether the digital LED component is read-only.
 * @prop {string} [defaultState] The default state of the component when initialized.
 * @prop {string} [align] The alignment of the digital LED component within its container.
 * @prop {string} [dataStruct] The data structure associated with the component.
 * @memberof TComponents
 */

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

/**
 * @description Digital LED component that displays a digital signal and triggers callbacks when the signal changes or the component is clicked.
 * This class focuses on the specific propertiesof the DigitalLed 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.DigitalLed
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} parent - The parent HTML element of the component.
 * @param {TComponents.SignalIndicatorProps} [props={}] - The properties of the Digital LED component.
 * @property {TComponents.SignalIndicatorProps} _props - The properties object of the Digital LED component.
 * @example
 * const digitalLed = new TComponents.DigitalLed(document.body, {
 *   position: 'absolute',
 *   zIndex: 1000,
 *   text: '1',
 * });
 *
 * // Render the component
 * digitalLed.render();
 */
export class DigitalLed extends Component_A {
  constructor(parent, props = {}) {
    super(parent, props);

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

    this._dig = new FP_Digital_A();

    /*
     * If bound to web data, `this._bindData` will have the format: { type: 'webdata', key: 'xxx' }.
     * If bound to digital signal data, `this._bindData` will have the format: { type: 'digitalsignal', key: 'xxxx' }.
     * If bound to rapid data, `this._bindData` will have the format: { type: 'rapiddata', dataType: 'xxx', module: 'xxx', name: 'xxx', task: 'xxx' }.
     */
    this._bindData = null;
  }

  /**
   * @description Returns the default values of class properties (excluding parent properties).
   * @member TComponents.DigitalLed#defaultProps
   * @method
   * @protected
   * @returns {TComponents.SignalIndicatorProps} The default properties of the Digital LED component.
   */
  defaultProps() {
    return {
      options: {
        responsive: false,
      },
      tips: '',
      // life cycle
      onCreated: '',
      onMounted: '',
      onDispose: '',
      // X/Y/W/H/B/R
      position: 'static',
      width: 40,
      height: 40,
      top: 0,
      left: 0,
      rotation: 0,
      zIndex: 0,
      borderRadius: 0, // Invalid attribute
      // background
      color: '#f0f0f0',
      // size template
      size: '',
      // font
      font: {
        fontSize: 20,
        fontFamily: 'Segoe UI',
      },
      // Input variable binding properties.
      text: '0',
      inputVar: {
        type: Component_A.INPUTVAR_TYPE.BOOL,
        func: Component_A.INPUTVAR_FUNC.CUSTOM,
        value: '0', // string
        isHidden: false,
      },
      onChange: '//Please use onClick, this event will be deprecated soon.',
      // Special properties.
      onClick: '',
      readOnly: false,
      defaultState: 'show_enable',
      align: 'center',
      dataStruct: '',
    };
  }

  /**
   * @description Initializes the component and sets up click event handling.
   * @member TComponents.DigitalLed#onInit
   * @method
   * @async
   * @returns {Promise<void>}
   */
  async onInit() {
    this._dig.onclick = this._cbOnClick.bind(this);
  }

  /**
   * @description there are something need to do after render once
   * @member TComponents.DigitalLed#afterRenderOnce
   * @method
   * @protected
   * @returns {void}
   */
  afterRenderOnce() {
    if (this._props.inputVar.func == Component_A.INPUTVAR_FUNC.SYNC) {
      this._bindData = Component_A.getBindData(this._props.inputVar.value, this);
    }
  }

  /**
   * @description Renders the component on the screen and applies the necessary styles and event listeners.
   * @member TComponents.DigitalLed#onRender
   * @method
   * @throws {Error} Throws an error if rendering fails.
   * @returns {void}
   */
  onRender() {
    try {
      this.removeAllEventListeners();

      const wrap = this.find('.tc-digital-container');
      if (Component_A._isHTMLElement(wrap)) {
        const align = this._props.align;
        wrap.style.justifyContent = `${align == 'left' ? 'flex-start' : align == 'right' ? 'flex-end' : 'center'}`;
      }

      this._dig.attachToElement(wrap);

      if (this._props.onChange) {
        const fn = Component_A.genFuncTemplate(this._props.onChange, this);
        if (fn) this._onChange(fn);
      }

      const digitalEl = this.find('.fp-components-digital-a');
      if (Component_A._isHTMLElement(digitalEl)) {
        digitalEl.style.color = `${this._props.color}`;
        digitalEl.style.fontSize = `${this._props.font.fontSize}px`;
        digitalEl.style.fontFamily = `${this._props.font.fontFamily}`;
      }
      this._dig.readOnly = this._props.readOnly ? true : false;

      if (this.validateText(this._props.text)) {
        this.textX = this._props.text; // Use the setter to ensure proper handling of text and active state
      }

      this._addTips();
      this._compatibilityRender();
      Component_A.resolveBindingExpression(this._props.text, this, 'textX');
    } catch (e) {
      // Runtime errors: write specific content to the log, throw error code
      Logger.e(
        logModule,
        ErrorCode.FailedToRunOnRender,
        `Error happens on onRender of digitalled component ${this.compId}.`,
        e,
      );
      throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
    }
  }

  /**
   * @description Add a compatibility render for lower version browser, such as standard mode FlexPendant's built-in browser
   * @member TComponents.DigitalLed~_compatibilityRender
   * @method
   * @private
   * @returns {void}
   */
  _compatibilityRender() {
    if (!checkCssSupport('aspect-ratio', '1/1')) {
      setTimeout(() => {
        const {clientWidth, clientHeight} = this.container;
        let min = Math.min(clientWidth, clientHeight);
        const digitalContainer = this.find('.fp-components-digital-a-container');
        if (min <= 0) {
          min = this.height;
        }
        digitalContainer.style.cssText = `
            width: ${min}px; 
            height: ${min}px;
            padding:0;
        `;
      });
    }
  }

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

  /**
   * @returns {string}
   */
  get text() {
    return this._props.text;
  }

  /**
   * @description Sets the text value of the Digital LED component and updates the active state accordingly.
   * @member {string} TComponents.DigitalLed#text
   * @instance
   * @param {string|boolean|number} t - The text value to set.
   * @example
   * const digitalLed = new TComponents.DigitalLed(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: '1',
   * });
   *
   * // Render the component
   * digitalLed.render();
   *
   * // Update the text value
   * digitalLed.text = '0';
   */
  set text(t) {
    this._updateActiveFromText(t);
  }

  /**
   * @returns {string}
   */
  get textX() {
    return this._props.text;
  }

  /**
   * @description Sets the text value of the Digital LED component and updates the active state accordingly.
   * The update is performed silently without triggering change events
   * or user-originated side effects.
   * @member {string} TComponents.DigitalLed#textX
   * @instance
   * @param {string|boolean|number} t - The text value to set.
   * @example
   * const digitalLed = new TComponents.DigitalLed(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: '1',
   * });
   *
   * // Render the component
   * digitalLed.render();
   *
   * // Update the text value
   * digitalLed.textX = '0';
   */
  set textX(t) {
    const next = this.convertDataToBool(t);
    if (next == this.active) return;
    this._dig.active = next;
    this.commitProps({text: next ? '1' : '0'});
  }

  /**
   * This attribute is used to set the text of the Digital LED component.
   * When using this method to set the text, the change will be synchronized with the controller,
   * But it will not trigger the onchange callback function or any user-originated side effects.
   * @member {string} TComponents.DigitalLed#setText
   * @method
   * @param {string} text
   * @example
   * const digitalLed = new TComponents.DigitalLed(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: 1
   * });
   *
   * // Render the component.
   * digitalLed.render();
   *
   * digitalLed.setText(0);
   */
  async setText(text) {
    const next = this.convertDataToBool(text);
    if (this._props.readOnly || next == this.active) return;
    await this.syncInputData(next);
    this._dig.active = next;
    this._props.text = next ? '1' : '0';
  }

  /**
   * @returns {boolean}
   */
  get active() {
    return this._dig.active;
  }

  /**
   * @description Sets the active state of the Digital LED component.
   * @member {boolean} TComponents.DigitalLed#active
   * @instance
   * @param {boolean} value - The active state to set.
   * @protected
   * @example
   * const digitalLed = new TComponents.DigitalLed(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: '1',
   * });
   *
   * // Render the component
   * digitalLed.render();
   *
   * // Update the active state
   * digitalLed.active = true;
   */
  set active(value) {
    if (value != this.active && this.enabled) {
      this.trigger('change');
      this._dig.active = value;
      this.commitProps({text: value ? '1' : '0'});
    }
  }

  /**
   * @returns {boolean}
   */
  get activeX() {
    return this._dig.active;
  }

  /**
   * @description Sets the active state of the Digital LED component.
   * This method updates the active value internally without triggering
   * change events or user-originated side effects.
   * @member {boolean} TComponents.DigitalLed#activeX
   * @instance
   * @param {boolean} value - The active state to set.
   * @protected
   * @example
   * const digitalLed = new TComponents.DigitalLed(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: '1',
   * });
   *
   * // Render the component
   * digitalLed.render();
   *
   * // Update the active state
   * digitalLed.activeX = true;
   */
  set activeX(value) {
    if (value == this.active) return;
    this._dig.active = value;
    this.commitProps({text: value ? '1' : '0'});
  }

  /**
   * @description Adds a callback function to be called when the signal changes its state.
   * @member TComponents.DigitalLed~_onChange
   * @method
   * @private
   * @param {Function} func - The callback function to be called when the signal changes.
   * @returns {void}
   */
  _onChange(func) {
    this.cleanEvent('change');
    this.on('change', func);
  }

  /**
   * @description Callback function that is called when the indicator is pressed, and triggers any function registered with {@link TComponents.Digital_A#onClick}.
   * @member TComponents.DigitalLed~_cbOnClick
   * @method
   * @private
   * @async
   * @returns {Promise<void>}
   */
  async _cbOnClick() {
    const targetValue = this.active ? false : true;
    if (this._props.readOnly || targetValue == this.active || !this.enabled) return;

    try {
      await this.syncInputData(targetValue);
      this._dig.active = targetValue;
      this.commitProps({text: targetValue ? '1' : '0'});
    } catch (e) {
      this._dig.active = !targetValue;
      Component_A.popupEventError(e, 'syncInputData', logModule);
      return;
    }

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

    // Updates to bound variables are done through subscriptions
    // if (this.inputVar.func == Component_A.INPUTVAR_FUNC.CUSTOM) {
    this.trigger('change');
    // }
  }

  /**
   * @returns {object}
   */
  get inputVar() {
    return this._props.inputVar;
  }

  /**
   * @description Sets `inputVar` property.
   * This property is used to set the `inputVar` configuration.
   * If the configuration is invalid or doesn't meet the expected conditions,
   * an exception will be triggered in the input component.
   * **Note:** Invalid `inputVar` configurations may cause the input component to throw an error.
   * @member {object} TComponents.DigitalLed#inputVar
   * @instance
   * @param {object} t - The new `inputVar` configuration object. It should contain a valid `type`, `func`, and `value` as per the component's requirements.
   * @example
   *  const digitalLed = new TComponents.DigitalLed(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: '1',
   * });
   *
   * // Render the component
   * digitalLed.render();
   *
   * // Update the inputVar configuration
   * digitalLed.inputVar = {
   *   type: TComponents.Component_A.INPUTVAR_TYPE.BOOL,
   *   func: TComponents.Component_A.INPUTVAR_FUNC.CUSTOM,
   *   value: true
   * };
   */
  set inputVar(t) {
    this.setProps({inputVar: t});
  }

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

  /**
   * @description Sets the click event handler.
   * 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 digitalLed 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.DigitalLed#onClick
   * @instance
   * @param {Function|string} t - The new click handler function.
   * @example
   *  const digitalLed = new TComponents.DigitalLed(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: '1',
   * });
   *
   * // Render the component
   * digitalLed.render();
   *
   * // Example 1: Using a string as the handler:
   * digitalLed.onClick = "console.log('hello');"
   *
   * // Example 2: Using a arrow function as the handler:
   * digitalLed.onClick = () => { console.log(digitalLed.text); }
   *
   * // Example 3: Using function with async operation:
   * digitalLed.onClick = async function () {
   *  console.log('hello');
   * };
   */
  set onClick(t) {
    this.setProps({onClick: t});
  }

  /**
   * @returns {Function|undefined}
   * @deprecated The `onChange` event will be deprecated soon, please use `onClick` instead. The `onChange` event may not work properly in future versions.
   */
  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 Sets the `onChange` event handler for the Digital LED 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 digital led 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.DigitalLed#onChange
   * @param {Function|string} t
   * @example
   * const digitalLed = new TComponents.DigitalLed(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: '1',
   * });
   *
   * // Render the component
   * digitalLed.render();
   *
   * // Example 1: Using a string as the handler:
   * digitalLed.onChange = "console.log('hello');"
   * @example
   * // Example 2: Using a arrow function as the handler:
   * // Note that the `this` context will not refer to the digitalLed object
   * digitalLed.onChange = () => { console.log(digitalLed.text); }
   * @example
   * // Example 3: Using function with async operation:
   * digitalLed.onChange = async function () {
   *  console.log('hello', this.text);
   * }
   */
  set onChange(t) {
    this.setProps({onChange: t});
  }

  /**
   * @returns {boolean}
   */
  get readOnly() {
    return this._props.readOnly;
  }

  /**
   * @description Sets the read-only state of the Digital LED component
   * @member {boolean} TComponents.DigitalLed#readOnly
   * @instance
   * @param {boolean} t - The read-only state.
   * @example
   * const digitalLed = new TComponents.DigitalLed(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: '1',
   * });
   *
   * // Render the component
   * digitalLed.render();
   *
   * // Update the readOnly state
   * digitalLed.readOnly = true;
   */
  set readOnly(t) {
    this.setProps({readOnly: t});
  }
}

if (checkCssSupport('aspect-ratio', '1/1')) {
  //special style for aspect-ratio supported browsers
  DigitalLed.loadCssClassFromString(`
  .tc-digital-container .fp-components-digital-a-container,
  .tc-digital-container .fp-components-digital-a-disabled {
    height: 100%;
    aspect-ratio: 1 / 1;
    max-width: 100%;
    max-height: 100%;
    display: block;
    position: relative;
    overflow: hidden;
    padding:0;
  }

  .tc-digital-container .fp-components-digital-a {
    width: 100%;
    max-height: 100%;
    aspect-ratio: 1 / 1;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    display:flex;
    box-sizing: border-box;
    border-width:3px;
    height:auto;
  }
`);
} else {
  //special style for aspect-ratio not supported browsers
  DigitalLed.loadCssClassFromString(`
    .tc-digital-container .fp-components-digital-a {
      width:100%;
      height:100%;
      border-radius:50%;
      display:flex;
      align-items:center;
      justify-content:center;
      margin:0;
      padding:0;
      box-sizing: border-box;
    }
  `);
}

/**
 * @description Add css properties to the component
 * @member TComponents.DigitalLed.loadCssClassFromString
 * @method
 * @static
 * @param {string} css - The css string to be loaded into style tag
 * @returns {void}
 * @example
 * TComponents.DigitalLed.loadCssClassFromString(`
 *   .tc-digital-container {
 *    height: 100%;
 *    min-width: 0px;
 *   }
 * `);
 */
DigitalLed.loadCssClassFromString(`
  .tc-digital-container {
    height: 100%;
    min-width: 0px;
    min-height: 0px;
    padding: 0px;
    margin: 0px;
    align-items: flex-start;
    justify-content: flex-start;
  }

  .tc-digital-container .fp-components-digital-a-disabled {
    cursor: not-allowed !important;
  }

  .tc-digital-container .fp-components-digital-a:hover {
    opacity:0.7;
  }
`);