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