basic_as-component.js

import API from '../../api/index.js';
import {Accessors_A} from './as-accessors.js';
import {Eventing_A} from './as-event.js';
import {Popup_A} from '../as-popup.js';
import {
  ErrorCode,
  ExceptionIdMap,
  getExceptionIdByErrorCode,
  checkIfKnownRWSError,
} from './../../exception/exceptionDesc.js';
import {Signal, Rapid, Variable, UAS, Success} from '../../store/const.js';
import Store from '../../store/store.js';
import {appendDataToErrInstance} from '../../utils/utils.js';

/**
 * @description Loads a CSS file
 * @alias tComponentsLoadCSS
 * @memberof TComponents
 * @param {string} href - Path of css file
 */
function tComponentsLoadCSS(href) {
  let head = document.getElementsByTagName('head')[0];
  let link = document.createElement('link');
  link.rel = 'stylesheet';
  link.type = 'text/css';
  link.href = href;
  head.appendChild(link);
}

tComponentsLoadCSS('framework/components/style/t-components.css');

/**
 * @typedef TComponents.ComponentProps
 * @prop {string} [label] Label text
 * @prop {string} [labelPos] Label position: "top|bottom|left|right|top-center|bottom-center"
 * @prop {object} [options] A set of options to modify the behaviour of the component
 * - async : if true, the subcomponents are instantiated asynchronously and onRender is executed inmediatelly without
 * waiting for the subcomponents to finish.
 */

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

/**
 * @description Creates an instance of TComponents.Component class.
 * This is the base parent class of all TComponent.
 * @class TComponents.Component_A
 * @extends TComponents.Accessors_A
 * @memberof TComponents
 * @param {HTMLElement} parent - HTMLElement that is going to be the parent of the component.
 * @param {TComponents.ComponentProps} props Parameters to create the component.
 * @example
 * const component = new TComponents.Component_A(document.body, {
 *   label: 'My Component',
 *   labelPos: 'top',
 *   options: {
 *     async: true
 *   }
 * });
 */
export class Component_A extends Accessors_A {
  constructor(parent, props = {}) {
    super(props);

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

    if (!Component_A._isHTMLElement(parent) && parent !== null) {
      Logger.e(
        logModule,
        ErrorCode.FailedToFindParentElement,
        'The parent element is not a valid HtMLElement:',
        parent,
      );
      throw ErrorCode.FailedToFindParentElement;
    }
    this._conditionDefaultProps = Object.assign(this.defaultProps(), props);

    this.compId = `${this.constructor.name}_${API.generateUUID()}`;

    this.child = null;

    /**
     * Parent HTML element where the component is attached to.
     * @type {HTMLElement}
     */
    this.parent = parent;
    this.container = document.createElement('div');
    this.container.id = this.compId;
    this.container.classList.add('t-component');
    this.parentComponentId = '';

    this.template = null;
    this.initialized = false;
    this._initCalled = false;
    this._enabled = true;

    this._deinstallFunction = null;
    this._eventListeners = new Map();
    this._fUpdate = false;
    Object.defineProperty(this, '_isTComponent', {
      value: true,
      writable: false,
    });

    this.once('before:init', () => {
      this.onCreated();
    });

    this.once('after:render', () => {
      // this.initState();
      this.onMounted();
      this.afterRenderOnce();
    });

    this.once('before:destroy', () => {
      if (this._props.onDestroy) {
        this.onDispose();
      }
    });
  }

  /**
   * @description Lifecycle hook called once after the component is rendered for the first time.
   * @member TComponents.Component_A#afterRenderOnce
   * @method
   * @protected
   * @returns {void}
   */
  afterRenderOnce() {}

  /**
   * @description Lifecycle hook called when the component is created.
   * @member TComponents.Component_A#onCreated
   * @method
   * @protected
   * @async
   * @returns {Promise<void>}
   */
  async onCreated() {
    const fn = Component_A.genFuncTemplate(this._props.onCreated, this);
    fn && (await fn());
  }

  /**
   * @description Lifecycle hook called when the component is mounted.
   * @member TComponents.Component_A#onMounted
   * @method
   * @protected
   * @async
   * @returns {Promise<void>}
   */
  async onMounted() {
    const fn = Component_A.genFuncTemplate(this._props.onMounted, this);
    fn && (await fn());
  }

  /**
   * @description Lifecycle hook called when the component is about to be disposed.
   * This hook is triggered when the component is being removed from the renderer
   * and all associated resources should be released.
   * @member TComponents.Component_A#onDispose
   * @method
   * @protected
   * @async
   * @returns {Promise<void>}
   */
  async onDispose() {
    const fn = Component_A.genFuncTemplate(this._props.onDispose, this);
    fn && (await fn());
  }

  /**
   * @description Replace the content of the component's container.
   * @member TComponents.Component_A#replaceContent
   * @method
   * @param {string|HTMLElement} content - The new content to replace the current content.
   * @returns {void}
   * @example
   * const component = new TComponents.Component_A(document.body, {});
   * component.replaceContent('New Content');
   */
  replaceContent(content) {
    this.container.innerHTML = '';
    if (typeof content == 'string') {
      this.container.innerHTML = content;
    } else if (content instanceof HTMLElement) {
      this.container.appendChild(content);
    } else {
      this.container.innerHTML = content;
    }
  }

  /**
   * @description Returns the default values of class properties (excluding parent properties).
   * @member TComponents.Component_A#defaultProps
   * @method
   * @protected
   * @returns {TComponents.ComponentProps}
   */
  defaultProps() {
    return {label: '', labelPos: 'top'};
  }

  /**
   * @description Initialization of a component. Any asynchronous operation (like access to controller) is done here.
   * The {@link TComponents.Component_A#onInit()} method of the component is triggered by this method.
   * @member TComponents.Component_A#init
   * @method
   * @async
   * @returns {Promise<object>} The TComponents instance on which this method was called.
   */
  async init() {
    try {
      this.trigger('before:init', this);
      /**
       * Clean up before initializing. Relevant from the second time the component is initialized.
       * - Remove all event listeners
       */
      this.removeAllEventListeners();

      /**
       * Parent HTML element to which the component is attached.
       */
      this.parent;

      /**
       * Initialization of internal states
       * @private
       */
      this.initialized = false;
      this._initCalled = true;

      /**
       * Reset values before onInit
       * Resetting enabled only if previously an error occurred, for a next try, otherwise it was explicitly disabled by the user
       */
      if (this.error) this.enabled = true;
      this.error = false;

      try {
        this._deinstallFunction = await this.onInit();
      } catch (e) {
        this.error = true;
        // throw e;
        Logger.e(logModule, ErrorCode.FailedToInitComponent, `Failed to run onInit() for component ${this.compId}.`, e);
      }

      this.trigger('init', this);

      return await this.render();
    } catch (e) {
      Logger.e(logModule, ErrorCode.FailedToInitComponent, `Failed to initialize the component ${this.compId}.`, e);
    }
  }

  /**
   * @description Update the content of the instance into the Document Object Model (DOM).
   * The {@link TComponents.Component_A#onRender()} method of this component and eventual initialization
   * of sub-components are managed by this method.
   * @member TComponents.Component_A#render
   * @method
   * @async
   * @param {object} [data] - Data that can be passed to the component, which may be required for the rendering process.
   * @returns {Promise<object>} The TComponents instance on which this method was called.
   */
  async render(data = null) {
    this.container.innerHTML = '';
    try {
      this.trigger('before:render', this);
      if (this._initCalled === false) {
        await this.init();
        return;
      }

      this._handleData(data);
      this._createTemplate();

      await this.initChildrenComponents();
    } catch (e) {
      this.error = true;
      Logger.e(logModule, ErrorCode.FailedToRenderComponent, `Failed to render the component ${this.compId}.`, e);
    }

    if (this.container.hasChildNodes() && this._labelStart()) {
      // Insert before the first child node
      this.container.insertBefore(this.template.content, this.container.firstChild);
    } else {
      this.container.appendChild(this.template.content);
    }
    this.parent && this.attachToElement(this.parent);
    this.error && (this.enabled = false);

    this.setContainerBasicCss();
    this.onRender();

    this.initialized = true;

    this.trigger('render', this);
    this.trigger('after:render', this);
    return this;
  }

  /**
   * @description Add the tooltip HTML Element for the component.
   * @member TComponents.Component_A#addTips
   * @method
   * @protected
   * @returns {void}
   */
  addTips() {
    this._addTips();
  }

  /**
   * @member TComponents.Component_A~_addTips
   * @method
   * @private
   * @returns {void}
   */
  _addTips() {
    if (this._props.tips) {
      this.addEventListener(this.container, 'mouseenter', this._enterTip.bind(this));
      this.addEventListener(this.container, 'mouseleave', this._leaveTip.bind(this));
    }
  }

  /**
   * @description Shows the tooltip when mouse enters.
   * @member TComponents.Component_A#enterTip
   * @method
   * @protected
   * @returns {void}
   */
  enterTip() {
    this._enterTip();
  }

  /**
   * @member TComponents.Component_A~_enterTip
   * @method
   * @private
   * @returns {void}
   */
  _enterTip() {
    if (!this.enabled) {
      const tips = document.createElement('div');
      tips.classList.add('t-component-tips');
      tips.textContent = Component_A.tParse(this._props.tips);
      this.container.appendChild(tips);

      const maxTipWidth = 240;
      const minTipWidth = 80;
      const naturalWidth = tips.scrollWidth;
      const safeWidth = Math.max(minTipWidth, Math.min(maxTipWidth, naturalWidth));
      tips.style.width = `${safeWidth}px`;

      const containerTop = this.container.getBoundingClientRect().top;
      const tipsHeight = tips.getBoundingClientRect().height;
      const topGap = 8;
      if (containerTop < tipsHeight + topGap) {
        // tips.style.bottom = 'auto';
        // tips.style.top = '1px';
      }
    }
  }

