as-droppable-placeholder-a.js

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

/**
 * @ignore
 */
const logModule = 'as-droppable-placeholder-a';

/**
 * @typedef TComponents.DroppablePlaceholderProps
 * @prop {object} [options] Configuration options for the placeholder component.
 * Items in this object include:
 * - **responsive** (boolean, default: false): Whether the placeholder should be responsive.
 * @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 {string} [border] Border style of the component.
 * @prop {number} [borderRadius] Border radius of the component.
 * @prop {string} [backgroundColor] Background color of the component.
 * @prop {object} [content] Layout configuration for the placeholder content.
 * This object controls how child components are arranged:
 * - **children** (string[]): Identifiers of child components inside the placeholder.
 * - **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 {Array} [children] Additional mapped child components.
 * @prop {Function} [onCreated] Function to be called when the component is created.
 * @prop {Function} [onMounted] Function to be called when the component is mounted.
 * @memberof TComponents
 */

/**
 * @description DroppablePlaceholder_A is a component that represents a droppable placeholder in the TComponents framework.
 * It allows for the insertion of child components and manages their layout and styling.
 * @class TComponents.DroppablePlaceholder_A
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} [parent=null] The parent element to which this component will be attached.
 * @param {TComponents.DroppablePlaceholderProps} [props={}] The properties for the droppable placeholder component.
 * @example
 * const droppablePlaceholder = new TComponents.DroppablePlaceholder_A(document.body, {
 *   position: 'absolute',
 *   width: 200,
 *   height: 100,
 *   zIndex: 1000,
 *   border: '1px dashed #787878',
 *   borderRadius: 4,
 *   backgroundColor: 'rgba(245,245,245,0.3)',
 * });
 *
 * droppablePlaceholder.render();
 */
export class DroppablePlaceholder_A extends Component_A {
  constructor(parent = null, props = {}) {
    super(parent, props);

    this._contentChildren = new Map();

    this._nativeContent = null;
    this._newChildren = new Map();

    this.initPropsDep('content');
  }

  /**
   * @description Returns the native content root element of the layout placeholder component.
   * The obtained value should be treated as a read-only property and should not be used for manipulation.
   * @member {HTMLElement|null} TComponents.DroppablePlaceholder_A#contentRoot
   * @instance
   * @example
   * const droppablePlaceholder = new TComponents.DroppablePlaceholder_A(document.body, {
   *   position: 'absolute',
   *   width: 200,
   *   height: 100,
   *   zIndex: 1000,
   *   border: '1px dashed #787878',
   *   borderRadius: 4,
   *   backgroundColor: 'rgba(245,245,245,0.3)',
   * });
   *
   * await droppablePlaceholder.render();
   *
   * // Get the content root element
   * const contentRoot = droppablePlaceholder.contentRoot;
   */
  get contentRoot() {
    return this._nativeContent;
  }

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

  /**
   * @description Updates the enabled state of the layout placeholder component.
   * @member {boolean} TComponents.DroppablePlaceholder_A#enabled
   * @instance
   * @param {boolean} t - `true` to enable the component, `false` to disable it.
   * @example
   * const droppablePlaceholder = new TComponents.DroppablePlaceholder_A(document.body, {
   *   position: 'absolute',
   *   width: 200,
   *   height: 100,
   *   zIndex: 1000,
   *   border: '1px dashed #787878',
   *   borderRadius: 4,
   *   backgroundColor: 'rgba(245,245,245,0.3)',
   * });
   *
   * await droppablePlaceholder.render();
   *
   * // Disable the droppable placeholder
   * droppablePlaceholder.enabled = false;
   */
  set enabled(t) {
    this._enabled = t === true ? true : false;
    const container = this.find('.tc-droppable-placeholder-a');
    if (Component_A._isHTMLElement(container)) {
      container.classList.toggle('tc-layout-placeholder-disabled', !this._enabled);
    }
  }

  /**
   * @description Returns class properties default values.
   * @member TComponents.DroppablePlaceholder_A#defaultProps
   * @method
   * @returns {TComponents.DroppablePlaceholderProps} Default property values.
   */
  defaultProps() {
    return {
      options: {
        responsive: false,
      },
      position: 'absolute',
      width: 100,
      height: 100,
      top: 0,
      left: 0,
      zIndex: 0,
      border: '1px dashed #787878',
      borderRadius: 4,
      backgroundColor: 'rgba(245,245,245,0.3)',
      content: {
        children: [`droppable-placeholder_native_${API.generateUUID()}`],
        row: true,
        box: false,
        width: '100%',
        height: '100%',
        classNames: ['flex-row', 'justify-stretch'],
      },
      children: [],
      onCreated: '',
      onMounted: '',
      onDispose: '',
    };
  }

