as-button.js

import {Component_A} from './basic/as-component.js';
import {FP_Button_A} from './fp-ext/fp-button-ext.js';
import {ErrorCode} from './../exception/exceptionDesc.js';

/**
 * @typedef TComponents.ButtonProps
 * @prop {Function} [onClick] Function to be called when button is pressed
 * @prop {string|null} [icon] - Path to image file
 * @prop {string} [text] Button text
 * @prop {string} [color] Button text color
 * @prop {string} [backgroundColor] Button background color
 * @prop {string} [border] Button border style
 * @prop {number} [borderRadius] Button border radius
 * @prop {Function} [onPointerRelease] Function to be called when pointer is released
 * @prop {Function} [onPointerDown] Function to be called when pointer is pressed down
 * @prop {string} [tips] Tooltip text for the button
 * @prop {object} [font] Font properties for the button text
 * This object controls text appearance:
 * - **fontSize** (number, default: 12): Font size in pixels.
 * - **fontFamily** (string, default: 'Segoe UI'): Font family name.
 * - **style** (object): Font style configuration, containing `fontStyle`, `fontWeight`, `textDecoration`.
 * @prop {object} [options] Additional options for the button
 * Items in this object include:
 * - **responsive** (boolean, default: false): Whether the button is responsive
 * @prop {Function} [onCreated] Function to be called when the button is created
 * @prop {Function} [onMounted] Function to be called when the button is mounted
 * @prop {object} [functionality] - Configuration object for component functionality.
 * This object contains settings to control the component's behavior:
 * - **type** (string): Defines the specific type of functionality to enable.
 * - **params** (string): Additional parameters or arguments required by the functionality type.
 * - **isHidden** (boolean, default: false): If true, the functionality is visually hidden from the user interface.
 * @prop {string} [position] CSS position property, default is 'static'
 * @prop {number} [width] Button width in pixels, default is 100
 * @prop {number} [height] Button height in pixels, default is 32
 * @prop {number} [top] Button top position in pixels, default is 0
 * @prop {number} [left] Button left position in pixels, default is 0
 * @prop {number} [rotation] Button rotation in degrees, default is 0
 * @prop {number} [zIndex] Button z-index, default is 0
 * @prop {string} [size] Size style template
 * @prop {string} [styleTemplate] Style template
 * @prop {string} [defaultState] Default state of the button, default is 'show_enable'
 * @prop {string} [dataStruct] Data structure associated with the button
 * @memberof TComponents
 */

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

/**
 * @description Rounded button that triggers a callback when pressed. Additional callbacks can be added with the {@link TComponents.Button#onClick|onClick} method.
 * This class focuses on the specific properties of the Button 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.Button
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} parent - HTML element that is going to be the parent of the component
 * @param {TComponents.ButtonProps} props - Button properties
 * @example
 * const button = new TComponents.Button(document.body, {
 *   position: 'absolute',
 *   zIndex: 1000,
 *   text: 'Click me'
 * });
 *
 * // Render the button
 * button.render();
 */
export class Button extends Component_A {
  constructor(parent, props = {}) {
    super(parent, props);

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

    this._btn = new FP_Button_A();

    this._mouseState = '';
  }

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

      onClick: '',
      onPointerRelease: '',
      onPointerDown: '',