  /**
   * @description Hides the tooltip when mouse leaves.
   * @member TComponents.Component_A#leaveTip
   * @method
   * @protected
   * @returns {void}
   */
  leaveTip() {
    this._leaveTip();
  }

  /**
   * @member TComponents.Component_A~_leaveTip
   * @method
   * @private
   * @returns {void}
   */
  _leaveTip() {
    if (!this.enabled) {
      const tips = this.container.querySelectorAll('.t-component-tips');
      if (tips) tips.forEach((tip) => tip.remove());
    }
  }

  /**
   * @description Contains component specific asynchronous implementation (like access to controller).
   * This method is called internally during initialization process orchestrated by {@link TComponents.Component_A#init init}.
   * @member TComponents.Component_A#onInit
   * @method
   * @abstract
   * @async
   */
  async onInit() {}

  /**
   * @description Contains all synchronous operations/setups that may be required for any sub-component after its initialization and/or manipulation of the DOM.
   * This method is called internally during rendering process orchestrated by {@link TComponents.Component_A#render render}.
   * @member TComponents.Component_A#onRender
   * @method
   * @abstract
   */
  onRender() {}

  /**
   * @description Synchronously initializes all child TComponents returned by {@link #mapComponents() mapComponents} method.
   * This method is internally called by {@link TComponents.Component_A#render() render} method.
   * @member TComponents.Component_A#initChildrenComponents
   * @method
   * @private
   * @async
   * @returns {void}
   */
  async initChildrenComponents() {
    const newChildren = this.mapComponents();
    const toDispose = [];
    if (Object.keys(newChildren).length === 0) return;

    // Initialize this.child if it's not already initialized
    if (!this.child) {
      this.child = newChildren;
    } else {
      for (const key in newChildren) {
        const newChild = newChildren[key];
        const oldChild = this.child[key];

        if (Component_A.isTComponent(oldChild) && Component_A.isTComponent(newChild)) {
          // const shouldUpdate = !Base_A._equalProps(oldChild._props, newChild._props) || oldChild._fUpdate;
          const shouldUpdate = oldChild._fUpdate || !Component_A._equalProps(oldChild._props, newChild._props);

          if (shouldUpdate) {
            // If the properties are not equal or if _fUpdate is true,
            // Ensure old child is properly destroyed
            toDispose.push(oldChild);
            // Replace the existing child
            this.child[key] = newChild;
          } else {
            // If the properties are equal and _fUpdate is false, just attach the old child to the new DOM element
            if (newChild.compId !== oldChild.compId) {
              // cleaning up newChild since not needed, but just if it is a different instace as the old one
              oldChild.attachToElement(newChild.parent);
              newChild.destroy();
            }
          }
        } else {
          // If not a TComponent, replace the existing child anyway
          this.child[key] = newChild;
        }
      }
    }

    const arrAll = Object.entries(this.child).reduce((acc, [key, value]) => {
      if (value instanceof Promise)
        throw new Error(`Promise detected but not expected at ${this.compId}--mapComponent element ${key}...`);

      const sortComponent = (value) => {
        if (Component_A.isTComponent(value)) {
          value.parentComponentId = this.compId;
          acc.push(value);
        }
      };

      if (Array.isArray(value)) {
        value.forEach((v) => {
          sortComponent(v);
        });
      } else {
        sortComponent(value);
      }
      return acc;
    }, []);

    const initChildren = function () {
      return arrAll.map((child) => {
        return child._initCalled ? child : child.init();
      });
    };

    const status = this._props.options.async ? initChildren() : await Promise.all(initChildren());

    // Clean up the replaced old children objects
    toDispose.forEach((child) => child.destroy());
  }

  /**
   * @description Instantiation of TComponents sub-components that shall be initialized in a synchronous way.
   * All these components are then accessible within {@link TComponents.Component_A#onRender onRender} method by using this.child.<component-instance>
   * @member TComponents.Component_A#mapComponents
   * @method
   * @abstract
   * @returns {object} Contains all child TComponents instances used within the component.
   */
  mapComponents() {
    return {};
  }

  /**
   * @description Generates the HTML definition corresponding to the component.
   * @member TComponents.Component_A#markup
   * @method
   * @abstract
   * @param {object} self - The TComponents instance on which this method was called.
   * @returns {string}
   */
  markup(self) {
    return /*html*/ '';
  }

  /**
   * @description Adds an event listener to the specified element and keeps tracking of it.
   * @member TComponents.Component_A#addEventListener
   * @method
   * @protected
   * @param {HTMLElement} element - The target element to which the event listener will be added.
   * @param {string} eventType - The type of the event to listen for (e.g., 'click', 'mouseover').
   * @param {Function} listener - The function that will be called when the event is triggered.
   * @param {boolean|AddEventListenerOptions} [options] - Optional options object or useCapture flag.
   * @throws {Error} Throws an error if the specified element is not found.
   * @returns {void}
   */
  addEventListener(element, eventType, listener, options) {
    if (!element) throw new Error('Element not found');
    element.addEventListener(eventType, listener, options);

    if (!this._eventListeners.has(element)) {
      this._eventListeners.set(element, []);
    }
    this._eventListeners.get(element).push({eventType, listener});
  }

  /**
   * @description Removes all the event listeners that were added using addEventListener.
   * @member TComponents.Component_A#removeAllEventListeners
   * @method
   * @protected
   * @returns {void}
   */
  removeAllEventListeners() {
    if (!this._eventListeners) return;
    this._eventListeners.forEach((listeners, element) => {
      listeners.forEach(({eventType, listener}) => {
        element.removeEventListener(eventType, listener);
      });
    });
    this._eventListeners.clear();
  }

  /**
   * @description Clean up before initializing. Relevant from the second time the component is initialized.
   * - Call onDestroy method
   * - Call return function from onInit method if it exists
   * - Detach the component from the parent element
   * - Remove all local events, like this.on('event', callback)
   * - Remove all event listeners attached with this.addEventListener
   * @member TComponents.Component_A#destroy
   * @method
   * @returns {void}
   */
  destroy() {
    // calling instance specific onDestroy method
    try {
      this.trigger('before:destroy', this);
      this.onDestroy();
    } catch (error) {
      Logger.w(logModule, ErrorCode.FailedToDestroyComponent, 'Failed to destroy component', error);
    }

    // clean reference to attached callbacks
    // Before the component is destroyed, it should clean up all events.
    this.cleanUpEvents([]);

    // deinstall function (returned by onInit method)
    if (this._deinstallFunction && typeof this._deinstallFunction === 'function') this._deinstallFunction();

    if (this.container.parentElement) this.container.parentElement.removeChild(this.container);

    this.removeAllEventListeners();
    if (this.child) {
      Object.keys(this.child).forEach((key) => {
        if (Component_A.isTComponent(this.child[key])) this.child[key].destroy();
      });
    }
  }

  /**
   * @description This method is called internally during clean up process orchestrated by {@link destroy() destroy}.
   * @member TComponents.Component_A#onDestroy
   * @method
   * @abstract
   * @async
   */

  onDestroy() {}

  /**
   * @description Static method to check if an object is a TComponent.
   * @member TComponents.Component_A.isTComponent
   * @static
   * @param {object} obj - The object to check.
   * @returns {boolean} True if the object is a TComponent, false otherwise.
   */
  static isTComponent(obj) {
    return obj && obj._isTComponent ? true : false;
  }

  /**
   * @description Compares the properties of two objects to determine if they are equal.
   * @member TComponents.Component_A._equalProps
   * @static
   * @private
   * @param {object} newProps
   * @param {object} prevProps
   * @returns {boolean}
   */
  static _equalProps(newProps, prevProps) {
    // use JSON.stringify with helper function to convert function to string to compare objects
    const stringify = (obj) => {
      return JSON.stringify(obj, (key, value) => {
        if (Component_A.isTComponent(value)) {
          return value.compId;
        }
        if (typeof value === 'function') {
          return value.toString();
        }
        return value;
      });
    };

    return stringify(newProps) === stringify(prevProps);
  }

  /**
   * @description Changes the DOM element in which this component is to be inserted.
   * @member TComponents.Component_A#attachToElement
   * @method
   * @param {HTMLElement | Element} element - Container DOM element
   * @returns {void}
   */
  attachToElement(element) {
    if (!Component_A._isHTMLElement(element)) {
      Logger.w(logModule, ErrorCode.FailedToAttachToElement, 'Element to be attached is not HTML element');
      return;
    }
    // throw new Error(`HTML element container required but not detected`);

    if (!this.parent) {
      this.parent = element;
      this.parent.appendChild(this.container);
    } else if (this.parent === element) {
      // only add if it not already exists
      this.parent.contains(this.container) === false && this.parent.appendChild(this.container);
    } else {
      // remove from old parent to attach to new one
      this.parent.contains(this.container) && this.parent.removeChild(this.container);
      this.parent = element;
      this.parent.appendChild(this.container);
    }
  }

