as-layoutInfobox.js

import API from '../api/ecosystem-base.js';
import {Component_A} from './basic/as-component.js';
import {Container_A} from './basic/as-container.js';
import {ErrorCode} from '../exception/exceptionDesc.js';

/**
 * @typedef {object} TComponents.LayoutInfoboxProps
 * @prop {object} [options] Options for the layout infobox component.
 * Items in this object include:
 * - **responsive** (boolean, default: false): Whether the infobox should be responsive.
 * @prop {string} [tips] Tooltip text for the component.
 * @prop {Function} [onCreated] Function to be called when the component is created.
 * @prop {Function} [onMounted] Function to be called when the component is mounted.
 * @prop {string} [position] CSS position property.
 * @prop {number} [width] Width of the component.
 * @prop {number} [height] Height of the component.
 * @prop {number} [top] Top position of the component.
 * @prop {number} [left] Left position of the component.
 * @prop {number} [zIndex] Z-index of the component.
 * @prop {number} [borderRadius] Border radius of the component.
 * @prop {number} [rotation] Rotation angle of the component.
 * @prop {string} [color] Text color of the component.
 * @prop {string} [backgroundColor] Background color of the component.
 * @prop {boolean} [useTitle] Whether to display the title area.
 * @prop {string} [title] Title of the infobox.
 * @prop {object} [font] Font settings for the title 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`.
 * - **textAlign** (string, default: 'left'): Text alignment of the title.
 * @prop {boolean} [useBorder] Whether to draw a border around the infobox.
 * @prop {string} [border] Border style of the infobox.
 * @prop {object} [content] Props to be passed to the internal content container.
 * This object controls how child components are arranged:
 * - **children** (string[]): Identifiers of child components inside the infobox.
 * - **row** (boolean, default: true): Whether to arrange children in a row.
 * - **box** (boolean, default: false): Whether to use box layout.
 * - **width** (string, default: '100%'): Content width.
 * - **height** (string, default: '100%'): Content height.
 * - **classNames** (string[]): Extra class names applied to the content container.
 * @prop {string} [defaultState] Default state of the component.
 * @prop {boolean} [expandHeightToParentBottom] Whether to expand the height to the parent's bottom.
 */

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

/**
 * @description LayoutInfobox is a component that displays a title and content in a box
 * This class focuses on the specific properties of the LayoutInfobox 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.LayoutInfobox
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} parent - HTMLE element that is going to be the parent of the component
 * @param {TComponents.LayoutInfoboxProps} [props] - Props for the layout infobox component
 * @example
 * const layoutInfoboxInstance = new TComponents.LayoutInfobox(document.body, {
 *   position: 'absolute',
 *   zIndex: 1000,
 *   title: 'Infobox Title'
 * });
 *
 * // Render the component.
 * await layoutInfoboxInstance.render();
 */