      icon: null,
      // ⭐ W/H/X/Y/B/R/Z: Component required attributes.
      position: 'static',
      width: 100,
      height: 32,
      top: 0,
      left: 0,
      borderRadius: 4,
      rotation: 0,
      zIndex: 0,
      // Font
      text: 'Button',
      color: '#ffffff',
      // font
      font: {
        fontSize: 12,
        fontFamily: 'Segoe UI',
        style: {
          fontStyle: 'normal',
          fontWeight: 'normal',
          textDecoration: 'none',
        },
      },
      // Background
      backgroundColor: '#3366ff',
      //  Border
      border: '0px solid #dbdbdb',
      // Function
      functionality: {type: '', params: '', isHidden: false},
      // Style.
      size: '',
      styleTemplate: '',
      defaultState: 'show_enable',
      dataStruct: '',
    };
  }

  /**
   * @description Initialization function for the button component.
   * @member TComponents.Button#onInit
   * @method
   * @async
   * @returns {Promise<void>}
   */
  async onInit() {}

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

  /**
   * @description Set the button disabled state and review the tips.
   * @member {string} TComponents.Button#tips
   * @instance
   * @param {string} t
   * @example
   * const button = new TComponents.Button(document.body, {
   *   position: 'absolute',
   *   left: 100,
   *   top: 100,
   *   zIndex: 1000,
   *   text: 'Click me'
   * });
   *
   * // Render the button
   * button.render();
   *
   * button.tips = 'New tips for the button';
   *
   * // Disable button to display tips
   * button.enabled = false;
   */
  set tips(t) {
    this.setProps({tips: t});
  }

  /**
   * @description Click event handler.
   * @member TComponents.Button~_cbOnClick
   * @method
   * @private
   * @async
   * @returns {Promise<void>}
   */
  async _cbOnClick() {
    return this._onClick();
  }

  /**
   * @deprecated Use {@link _cbOnClick} instead.
   * @description Click event handler.
   * @member TComponents.Button~_onClick
   * @method
   * @private
   * @async
   * @returns {Promise<void>}
   */
  async _onClick() {
    if (this.enabled && this._props.onClick) {
      try {
        const fn = Component_A.genFuncTemplate(this._props.onClick, this);
        this.enabled = false; // Disable the button to prevent multiple clicks
        if (typeof fn == 'function') {
          await fn();
        }
      } catch (e) {
        Component_A.popupEventError(e, 'onClick');
      } finally {
        this.enabled = true; // Re-enable the button after the click handler is done
      }
    }
  }

  /**
   * @description Pointer down event handler.
   * @member TComponents.Button~_onPointerDown
   * @method
   * @private
   * @returns {Promise<void>}
   */
  async _onPointerDown() {
    this._mouseState = 'down';
    if (this.enabled && this._props.onPointerDown) {
      try {
        const fn = Component_A.genFuncTemplate(this._props.onPointerDown, this);
        if (typeof fn == 'function') {
          await fn();
        }
      } catch (e) {
        Component_A.popupEventError(e, 'onPointerDown');
      }
    }
  }

  /**
   * @description Pointer up/leave event handler.
   * @member TComponents.Button~_onPointerRelease
   * @method
   * @private
   * @returns {Promise<void>}
   */
  async _onPointerRelease() {
    if (this.enabled && this._props.onPointerRelease) {
      try {
        const fn = Component_A.genFuncTemplate(this._props.onPointerRelease, this);
        if (typeof fn == 'function' && this._mouseState == 'down') {
          await fn();
        }
      } catch (e) {
        Component_A.popupEventError(e, 'onPointerRelease');
      }
    }
    this._mouseState = 'up';
  }

  /**
   * @description Renders the button component.
   * @member TComponents.Button#onRender
   * @method
   * @throws {Error} If an error occurs during rendering.
   * @returns {void}
   */
  onRender() {
    try {
      this.removeAllEventListeners();
      if (this._props.labelPos === 'left' || this._props.labelPos === 'right') {
        this.container.classList.add('justify-stretch');
      }

      this._btn.text = this._props.text;
      this._btn.icon = this._props.icon;
      this._btn.borderRadius = this._props.borderRadius;
      this._btn.color = this._props.color;
      this._btn.backgroundColor = this._props.backgroundColor;
      this._btn.border = this._props.border;
      this._btn.font = this._props.font;

      const btnContainer = this.find('.tc-button');
      if (btnContainer) this._btn.attachToElement(btnContainer);

      this.addEventListener(this._btn._root, 'click', this._cbOnClick.bind(this));
      this.addEventListener(this._btn._root, 'pointerdown', this._onPointerDown.bind(this));
      this.addEventListener(this._btn._root, 'pointerup', this._onPointerRelease.bind(this));
      this.addEventListener(this._btn._root, 'pointerleave', this._onPointerRelease.bind(this));

      this._addTips();

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

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

  /**
   * @returns {boolean}
   */
  get highlight() {
    return this._btn.highlight;
  }

  /**
   * @description Sets the highlight state of the button.
   * @member {boolean} TComponents.Button~highlight
   * @instance
   * @private
   * @param {boolean} value - The new highlight state.
   */
  set highlight(value) {
    this._btn.highlight = value;
  }

  /**
   * @returns {string|null}
   */
  get icon() {
    return this.props.icon;
  }

  /**
   * @description Sets the icon path.
   * @member {string|null} TComponents.Button#icon
   * @instance
   * @param {string|null} s
   * @example
   * const button = new TComponents.Button(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: 'Click me'
   * });
   *
   * // Render the button
   * button.render();
   *
   * button.icon = 'abb-icon abb-icon-abb_robot-tool_32';
   */
  set icon(s) {
    this.setProps({icon: s});
  }

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

  /**
   * @description Sets the text color of the button
   * @member {string} TComponents.Button#color
   * @instance
   * @param {string} s For example, '#ffffff'
   * @example
   * const button = new TComponents.Button(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: 'Click me'
   * });
   *
   * // Render the button
   * button.render();
   *
   * button.color = '#ffffff';
   */
  set color(s) {
    this.setProps({color: s});
  }

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

  /**
   * @description Sets the button text.
   * @member {string} TComponents.Button#text
   * @instance
   * @param {string} value
   * @example
   * const button = new TComponents.Button(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: 'Click me'
   * });
   *
   * // Render the button
   * button.render();
   *
   * button.text = 'Click Me';
   */
  set text(value) {
    this.commitProps({text: value});
    this._btn.text = value;
  }

  /**
   * @returns {Function|undefined}
   */
  get onClick() {
    const fn = Component_A.genFuncTemplate(this._props.onClick, this);
    if (typeof fn == 'function') return fn;
    else return undefined;
  }

  /**
   * @description Sets the `onClick` event handler for the button.
   * 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 button 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.Button#onClick
   * @instance
   * @param {Function} t - The new click handler function.
   * @example
   * const button = new TComponents.Button(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: 'Click me'
   * });
   *
   * // Render the button
   * button.render();
   *
   * // Example 1.
   * button.onClick = async function() {
   *   console.log('Button clicked!', this.text);
   * };
   * @example
   * // Example 2.
   * button.onClick = "console.log('Button clicked!', this.text);";
   * @example
   * // Example 3.
   * // Note that the `this` context will not refer to the button object
   * button.onClick = () => {
   *   console.log('Button clicked! ', button.text);
   * };
   */
  set onClick(t) {
    this.setProps({onClick: t});
  }

  /**
   * @returns {Function|undefined}
   */
  get onPointerRelease() {
    const fn = Component_A.genFuncTemplate(this._props.onPointerRelease, this);
    if (typeof fn == 'function') return fn;
    else return undefined;
  }

  /**
   * @description Sets the pointer release event handler for the button.
   * 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 button 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.onPointerRelease = "console.log('Action done.');"`
   * - Incorrect (Function Declaration): `xx.onPointerRelease = "function() { console.log('Action done.'); }"`
   * @member {Function} TComponents.Button#onPointerRelease
   * @instance
   * @param {Function} t - The new pointer release handler function.
   * @example
   * const button = new TComponents.Button(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: 'Click me'
   * });
   *
   * // Render the button
   * button.render();
   *
   * // Example 1.
   * button.onPointerRelease = async function() {
   *  console.log('Pointer released on button!', this.text);
   * }
   * @example
   * // Example 2.
   * button.onPointerRelease = "console.log('Pointer released on button!', this.text);";
   * @example
   * // Example 3.
   * // Note that the `this` context will not refer to the button object
   * button.onPointerRelease = () => {
   *  console.log('Pointer released on button! ', button.text);
   * };
   */
  set onPointerRelease(t) {
    this.setProps({onPointerRelease: t});
  }

  /**
   * @returns {Function|undefined}
   */
  get onPointerDown() {
    const fn = Component_A.genFuncTemplate(this._props.onPointerDown, this);
    if (typeof fn == 'function') return fn;
    else return undefined;
  }

  /**
   * @description Sets the pointer down event handler for the button.
   * 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 button 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.onPointerDown = "console.log('Action done.');"`
   * - Incorrect (Function Declaration): `xx.onPointerDown = "function() { console.log('Action done.'); }"`
   * @member {Function} TComponents.Button#onPointerDown
   * @instance
   * @param {Function} t - The new pointer down handler function.
   * @example
   * const button = new TComponents.Button(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   text: 'Click me'
   * });
   *
   * // Render the button
   * button.render();
   *
   * // Example 1.
   * button.onPointerDown = async function() {
   *  console.log('Pointer down on button!', this.text);
   * };
   * @example
   * // Example 2.
   * button.onPointerDown = "console.log('Pointer down on button!', this.text);";
   * @example
   * // Example 3.
   * // Note that the `this` context will not refer to the button object
   * button.onPointerDown = () => {
   *  console.log('Pointer down on button! ', button.text);
   * };
   */
  set onPointerDown(t) {
    this.setProps({onPointerDown: t});
  }
}

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

.tc-button .fp-components-button-disabled,
.tc-button .fp-components-button {
  height: 100%;
  width: 100%;
  min-width: 0px;
  min-height: 0px;
  padding: 0px;
  margin: 0px;
  overflow: hidden;
}

.tc-button > .fp-components-button,
.tc-button > .fp-components-button-disabled {
  min-width: 0px;
  min-height: 0px;
}

.tc-button > .fp-components-button-disabled {
  cursor: not-allowed !important;
  opacity:0.7;
}

@media (hover: hover) and (pointer: fine) {
  .tc-button > .fp-components-button:hover,
  .tc-button > .fp-components-button-disabled:hover {
    opacity: 0.7;
  }
}

@media (hover: none) and (pointer: coarse) {
  .tc-button > .fp-components-button:active {
    opacity: 0.7;
  }
}

.tc-button > .fp-components-button {
  min-width: 0px;
  padding: 0px;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  align-content: center;
  flex-wrap: wrap;
}

.tc-button  .fp-components-button-text {
  text-overflow: ellipsis;
  flex:none;
  margin:0 4px;
}

.tc-button  .fp-components-button-icon-font {
  margin:0 4px;
}

.tc-button > .fp-components-button-disabled {
  cursor: not-allowed !important;      
}  

.fp-components-button-disabled {
  position: relative;
}

.fp-components-tmp-img {
  height: 90%;
  width: 90%;
  padding: 5%;

  img {
    height: 100%;
    width: 100%;
  }
}
`);