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;
}
`);