as-image.js

import {Component_A} from './basic/as-component.js';
import {imgDefaultPng} from './style/img/images.js';
import {FP_Image_A} from './fp-ext/fp-img-ext.js';
import {ErrorCode} from '../exception/exceptionDesc.js';

/**
 * @typedef TComponents.ImageProps
 * @prop {object} [options] Additional options for the image component.
 * Items in this object include:
 * - **responsive** (boolean, default: false): Whether the image should be responsive.
 * @prop {Function} [onCreated] Function to be called when the component is created.
 * @prop {Function} [onMounted] Function to be called when the component is mounted.
 * @prop {string} [imgSrc] Path to the image file.
 * @prop {object} [imgStyle] Visual style configuration for the image.
 * This object controls how the image is rendered:
 * - **objectFit** (string, default: 'contain'): How the image should fit inside its container.
 * - **opacity** (number, default: 1.0): Opacity of the image, from 0 to 1.
 * @prop {Function} [onClick] Function to be called when the image is clicked.
 * @prop {string} [position] CSS position property.
 * @prop {number} [width] Width of the component.
 * @prop {number} [height] Height of the component.
 * @prop {number} [top] Top position of the component.
 * @prop {number} [left] Left position of the component.
 * @prop {number} [zIndex] Z-index of the component.
 * @prop {number} [rotation] Rotation angle of the component.
 * @prop {number} [borderRadius] Border radius of the component.
 * @prop {string} [defaultState] Default state of the component.
 * @memberof TComponents
 */

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

/**
 * Image component that can display an image with specific styles and handle events.
 * This class focuses on the specific properties of the Image 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.Image
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} parent - HTML element that is going to be the parent of the component
 * @param {TComponents.ImageProps} props - Properties to initialize the component
 * @example
 * const imgComponent = new TComponents.Image(document.body, {
 *   position: 'absolute',
 *   zIndex: 1000,
 *   width: 200,
 *   height: 150,
 *   imgSrc: 'https://xxx.png',
 *   imgStyle: { objectFit: 'contain' }
 * });
 *
 * imgComponent.render();
 */
export class Image extends Component_A {
  constructor(parent, props = {}) {
    super(parent, props);

    /**
     * @instance
     * @private
     */
    this._img = new FP_Image_A();
  }

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

      // imgObj: {label: imgDefaultPng, value: 'default'},
      imgSrc: imgDefaultPng,
      imgStyle: {objectFit: 'contain', opacity: 1.0},
      // imgFillStyle: 'fill',
      onClick: '',
      // ⭐ W/H/X/Y/B/R/Z: Component required attributes.
      position: 'static',
      width: 146,
      height: 106,
      top: 0,
      left: 0,
      zIndex: 0,
      rotation: 0,
      borderRadius: 4,
      defaultState: 'show_enable',
    };
  }

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

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

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

  /**
   * @description Handles the rendering process of the component, including setting up event listeners and applying image properties.
   * @member TComponents.Image#onRender
   * @method
   * @returns {void}
   */
  onRender() {
    try {
      this.removeAllEventListeners();

      this._img.src = this._evalImgSrc();
      this._img.fit = this._props.imgStyle.objectFit;
      this._img.opacity = this._props.imgStyle.opacity;
      this._img.borderRadius = this._props.borderRadius;

      if (this._props.onClick) {
        const fn = Component_A.genFuncTemplate(this._props.onClick, this);
        if (fn) this._img.onclick = fn;
      }

      const imgContainer = this.find('.tc-img');
      if (imgContainer) this._img.attachToElement(imgContainer);

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

  /**
   * @description Evaluates and retrieves the source of the image.
   * If the `imgSrc` property references a global variable, it returns the value of that variable.
   * Otherwise, it returns the original `imgSrc` property value.
   * @member TComponents.Image~_evalImgSrc
   * @method
   * @private
   * @throws {Error} Throws an error if the source image file cannot be found.
   * @returns {string} The evaluated image source.
   */
  _evalImgSrc() {
    try {
      const imgSrc = window[this._props.imgSrc];
      if (typeof imgSrc == 'string') return imgSrc;
      else {
        Logger.w(logModule, ErrorCode.FailedToFindSourceImage, `Failed to find source image file.`);
        return this._props.imgSrc;
      }
    } catch (e) {
      Logger.e(logModule, ErrorCode.FailedToFindSourceImage, `Failed to find source image file.`);
      throw ErrorCode.FailedToFindSourceImage;
    }
  }

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

  /**
   * @description Sets the source of the image.
   * @member {string} TComponents.Image#imgSrc
   * @instance
   * @param {string} s - The source URL of the image
   * @example
   * const imgComponent = new TComponents.Image(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   width: 200,
   *   height: 150,
   *   imgSrc: '',
   *   imgStyle: { objectFit: 'contain' }
   * });
   *
   * imgComponent.render();
   *
   * // Set a new image source
   * imgComponent.imgSrc = 'https://xxx.png';
   */
  set imgSrc(s) {
    this.setProps({imgSrc: s});
  }

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

  /**
   * @description Sets the style of the image.
   * @member {object} TComponents.Image#imgStyle
   * @instance
   * @param {object} s - The style object for the image
   * @example
   * const imgComponent = new TComponents.Image(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   width: 200,
   *   height: 150,
   *   imgSrc: '',
   *   imgStyle: { objectFit: 'contain' }
   * });
   *
   * imgComponent.render();
   *
   * // Set a new image style
   * imgComponent.imgStyle = { objectFit: 'cover'};
   */
  set imgStyle(s) {
    this.setProps({imgStyle: s});
  }

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

  /**
   * @description Sets the `onClick` event handler.
   * The handler can either be a string representing a function to be executed or a function itself.
   * 1. If you are using an arrow function, like `()=>{}`,
   * the `this` property of the scope may not refer to the image 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.Image#onClick
   * @instance
   * @param {Function} t - The new click handler function.
   * @example
   * const imgComponent = new TComponents.Image(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   width: 200,
   *   height: 150,
   *   imgSrc: '',
   *   imgStyle: { objectFit: 'contain' }
   * });
   *
   * imgComponent.render();
   *
   * // Example 1: Using a string as the handler:
   * imgComponent.onClick = "console.log('Image clicked!', this.imgSrc);";
   * @example
   * // Example 2: Using a arrow function as the handler:
   * // Note that the `this` context will not refer to the imgComponent object
   * imgComponent.onClick = () => { console.log('Image clicked!', imgComponent.imgSrc); };
   * @example
   * // Example 3: Using a common function as the handler:
   * imgComponent.onClick = async function() {
   *   console.log('Image clicked!', this.imgSrc);
   * };
   */
  set onClick(t) {
    this.setProps({onClick: t});
  }
}

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

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

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

.tc-img .fp-components-img:hover{
  opacity:0.7;
}
`);