  /**
   * @description Returns the first Element within the component that matches the specified selector. If no matches are found, null is returned.
   * @member TComponents.Component_A#find
   * @method
   * @param {string} selector - A string containing one selector to match. This string must be a valid CSS selector string
   * @returns {HTMLElement | Element} An Element object representing the first element within the component that matches the specified set of CSS selectors, or null if there are no matches.
   */
  find(selector) {
    const el = this.template && this.template.content.querySelector(selector);

    return el ? el : this.container.querySelector(selector);
  }

  /**
   * @description Returns an Array representing the component's elements that matches the specified selector. If no matches are found, an empty Array is returned.
   * @member TComponents.Component_A#all
   * @method
   * @param {string} selector - A string containing one selector to match. This string must be a valid CSS selector string
   * @returns {Element[]} An Array of Elements that matches the specified CSS selector, or an empty array if there are no matches.
   */
  all(selector) {
    var aContainer = Array.from(this.container.querySelectorAll(selector));
    var nlTemplate = this.template && this.template.content.querySelectorAll(selector);

    if (nlTemplate) {
      Array.from(nlTemplate).forEach(function (tElem) {
        var isDuplicate = aContainer.some((cElem) => tElem === cElem);
        if (!isDuplicate) {
          aContainer[aContainer.length] = tElem;
        }
      });
    }

    return aContainer;
  }

  /**
   * @description Initialize the state for component according to the "defaultState" property
   * @member TComponents.Component_A~initState
   * @method
   * @private
   * @param {string} defaultState - A string describing the initialization state, the value can be show_disable|show_enable|hide.
   * @returns {void}
   */
  initState(defaultState) {
    const state = defaultState || this.props.defaultState;
    if (!state) {
      return;
    }
    switch (state) {
      case 'show':
        this.show();
        break;
      case 'hide':
        this.hide();
        break;
      case 'enable':
        this.enabled = true;
        break;
      case 'disabled':
        this.enabled = false;
        break;
      case 'show_disable':
        this.show();
        this.enabled = false;
        break;
      case 'show_enable':
      default:
        this.show();
        this.enabled = true;
        break;
    }
  }

  /**
   * @description Changes visibility of the component to not show it in the view.
   * @member TComponents.Component_A#hide
   * @method
   * @returns {void}
   */
  hide() {
    this.container.classList.add('tc-hidden');
  }

  /**
   * @description Changes visibility of the component to show it in the view.
   * @member TComponents.Component_A#show
   * @method
   * @returns {void}
   */
  show() {
    this.container.classList.remove('tc-hidden');
  }

  /**
   * @returns {boolean}
   */
  get hidden() {
    return this.container.classList.contains('tc-hidden');
  }

  /**
   * @description Changes apperance of the component (border and background color) to frame it or not.
   * @member TComponents.Component_A#cssBox
   * @method
   * @param {boolean} enable - if true, the component is framed, if false, not frame is shown
   * @returns {void}
   */
  cssBox(enable = true) {
    enable ? this.container.classList.add('tc-container-box') : this.container.classList.remove('tc-container-box');
  }

  /**
   * @description Sets or returns the contents of a style declaration as a string.
   * @member TComponents.Component_A#css
   * @method
   * @param {string|Array} properties - Specifies the content of a style declaration.
   * E.g.: "background-color:pink;font-size:55px;border:2px dashed green;color:white;"
   * @returns {string}
   */
  css(properties) {
    if (!properties) {
      this.container.style.cssText = '';
      return;
    }

    let s = '';
    if (typeof properties === 'string') s = properties;
    else if (Array.isArray(properties)) {
      s = properties.join(';');
      s += ';';
    } else if (typeof properties === 'object') {
      for (const [key, val] of Object.entries(properties)) {
        s += `${key} : ${val};`;
      }
    }
    this.container.style.cssText = s;
  }

  /**
   * @description Adds a class to underlying element(s) containing the input selector
   * @member TComponents.Component_A#cssAddClass
   * @method
   * @param {string} selector - CSS selector, if class: ".selector", if identifier: "#selector"
   * @param {string | string[]} classNames - name of the class to appy (without dot)
   * @param {boolean} [all] - if true it will apply the class to all selector found, otherwise it applies to the first one found
   * @returns {void}
   */
  cssAddClass(selector, classNames, all = false) {
    if (!selector || !classNames) return;
    let arrClassNames = Array.isArray(classNames) ? classNames : [...classNames.replace(/^\s/g, '').split(' ')];

    // check if array is empty
    if (arrClassNames.length === 0) return;
    // filter out emmpty strings
    arrClassNames = arrClassNames.filter((c) => c !== '');

    if (selector === 'this') this.container.classList.add(...arrClassNames);
    else {
      const el = all ? this.all(selector) : this.find(selector);
      if (el)
        Array.isArray(el) ? el.forEach((el) => el.classList.add(...arrClassNames)) : el.classList.add(...arrClassNames);
    }
  }

  /**
   * @description Removes a class to underlying element(s) containing the input selector
   * @member TComponents.Component_A#cssRemoveClass
   * @method
   * @param {string} selector - CSS selector, if class: ".selector", if identifier: "#selector"
   * @param {string} classNames - name of the class to appy (without dot)
   * @param {boolean} [all] - if true it will apply the class to all selector found, otherwise it applies to the first one found
   * @returns {void}
   */
  cssRemoveClass(selector, classNames, all = false) {
    if (!selector || !classNames) return;
    let arrClassNames = Array.isArray(classNames) ? classNames : [...classNames.replace(/^\s/g, '').split(' ')];

    // check if array is empty
    if (arrClassNames.length === 0) return;
    // filter out emmpty strings
    arrClassNames = arrClassNames.filter((c) => c !== '');

    if (selector === 'this') this.container.classList.remove(...arrClassNames);
    else {
      const el = all ? this.all(selector) : this.find(selector);
      if (el)
        Array.isArray(el)
          ? el.forEach((el) => el.classList.remove(...arrClassNames))
          : el.classList.remove(...arrClassNames);
    }
  }

  /**
   * @description Force a rerender when a component is handled inside the mapComponents method of a higher order component.
   * Normally this happens only when the props has changed. If this function is called inside a component.
   * @member TComponents.Component_A#forceUpdate
   * @method
   * @private
   * @returns {void}
   */
  forceUpdate() {
    this._fUpdate = true;
  }

  /**
   * @description Synchronizes the input data.
   * This method checks whether the input data is valid and triggers synchronization
   * using an external component. It handles errors gracefully and provides
   * feedback through a popup if synchronization fails.
   * @member TComponents.Component_A#syncInputData
   * @method
   * @async
   * @param {any} value - The value to be synchronized. This can be of any type depending on the implementation.
   * @throws {Error} If binding data is null or synchronization fails unexpectedly.
   * @returns {Promise<boolean>} A promise that resolves to `true` if the synchronization is successful,
   *                             or `false` if an error occurs or binding data is null.
   */
  async syncInputData(value) {
    if (this._props.inputVar && this._props.inputVar.func === Component_A.INPUTVAR_FUNC.SYNC) {
      if (this._bindData !== null) {
        await Component_A.syncData(value, this);
      } else {
        throw new Error(ErrorCode.FailedToSyncBindingData, {
          cause: 'Binded input variable is empty',
        });
      }
    } else if (this._props.inputVar && this._props.inputVar.func === Component_A.INPUTVAR_FUNC.CUSTOM) {
      return true;
    } else {
      throw new Error(ErrorCode.FailedToSyncBindingData, {
        cause: 'Binded input variable is not valid.',
      });
    }
  }

  /**
   * @description Synchronizes the input data with a popup notification.
   * @member TComponents.Component_A#syncInputDataWithPopup
   * @method
   * @param {any} value - The value to be synchronized.
   * @throws {Error} If synchronization fails.
   * @returns {Promise<void>}
   */
  async syncInputDataWithPopup(value) {
    try {
      await this.syncInputData(value);
    } catch (e) {
      Logger.e(logModule, 'Failed to synchronize binding data for component', e);
      Popup_A.danger(
        `${ExceptionIdMap.FailedToSyncBindingData}-${Component_A.t(`framework:${ExceptionIdMap.FailedToSyncBindingData}.title`)}`,
        Component_A.t(`framework:${ExceptionIdMap.FailedToSyncBindingData}.causes`),
        e.cause,
      );
      throw new Error(ErrorCode.FailedToSyncBindingData, {
        cause: 'Failed to synchronize binding data for component.',
      });
    }
  }

  /**
   * @description Determines if the label should be positioned at the start (top or left).
   * @member TComponents.Component_A#_labelStart
   * @method
   * @private
   * @returns {boolean} True if the label should be at the start, false otherwise.
   */
  _labelStart() {
    return this._props.label && (this._props.labelPos.includes('top') || this._props.labelPos.includes('left'));
  }

  /**
   * @description Determines if the label should be positioned at the end (bottom or right).
   * @member TComponents.Component_A#_labelEnd
   * @method
   * @private
   * @returns {boolean} True if the label should be at the end, false otherwise.
   */
  _labelEnd() {
    return this._props.label && (this._props.labelPos.includes('bottom') || this._props.labelPos.includes('right'));
  }

  /**
   * @description Generates the markup for the component including the label if necessary.
   * @member TComponents.Component_A#_markupWithLabel
   * @method
   * @private
   * @returns {string} The HTML markup of the component.
   */
  _markupWithLabel() {
    const markup = this.markup(this);

    return /*html*/ `
      ${markup}
  `;
  }

