basic_as-container.js

import {ErrorCode, ExceptionIdMap} from '../../exception/exceptionDesc.js';
import {Component_A} from './as-component.js';
import {Popup_A} from '../as-popup.js';

/**
 * @typedef TComponents.ContainerProps
 * @prop {TComponents.Component_A | HTMLElement | TComponents.Component_A[] | HTMLElement[]} [children]
 * @prop {boolean} [box] If true, the container will have a box around it
 * @prop {string} [width] Width of the container. Default value is “auto”.
 * @prop {string} [height] Height of the container. Default value is “auto”.
 * @prop {string} [classNames] Additional class names to be added to the container.
 * It can be a string, e.g.  'flex-row items-start justify-start',
 * or an array of strings, e.g.  ['flex-row', 'items-start', 'justify-start']
 * @prop {string} [id] Name of container (optional). For instance, if container is part of a layout, the name of the prop
 * corresponding to the container shall be given further to the container. The name will be then exposed in the DOM element
 * as data-name attribute.
 */

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

/**
 * @description Container that can hold other components or HTML elements. It can be used to create row or column containers.
 * @class TComponents.Container_A
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} parent - HTML element that is going to be the parent of the component
 * @param {TComponents.ContainerProps} [props]
 * @example
 * const container = new TComponents.Container_A(document.body, {
 *   children: [new TComponents.Button(null, {})],
 *   box: true,
 *   width: '100%',
 *   height: '100%',
 *   classNames: ['flex-row', 'items-start', 'justify-start'],
 *   id: 'my-container'
 * });
 */
export class Container_A extends Component_A {
  constructor(parent, props = {}) {
    super(parent, props);
    this._children = new Map();

    this.initPropsDep('children');
  }

  /**
   * @returns {boolean}
   */
  get enabled() {
    return this._enabled;
  }

  /**
   * @description Updates the enabled state of the container component.
   * @member {boolean} TComponents.Container_A#enabled
   * @instance
   * @param {boolean} t - `true` to enable the component, `false` to disable it.
   * @example
   * const container = new TComponents.Container_A(document.body, {});
   * container.enabled = true;
   */
  set enabled(t) {
    this._enabled = t === true ? true : false;
    this.container.classList.toggle('t-component__container-disabled', !this._enabled);
  }

  /**
   * @description Returns the default values of class properties (excluding parent properties).
   * @member TComponents.Container_A#defaultProps
   * @method
   * @protected
   * @returns {TComponents.ContainerProps}
   */
  defaultProps() {
    return {
      children: [],
      row: false,
      box: false,
      width: '100%',
      height: '100%',
      classNames: ['flex-row', 'justify-stretch'],
      id: '',
    };
  }

