import {Component_A} from './basic/as-component.js';
import {FP_Table_A} from './fp-ext/fp-table-ext.js';
import {Popup_A} from './as-popup.js';
import {ErrorCode} from '../exception/exceptionDesc.js';
import {initDynamicOptions} from './utils/utils.js';
/**
* @typedef TComponents.TableProps
* @prop {object} [options] Additional options for the table component.
* Items in this object include:
* - **responsive** (boolean, default: false): Whether the table 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 {Function} [onRowClick] Function to be called when a table row is clicked.
* @prop {Function} [onCellClick] Function to be called when a table cell is clicked.
* @prop {Function} [onRowDblClick] Function to be called when a table row is double-clicked.
* @prop {Function} [onCellDblClick] Function to be called when a table cell is double-clicked.
* @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} [borderRadius] Border radius of the component.
* @prop {number} [rotation] Rotation angle of the component.
* @prop {number} [zIndex] Z-index of the component.
* @prop {object} [dataConfig] Configurations for the table data.
* This object defines the table content:
* - **columns** (Array<object>): Column definitions including `icon`, `title`, `width`, `sortable`.
* - **data** (Array<Array<any>>): Table row data, each row is an array of cell values.
* - **params** (object): Dynamic data configuration containing `mode`, `type`, `isHidden`, `variablePath`.
* @prop {string} [headerFontColor] Font color of the header row.
* @prop {string} [headerBackgroundColor] Background color of the header row.
* @prop {string} [bodyFontColor] Font color of the body rows.
* @prop {string} [bodyBackgroundColor] Background color of the body rows.
* @prop {object} [headerFont] Font configuration for the header row.
* This object controls header text appearance:
* - **fontSize** (number, default: 24): Font size in pixels.
* - **fontFamily** (string, default: 'Segoe UI'): Font family name.
* - **style** (object): Font style configuration, containing `fontStyle`, `fontWeight`, `textDecoration`.
* @prop {object} [bodyFont] Font configuration for the body rows.
* This object controls body text appearance:
* - **fontSize** (number, default: 16): Font size in pixels.
* - **fontFamily** (string, default: 'Segoe UI'): Font family name.
* - **style** (object): Font style configuration, containing `fontStyle`, `fontWeight`, `textDecoration`.
* @prop {string} [border] Border style of the table.
* @prop {string} [defaultState] Default state of the component.
* @prop {string|number} [selectedRow] Identifier of the selected row.
* @prop {string|number} [selectedCell] Identifier of the selected cell.
* @prop {string} [dataStruct] Custom data structure metadata for integration with other systems.
* @memberof TComponents
*/
/**
* @ignore
*/
const logModule = 'as-table';
/**
* @description This class focuses on the specific properties of the Table 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.Table
* @extends TComponents.Component_A
* @memberof TComponents
* @param {HTMLElement} parent - HTMLElement that is going to be the parent of the component
* @param {TComponents.TableProps} props
* @example
* const table = new TComponents.Table(document.body, {
* position: 'absolute',
* zIndex: 1000,
* width: 320,
* height: 120,
* dataConfig: {
* columns: [
* { title: 'Name', width: 100 },
* { title: 'Age', width: 100 },
* { title: 'Country' },
* ],
* data: [
* ['John Doe', 25, 'USA'],
* ['Jane Smith', 30, 'UK'],
* ['Sam Brown', 22, 'Canada'],
* ],
* params: {
* mode: 'fixed', //fixed, variable
* type: '',
* isHidden: false,
* variablePath: '',
* },
* },
* });
*
* // Render the component.
* table.render();
*/
export class Table extends Component_A {
constructor(parent, props = {}) {
super(parent, props);
/**
* @instance
* @private
* @type {TComponents.TableProps}
*/
this._props;
this._table = new FP_Table_A();
}
/**
* @description Returns the default values of class properties (excluding parent properties).
* @member TComponents.Table#defaultProps
* @method
* @protected
* @returns {TComponents.TableProps}
*/
defaultProps() {
return {
options: {
responsive: false,
},
tips: '',
// life cycle
onCreated: '',
onMounted: '',
onDispose: '',
onRowClick: '', // Function to be called when a row is clicked
onCellClick: '', // Function to be called when a cell is clicked
onRowDblClick: '', // Function to be called when a row is double-clicked
onCellDblClick: '', // Function to be called when a cell is double-clicked
// ⭐ W/H/X/Y/B/R/Z: Component required attributes.
position: 'static',
width: 420,
height: 140,
top: 0,
left: 0,
borderRadius: 4,
rotation: 0,
zIndex: 0,
dataConfig: {
columns: [
{
icon: '',
title: 'Title1',
width: 0,
sortable: false,
},
{
icon: '',
title: 'Title2',
width: 0,
sortable: false,
},
{
icon: '',
title: 'Title3',
width: 0,
sortable: false,
},
],
data: [
['data1-1', 'data1-2', 'data1-3'],
['data2-1', 'data2-2', 'data2-3'],
['data3-1', 'data3-2', 'data3-3'],
],
params: {
mode: 'fixed', //fixed,sync, variable
type: '',
isHidden: false,
variablePath: '',
},
},
headerFontColor: 'rgba(0, 0, 0, 1)',
headerBackgroundColor: 'rgba(198, 196, 196, 1)',
bodyFontColor: 'rgba(0, 0, 0, 1)',
bodyBackgroundColor: 'rgba(255, 255, 255, 1)',
headerFont: {
fontSize: 24,
fontFamily: 'Segoe UI',
style: {
fontStyle: 'normal',
fontWeight: 'normal',
textDecoration: 'none',
},
},
bodyFont: {
fontSize: 16,
fontFamily: 'Segoe UI',
style: {
fontStyle: 'normal',
fontWeight: 'normal',
textDecoration: 'none',
},
},
border: '1px solid #dbdbdb',
defaultState: 'show_enable',
selectedRow: '',
selectedCell: '',
dataStruct: '',
};
}
/**
* @description Initialization function for the table component.
* @member TComponents.Table#onInit
* @method
* @async
* @return {Promise<void>}
*/
async onInit() {}
/**
* @description there are something need to do after render once
* @member TComponents.Table#afterRenderOnce
* @method
* @protected
* @returns {void}
*/
afterRenderOnce() {
initDynamicOptions(this._props.dataConfig.params, (data) => {
// { text:'[1,2,3]', value:'[1,2,3]'}
const arr2 = data.map((item) => {
if (item.value) {
if (item.value instanceof Array) {
return item.value;
} else {
const raw = item.value.replace(/true/gi, 'true').replace(/false/gi, 'false');
const list = JSON.parse(raw);
return list.map((l) => JSON.stringify(l));
}
} else {
return item;
}
});
this.setProps({
dataConfig: Object.assign({}, this._props.dataConfig, {data: arr2}),
});
});
}
/**
* @description Renders the table component.
* @member TComponents.Table#onRender
* @method
* @throws {Error} Throws error code ErrorCode.FailedToRunOnRender when runtime error happens.
* @returns {void}
*/
async onRender() {
try {
this.removeAllEventListeners();
const columns = (this._props.dataConfig && this._props.dataConfig.columns) || [];
this._table.columns = columns.map((col) => {
return Object.assign({}, col, {
title: Component_A.tParse(col.title),
});
});
this._table.data = (this._props.dataConfig && this._props.dataConfig.data) || [];
this._table.headerStyle = `
background-color: ${this._props.headerBackgroundColor};
color: ${this._props.headerFontColor};
`;
this._table.bodyStyle = `
background-color: ${this._props.bodyBackgroundColor};
color: ${this._props.bodyFontColor};
`;
this._table.headerFont = this._props.headerFont;
this._table.bodyFont = this._props.bodyFont;
this._table.border = this._props.border;
this._table.height = this._props.height;
this._table.width = this._props.width;
this._table.borderRadius = this._props.borderRadius;
const btnContainer = this.find('.tc-table');
if (btnContainer) this._table.attachToElement(btnContainer);
this.addEventListener(this._table._root, 'click', this._cbOnClick.bind(this));
this.addEventListener(this._table._root, 'dblclick', this._cbOnDbClick.bind(this));
this._addTips();
} catch (e) {
// Runtime errors: write specific content to the log, throw error code
Logger.e(
logModule,
ErrorCode.FailedToRunOnRender,
`Error happens on onRender of table component ${this.compId}.`,
e,
);
throw new Error(ErrorCode.FailedToRunOnRender, {cause: e});
}
}
/**
* @description Returns the markup for the table component.
* @member TComponents.Table#markup
* @method
* @returns {string} HTML markup string.
*/
markup() {
return /*html*/ `<div class="tc-table"></div>`;
}
/**
* @description Returns the row data of the table component.
* @member TComponents.Table#copyRow
* @method
* @param {number} [rowIndex] - The index of the row to copy.
* @returns {any[]} The data of the copied row.
* @example
* const table = new TComponents.Table(document.body, {
* position: 'absolute',
* zIndex: 1000,
* width: 320,
* height: 120,
* dataConfig: {
* columns: [
* { title: 'Name', width: 100 },
* { title: 'Age', width: 100 },
* { title: 'Country' },
* ],
* data: [
* ['John Doe', 25, 'USA'],
* ['Jane Smith', 30, 'UK'],
* ['Sam Brown', 22, 'Canada'],
* ],
* params: {
* mode: 'fixed', //fixed, variable
* type: '',
* isHidden: false,
* variablePath: '',
* },
* },
* });
*
* table.render();
*
* const rowData = table.copyRow(0);
*
* // Logger the copied data.
* console.log(rowData) // ['John Doe', 25, 'USA']
*/
copyRow(rowIndex) {
if (!this._props.dataConfig || !this._props.dataConfig.data) {
this._props.dataConfig = {data: []};
}
if (rowIndex < 0 || rowIndex >= this._props.dataConfig.data.length) {
Logger.w(logModule, `Row index out of range: ${rowIndex} when copying row`);
throw new Error(ErrorCode.FailedToUpdateComponentWithMethod, {
cause: `Row index out of range: ${rowIndex} when copying row`,
});
}
const rowData = this._props.dataConfig.data[rowIndex];
return rowData;
}
/**
* @description Deletes the row data of the table component.
* @member TComponents.Table#deleteRow
* @method
* @param {number} [rowIndex] - The index of the row to delete.
* @throws {Error} Throws an error when calls deleteRow failed.
* @returns {void}
* @example
* const table = new TComponents.Table(document.body, {
* position: 'absolute',
* zIndex: 1000,
* width: 320,
* height: 120,
* dataConfig: {
* columns: [
* { title: 'Name', width: 100 },
* { title: 'Age', width: 100 },
* { title: 'Country' },
* ],
* data: [
* ['John Doe', 25, 'USA'],
* ['Jane Smith', 30, 'UK'],
* ['Sam Brown', 22, 'Canada'],
* ],
* params: {
* mode: 'fixed', //fixed, variable
* type: '',
* isHidden: false,
* variablePath: '',
* },
* },
* });
*
* table.render();
*
* table.deleteRow(0);
*/
deleteRow(rowIndex) {
if (typeof rowIndex !== 'number') {
Logger.w(logModule, `Row index is not a number: ${rowIndex} when deleting row`);
throw new Error(ErrorCode.FailedToUpdateComponentWithMethod, {
cause: `Row index is not a number: ${rowIndex} when deleting row`,
});
}
const optionsConfig = this._props.dataConfig.params;
if (optionsConfig && optionsConfig.mode == 'fixed') {
if (rowIndex < 0 || rowIndex >= this._props.dataConfig.data.length) {
Logger.w(logModule, `Row index out of bounds: ${rowIndex} when deleting row`);
throw new Error(ErrorCode.FailedToUpdateComponentWithMethod, {
cause: `Row index out of bounds: ${rowIndex} when deleting row`,
});
}
this._props.dataConfig.data.splice(rowIndex, 1);
this.setData(this._props.dataConfig.data);
} else {
throw new Error(ErrorCode.FailedToUpdateComponentWithMethod, {
cause: 'Cannot delete row when optionsConfig is not in fixed mode.',
});
}
}
/**
* @description Add row data to the table component.
* @member TComponents.Table#addRow
* @method
* @param {any[]} [rowData] - The data of the row to add.
* @throws {Error} Throws an error when calls deleteRow failed.
* @returns {void}
* @example
* const table = new TComponents.Table(document.body, {
* position: 'absolute',
* zIndex: 1000,
* width: 320,
* height: 120,
* dataConfig: {
* columns: [
* { title: 'Name', width: 100 },
* { title: 'Age', width: 100 },
* { title: 'Country' },
* ],
* data: [
* ['John Doe', 25, 'USA'],
* ['Jane Smith', 30, 'UK'],
* ['Sam Brown', 22, 'Canada'],
* ],
* params: {
* mode: 'fixed', //fixed, variable
* type: '',
* isHidden: false,
* variablePath: '',
* },
* },
* });
*
* table.render();
*
* table.addRow([1, 2, 3]);
*/
addRow(rowData) {
const optionsConfig = this._props.dataConfig.params;
if (optionsConfig && optionsConfig.mode == 'fixed') {
this._props.dataConfig.data.push(rowData);
this.setData(this._props.dataConfig.data);
} else {
throw new Error(ErrorCode.FailedToUpdateComponentWithMethod, {
cause: 'Cannot add row when optionsConfig is not in fixed mode.',
});
}
}
/**
* @description Set the data for the table component.
* This method updates the table's data configuration with the provided data.
* This operation will trigger the rebuild once.
* @member TComponents.Table#setData
* @method
* @param {any[][]} [data] - The data to set for the table.
* @returns {void}
* @example
* table.setData([[1, 2, 3], [4, 5, 6]]);
*/
setData(data) {
const optionsConfig = this._props.dataConfig.params;
if (optionsConfig && optionsConfig.mode == 'fixed') {
const newConfig = Object.assign({}, this._props.dataConfig, {
data: data,
});
this.setProps({dataConfig: newConfig}, null, false, true);
} else {
throw new Error(ErrorCode.FailedToUpdateComponentWithMethod, {
cause: 'Cannot set data when optionsConfig is not in fixed mode.',
});
}
}
/**
* @description Sets the header configuration for the table component.
* This method updates the table's header configuration with the provided headerConfig.
* This operation will trigger the rebuild once.
* @member TComponents.Table#setHeader
* @method
* @param {object[]} [headerConfig] - The header configuration to set for the table.
* @returns {void}
* @example
* table.setHeader([{title: 'Column 1', width: 200}, {title: 'Column 2', width: 200}, {title: 'Column 3'}]);
*/
setHeader(headerConfig) {
const newConfig = Object.assign({}, this._props.dataConfig, {
columns: headerConfig,
});
this.setProps({dataConfig: newConfig}, null, false, true);
}
/**
* @description Set the header and data for the table component.
* This method updates the table's header and data configuration with the provided config.
* The config should contain both header and data. And this operation only trigger the rebuild once.
* @member TComponents.Table#setHeaderAndData
* @method
* @param {object[]} [header] - The header configuration to set for the table.
* @param {object[]} [data] - The data to set for the table.
* @throws {Error}
* @returns {void}
* @example
* table.setHeaderAndData([{title: 'Column 1', width: 200}, {title: 'Column 2', width: 200}, {title: 'Column 3'}], [[1, 2, 3], [4, 5, 6]]);
*/
setHeaderAndData(header, data) {
const optionsConfig = this._props.dataConfig.params;
if (optionsConfig && optionsConfig.mode == 'fixed') {
const newConfig = Object.assign({}, this._props.dataConfig, {
columns: header,
data: data,
});
this.setProps({dataConfig: newConfig}, null, false, true);
} else {
throw new Error(ErrorCode.FailedToUpdateComponentWithMethod, {
cause: 'Cannot set header and data when optionsConfig is not in fixed mode.',
});
}
}
/**
* @description Handles the click event on the table component.
* @member TComponents.Table~_cbOnClick
* @method
* @private
* @param {MouseEvent} e - The click event.
* @throws {Error} Throws error code ErrorCode.FailedToExecuteEvent when user operation fails.
* @returns {Promise<void>}
*/
async _cbOnClick(e) {
if (this.enabled) {
const available = e.target.dataset.available;
const columnIndex = e.target.dataset.columnIndex;
const rowIndex = e.target.dataset.rowIndex;
if (!available) {
return;
}
try {
var rowFn = Component_A.genFuncTemplate(this._props.onRowClick, this);
var cellFn = Component_A.genFuncTemplate(this._props.onCellClick, this);
this.enabled = false; // Disable the button to prevent multiple clicks
this.selectedCell = this._table.data[rowIndex][columnIndex];
this.selectedRow = this._table.data[rowIndex];
this._table.activeRowIndex = rowIndex;
this._table.activeRow();
if (typeof rowFn == 'function') {
await rowFn();
}
if (typeof cellFn == 'function') {
await cellFn();
}
// this.selectedCell = null;
// this.selectedRow = null;
} catch (e) {
Component_A.popupEventError(e, 'onClick', logModule);
} finally {
this.enabled = true; // Re-enable the button after the click handler is done
}
}
}
/**
* @description Handles the double-click event on the table component.
* @member TComponents.Table~_cbOnDbClick
* @method
* @private
* @async
* @param {MouseEvent} e - The double-click event.
* @return {Promise<void>}
*/
async _cbOnDbClick(e) {
if (this.enabled) {
const available = e.target.dataset.available;
const columnIndex = e.target.dataset.columnIndex;
const rowIndex = e.target.dataset.rowIndex;
if (!available) {
return;
}
try {
var rowFn = Component_A.genFuncTemplate(this._props.onRowDblClick, this);
} catch (e) {
return;
}
try {
var cellFn = Component_A.genFuncTemplate(this._props.onCellDblClick, this);
} catch (e) {
return;
}
this.enabled = false; // Disable the button to prevent multiple clicks
this.selectedCell = this._table.data[rowIndex][columnIndex];
this.selectedRow = this._table.data[rowIndex];
this._table.activeRowIndex = rowIndex;
this._table.activeRow();
if (typeof rowFn == 'function') {
try {
await rowFn();
} catch (e) {
Component_A.popupEventError(e, 'onRowDblClick', logModule);
}
}
if (typeof cellFn == 'function') {
try {
await cellFn();
} catch (e) {
Component_A.popupEventError(e, 'onCellDblClick', logModule);
}
}
this.enabled = true; // Re-enable the button after the click handler is done
// this.selectedCell = null;
// this.selectedRow = null;
}
}
/**
* @returns {number|string}
*/
get selectedCell() {
return this._props.selectedCell;
}
/**
* @description Sets the selected cell.
* @member {string} TComponents.Table#selectedCell
* @instance
* @param {string|number} t - The selected cell value.
* @example
* table.selectedCell = 'USA';
*/
set selectedCell(t) {
this.commitProps({selectedCell: t});
}
/**
* @returns {number}
*/
get selectedRow() {
return this._props.selectedRow;
}
/**
* @description Sets the selected row value.
* @member {string} TComponents.Table#selectedRow
* @instance
* @param {string} t - The selected row value.
* @example
* table.selectedRow = ['Jane Smith', 30, 'UK'];
*/
set selectedRow(t) {
this.commitProps({selectedRow: t});
}
}
/**
* @description Add css properties to the component
* @alias loadCssClassFromString
* @member TComponents.Table.loadCssClassFromString
* @method
* @static
* @param {string} css - The css string to be loaded into style tag
* @returns {void}
* @example
* TComponents.Table.loadCssClassFromString(`
* .tc-table {
* height: 100%;
* width: 100%;
* }
* `);
*/
Table.loadCssClassFromString(/*css*/ `
.tc-table {
height: 100%;
width: 100%;
}
.tc-table table th{
position:static;
background-color: transparent;
}
.tc-table > .fp-components-table-disabled *{
cursor: not-allowed !important;
}
.tc-table > .fp-components-table-disabled,
.tc-table > .fp-components-table:hover,
.tc-table > .fp-components-table-disabled:hover {
opacity:0.7;
}
.tc-table .fp-table-row-active{
background: rgba(68, 102, 255, 0.3) !important;
}
`);