  /**
   * @description Handles and updates the internal data object with the provided data.
   * @member TComponents.Component_A#_handleData
   * @method
   * @private
   * @param {object} data - The data to handle and update.
   * @returns {void}
   */
  _handleData(data) {
    if (data) {
      if (!this._data) this._data = {};
      Object.keys(data).forEach((key) => (this._data[key] = data[key]));
    }
  }

  /**
   * @description Creates a new template element and sets its innerHTML with the component's markup including the label.
   * @member TComponents.Component_A#_createTemplate
   * @method
   * @private
   * @returns {void}
   */
  _createTemplate() {
    this.template = document.createElement('template');
    this.template.innerHTML = this._markupWithLabel();
  }

  /**
   * @description Recursively search for property of an object and underlying objects of type TComponents and FPComponents.
   * @member TComponents.Component_A._hasChildOwnProperty
   * @method
   * @static
   * @private
   * @param {object} obj
   * @param {string} property
   * @param {object[]} [result=[]] result - Array of objects already found during the recursively execution
   * @returns {object[]} Array of objects found or empty array if nothing found
   */
  static _hasChildOwnProperty(obj, property, result = []) {
    if (typeof obj === 'object' && obj !== null && (Component_A.isTComponent(obj) || Component_A._isFPComponent(obj))) {
      for (const val of Object.values(obj)) {
        if (typeof val === 'object' && val !== null && val !== obj && !Component_A._isHTMLElement(val)) {
          if (Object.prototype.hasOwnProperty.call(val, property)) {
            result.push(val);
          }
          Component_A._hasChildOwnProperty(val, property, result);
        }
      }
    }
    return result;
  }

  /**
   * @description Recursively check for instances of type TComponent and FPComponent
   * @member TComponents.Component_A._hasChildComponent
   * @method
   * @static
   * @returns {boolean} true if the object has child components, false otherwise
   */
  static _hasChildComponent(obj, result = []) {}

  /**
   * @description Checks if the object is an instance of any FPComponent.
   * @member TComponents.Component_A._isFPComponent
   * @method
   * @static
   * @param {any} o
   * @returns {boolean} true if the object is an FPComponent, false otherwise
   */
  static _isFPComponent(o) {
    return Object.values(FPComponents).some((FPComponent) => o instanceof FPComponent);
  }

  /**
   * @description Check if an entry is HTML Element
   * @member TComponents.Component_A._isHTMLElement
   * @method
   * @static
   * @param {any} o
   * @returns {boolean} true if entry is an HTMLElement, false otherwise
   */
  static _isHTMLElement(o) {
    return typeof HTMLElement === 'object'
      ? o instanceof HTMLElement //DOM2
      : o && typeof o === 'object' && o !== null && o.nodeType === 1 && typeof o.nodeName === 'string';
  }

  /**
   * @description Loads a CSS stylesheet from a string and inserts it into the document.
   * This method uses a hash to avoid injecting the same CSS twice.
   * @member TComponents.Component_A.loadCssClassFromString
   * @method
   * @static
   * @param {string} css - The CSS string to load.
   * @throws {Error} If the provided css argument is not a string.
   * @returns {void}
   */
  static loadCssClassFromString(css) {
    if (typeof css !== 'string') throw new Error('css must be a string');
    const existingStyles = document.querySelectorAll('style');
    // Check if any existing <style> tag has the same CSS content
    for (let style of existingStyles) {
      if (style.innerHTML === css) {
        // A matching <style> tag is found, so we don't need to insert a new one
        return;
      }
    }
    // No matching <style> tag found, proceed to insert a new one
    const tComponentStyle = document.createElement('style');
    tComponentStyle.innerHTML = css;
    const ref = document.querySelector('script');
    if (ref) {
      ref.parentNode.insertBefore(tComponentStyle, ref);
    }
  }

  /**
   * @description Loads a unique CSS stylesheet from a string and inserts it into the document.
   * This method uses a hash to avoid injecting the same CSS twice.
   * @member TComponents.Component_A.loadCssClassFromStringUnique
   * @method
   * @static
   * @param {string} css - The CSS string to load.
   * @param {string} selector - The CSS selector to apply the styles to.
   * @throws {Error} If the provided arguments are not a string.
   * @returns {void}
   */
  static loadCssClassFromStringUnique(css, selector) {
    if (typeof css !== 'string') throw new Error('css must be a string');
    if (!selector || typeof selector !== 'string') throw new Error('selector must be a string');

    const existingStyle = document.querySelector(selector);
    // Check if any existing <style> tag has the same CSS content
    if (existingStyle) {
      existingStyle.innerHTML = css;
    } else {
      // No matching <style> tag found, proceed to insert a new one
      const tComponentStyle = document.createElement('style');
      if (selector.startsWith('.')) {
        tComponentStyle.setAttribute('class', selector.substring(1));
      } else if (selector.startsWith('#')) {
        tComponentStyle.setAttribute('id', selector.substring(1));
      }
      tComponentStyle.innerHTML = css;
      const ref = document.querySelector('script');
      if (ref) {
        ref.parentNode.insertBefore(tComponentStyle, ref);
      }
    }
  }

  /**
   * @description Returns markup based on a condition.
   * @member TComponents.Component_A.mIf
   * @method
   * @static
   * @param {boolean} condition - The condition to evaluate.
   * @param {string} markup - The markup to return if the condition is true.
   * @param {string} [elseMarkup=''] - The markup to return if the condition is false.
   * @returns {string} The appropriate markup based on the condition.
   */
  static mIf(condition, markup, elseMarkup = '') {
    return condition ? markup : elseMarkup;
  }

  /**
   * @description Maps an array to a string of markup.
   * @member TComponents.Component_A.mFor
   * @method
   * @static
   * @param {any[]} array - The array to map.
   * @param {Function} markup - The function to generate markup for each item.
   * @returns {string} The concatenated string of markup.
   */
  static mFor(array, markup) {
    return array.map((item, index) => markup(item, index)).join('');
  }

  /**
   * @description Validates if the provided text can be interpreted as a boolean or number. (specific for switch,digitalled)
   * @member TComponents.Component_A#validateText
   * @method
   * @protected
   * @param {string|boolean|number} t - The text value to be validated.
   * @returns {boolean} True if the text is valid, false otherwise.
   */
  validateText(t) {
    if (typeof t === 'boolean' || typeof t === 'number') {
      return true;
    } else if (typeof t === 'string') {
      const lowerCaseText = t.trim().toLowerCase();
      if (
        lowerCaseText === '1' ||
        lowerCaseText === 'true' ||
        lowerCaseText === '0' ||
        lowerCaseText === 'false' ||
        lowerCaseText === ''
      ) {
        return true;
      } else {
        return false;
      }
    }
    return false;
  }

  /**
   * @description Converts the provided text to a boolean value. (specific for switch,digitalled)
   * @member TComponents.Component_A#convertDataToBool
   * @method
   * @protected
   * @param {string|boolean|number} t - The text value to be converted.
   * @returns {boolean} True if the text is true|TRUE|1, false otherwise.
   */
  convertDataToBool(t) {
    if (typeof t === 'boolean' || typeof t === 'number') {
      return t === true || t === 1;
    } else if (typeof t === 'string') {
      const lowerCaseText = t.trim().toLowerCase();
      return lowerCaseText === '1' || lowerCaseText === 'true';
    }
    return false;
  }

  /**
   * @description Updates the active state based on the provided text value. (specific for switch,digitalled)
   * @member TComponents.Component_A~_updateActiveFromText
   * @method
   * @private
   * @param {string|boolean|number } t - The text value to be converted.
   * @throws {Error} If the provided text is invalid.
   * @returns {void}
   */
  _updateActiveFromText(t) {
    try {
      if (typeof t === 'boolean') {
        this.active = t;
      } else if (typeof t === 'number') {
        this.active = t === 1;
      } else if (typeof t === 'string') {
        const lowerCaseText = t.trim().toLowerCase();
        if (lowerCaseText === '1' || lowerCaseText === 'true') {
          this.active = true;
        } else if (lowerCaseText === '0' || lowerCaseText === 'false') {
          this.active = false;
        } else if (lowerCaseText === '') {
          // do nothing
        } else {
          throw new Error(ErrorCode.FailedToSetText, {
            cause: 'Invalid text value',
          });
        }
      } else {
        this.active = false;
      }
    } catch (error) {
      Logger.e(logModule, ErrorCode.FailedToSetText, `Invalid text data for component ${this.compId}.`, t);
      this.active = false;
      throw new Error(ErrorCode.FailedToSetText, {cause: error});
    }
  }

  /**
   * @description Updates the properties of the component.
   * @member TComponents.Component_A#updateProps
   * @method
   * @param {object} [_newProps={}] - The new properties to merge with the current properties.
   * @returns {void}
   */
  updateProps(_newProps = {}) {
    this.props = Object.assign(this.props, _newProps || this.defaultProps());
  }

