as-page.js

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

/**
 * @typedef TComponents.PageProps
 * @prop {Function} [onCreated] Function to be called when the page is created.
 * @prop {Function} [onMounted] Function to be called when the page is mounted.
 * @prop {string} [position] CSS position property of the page.
 * @prop {number} [width] Width of the page.
 * @prop {number} [height] Height of the page.
 * @prop {number} [top] Top offset of the page.
 * @prop {number} [left] Left offset of the page.
 * @prop {number} [zIndex] Z-index of the page.
 * @prop {string} [backgroundColor] Background color of the page.
 * @prop {string} [id] Unique identifier for the page.
 * @prop {string} [name] Name of the page.
 * @prop {any[]} [children] Child components of the page.
 * @memberof TComponents
 */

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

/**
 * @description Page component that extends the base Component_A class. This component handles the layout and rendering of a page.
 * This class focuses on the specific properties of the Page 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.Page
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} parent - The parent HTML element that will contain the Page component.
 * @param {TComponents.PageProps} [props={}] - The properties to initialize the Page component.
 * @example
 * const page = new TComponents.Page(document.body,{
 *   position: 'absolute',
 *   zIndex: 1000,
 *   width: 200,
 *   height: 150,
 *   backgroundColor: 'blue',
 * })
 *
 * // Render the component.
 * page.render();
 */
export class Page extends Component_A {
  constructor(parent, props = {}) {
    super(parent, props);

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

    this._children = new Map();
  }

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

      // X/Y/W/H/B/R
      position: 'absolute',
      width: 1024,
      height: 680,
      top: 0,
      left: 0,
      zIndex: 0,