export class LayoutInfobox extends Component_A {
  constructor(parent, props) {
    super(parent, props);

    this._newChildren = new Map();

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

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

  /**
   * @description Sets the border usage state.
   * @member {boolean} TComponents.LayoutInfobox#useBorder
   * @instance
   * @param {boolean} b - The new border usage state.
   * @example
   * const layoutInfoboxInstance = new TComponents.LayoutInfobox(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   title: 'Infobox Title'
   * });
   *
   * // Render the component.
   * await layoutInfoboxInstance.render();
   *
   * layoutInfoboxInstance.useBorder = false;
   */
  set useBorder(b) {
    this.setProps({useBorder: b});
  }

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

  /**
   * @description Sets the title usage state.
   * @member {boolean} TComponents.LayoutInfobox#useTitle
   * @instance
   * @param {boolean} b - The new title usage state.
   * @example
   * const layoutInfoboxInstance = new TComponents.LayoutInfobox(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   title: 'Infobox Title'
   * });
   *
   * // Render the component.
   * await layoutInfoboxInstance.render();
   *
   * layoutInfoboxInstance.useTitle = false;
   */
  set useTitle(b) {
    this.setProps({useTitle: b});
  }

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

  /**
   * @description Sets the title of the infobox.
   * @member {string} TComponents.LayoutInfobox#title
   * @instance
   * @param {string} s - The new title.
   * @example
   * const layoutInfoboxInstance = new TComponents.LayoutInfobox(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   title: 'Infobox Title'
   * });
   *
   * // Render the component.
   * await layoutInfoboxInstance.render();
   *
   * layoutInfoboxInstance.title = 'New Infobox Title';
   */
  set title(s) {
    this.setProps({title: s});
  }

  /**
   * @description Gets the content container instance.
   * The obtained value should be treated as a read-only property and should not be used for manipulation.
   * @member {HTMLElement} TComponents.LayoutInfobox#contentRoot
   * @instance
   * @returns {HTMLElement|null} The content container instance or null if not available.
   * @example
   * const layoutInfoboxInstance = new TComponents.LayoutInfobox(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   title: 'Infobox Title'
   * });
   *
   * // Render the component.
   * await layoutInfoboxInstance.render();
   *
   * const contentContainer = layoutInfoboxInstance.contentRoot;
   * console.log(contentContainer);
   */
  get contentRoot() {
    const t = (this.child.content && this.child.content.container) || null;
    return t;
  }

  /**
   * @description Returns the default values of class properties (excluding parent properties).
   * @member TComponents.LayoutInfobox#defaultProps
   * @method
   * @protected
   * @returns {TComponents.LayoutInfoboxProps}
   */
  defaultProps() {
    return {
      options: {
        responsive: false,
      },
      tips: '',
      // life cycle
      onCreated: '',
      onMounted: '',
      onDispose: '',
      position: 'static',
      width: 200,
      height: 200,
      top: 0,
      left: 0,
      zIndex: 0,
      borderRadius: 4,
      rotation: 0,
      color: '#000000',
      backgroundColor: 'transparent',
      useTitle: true,
      title: 'default',
      font: {
        fontSize: 12,
        fontFamily: 'Segoe UI',
        style: {
          fontStyle: 'normal',
          fontWeight: 'normal',
          textDecoration: 'none',
        },
        textAlign: 'left',
      },
      useBorder: true,
      border: '1px solid #dbdbdb',
      content: {
        children: [`Container_${API.generateUUID()}`],
        row: true,
        box: false,
        width: '100%',
        height: '100%',
        classNames: ['flex-row', 'justify-stretch'],
      },
      defaultState: 'show_enable',
      expandHeightToParentBottom: false,
    };
  }

  /**
   * @description Initializes the layoutInfobox
   * @member TComponents.LayoutInfobox#onInit
   * @method
   * @returns {void}
   */
  onInit() {
    this._newChildren.clear();
  }

  /**
   * @description Maps the components.
   * @member TComponents.LayoutInfobox#mapComponents
   * @method
   * @returns {object} The mapped components.
   */
  mapComponents() {
    let props = Object.assign({}, this._props.content);
    props = Object.assign(props, {id: 'content'});

    const content = this._processContent(props);

    return Object.assign({}, {content: content});
  }

  /**
   * @description Handles rendering of the component.
   * @member TComponents.LayoutInfobox#onRender
   * @method
   * @throws {Error} Throws an error if rendering fails.
   * @returns {void}
   */
  onRender() {
    try {
      this.removeAllEventListeners();

      this.container.classList.add('layout-container');
      this.container.classList.add('is-container');

      this.find('.fp-components-emptycontainer').style.cssText = this.getWrapperStyle();
      if (this._props.useTitle) this.find('.layout-title').style.cssText = this.getTitleLayoutStyle();
      if (this._props.useTitle) this.find('p').style.cssText = this.getTitleStyle();
      this.find('.layout-infobox-content').style.cssText = this.getContentStyle();

      // Attach the new children to the content root
      const contentRoot = this.contentRoot;
      if (Component_A._isHTMLElement(contentRoot)) {
        this._newChildren.forEach((_, key) => {
          if (Component_A.isTComponent(key)) {
            key.attachToElement(contentRoot);
          } else if (Component_A._isHTMLElement(key)) {
            if (!contentRoot.contains(key)) {
              contentRoot.appendChild(key);
            }
          }
        });
      }
      this._addTips();
      this.contentRoot.classList.toggle('t-component__container-disabled', !this._enabled);
    } catch (e) {
      // Runtime errors: write specific content to the log, throw error code
      Logger.e(
        logModule,
        ErrorCode.FailedToRunOnRender,
        `Error happens on onRender of layoutInfobox component ${this.compId}.`,
        e,
      );
      throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
    }
  }

  /**
   * @description Generates the markup for the component.
   * @member TComponents.LayoutInfobox#markup
   * @method
   * @returns {string} The HTML markup for the component.
   */
  markup() {
    // TODO: Move this style modification function to the `onRender` function.
    return /*html*/ `
    <div class="tc-layout-infobox">
      <div class="fp-components-emptycontainer layout-infobox tc-container-box">
        ${
          this._props.useTitle
            ? /*html*/ `
              <div class="flex-row justify-center layout-title">
                <p>
                  ${Component_A.tParse(this._props.title)}
                </p>
              </div>`
            : ''
        }
          <div class="layout-infobox-content fle-row justify-stretch">
          </div>
        </div>
      </div>
    <div>
    `;
  }

  /**
   * @description Gets the CSS text for the wrapper style.
   * @member TComponents.LayoutInfobox#getWrapperStyle
   * @method
   * @returns {string} The CSS text for the wrapper style.
   * @example
   * const layoutInfoboxInstance = new TComponents.LayoutInfobox(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   title: 'Infobox Title'
   * });
   *
   * // Render the component.
   * await layoutInfoboxInstance.render();
   *
   * const wrapperStyle = layoutInfoboxInstance.getWrapperStyle();
   *
   * console.log(wrapperStyle);
   */
  getWrapperStyle() {
    const cssText = `background-color: ${this._props.backgroundColor};
      border-radius: ${this._props.borderRadius}px;
      border: ${this._props.useBorder ? this._props.border : 'none'};
      box-sizing: border-box;`
      .replace(/\s+/g, ' ')
      .trim();
    return cssText;
  }

  /**
   * @description Gets the CSS text for the title layout style.
   * @member TComponents.LayoutInfobox#getTitleLayoutStyle
   * @method
   * @returns {string} The CSS text for the title layout style.
   * @example
   * const layoutInfoboxInstance = new TComponents.LayoutInfobox(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   title: 'Infobox Title'
   * });
   *
   * // Render the component.
   * await layoutInfoboxInstance.render();
   *
   * const titleLayoutStyle = layoutInfoboxInstance.getTitleLayoutStyle();
   */
  getTitleLayoutStyle() {
    const cssText = `background-color: ${this._props.backgroundColor};
      border-radius: ${this._props.borderRadius}px ${this._props.borderRadius}px 0px 0px;`
      .replace(/\s+/g, ' ')
      .trim();
    return cssText;
  }

  /**
   * @description Gets the CSS text for the title style.
   * @member TComponents.LayoutInfobox#getTitleStyle
   * @method
   * @returns {string} The CSS text for the title style.
   * @example
   * const layoutInfoboxInstance = new TComponents.LayoutInfobox(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   title: 'Infobox Title'
   * });
   *
   * // Render the component.
   * await layoutInfoboxInstance.render();
   *
   * const titleStyle = layoutInfoboxInstance.getTitleStyle();
   */
  getTitleStyle() {
    const cssText = `color: ${this._props.color}; 
      font-family: ${this._props.font.fontFamily};
      font-size: ${this._props.font.fontSize}px;
      font-weight: ${this._props.font.style.fontWeight};
      font-style: ${this._props.font.style.fontStyle};
      text-decoration: ${this._props.font.style.textDecoration};
      text-align: ${this._props.font.textAlign}`
      .replace(/\s+/g, ' ')
      .trim();

    return cssText;
  }

  /**
   * @description Gets the CSS text for the content style.
   * @member TComponents.LayoutInfobox#getContentStyle
   * @method
   * @returns {string} The CSS text for the content style.
   * @example
   * const layoutInfoboxInstance = new TComponents.LayoutInfobox(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   title: 'Infobox Title'
   * });
   *
   * // Render the component.
   * await layoutInfoboxInstance.render();
   *
   * const contentStyle = layoutInfoboxInstance.getContentStyle();
   */
  getContentStyle() {
    const cssText = `background-color: ${this._props.backgroundColor};
      border-radius: ${this._props.useTitle ? 0 : this._props.borderRadius}px ${this._props.useTitle ? 0 : this._props.borderRadius}px ${this._props.borderRadius}px ${this._props.borderRadius}px;`
      .replace(/\s+/g, ' ')
      .trim();
    return cssText;
  }

  /**
   * @deprecated This API is going to be deprecated and is not recommended for future use.
   * @description Creates an empty container.
   * @member TComponents.LayoutInfobox#createEmptyContainer
   * @method
   * @param {any} props - Properties for the container.
   * @returns {TComponents.Container_A} The created container.
   * @example
   * const layoutInfoboxInstance = new TComponents.LayoutInfobox(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   title: 'Infobox Title'
   * });
   *
   * // Render the component.
   * await layoutInfoboxInstance.render();
   *
   * const emptyContainer = layoutInfoboxInstance.createEmptyContainer({
   *   children: ['child1-uuid-xxxxx']
   * });
   *
   * console.log(emptyContainer);
   */
  createEmptyContainer(props) {
    const tmpC = props.children[0];
    if (Component_A._isHTMLElement(tmpC)) {
      const container_a = new Container_A(this.find('.layout-infobox-content'), props);
      return container_a;
    } else if (typeof tmpC === 'string') {
      const container_b = new Container_A(this.find('.layout-infobox-content'), {id: tmpC});
      container_b.container.id = tmpC;
      return container_b;
    }
  }

  /**
   * @description Appends a child component to the current component.
   * If the child is a TComponent, it will be rendered and attached to the content root.
   * @member TComponents.LayoutInfobox#appendChild
   * @method
   * @param {TComponents.Container_A|HTMLElement} t The child component instance to be added.
   * @param {string} name The name of the child component to be appended.
   * @throws {Error} If the `t` parameter is not a valid object.
   * @returns {void}
   * @example
   * const layoutInfoboxInstance = new TComponents.LayoutInfobox(document.body, {
   *   position: 'absolute',
   *   zIndex: 1000,
   *   title: 'Infobox Title'
   * });
   *
   * // Render the component.
   * await layoutInfoboxInstance.render();
   *
   * // Create a button component.
   * const childComponent = new TComponents.Button(null, {});
   *
   * layoutInfoboxInstance.appendChild(childComponent, 'childComponentName');
   */
  appendChild(t, name = '') {
    const contentRoot = this.contentRoot;
    if (contentRoot === null) {
      throw new Error('Component has not been rendered yet. Cannot append child.');
    }

    if (Component_A.isTComponent(t)) {
      this._newChildren.set(t, name || t.compId);
      this.child.newChildren = [...this._newChildren.keys()];
      t.render();
      t.attachToElement(contentRoot);
    } else if (Component_A._isHTMLElement(t)) {
      if (!contentRoot.contains(t)) {
        this._newChildren.set(t, name);
        this.child.newChildren = [...this._newChildren.keys()];
        contentRoot.appendChild(t);
      }
    } else {
      // Runtime errors: write specific content to the log, throw error code
      Logger.e(
        logModule,
        ErrorCode.InvalidChildElement,
        `Invalid child component type: ${typeof t}. Expected TComponent or HTMLElement.`,
      );
      throw ErrorCode.InvalidChildElement;
    }
  }

  /**
   * @description Removes a child component from the current component.
   * It can remove the child either by name (if `t` is a string) or by component instance (if `t` is an object with a `compId`).
   * After removing the child, the method updates the `child` property by calling `mapComponents()`
   * and triggers a re-render of the component.
   * @member TComponents.LayoutInfobox#removeChild
   * @method
   * @param {string|object} t The child component to remove. It can be either:
   * - A string representing the child component's name (if the child is in `_newChild`).
   * - An object with a `compId` property, representing the child component to be removed.
   * @throws {Error} If the `t` parameter is neither a string nor an object with a `compId`.
   * @returns {void}
   * @example
   * layoutInfoboxInstance.removeChild('childComponent'); // Removes by name
   * layoutInfoboxInstance.removeChild(someChildInstance); // Removes by component instance
   */
  removeChild(t) {
    try {
      if (typeof t === 'string') {
        let done = false;
        this._newChildren.forEach((value, key) => {
          if (done) return;

          if (value === t) {
            this._removeChild(key);
            done = true;
          }
        });
      } else if (typeof t === 'object') {
        const value = this._newChildren.get(t);
        if (typeof value === 'string') this._removeChild(t);
      }
    } catch (e) {
      // Runtime errors: write specific content to the log, throw error code
      Logger.e(logModule, ErrorCode.FailedToRemoveChildElement, `Failed to remove child component:`, e);
      throw ErrorCode.FailedToRemoveChildElement;
    }
  }

  /**
   * @description Build and return a {@link TComponent.Container_A} mounted under the infobox content area.
   * @member TComponents.LayoutInfobox#processContent
   * @method
   * @protected
   * @param {TComponents.ContainerProps} props - Properties for the container.
   * @returns {TComponents.Container_A} The created container.
   */
  processContent(props) {
    return this._processContent(props);
  }

  /**
   * @member TComponents.LayoutInfobox~_processContent
   * @method
   * @private
   * @param {TComponents.ContainerProps} props
   * @returns {TComponents.Container_A}
   */
  _processContent(props) {
    const parent = this.find('.layout-infobox-content');
    const firstChild = props.children[0];

    if (Component_A._isHTMLElement(firstChild)) {
      return new Container_A(parent, props);
    }

    if (typeof firstChild === 'string') {
      const container = new Container_A(parent, {id: firstChild});
      container.container.id = firstChild;
      return container;
    }
  }

  /**
   * @description Internal method to remove a child component and clean up references.
   * @member TComponents.LayoutInfobox~_removeChild
   * @method
   * @private
   * @param {TComponents.Component_A|HTMLElement} t
   * @returns {void}
   */
  _removeChild(t) {
    if (Component_A.isTComponent(t)) {
      t.parent = null;
      t.destroy();
    } else if (Component_A._isHTMLElement(t)) {
      if (t.parentNode) t.parentNode.removeChild(t);
    } else {
      return;
    }

    this._newChildren.delete(t);
    this.child.newChildren = [...this._newChildren.keys()];
  }
}

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

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


.layout-container .tc-container-box {
  margin: 0px;
  padding: 0px;
}

.layout-infobox-content {
  position: relative;
  max-height: 100%;
  overflow: auto;
}

.layout-infobox {
  display: flex;
  flex-direction: column;
  max-height: 100%;
  height: 100%;
}

.layout-infobox > .layout-title {
  position: relative;
  /* background-color: #dddddd; */
  max-height: 30px;
  min-height: 30px;

  border-bottom-style: solid;
  border-bottom-color: #d5d5d5;
  border-bottom-width: 3px;
  /* border-radius: 10px; */
  display: flex;
  align-items: center;
  /* padding-left: 8px; */
  /* margin-top: 0.2rem; */
  /* margin-bottom: 0.4rem; */
  align-content: center;
  overflow:hidden;
}

.layout-infobox > .layout-title > p {
  font-size: 12px;
  width: 100%;
  word-wrap: break-word;
  white-space: normal;
  overflow-wrap: break-word;
  box-sizing: border-box;
}

.layout-infobox > :not(.layout-title)  {
  /* background-color: white; */
  flex-grow: 1;
  /* padding: 8px; */

  min-height: 30px;
  min-width: 80px;
}

`);