  /**
   * @description Handles component state changes based on resource and action.
   * @member TComponents.Component_A.handleComponentOn
   * @method
   * @static
   * @param {object} self - The component instance.
   * @param {object} options - The options for the handler.
   * @param {string} options.resource - The resource to monitor.
   * @param {object} options.instance - The instance information.
   * @param {string} options.state - The state to monitor.
   * @param {string} action - The action to perform.
   * @returns {void}
   */
  static async handleComponentOn(self, {resource, instance, state}, action) {
    const actions = {
      disable: (condition) => (condition ? (self.enabled = false) : (self.enabled = true)),
      hide: (condition) => (condition ? self.hide() : self.show()),
      update: (condition) => {
        // eslint-disable-next-line no-undef
        condition && self.updateProps(updateProps);
      },
    };

    const eventHandlers = {
      OpMode: {
        event: 'op-mode',
        monitorFn: API.CONTROLLER.monitorOperationMode,
        callback: monitorOpMode,
        states: [API.CONTROLLER.OPMODE.Auto, API.CONTROLLER.OPMODE.ManualR],
      },
      Execution: {
        event: 'execution-state',
        monitorFn: API.RAPID.monitorExecutionState,
        callback: monitorExecutionState,
        states: [API.RAPID.EXECUTIONSTATE.Running, API.RAPID.EXECUTIONSTATE.Stopped],
      },
      Motor: {
        event: 'controller-state',
        monitorFn: API.CONTROLLER.monitorControllerState,
        callback: monitorControllerState,
        states: [API.CONTROLLER.STATE.MotorsOn, API.CONTROLLER.STATE.MotorsOff],
      },
      Error: {
        event: 'controller-state',
        monitorFn: API.CONTROLLER.monitorControllerState,
        callback: monitorControllerState,
        states: [API.CONTROLLER.STATE.SysFailure, API.CONTROLLER.STATE.EStop, API.CONTROLLER.STATE.GuardStop],
      },
    };

    const handler = eventHandlers[resource];

    if (handler) {
      handler.states.forEach(async (s) => {
        const cb = (state) => {
          actions[action](state === s);
        };

        if (state === s) {
          Component_A.globalEvents.on(handler.event, cb);

          // trigger once the callback depending on the event
          let currentState;
          if (handler.event === 'op-mode') {
            currentState = await RWS.Controller.getOperationMode();
          } else if (handler.event === 'execution-state') {
            currentState = await RWS.Rapid.getExecutionState();
          } else if (handler.event === 'controller-state') {
            currentState = await RWS.Controller.getControllerState();
          }
          cb(currentState);
        }
      });

      if (Component_A.globalEvents.count(handler.event) <= 1) {
        try {
          await handler.monitorFn(handler.callback);
        } catch (e) {
          Popup_A.error(e, 'TComponents.Component_A');
        }
      }
    }
  }

  /**
   * @description Disables the component based on a condition.
   * @member TComponents.Component_A.disableComponentOn
   * @method
   * @static
   * @param {object} self - The component instance.
   * @param {object} condition - The condition to evaluate.
   * @returns {void}
   */
  static disableComponentOn(self, condition) {
    this.handleComponentOn(self, condition, 'disable');
  }

  /**
   * @description Hides the component based on a condition.
   * @member TComponents.Component_A.hideComponentOn
   * @method
   * @static
   * @param {object} self - The component instance.
   * @param {object} condition - The condition to evaluate.
   * @returns {void}
   */
  static hideComponentOn(self, condition) {
    this.handleComponentOn(self, condition, 'hide');
  }

  /**
   * @description Processes conditions and updates the component properties accordingly.
   * @member TComponents.Component_A.conditionProcessing
   * @method
   * @static
   * @param {object} self - The component instance.
   * @param {any[]} states - The states to process.
   * @returns {void}
   */
  static conditionProcessing(self, states) {
    // Defined namespace mapping to store
    const namespaceMapping = {
      signal: Signal,
      variable: Rapid,
      rapid: Rapid,
      uas: UAS,
    };

    // Loop through each state and set up monitors for the conditions
    const monitors = new Map(); // To avoid setting up multiple monitors for the same key
    for (const state of states) {
      state.condition.forEach((cond) => {
        if (!monitors.has(`${cond.mode}-${cond.key}`)) {
          monitors.set(`${cond.mode}-${cond.key}`, {
            mode: cond.mode,
            type: cond.type,
            key: cond.key,
            states: [],
          });
        }
        if (!monitors.get(`${cond.mode}-${cond.key}`).states.find((s) => s.uuid === state.uuid)) {
          monitors.get(`${cond.mode}-${cond.key}`).states.push(state);
        }
      });
    }

    // Build an array of monitor values in a way compatible
    // Since Map.prototype.values() is not supported with older browsers
    const monitorValuesArray = [];
    monitors.forEach(function (v) {
      monitorValuesArray.push(v);
    });

    monitorValuesArray.forEach((cond) => {
      switch (cond.mode) {
        case Signal: {
          if (cond.key) {
            const vs = cond.key.split('/');
            const mappingKey = Store.generateMappingKey(Signal, vs);
            API.SIGNALMONITOR.monitorDigitalSignal(
              {
                type: cond.type,
                network: vs[1],
                device: vs[2],
                name: vs[3],
              },
              (vvv) => {
                Store.getNamespace(Signal).setMapping(mappingKey, {
                  state: Success,
                  value: vvv,
                });
                triggerState(cond.states);
              },
            );
          }
          break;
        }

        case Variable:
        case Rapid: {
          var vs = cond.key.split('/');
          const mappingKey = Store.generateMappingKey(Rapid, vs); // type,task,module,name
          API.VARIABLEMONITOR.monitorVariable(
            {
              type: cond.type,
              task: vs[1],
              module: vs[2],
              name: vs[3],
            },
            (vvv) => {
              Store.getNamespace(Rapid).setMapping(mappingKey, {
                state: Success,
                value: vvv,
              });
              triggerState(cond.states);
            },
          );
          break;
        }

        case UAS: {
          if (cond.key) {
            const grantKey = cond.key;
            API.UAS.monitorUserGrant(grantKey, () => {
              triggerState(cond.states);
            });
          }
          break;
        }

        default:
          break;
      }
    });

    // Function to check all states and update component properties accordingly
    function triggerState(states) {
      for (const item of states) {
        let pass = true;
        for (var cond of item.condition) {
          //     mode:'signal' | 'variable',
          //     type:'bool' | 'num' | 'string',
          //     key:'ACOK' | 'Rapid/T_ROB1/BASE/n155',
          const namespace = namespaceMapping[cond.mode];
          const vs = cond.key.split('/');
          const parameters = vs;

          const mappingKey = Store.generateMappingKey(namespace, parameters);
          const namespaceStore = Store.getNamespace(namespace);
          const mapping = namespaceStore && namespaceStore.getMapping(mappingKey);

          const {value: current_value = undefined} = mapping || {
            state: undefined,
            value: undefined,
          };

          let currentValue = current_value;
          const triggerValue = cond.value;

          if (vs.length > 4) {
            // array with subindex or record with field, example: T_TOB1.BASE.bArray.1 or T_TOB1.BASE.rData.subfield
            const indentifier = [cond.type, `${vs[1]}.${vs[2]}`].concat(vs.slice(3)); // formater: bool,T_TOB1.BASE, barray, 1
            const {a: ap, r: rp, d: rd} = TComponents.Component_A.getDataStruct(indentifier, self);
            let a = ap;
            let r = rp;
            const d = rd;

            if (a.length > 0) {
              currentValue = API.RAPID.parseRapidArrayValue(currentValue, a);
            }
            if (d) {
              d.parseFromRapid(currentValue);
              currentValue = d.getValueByNamePath(r);
            }
          }

          if (!pass) break;
          switch (cond.judgment) {
            case '≠':
              if (!(currentValue != triggerValue)) {
                pass = false;
              }
              break;
            case '>':
              if (!(currentValue > triggerValue)) {
                pass = false;
              }
              break;
            case '<':
              if (!(currentValue < triggerValue)) {
                pass = false;
              }
              break;
            case '≥':
              if (!(currentValue >= triggerValue)) {
                pass = false;
              }
              break;
            case '≤':
              if (!(currentValue <= triggerValue)) {
                pass = false;
              }
              break;
            default:
              if (!(currentValue == triggerValue)) {
                pass = false;
              }
              break;
          }
        }

        if (pass) {
          if (item.tips) {
            Object.assign(item.props, {tips: item.tips});
          }
          self.updateProps(item.props);
          switch (item.action) {
            case 'show':
              self.show();
              break;
            case 'hide':
              self.hide();
              break;
            case 'enable':
              self.enabled = true;
              break;
            case 'disabled':
              self.enabled = false;
              break;
            case 'show_enable':
              self.show();
              self.enabled = true;
              break;
            case 'show_disable':
              self.show();
              self.enabled = false;
              break;
          }
        }
      }
    }
  }

  /**
   * @description Generates dynamic text based on the input type.
   * @member TComponents.Component_A.dynamicText
   * @method
   * @static
   * @param {string|number} text - The text to process.
   * @param {object} self - The component instance.
   * @returns {string|number} The processed text.
   */
  static dynamicText(text, self) {
    return Component_A.dynamicProperty(text, self, 'text');
  }