  /**
   * @description Initializes the component. This method is called after the component is created and added to the DOM.
   * @member TComponents.Container_A#onInit
   * @method
   * @async
   * @throws {Error} Throws an error if a child element cannot be found in the DOM.
   * @returns {Promise<void>}
   */
  async onInit() {
    this._children.clear();

    const childrenArray = Array.isArray(this._props.children)
      ? this._props.children
      : [this._props.children ? this._props.children : []];

    childrenArray.forEach((child, idx) => {
      if (typeof child === 'string') {
        // chekc if child has # and remove it
        const elementId = child.replace(/^#/, '');
        child = elementId.startsWith('.') ? document.querySelector(elementId) : document.getElementById(elementId);
        if (!child) {
          Logger.e(
            logModule,
            ErrorCode.FailedToFindChildElement,
            `Container_A: Could not find element with selector/id: ${elementId} in the DOM.`,
          );
          Popup_A.danger(
            `${ExceptionIdMap.FailedToFindChildElement}-${Component_A.t(`framework:${ExceptionIdMap.FailedToFindChildElement}.title`, {elementId: elementId})}`,
            Component_A.t(`framework:${ExceptionIdMap.FailedToFindChildElement}.causes`),
          );
        }

        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 {
        Logger.e(
          logModule,
          ErrorCode.InvalidChildElement,
          `Container_A: Unexpected type of child detected: ${typeof child}. Expected TComponent or HTMLElement.`,
        );
        Popup_A.danger(
          `${ExceptionIdMap.InvalidChildElement}-${Component_A.t(`framework:${ExceptionIdMap.InvalidChildElement}.title`, {type: typeof child})}`,
          Component_A.t(`framework:${ExceptionIdMap.InvalidChildElement}.causes`),
        );
      }
    });

    this._props.children = [...this._children.values()];
    // Ensure the component ID is set, generating one if not provided.
    this.compId = typeof this._props.id === 'string' ? this._props.id : this.compId;
  }

  /**
   * @description Maps the components in the container.
   * @member TComponents.Container_A#mapComponents
   * @method
   * @returns {object} An object containing the mapped components.
   */
  mapComponents() {
    return {children: [...this._children.keys()]};
  }

  /**
   * @description Renders the container and its children.
   * @member TComponents.Container_A#onRender
   * @method
   * @returns {void}
   */
  onRender() {
    // Always ensure a component ID is set, generating one if not provided.
    this.container.id = typeof this._props.id === 'string' ? this._props.id : this.compId;
    // this.container.dataset.containerId = this._props.id ? this._props.id : this.compId;
    this.container.dataset.tcContainer = 'true';

    this._children.forEach((id, child) => {
      if (Component_A.isTComponent(child)) {
        child.attachToElement(this.container);
      } else {
        if (child.parentNode) {
          child.parentNode.removeChild(child);
        }
        this.container.append(child);
      }
    });

    if (this._props.box) this.cssBox();
    this.cssAddClass('this', [
      't-component__container',
      'flex-row',
      // `${this._props.row ? 'flex-row' : 'flex-row'}`,
    ]);
    if (this._props.classNames) this.cssAddClass('this', this._props.classNames ? this._props.classNames : 'flex-row');

    this.container.style.width = this._props.width;
    this.container.style.height = this._props.height;
    this.container.style.setProperty('padding', '0px', 'important');
    this.container.style.setProperty('margin', '0px', 'important');
  }

  /**
   * @description Generates an ID for a child element based on its index.
   * @member TComponents.Container_A~_childId
   * @private
   * @param {number} idx Index of the child element.
   * @returns {string} Generated ID for the child element.
   */
  _childId(idx) {
    return `${this.compId}__child-${idx}`;
  }

  /**
   * @description Generates a selector for a child element based on its index.
   * @member TComponents.Container_A~_childSelector
   * @method
   * @private
   * @param {number} idx Index of the child element.
   * @returns {string} Generated selector for the child element.
   */
  _childSelector(idx) {
    return `child-${idx}`;
  }

  /**
   * @description Add/remove a class to a specific child element. The index of the child element is the same as the index of the child element in the children array.
   * @member TComponents.Container_A#cssItem
   * @method
   * @param {number} index Index of the child element to be styled.
   * @param {string} className Name of the class to be added (removed).
   * @param {boolean} remove If true, the class will be removed.
   * @returns {void}
   */
  cssItem(index, className, remove = false) {
    if (remove) this.cssRemoveClass(`.${this._childId(index)}`, className);
    else this.cssAddClass(`.${this._childId(index)}`, className);
  }

  /**
   * @description Add/remove a class to all child elements.
   * @member TComponents.Container_A#cssItems
   * @method
   * @param {string} className Name of the class to be added (removed).
   * @param {boolean} remove If true, the class will be removed.
   * @returns {void}
   */
  cssItems(className, remove = false) {
    if (remove) this.cssRemoveClass('.child__container', className);
    else this.cssAddClass('.child__container', className, true);
  }
}

/**
 * @description Add css properties to the component
 * @member TComponents.Container_A.loadCssClassFromString
 * @method
 * @static
 * @param {string} css - The css string to be loaded into style tag
 * @returns {void}
 * @example
 * Container_A.loadCssClassFromString(`
 * .t-component__container {
 *   max-width: inherit;
 *   max-height: inherit;
 *   width: 100% !important;
 *   height: 100% !important;
 * }
 *
 * .t-component__container-disabled {
 *   cursor: not-allowed;
 *   opacity: 0.7;
 * }
 *
 * .t-component__container-disabled > * {
 *   pointer-events: none;
 * }
 * `);
 */
Container_A.loadCssClassFromString(/*css*/ `
.t-component__container {
  max-width: inherit;
  max-height: inherit;
  width: 100% !important;
  height: 100% !important;
}

.t-component__container-disabled {
  cursor: not-allowed;
  opacity: 0.7;
}

.t-component__container-disabled > * {
  pointer-events: none;
}

`);