  /**
   * @description Initializes the component by clearing the direct children and populating them from the props.
   * @member TComponents.DroppablePlaceholder_A#onInit
   * @method
   * @throws {Error} If initialization fails.
   * @returns {void}
   */
  onInit() {
    try {
      this._newChildren.clear();
      this._contentChildren.clear();

      const content = this._props.content;
      const childrenArray = content.children || [];

      childrenArray.forEach((child, index) => {
        const content = this._processContent(child);
        if (index === 0) {
          this._nativeContent = content;
        }
      });
    } catch (e) {
      // Runtime errors: write specific content to the log, throw error code
      Logger.e(
        logModule,
        ErrorCode.FailedToInitComponent,
        `Error happens on onInit of DroppablePlaceholder_A component ${this.compId}.`,
        e,
      );
      throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
    }
  }

  /**
   * @description Returns an object containing the components mapped to their identifiers.
   * @member TComponents.DroppablePlaceholder_A#mapComponents
   * @method
   * @returns {object} An object containing the components mapped to their identifiers.
   */
  mapComponents() {
    return {children: [...this._props.children]};
  }

  /**
   * @description Returns the HTML markup for the component.
   * @member TComponents.DroppablePlaceholder_A#markup
   * @method
   * @returns {string} The HTML markup for the component.
   */
  markup() {
    return /** HTML */ `
      <div class="tc-droppable-placeholder-a">
        <div class="tc-droppable-placeholder-a-native__content"></div>
        <!-- <div class="tc-droppable-placeholder-a-mapped__content"></div> -->
      </div>
    `;
  }

  /**
   * @description Renders the component by setting up the container and attaching direct and slotted children.
   * This method is called after the component has been initialized and its properties have been set.
   * @member TComponents.DroppablePlaceholder_A#onRender
   * @method
   * @throws {Error} If rendering fails.
   * @returns {void}
   */
  onRender() {
    try {
      const contentChildren = Array.from(this._contentChildren.keys());
      const isNotHtml = contentChildren.some((child) => !(child instanceof HTMLElement));
      if (contentChildren.length === 0 || isNotHtml) {
        throw new Error('No valid content children to render.');
      }

      contentChildren.forEach((content, index) => {
        if (index === 0) {
          this._attachDirectContent(content);
        } else {
          this._attachMappedContent(content);
        }
      });
    } catch (error) {
      Logger.e(
        logModule,
        ErrorCode.FailedToRenderComponent,
        `Error happens on onRender of DroppablePlaceholder_A component ${this.compId}.`,
        error,
      );
      throw new Error(ErrorCode.FailedToRenderComponent);
    }
  }

  /**
   * @description Concatenates an array of content elements to the existing content children.
   * **Note that this function should ideally be executed before component initialization.**
   * @member TComponents.DroppablePlaceholder_A#concatContent
   * @method
   * @param {any[]} tArray The array of content elements to concatenate.
   * @param {boolean} update Whether to update the component props after concatenation.
   * @returns {void}
   * @example
   * const droppablePlaceholder = new TComponents.DroppablePlaceholder_A(document.body, {
   *   position: 'absolute',
   *   width: 200,
   *   height: 100,
   *   zIndex: 1000,
   *   border: '1px dashed #787878',
   *   borderRadius: 4,
   *   backgroundColor: 'rgba(245,245,245,0.3)',
   * });
   *
   * // Concatenate new content before rendering
   * droppablePlaceholder.concatContent([document.createElement('div')], true);
   *
   * await droppablePlaceholder.render();
   */
  concatContent(tArray, update = false) {
    const content = this._props.content;
    content.children = [...content.children, ...tArray];
    if (update) {
      this.setProps({content: content});
    }
  }

  /**
   * @description Concatenates an array of child elements to the existing children.
   * **Note that this function should ideally be executed before component initialization.**
   * @member TComponents.DroppablePlaceholder_A#concatChildren
   * @method
   * @param {any[]} tArray The array of child elements to concatenate.
   * @param {boolean} update Whether to update the component props after concatenation.
   * @returns {void}
   * @example
   * const droppablePlaceholder = new TComponents.DroppablePlaceholder_A(document.body, {
   *   position: 'absolute',
   *   width: 200,
   *   height: 100,
   *   zIndex: 1000,
   *   border: '1px dashed #787878',
   *   borderRadius: 4,
   *   backgroundColor: 'rgba(245,245,245,0.3)',
   * });
   *
   * // Concatenate new children before rendering
   * droppablePlaceholder.concatChildren([new TComponents.Button(null, {})], true);
   *
   * await droppablePlaceholder.render();
   */
  concatChildren(tArray, update = false) {
    const children = this._props.children;
    const newChildren = [...children, ...tArray];

    if (update) {
      this.setProps({children: newChildren});
    } else {
      this.commitProps({children: newChildren});
    }
  }