  /**
   * @deprecated This interface is to be deprecated.
   * If you need to perform multilingual parsing, please use {@link TComponent.tParse}.
   * @description Generates dynamic value based on the input type.
   * @member TComponents.Component_A.dynamicProperty
   * @method
   * @static
   * @param {string|number} text - The text to process.
   * @param {object} self - The component instance.
   * @param {String} propName - The property to update.
   * @returns {string|number} The processed text.
   */
  static dynamicProperty(text, self, propName = 'text') {
    if (!(typeof text == 'string' && text.trim() !== '')) {
      return text;
    }

    //{{App.container1.a}}-----------webdata
    if (text.startsWith('{{') && text.endsWith('}}')) {
      const key = text.slice(2, -2);
      self &&
        API.WEBDATAMONITOR.monitorWebdata(key, (vvv) => {
          self.updateProps({
            [propName]: API.formatValue(vvv),
          });
        });
      return text;
    }

    //$$digitalsignal.ManualMode$$------------signal
    else if (text.startsWith('$$') && text.endsWith('$$')) {
      const vs = text.slice(2, -2).split('.');
      if (vs.length === 2) {
        //digitalsignal.signal
        self &&
          API.SIGNALMONITOR.monitorSignal(
            {
              type: vs[0],
              name: vs[1],
            },
            (vvv) => {
              self.updateProps({
                [propName]: API.formatValue(vvv),
              });
            },
          );
      } else if (vs.length === 4) {
        //digitalsignal.network.device.signal
        self &&
          API.SIGNALMONITOR.monitorSignal(
            {
              type: vs[0],
              network: vs[1],
              device: vs[2],
              name: vs[3],
            },
            (vvv) => {
              self.updateProps({
                [propName]: API.formatValue(vvv),
              });
            },
          );
      }
      return text;
    }

    //@@tooldata|task1.module1|tooldata1@@----------rapid
    else if (text.startsWith('@@') && text.endsWith('@@')) {
      const vs = text.slice(2, -2).split('|');

      var task_module = vs[1].split('.');
      if (vs.length === 3) {
        self &&
          API.VARIABLEMONITOR.monitorVariable(
            {
              type: vs[0],
              task: task_module[0],
              module: task_module[1],
              name: vs[2],
            },
            (vvv) => {
              self.updateProps({
                [propName]: API.formatValue(vvv),
              });
            },
          );
      } else if (vs.length > 3) {
        // Trying to subscribe specific part of a RAPID variable
        // @@robtarget|task1.module1|r1|trans|x@@----------rapid => subscribe the x part of the robtarget r1
        const {a: ap, r: rp, d: rd} = TComponents.Component_A.getDataStruct(vs, self);

        self &&
          API.VARIABLEMONITOR.monitorVariable(
            {
              type: vs[0],
              task: task_module[0],
              module: task_module[1],
              name: vs[2],
            },
            (vvv) => {
              try {
                let a = ap;
                let r = rp;
                const d = rd;
                let v = API.formatValue(vvv);
                if (a.length > 0) {
                  v = API.RAPID.parseRapidArrayValue(v, a);
                }
                if (d) {
                  d.parseFromRapid(v);
                  v = d.getValueByNamePath(r);
                }

                self.updateProps({
                  [propName]: API.formatValue(v),
                });
              } catch (e) {
                Logger.e(
                  logModule,
                  ErrorCode.FailedToParseMonitoredData,
                  'Failed to parse monitored data with stored data structure.',
                  e,
                );
                Popup_A.danger(
                  `${ExceptionIdMap.FailedToParseMonitoredData}-${Component_A.t(`framework:${ExceptionIdMap.FailedToParseMonitoredData}.title`)}`,
                  Component_A.t(`framework:${ExceptionIdMap.FailedToParseMonitoredData}.causes`),
                );
              }
            },
          );
      }
      return text;
    } else if (text.startsWith('!!') && text.endsWith('!!')) {
      return Component_A.t(text.slice(2, -2));
    } else {
      return text || '';
    }
  }

  /**
   * @description Parse the specific value according to the bound text.
   * @member TComponents.Component_A.resolveBindingExpression
   * @method
   * @static
   * @param {string|number} text - The text to process.
   * @param {object} self - The component instance.
   * @param {String} propName - The property to update.
   * @returns {string|number} The processed text.
   */
  static resolveBindingExpression(text, self, propName = 'text') {
    if (!(typeof text == 'string' && text.trim() !== '')) {
      return text;
    }

    //{{App.container1.a}}-----------webdata
    if (text.startsWith('{{') && text.endsWith('}}')) {
      const key = text.slice(2, -2);
      self &&
        API.WEBDATAMONITOR.monitorWebdata(key, (vvv) => {
          if (self[propName] !== undefined) {
            self[propName] = API.formatValue(vvv);
            return;
          }
          self.updateProps({
            [propName]: API.formatValue(vvv),
          });
        });
      return text;
    }

    //$$digitalsignal.ManualMode$$------------signal
    else if (text.startsWith('$$') && text.endsWith('$$')) {
      const vs = text.slice(2, -2).split('.');
      if (vs.length === 2) {
        //digitalsignal.signal
        self &&
          API.SIGNALMONITOR.monitorSignal(
            {
              type: vs[0],
              name: vs[1],
            },
            (vvv) => {
              if (self[propName] !== undefined) {
                self[propName] = API.formatValue(vvv);
                return;
              }
              self.updateProps({
                [propName]: API.formatValue(vvv),
              });
            },
          );
      } else if (vs.length === 4) {
        //digitalsignal.network.device.signal
        self &&
          API.SIGNALMONITOR.monitorSignal(
            {
              type: vs[0],
              network: vs[1],
              device: vs[2],
              name: vs[3],
            },
            (vvv) => {
              if (self[propName] !== undefined) {
                self[propName] = API.formatValue(vvv);
                return;
              }
              self.updateProps({
                [propName]: API.formatValue(vvv),
              });
            },
          );
      }

      return text;
    }

    //@@tooldata|task1.module1|tooldata1@@----------rapid
    else if (text.startsWith('@@') && text.endsWith('@@')) {
      const vs = text.slice(2, -2).split('|');
      if (vs.length < 3) return text;

      var task_module = vs[1].split('.');
      if (vs.length === 3) {
        self &&
          API.VARIABLEMONITOR.monitorVariable(
            {
              type: vs[0],
              task: task_module[0],
              module: task_module[1],
              name: vs[2],
            },
            (vvv) => {
              if (self[propName] !== undefined) {
                self[propName] = API.formatValue(vvv);
                return;
              }
              self.updateProps({
                [propName]: API.formatValue(vvv),
              });
            },
          );
      } else if (vs.length > 3) {
        // Trying to subscribe specific part of a RAPID variable
        // @@robtarget|task1.module1|r1|trans|x@@----------rapid => subscribe the x part of the robtarget r1
        const {a: ap, r: rp, d: rd} = TComponents.Component_A.getDataStruct(vs, self);

        self &&
          API.VARIABLEMONITOR.monitorVariable(
            {
              type: vs[0],
              task: task_module[0],
              module: task_module[1],
              name: vs[2],
            },
            (vvv) => {
              try {
                let a = ap;
                let r = rp;
                const d = rd;
                let v = API.formatValue(vvv);
                if (a.length > 0) {
                  v = API.RAPID.parseRapidArrayValue(v, a);
                }
                if (d) {
                  d.parseFromRapid(v);
                  v = d.getValueByNamePath(r);
                }

                if (self[propName] !== undefined) {
                  self[propName] = API.formatValue(v);
                  return;
                }
                self.updateProps({
                  [propName]: API.formatValue(v),
                });
              } catch (e) {
                Logger.e(
                  logModule,
                  ErrorCode.FailedToParseMonitoredData,
                  'Failed to parse monitored data with stored data structure.',
                  e,
                );
                Popup_A.danger(
                  `${ExceptionIdMap.FailedToParseMonitoredData}-${Component_A.t(`framework:${ExceptionIdMap.FailedToParseMonitoredData}.title`)}`,
                  Component_A.t(`framework:${ExceptionIdMap.FailedToParseMonitoredData}.causes`),
                );
              }
            },
          );
      }
      return text;
    } else if (text.startsWith('!!') && text.endsWith('!!')) {
      if (self[propName] !== undefined) {
        self[propName] = Component_A.t(text.slice(2, -2));
        return;
      }
      self.updateProps({
        [propName]: Component_A.t(text.slice(2, -2)),
      });

      return text;
    } else {
      return text || '';
    }
  }

  /**
   * Extracts data structure information from the variable string.
   * @member TComponents.Component_A.getDataStruct
   * @method
   * @static
   * @param {any} vs - The variable string parts.
   * @param {any} self - The component instance.
   * @returns {object} The extracted data structure information.
   */
  static getDataStruct(vs, self) {
    try {
      const a = vs
        .slice(2)
        .filter((vvs) => {
          return vvs.match(/^\d+/);
        })
        .map(Number);
      const r = vs.slice(a.length + 3);
      let dataStruct = self._props.dataStruct ? JSON.parse(self._props.dataStruct) : undefined;

      let d;
      if (dataStruct) {
        d = API.RAPID.getRecordData(vs[0], '');
      } else {
        try {
          //get system dataStructure
          d = API.RAPID.getRecordData(vs[0], '');
          dataStruct = API.RAPID.getDataStructure(d.type);
        } catch (error) {
          console.log(error);
          Logger.w(logModule, 'No data structure stored or stored data structure is not valid.', error);
          d = undefined;
        }
      }

      if (d && dataStruct) {
        d = d.fromJSON(dataStruct);
      }
      return {a, r, d};
    } catch (e) {
      Logger.e(logModule, ErrorCode.FailedToGetDataStructure, e);
      throw ErrorCode.FailedToGetDataStructure;
    }
  }