      // Special properties
      backgroundColor: 'white',
      id: '',
      name: '',
      children: [],
    };
  }

  /**
   * @description Initializes the Page component.
   * This method is called during the component's initialization phase.
   * @member TComponents.Page#onInit
   * @method
   * @throws {Error} Throws an error if initializing fails.
   * @returns {void}
   */
  onInit() {
    try {
      this._children.clear();
      const childrenArray = Object.values(this._props.children || []);
      childrenArray.forEach((child, idx) => {
        if (typeof child === 'string') {
          // check if child has # and remove it
          const elementId = child.replace(/^#/, '');
          child = elementId.startsWith('.') ? document.querySelector(elementId) : document.getElementById(elementId);
          if (!child) {
            throw new Error(`Container_A: Could not find element with selector/id: ${elementId} in the DOM.
          Check the selector or if inside a TComponent, then try adding the child as Element or Component_A instance,
          since this may not be yet available in the DOM.`);
          }

          if (!child.id) {
            child.id = this._childId(idx);
          }
          this._children.set(child, child.id);
        } else if (Component_A.isTComponent(child)) {
          this._children.set(child, child.compId);
        } else if (Component_A._isHTMLElement(child)) {
          if (!child.id) {
            child.id = this._childId(idx);
          }
          this._children.set(child, child.id);
        } else throw new Error(`Unexpected type of child detected: ${typeof child}`);
      });
    } catch (e) {
      Logger.e(logModule, ErrorCode.FailedToRunOnInit, `Error happens on onInit of Page component ${this.compId}.`, e);
      throw ErrorCode.FailedToRunOnInit;
    }
  }

  /**
   * @description Maps the child components of the Page.
   * @member TComponents.Page#mapComponents
   * @method
   * @returns {object}
   */
  mapComponents() {
    return {children: [...this._children.keys()]};
  }

  /**
   * @description Renders the Page component. This method is responsible for cleaning up events and updating the DOM.
   * @member TComponents.Page#onRender
   * @method
   * @throws {Error} Throws an error if rendering fails.
   * @returns {void}
   */
  onRender() {
    try {
      this.removeAllEventListeners();

      if (Component_A._isHTMLElement(this.parent)) {
        this.parent.style.height = `${this._props.height}`;
        this.parent.style.width = `${this._props.width}`;
        this.parent.style.top = `${this._props.top}`;
        this.parent.style.left = `${this._props.left}`;
        this.parent.style.zIndex = `${this._props.zIndex}`;
        this.parent.style.backgroundColor = `${this._props.backgroundColor}`;

        if (this._props.id !== '') {
          const realPageElem = this.parent.querySelector(`#${this._props.id}`);
          if (Component_A._isHTMLElement(realPageElem)) {
            realPageElem.style.cssText = this.container.style.cssText;
            realPageElem.className = this.container.className;
          }
        }

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

  /**
   * @description Generates the HTML markup for the Page component.
   * @member TComponents.Page#markup
   * @method
   * @returns {string} The HTML markup for the Page component.
   */
  markup() {
    return /*html*/ ``;
  }

  /**
   * @description Generates the CSS text for the Page component based on its properties.
   * @member TComponents.Page#getCssText
   * @method
   * @returns {string} The CSS text for the Page component.
   */
  getCssText() {
    const cssText = `position: ${this._props.position}; 
    left: ${this._props.left}px;
    top: ${this._props.top}px;
    height: ${this._props.height}px;
    width: ${this._props.width}px;
    background-color: ${this._props.backgroundColor};
    z-index: ${this._props.zIndex};`
      .replace(/\s+/g, ' ')
      .trim();

    return cssText;
  }

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

  /**
   * Sets the properties of the Page component.
   * **Note that changing this property will not cause the component to refresh directly.**
   * @member {TComponents.PageProps} TComponents.Page#properties
   * @instance
   * @param {TComponents.PageProps} t - The new properties of the Page component.
   * @example
   * const page = new TComponents.Page(document.body,{
   *   position: 'absolute',
   *   zIndex: 1000,
   *   width: 200,
   *   height: 150,
   *   backgroundColor: 'blue',
   * })
   *
   * // Render the component.
   * page.render();
   *
   * page.properties = {backgroundColor: 'red'};
   */
  set properties(t) {
    this._props = t;
  }

  /**
   * @description Appends a child component to the Page.
   * @member TComponents.Page#appendChild
   * @method
   * @param {TComponents.Component_A} t - The child component to be appended.
   * @example
   * const page = new TComponents.Page(document.body,{
   *   position: 'absolute',
   *   zIndex: 1000,
   *   width: 200,
   *   height: 150,
   *   backgroundColor: 'blue',
   * })
   *
   * // Render the component.
   * page.render();
   *
   * // Create a button
   * const button = new TComponents.Button(null, {text: 'Click me'});
   *
   * page.appendChild(button);
   */
  appendChild(t) {
    try {
      if (typeof t == 'object' && typeof t.render == 'function' && typeof t.destroy == 'function') {
        t.destroy();
        t.parent = this.parent;
        t.render();
      }
    } catch (e) {
      Logger.e(
        logModule,
        ErrorCode.FailedToAppendChild,
        `Error happens on appendChild of Page component ${this.compId}.`,
        e,
      );
      throw new Error(ErrorCode.FailedToAppendChild, {cause: e});
    }
  }

  /**
   * @description Removes a child component from the Page.
   * @member TComponents.Page#removeChild
   * @method
   * @param {TComponents.Component_A} t - The child component to be removed.
   * @example
   * const page = new TComponents.Page(document.body,{ ... });
   *
   * // Remove the button.
   * page.removeChild(button)
   */
  removeChild(t) {
    try {
      if (typeof t == 'object' && typeof t.destroy == 'function') {
        t.destroy();
      }
    } catch (e) {
      Logger.e(
        logModule,
        ErrorCode.FailedToRemoveChild,
        `Error happens on removeChild of Page component ${this.compId}.`,
        e,
      );
      throw new Error(ErrorCode.FailedToRemoveChild, {cause: e});
    }
  }
}

/**
 * @description Add css properties to the component
 * @alias loadCssClassFromString
 * @member TComponent.Page.loadCssClassFromString
 * @method
 * @static
 * @param {string} css - The css string to be loaded into style tag
 * @returns {void}
 * @example
 * TComponent.Page.loadCssClassFromString(`
 *   .tc-page {
 *     background-color: 'red';
 *   }
 * `)
 */
Page.loadCssClassFromString(``);