as-table.js

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


`);