  /**
   * @description Extracts binding data from a string.
   * @member TComponents.Component_A.getBindData
   * @method
   * @static
   * @param {string} strData - The string to extract data from.
   * @param {object} self - The component instance.
   * @returns {Object|null} The binding data or null if not applicable.
   */
  static getBindData(strData, self) {
    let bindData = null;

    if (typeof strData !== 'string') {
      return bindData;
    }

    // {{App.container1.a}} 标识webdata
    if (strData.indexOf('{{') === 0 && strData.lastIndexOf('}}') === strData.length - 2) {
      bindData = {
        type: 'webdata',
        key: strData.replace(/{{/g, '').replace(/}}/g, ''),
      };
    }

    // $$digitalsignal.ManualMode$$ 标识signal
    if (strData.indexOf('$$') === 0 && strData.lastIndexOf('$$') === strData.length - 2) {
      const vs = strData.replace(/\$\$/g, '').split('.');
      bindData = {
        type: vs[0],
        key: vs[vs.length - 1],
      };
    }

    // @@tooldata|task1.module1|tooldata1@@
    if (strData.indexOf('@@') === 0 && strData.lastIndexOf('@@') === strData.length - 2) {
      const vs = strData.replace(/@@/g, '').split('|');
      const variablePath = vs[1].split('.').concat([vs[2]]);
      bindData = {
        type: 'rapiddata',
        dataType: vs[0],
        task: variablePath[0],
        module: variablePath[1],
        name: variablePath[2],
      };
      // Trying to bind specific part of a RAPID variable
      // @@robtarget|task1.module1|r1|trans|x@@----------rapid => bind the x part of the robtarget r1
      if (vs.length > 3) {
        try {
          const {a: ap, r: rp, d: rd} = Component_A.getDataStruct(vs, self);
          bindData.arrPath = ap;
          bindData.recPath = rp;
          bindData.dataStruct = rd;
        } catch (e) {
          Logger.e(logModule, ErrorCode.FailedToGetBindedVariable, e);
          throw ErrorCode.FailedToGetBindedVariable;
        }
      }
    }

    return bindData;
  }

  /**
   * @description Synchronizes data with a specified value.
   * @member TComponents.Component_A.syncData
   * @method
   * @static
   * @async
   * @param {any} value - The value to synchronize.
   * @param {object} self - The component instance.
   * @returns {Promise<boolean>} A promise that resolves to true if synchronization is successful, otherwise false.
   */
  static async syncData(value, self) {
    // try {
    if (self._bindData.type === 'webdata') {
      await API.WEBDATAMONITOR.setWebdata(self._bindData.key, value);
    }

    if (self._bindData.type === 'digitalsignal') {
      let numVar = value;

      if (typeof value == 'boolean') numVar = value == true ? 1 : 0;
      if (typeof value == 'string') numVar = Number(value);

      if (isNaN(numVar)) {
        throw new Error(ErrorCode.InvalidDataForSync);
      }

      await API.RWS.SIGNAL.setSignalValue(self._bindData.key, numVar);
    }

    if (self._bindData.type === 'groupsignal') {
      let numVar = value;

      if (typeof value == 'string') numVar = Number(value);

      if (isNaN(numVar)) {
        throw new Error(ErrorCode.InvalidDataForSync);
      }

      await API.RWS.SIGNAL.setSignalValue(self._bindData.key, numVar);
    }

    if (self._bindData.type === 'rapiddata') {
      //  Do not sync PERS if controller in running execution status
      // let executionStatus = await RWS.Rapid.getExecutionState();
      // const isRunning = executionStatus === RWS.Rapid.ExecutionStates.running;
      if (self._bindData.arrPath || self._bindData.recPath) {
        // try {
        let rawValue = await API.RWS.RAPID.getVariableRawValue(
          self._bindData.module,
          self._bindData.name,
          self._bindData.task,
        );
        const a = self._bindData.arrPath;
        const r = self._bindData.recPath;
        let d = self._bindData.dataStruct;
        let v = rawValue;

        // If subscribing to a specific part of a RECORD variable belongs to an ARRAY
        // Need to parse the value of the ARRAY first
        if (a && a.length > 0 && r && r.length > 0) {
          v = API.RAPID.parseRapidArrayValue(v, a);
        }
        // If subscribing to a specific part of a RECORD variable belongs to an ARRAY
        // Need to parse the value of the ARRAY first
        if (r && r.length > 0) {
          d.parseFromRapid(v);
          d.setValueByNamePath(r, value);
          value = d.getRapidValue();
        }
        if (a && a.length > 0) {
          v = API.RAPID.generateRapidArrayValue(value, rawValue, a);
          value = v;
        }
        // } catch (error) {
        //   throw new Error(ErrorCode.InvalidDataForSync);
        // }
      }
      await API.RAPID.setVariableValue(self._bindData.module, self._bindData.name, value, self._bindData.task, {
        syncPers: false,
      });
    }
    // } catch (e) {
    //   throw new Error(ErrorCode.FailedToSyncBindingData);
    // }
  }

  /**
   * @description Generates a function template from a string.
   * @member TComponents.Component_A.genFuncTemplate
   * @method
   * @static
   * @param {string | Function | any} data - The string containing the function body.
   * @param {object} self - The component instance.
   * @returns {Function|null} The generated function or null if the input is not a valid function.
   */
  static genFuncTemplate(data, self) {
    try {
      let tempFn = null;

      if (typeof data === 'string') {
        if (!data) return null;

        eval(`tempFn = async function(...args){
          ${data}
        }`);
      } else if (typeof data === 'function') {
        tempFn = data;
      }

      return typeof tempFn === 'function' ? tempFn.bind(self) : null;
    } catch (e) {
      const errorInst = new Error(ErrorCode.FailedToGenerateFunction, {
        cause: `${JSON.stringify(e)}`,
      });
      appendDataToErrInstance(errorInst, {
        msgParams: {name: 'onChange'},
        severity: 'danger',
      });
      throw errorInst;
    }
  }

  /**
   * @description Generates a function template from a string and pop up dialog if error happens.
   * @member TComponents.Component_A.genFuncTemplateWithPopup
   * @method
   * @deprecated Will remove this
   * @static
   * @param {any} data
   * @param {any} self
   */
  static genFuncTemplateWithPopup(data, self) {
    try {
      return this.genFuncTemplate(data, self);
    } catch (e) {
      if (ErrorCode.FailedToGenerateFunction == e.message) {
        Popup_A.danger(
          `${ExceptionIdMap.FailedToGenerateFunction}-${Component_A.t(`framework:${ExceptionIdMap.FailedToGenerateFunction}.title`, {name: 'onChange'})}`,
          Component_A.t(`framework:${ExceptionIdMap.FailedToGenerateFunction}.causes`),
          e.cause,
        );
      }
      throw new Error(ExceptionIdMap.FailedToGenerateFunction);
    }
  }

  /**
   * @description Sets the language adapter for the component.
   * @member TComponents.Component_A.setLanguageAdapter
   * @method
   * @static
   * @param {object} adapter - The language adapter.
   * @param {Function} adapter.t - The translation function.
   * @throws {Error} If adapter.t is not a function.
   * @returns {void}
   */
  static setLanguageAdapter(adapter) {
    if (typeof adapter.t !== 'function') throw new Error('func must be a function');
    Component_A.languageAdapter = adapter;
  }

  /**
   * @description Translates a key using the language adapter.
   * @member TComponents.Component_A.t
   * @method
   * @static
   * @param {string} key - The key to translate.
   * @returns {string} The translated key.
   */
  static t(key, params = {}) {
    if (Component_A.languageAdapter && Component_A.languageAdapter.t) return Component_A.languageAdapter.t(key, params);
    return key;
  }

  /**
   * @description Parses translation keys and replaces them with their corresponding values.
   * @member TComponents.Component_A.tParse
   * @method
   * @static
   * @param {string} key - The key to translate.
   * @returns {string} The translated value.
   */
  static tParse(key) {
    if (typeof key === 'string' && key.startsWith('!!') && key.endsWith('!!') && key.trim().length > 4) {
      return Component_A.t(key.slice(2, -2));
    }
    return key;
  }

  /**
   * @description Retrieve instance attributes from the global instance table based on the key value
   * @member TComponents.Component_A.getInstanceProperty
   * @method
   * @static
   * @param {string} key - The key name of the instance.
   * @returns {Component_A} The target instance.
   */
  static getInstanceProperty(key) {
    if (window.Instance && window.Instance[key]) {
      return window.Instance[key];
    }
    return null;
  }

  /**
   * Enables or disables any FPComponent component (see Omnicore App SDK) declared within the component as an own property (e.g. this.btn = new FPComponent()).
   * @returns {boolean}
   * @see {@link https://developercenter.robotstudio.com/omnicore-sdk|Omnicore-SDK}
   */
  get enabled() {
    return this._enabled;
  }

  /**
   * @description Enables or disables the component and all its child components.
   * @member TComponents.Component_A#enabled
   * @instance
   * @param {boolean} en - The enabled state to set.
   * @example
   * this.enabled = true;
   */
  set enabled(en) {
    this._enabled = en;
    //Support user enable all the children by api
    const objects = Component_A._hasChildOwnProperty(this, '_enabled');
    objects.forEach((o) => {
      o.enabled = en;
    });
    if (this.child) {
      if (this.child instanceof Array) {
        this.child.forEach((c) => {
          c.enabled = en;
        });
      } else if (this.child instanceof Object) {
        for (const key in this.child) {
          if (this.child[key] && Object.prototype.hasOwnProperty.call(this.child[key], '_enabled')) {
            this.child[key].enabled = en;
          }
        }
      }
    }

    this.container.setAttribute('enable', en);
  }