  /**
   * @description Appends a child component or element to the current component.
   * @member TComponents.DroppablePlaceholder_A#appendChild
   * @method
   * @param {TComponents.Component_A | HTMLElement} t The child component or element to append.
   * @param {string} name The name to associate with the child component.
   * @returns {void}
   * @example
   * const droppablePlaceholder = new TComponents.DroppablePlaceholder_A(document.body, {
   *   position: 'absolute',
   *   width: 200,
   *   height: 100,
   *   zIndex: 1000,
   *   border: '1px dashed #787878',
   *   borderRadius: 4,
   *   backgroundColor: 'rgba(245,245,245,0.3)',
   * });
   *
   * await droppablePlaceholder.render();
   *
   * // Append a new button component
   * const button = new TComponents.Button(null, { text: 'Click Me' });
   * droppablePlaceholder.appendChild(button, 'myButton');
   */
  appendChild(t, name = '') {
    const contentRoot = this._nativeContent;
    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 new Error(ErrorCode.InvalidChildElement);
    }
  }

  /**
   * @description Removes a child component or element from the current component.
   * @member TComponents.DroppablePlaceholder_A#removeChild
   * @method
   * @param {string | TComponents.Component_A} t The child component or element to remove, identified by its name or reference.
   * @returns {void}
   * @example
   * const droppablePlaceholder = new TComponents.DroppablePlaceholder_A(document.body, {
   *   position: 'absolute',
   *   width: 200,
   *   height: 100,
   *   zIndex: 1000,
   *   border: '1px dashed #787878',
   *   borderRadius: 4,
   *   backgroundColor: 'rgba(245,245,245,0.3)',
   * });
   *
   * await droppablePlaceholder.render();
   *
   * // Remove a child component
   * droppablePlaceholder.removeChild('myButton');
   */
  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 new Error(ErrorCode.FailedToRemoveChildElement);
    }
  }

  /**
   * @description Internal method to remove a child component and clean up references.
   * @member TComponents.DroppablePlaceholder_A~_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()];
  }

  /**
   * @description Processes a content item, which can be a string (ID) or an HTMLElement, and returns the corresponding HTMLElement.
   * @member TComponents.DroppablePlaceholder_A~_processContent
   * @method
   * @private
   * @param {string | HTMLElement} t The content item to process.
   * @returns {HTMLElement} The processed HTMLElement.
   */
  _processContent(t) {
    if (typeof t === 'string') {
      const elem = document.createElement('div');
      elem.id = t;
      this._contentChildren.set(elem, t);
      return elem;
    } else if (Component_A._isHTMLElement(t)) {
      this._contentChildren.set(t, t.id || API.generateUUID());
      return t;
    } else {
      throw new Error('Content type is not supported.');
    }
  }

  /**
   * @description Sets the style for the content element.
   * @member TComponents.DroppablePlaceholder_A~_setContentStyle
   * @method
   * @private
   * @param {HTMLElement} content The content element to style.
   * @returns {void}
   */
  _setContentStyle(content, useBorder = true) {
    content.classList.add('t-component__container', ...this._props.content.classNames);
    content.dataset.tcContainer = 'true';

    if (useBorder) {
      Object.assign(content.style, {
        border: this._props.border,
        borderRadius: `${this._props.borderRadius}px`,
        backgroundColor: this._props.backgroundColor,
      });
    }
  }

  /**
   * @description Attaches the direct content to the component.
   * @member TComponents.DroppablePlaceholder_A~_attachDirectContent
   * @method
   * @private
   * @param {HTMLElement} content The content element to attach.
   * @returns {void}
   */
  _attachDirectContent(content) {
    this.find('.tc-droppable-placeholder-a-native__content').appendChild(content);
    this._setContentStyle(content);

    // Attach the new children to the native content area.
    this._newChildren.forEach((_, key) => {
      if (Component_A.isTComponent(key)) {
        key.attachToElement(content);
      } else if (Component_A._isHTMLElement(key)) {
        if (!content.contains(key)) {
          content.appendChild(key);
        }
      }
    });
  }

  /**
   * @description Attaches the mapped content to the component.
   * @member TComponents.DroppablePlaceholder_A~_attachMappedContent
   * @method
   * @private
   * @param {HTMLElement} content The content element to attach.
   * @returns {void}
   */
  _attachMappedContent(content) {
    this.find('.tc-droppable-placeholder-a').appendChild(content);
    content.classList.add('tc-droppable-placeholder-a-mapped__content');
    this._setContentStyle(content, false);
  }
}

/**
 * @description Add css properties to the component
 * @alias loadCssClassFromString
 * @member TComponents.DroppablePlaceholder_A.loadCssClassFromString
 * @method
 * @static
 * @param {string} css - The css string to be loaded into style tag
 * @returns {void}
 * @example
 * TComponents.DroppablePlaceholder_A.loadCssClassFromString(`.tc-droppable-placeholder-a { background-color: red; }`);
 */
DroppablePlaceholder_A.loadCssClassFromString(`
.tc-droppable-placeholder-a {
 height: 100%;
 width: 100%;
}

.tc-droppable-placeholder-a-disabled {
  opacity: 0.7;
  cursor: not-allowed;
}

.tc-droppable-placeholder-a-disabled > * {
  pointer-events: none;
}

.tc-droppable-placeholder-a-native__content,
.tc-droppable-placeholder-a-mapped__content {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 100%;
}

.tc-droppable-placeholder-a-native__content > *:nth-child(1) {
  box-sizing: border-box;
}

.tc-droppable-placeholder-a-mapped__content {
  pointer-events: none;
}

.tc-droppable-placeholder-a-mapped__content > * {
  pointer-events: auto;
}
`);