  /**
   * @description Processes event execution error.
   * @member TComponents.Component_A.processEventError
   * @method
   * @static
   * @private
   * @param {any} e Error to be processed.
   * @param {string} eventName Event name.
   */
  static processEventError(e, eventName) {
    let causeArray = [Component_A.t(`framework:${ExceptionIdMap.FailedToExecuteEvent}.causes`), ''];
    if (e instanceof Error) {
      if (e.cause && e.cause.message) {
        causeArray.push(String(e.cause.message));
      } else {
        causeArray.push(e);
      }
    } else if (typeof e === 'string') {
      causeArray.push(e);
    } else {
      causeArray.push(String(e));
    }
    Popup_A.danger(
      `${ExceptionIdMap.FailedToExecuteEvent}-${Component_A.t(`framework:${ExceptionIdMap.FailedToExecuteEvent}.title`, {name: eventName})}`,
      causeArray,
    );
  }

  /**
   * To merge to processEventError if possible
   * @param {*} e
   * @param {*} eventName
   */
  static popupEventError(e, eventName, _logModule = logModule) {
    if (!e) {
      Logger.e(_logModule, `Error happened in event ${eventName}, and the error object is empty or undefined.`);
      return;
    }
    const errorCode = e.message || (e instanceof Error ? e.message : String(e));
    const exceptionId = getExceptionIdByErrorCode(errorCode);
    const popFunc = e.severity === 'warning' ? Popup_A.warning : Popup_A.danger;
    const loggerFunc = e.severity === 'warning' ? Logger.w : Logger.e;
    const extraMsg = e.description || '';
    // handle the case when the error is known with translation
    if (exceptionId) {
      if (extraMsg) {
        loggerFunc(_logModule, `The error happened in event ${eventName}: `, e, ` Extra message: ${extraMsg}`);
      } else {
        loggerFunc(_logModule, `The error happened in event ${eventName}: `, e);
      }
      const causes = [];
      if (
        e &&
        e.cause &&
        e.cause.controllerStatus &&
        e.cause.controllerStatus.code &&
        checkIfKnownRWSError(e.cause.controllerStatus.code)
      ) {
        // handle the known error from omnicore-app sdk
        // if there is specific cause for the error, use it, otherwise just show the RWS error code
        if (Component_A.languageAdapter.exists(`framework:${exceptionId}.causes`)) {
          causes.push(Component_A.t(`framework:${exceptionId}.causes`));
          causes.push(JSON.stringify(e.cause));
        } else {
          causes.push(Component_A.t(`framework:${e.cause.controllerStatus.code}`));
        }
      } else {
        // TODO: in case there is no "causes", in case it is from RWS directly (should be same handling as omnicore-app sdk)
        // TODO: remove below hard code caused by the Masterhip handling in Omnicore-app SDK
        if (e && e.cause && e.cause.message && e.cause.message.includes('Failed to get Mastership.')) {
          if (typeof window.appSpocWriteAccessRequired === 'function') {
            // RW8 + Masterhip related error, ignore because appSpocWriteAccessRequired will handle it
            return;
          } else {
            // RW7 + Masterhip related error, show passed specific message directly
            causes.push(JSON.stringify(e.cause));
          }
        } else {
          if (Component_A.languageAdapter.exists(`framework:${exceptionId}.causes`)) {
            causes.push(Component_A.t(`framework:${exceptionId}.causes`));
          } else if (e && e.cause) {
            // no translated cause available, just show the cause directly if it exists
            causes.push(JSON.stringify(e.cause));
          } else {
            causes.push('No specific cause available for this error.');
          }
        }
      }
      const msgParams = e.msgParams || {};
      popFunc(
        `${exceptionId}-${Component_A.t(`framework:${exceptionId}.title`, Object.assign({}, msgParams))}`,
        causes,
      );
      return;
    }
    // handle the case where error code is not handled by users and captured by event
    if (extraMsg) {
      loggerFunc(
        _logModule,
        `The error happened in event ${eventName}, and the error code is ${errorCode}. Extra message: ${extraMsg}. `,
        e,
      );
    } else {
      loggerFunc(_logModule, `The error happened in event ${eventName}, and the error code is ${errorCode}`, e);
    }
    popFunc(
      `${ExceptionIdMap.FailedToExecuteEvent}-${Component_A.t(`framework:${ExceptionIdMap.FailedToExecuteEvent}.title`, {name: eventName})}`,
      [
        Component_A.t(`framework:${ExceptionIdMap.FailedToExecuteEvent}.causes`),
        e.cause
          ? JSON.stringify(e.cause)
          : typeof e === 'string'
            ? e
            : e instanceof Error
              ? e.message
              : JSON.stringify(e),
      ],
    );
  }
}

/**
 * @description language adapter for the Component_A class.
 * @member {object} TComponents.Component_A#languageAdapter
 * @instance
 */
Component_A.languageAdapter = null;

/**
 * @description Global event handler for the Component_A class.
 * @member {Eventing_A} TComponents.Component_A#globalEvents
 * @instance
 */
Component_A.globalEvents = new Eventing_A();

/**
 * @description Enum-like object that defines various input variable types.
 * These types are used to categorize the types of input data that can be handled by the component.
 * @member {object} TComponents.Component_A#INPUTVAR_TYPE
 * @instance
 * @property {string} NUM - Represents a numeric input type.
 * @property {string} ANY - Represents a generic input type, not limited to a specific data type.
 * @property {string} BOOL - Represents a boolean input type (true/false).
 * @property {string} STRING - Represents a string input type.
 */
Component_A.INPUTVAR_TYPE = {
  NUM: 'num',
  ANY: 'any',
  BOOL: 'bool',
  STRING: 'string',
};

/**
 * @description Enum-like object that defines various input variable handling strategies.
 * These strategies specify how input variables are processed within the component.
 * @member {object} TComponents.Component_A#INPUTVAR_FUNC
 * @instance
 * @property {string} CUSTOM - A custom processing strategy for input variables.
 * @property {string} SYNC - A synchronous processing strategy for input variables.
 */
Component_A.INPUTVAR_FUNC = {
  CUSTOM: 'custom',
  SYNC: 'sync',
};

/**
 * @description Monitors and triggers the operation mode event.
 * @async
 * @param {string} value - The operation mode to set.
 */
const monitorOpMode = async (value) => {
  Component_A.globalEvents.trigger('op-mode', value);
};

/**
 * @description Monitors and triggers the execution state event.
 * @async
 * @param {string} value - The execution state to set.
 */
const monitorExecutionState = async (value) => {
  Component_A.globalEvents.trigger('execution-state', value);
};

/**
 * @description Monitors and triggers the controller state event.
 * @async
 * @param {string} value - The controller state to set.
 */
const monitorControllerState = async (value) => {
  Component_A.globalEvents.trigger('controller-state', value);
};

/**
 * @description Maximum allowed gap in rem units.
 * @constant {number}
 */
const maxGap = 16;

/**
 * @description Maximum allowed padding in rem units.
 * @constant {number}
 */
const maxPadding = 16;

/**
 * @description Maximum allowed margin in rem units.
 * @constant {number}
 */
const maxMargin = 16;

/**
 * @description Generates CSS styles for padding classes.
 * @returns {string} - The generated CSS styles for padding.
 */
const generatePaddingStyles = () => {
  let styles = '';
  for (let i = 1; i <= maxPadding; i++) {
    const paddingValue = (i * 0.25).toFixed(2); // Calculate padding value based on class number.
    styles += `
      .pl-${i} { padding-left: ${paddingValue}rem; /* ${i * 4}px */ }
      .pr-${i} { padding-right: ${paddingValue}rem; /* ${i * 4}px */ }
      .pt-${i} { padding-top: ${paddingValue}rem; /* ${i * 4}px */ }
      .pb-${i} { padding-bottom: ${paddingValue}rem; /* ${i * 4}px */ }
      .px-${i} { padding-left: ${paddingValue}rem; padding-right: ${paddingValue}rem; /* ${i * 4}px */ }
      .py-${i} { padding-top: ${paddingValue}rem; padding-bottom: ${paddingValue}rem; /* ${i * 4}px */ }
    `;
  }
  return styles;
};

/**
 * @description Generates CSS styles for margin classes.
 * @returns {string} - The generated CSS styles for margin.
 */
function generateMarginStyles() {
  let styles = '';

  for (let i = 1; i <= maxMargin; i++) {
    const value = i * 0.25;
    styles += `
      .ml-${i} { margin-left: ${value}rem; /* ${i * 4}px */ }
      .mr-${i} { margin-right: ${value}rem; /* ${i * 4}px */ }
      .mt-${i} { margin-top: ${value}rem; /* ${i * 4}px */ }
      .mb-${i} { margin-bottom: ${value}rem; /* ${i * 4}px */ }
      .mx-${i} { margin-left: ${value}rem; margin-right: ${value}rem; /* ${i * 4}px */ }
      .my-${i} { margin-top: ${value}rem; margin-bottom: ${value}rem; /* ${i * 4}px */ }
    `;
  }

  return styles;
}

/**
 * @description Generates CSS styles for gap classes.
 * @returns {string} - The generated CSS styles for gap.
 */
function generateGapStyles() {
  let styles = '';

  for (let i = 0; i <= maxGap; i++) {
    const value = i * 0.25;
    styles += `
      .flex-row.gap-${i} > * + * { margin-left: ${value}rem; /* ${i * 4}px */ }
      .flex-col.gap-${i} > * + * { margin-top: ${value}rem; /* ${i * 4}px */ }
    `;
  }

  return styles;
}

/**
 * Loads CSS classes from a string and applies them to the Component_A.
 * @param {string} cssString - The CSS string to be loaded.
 */
Component_A.loadCssClassFromString(/*css*/ `
  ${generatePaddingStyles()}
  ${generateMarginStyles()}
  ${generateGapStyles()}
`);