Source: tables/table.js


import {AssertionError} from '$qui/base/errors.js'
import List             from '$qui/lists/list.js'
import * as ObjectUtils from '$qui/utils/object.js'

import SimpleTableCell from './common-cells/simple-table-cell.js'
import TableRow        from './table-row.js'


/**
 * A table view.
 * @alias qui.tables.Table
 * @extends qui.lists.List
 */
class Table extends List {

    /**
     * @constructs
     * @param {Array} [header] optional table header
     * @param {qui.tables.TableRow} [headerRow] optional table header row
     * ({@link qui.tables.commoncells.SimpleTableCell} cells will be used by default)
     * @param {String[]} [widths] table column widths, in percents or absolute values with units
     * @param {Boolean[]} [visibilities] table column visibilities
     * @param {String[]} [horizontalAlign] default horizontal cell alignment for each column; a list containing one of:
     *  * {@link qui.tables.TABLE_CELL_ALIGN_LEFT}
     *  * {@link qui.tables.TABLE_CELL_ALIGN_CENTER} (default)
     *  * {@link qui.tables.TABLE_CELL_ALIGN_RIGHT}
     * @param {String[]} [verticalAlign] default vertical cell alignment for each column; a list containing one of:
     *  * {@link qui.tables.TABLE_CELL_ALIGN_TOP}
     *  * {@link qui.tables.TABLE_CELL_ALIGN_CENTER} (default)
     *  * {@link qui.tables.TABLE_CELL_ALIGN_BOTTOM}
     * @param {qui.tables.TableCell[]|Object[]} [rowTemplate] an optional row template to use when adding new rows with
     * {@link qui.tables.Table#addRowValues}; if a list of objects is supplied, each object must contain a `class`
     * property indicating the table cell class, while the rest of properties are used as constructor parameters
     * @param {qui.tables.TableRow[]} [initialRows] initial table rows
     * @param {Array[]} [initialValues] initial table values
     * @param {...*} args parent class parameters
     */
    constructor({
        header = null,
        headerRow = null,
        widths = null,
        visibilities = null,
        horizontalAlign = null,
        verticalAlign = null,
        rowTemplate = null,
        initialRows = null,
        initialValues = null,
        ...args
    } = {}) {

        /* Ensure no items are passed via constructor; we have initialRows for that */
        delete args.items

        super(args)

        this._header = header
        this._headerRow = headerRow
        this._widths = widths
        this._visibilities = visibilities
        this._horizontalAlign = horizontalAlign
        this._verticalAlign = verticalAlign
        this._rowTemplate = rowTemplate
        this._initialRows = initialRows
        this._initialValues = initialValues

        this._numColumns = null
        this._computedWidths = null
    }

    initHTML(html) {
        super.initHTML(html)

        html.addClass('qui-table')
    }

    init() {
        super.init()

        if (this._initialRows) {
            this.setItems(this._initialRows)
            delete this._initialRows /* Don't waste memory */
        }

        if (this._header) {
            this.prepareHeader()
            this._addHeaderRow()
        }

        if (this._initialValues) {
            this.setValues(this._initialValues)
            delete this._initialValues /* Don't waste memory */
        }
    }


    /* Alignment */

    /**
     * Return the default horizontal alignment for each column.
     * @returns {String[]} a list containing one of:
     *  * {@link qui.tables.TABLE_CELL_ALIGN_LEFT}
     *  * {@link qui.tables.TABLE_CELL_ALIGN_CENTER}
     *  * {@link qui.tables.TABLE_CELL_ALIGN_RIGHT}
     */
    getHorizontalAlign() {
        return this._horizontalAlign
    }

    /**
     * Return the default vertical alignment for each column.
     * @returns {String[]} a list containing one of:
     *  * {@link qui.tables.TABLE_CELL_ALIGN_TOP}
     *  * {@link qui.tables.TABLE_CELL_ALIGN_CENTER}
     *  * {@link qui.tables.TABLE_CELL_ALIGN_BOTTOM}
     */
    getVerticalAlign() {
        return this._verticalAlign
    }


    /* Header */

    /**
     * Return the table header row.
     * @returns {?qui.tables.TableRow}
     */
    getHeaderRow() {
        return this._headerRow
    }

    /**
     * Set table header row. If `null` is passed, {@link qui.tables.commoncells.SimpleTableCell} are used to build a
     * table row.
     * @param {?qui.tables.TableRow} headerRow table header row
     */
    setHeaderRow(headerRow) {
        if (this._headerRow) {
            this._removeHeaderRow()
        }

        this._headerRow = headerRow
        if (this._header) {
            this.prepareHeader()
            this._addHeaderRow()
        }
    }

    /**
     * Return the table header.
     * @returns {?Array}
     */
    getHeader() {
        return this._header
    }

    /**
     * Set or clear the table header.
     * @param {?Array} header
     */
    setHeader(header) {
        if (this._headerRow) {
            this._removeHeaderRow()
        }

        this._header = header
        if (this._header) {
            this.prepareHeader()
            this._addHeaderRow()
        }
    }

    _addHeaderRow() {
        if (this._searchElem) {
            this._searchElem.after(this._headerRow.getHTML())
        }
        else {
            this.getBody().prepend(this._headerRow.getHTML())
        }
    }

    _removeHeaderRow() {
        this._headerRow.getHTML().remove()
    }


    /* Rows */

    /**
     * Update the row template to use when adding new rows with {@link qui.tables.Table#addRowValues}; if a list of
     * objects is supplied, each object must contain a `class`.
     *
     * A call to this method won't affect the currently added rows.
     *
     * @param {qui.tables.TableCell[]|Object[]} rowTemplate
     */
    setRowTemplate(rowTemplate) {
        this._rowTemplate = rowTemplate
    }

    /**
     * Return all rows.
     * @returns {qui.tables.TableRow[]}
     */
    getRows() {
        return /** @type {qui.tables.TableRow[]} */ this.getItems().filter(i => i instanceof TableRow)
    }

    /**
     * Set the rows of the list.
     * @param {qui.tables.TableRow[]} rows table rows
     */
    setRows(rows) {
        this.invalidateColumns()
        this.setItems(rows)
    }

    /**
     * Update one row.
     * @param {Number} index the index where to perform the update
     * @param {qui.tables.TableRow} row the row to update
     */
    setRow(index, row) {
        this.invalidateColumns()
        this.setItem(index, row)
    }

    /**
     * Add one row to the list.
     * @param {Number} index the index where the row should be added; `-1` will add the row at the end
     * @param {qui.tables.TableRow} row the row
     */
    addRow(index, row) {
        this.invalidateColumns()
        this.addItem(index, row)
    }

    /**
     * Add one row to the list using row template. Table must have a row template.
     * @param {Number} index the index where the row should be added; `-1` will add the row at the end
     * @param {Array} values values to set to new row
     * @param {*} [data] optional data to pass to row
     * @returns {qui.tables.TableRow} the added row
     */
    addRowValues(index, values, data = null) {
        if (!this._rowTemplate) {
            throw new AssertionError('addRowValues() called on table without row template')
        }

        let cells = this._rowTemplate.map(function (c) {
            let CellClass
            let params = {}
            if (c.class) {
                CellClass = ObjectUtils.pop(c, 'class')
                Object.assign(params, c)
            }
            else {
                CellClass = c
            }

            return new CellClass(params)
        })

        let row = new TableRow({cells, initialValues: values, data})

        this.addRow(index, row)

        return row
    }

    /**
     * Remove the row at a given index.
     * @param {Number} index the index of the row to remove
     * @returns {?qui.tables.TableRow} the removed row
     */
    removeRowAt(index) {
        this.invalidateColumns()
        return /** @type ?qui.tables.TableRow */ this.removeItemAt(index)
    }

    /**
     * Remove a specific row.
     * @param {qui.tables.TableRow} row the row to remove
     * @returns {Boolean} `true` if row found and removed, `false` otherwise
     */
    removeRow(row) {
        this.invalidateColumns()
        return this.removeItem(row)
    }

    /**
     * Remove all rows that match a condition.
     * @param {qui.tables.TableRowMatchFunc} matchFunc
     * @returns {qui.tables.TableRow[]} the removed rows
     */
    removeRows(matchFunc) {
        this.invalidateColumns()
        return /** @type qui.tables.TableRow[] */ this.removeItems(/** @type qui.lists.ListItemMatchFunc */ matchFunc)
    }

    prepareItem(item) {
        super.prepareItem(item)

        /* Set column widths */
        item.getHTML().css('grid-template-columns', this.getComputedWidths(item).join(' '))

        if (this._visibilities) {
            item.getCells().forEach(function (cell, i) {
                if (!this._visibilities[i]) {
                    cell.hide()
                }
            }.bind(this))
        }
    }

    prepareHeader() {
        if (!this._headerRow) {
            this._headerRow = new TableRow({cells: this._header.map(_ => new SimpleTableCell())})
        }

        this._headerRow.setList(this)
        this._headerRow.getHTML().addClass('qui-table-header')
        this._headerRow.setValues(this._header)
        this._headerRow.getHTML().css('grid-template-columns', this.getComputedWidths(this._headerRow).join(' '))

        if (this._visibilities) {
            this._headerRow.getCells().forEach(function (cell, i) {
                if (!this._visibilities[i]) {
                    cell.hide()
                }
            }.bind(this))
        }
    }


    /* Selection */

    /**
     * Return the currently selected rows.
     * @returns {qui.tables.TableRow[]}
     */
    getSelectedRows() {
        return this.getRows().filter(r => r.isSelected())
    }

    /**
     * Update current selection.
     * @param {qui.tables.TableRow[]} rows the list of new rows to select; empty list clears selection
     */
    setSelectedRows(rows) {
        this.setSelectedItems(rows)
    }


    /* Columns */

    invalidateColumns() {
        this._numColumns = null
        this._computedWidths = null
    }

    /**
     * Return the number of table columns.
     * @param {qui.tables.TableRow} [row] an optional row about to be added
     * @returns {Number}
     */
    getNumColumns(row = null) {
        if (this._numColumns == null) {
            let rows = this.getRows()
            if (row) {
                rows.push(row)
            }

            this._numColumns = Math.max(...rows.map(r => r.getCells().length))
        }

        return this._numColumns
    }

    /**
     * Set column widths.
     * @param {String[]} widths table column widths, in percents or absolute values with units
     */
    setWidths(widths) {
        this._widths = widths
        this._computedWidths = null

        this.getRows().forEach(r => r.getHTML().css('grid-template-columns', this.getComputedWidths().join(' ')))
        if (this._headerRow) {
            this._headerRow.getHTML().css('grid-template-columns', this.getComputedWidths().join(' '))
        }
    }

    /**
     * Return column widths.
     * @returns {String[]}
     */
    getWidths() {
        return this._widths
    }

    /**
     * Return computed column widths.
     * @param {qui.tables.TableRow} [row] an optional row about to be added
     * @returns {String[]}
     */
    getComputedWidths(row = null) {
        if (this._computedWidths == null) {
            this._computedWidths = this._widths
            let numColumns = this.getNumColumns(row)

            if (this._computedWidths == null) {
                this._computedWidths = []
            }

            while (this._computedWidths.length < numColumns) {
                this._computedWidths.push('1fr')
            }

            this._computedWidths = this._computedWidths.map(function (width, i) {
                if (this._visibilities && !this._visibilities[i]) {
                    return '0'
                }
                else {
                    return width
                }
            }.bind(this))
        }

        return this._computedWidths
    }

    /**
     * Set column visibilities.
     * @param {?Boolean[]} visibilities table column visibilities
     */
    setVisibilities(visibilities) {
        this._visibilities = visibilities

        /* Recalculate widths as they depend on visibilities */
        this.setWidths(this._widths)

        let rows = this.getRows()
        if (this._headerRow) {
            rows.push(this._headerRow)
        }

        rows.forEach(function (row) {
            row.getCells().forEach(function (cell, i) {
                if (!this._visibilities || this._visibilities[i]) {
                    cell.show()
                }
                else {
                    cell.hide()
                }
            }.bind(this))
        }.bind(this))
    }

    /**
     * Return column visibilities.
     * @returns {Boolean[]}
     */
    getVisibilities() {
        return this._visibilities
    }

    /* Values */

    /**
     * Return the current values of the table.
     * @returns {Array[]}
     */
    getValues() {
        return this.getRows().map(r => r.getValues())
    }

    /**
     * Set table values. If more values than needed are supplied, extra values are ignored. If less values are supplied,
     * extra rows are left unchanged.
     * @param {Array[]} values
     * @param {Number} [index] optional start index (defaults to `0`)
     */
    setValues(values, index = 0) {
        let rows = this.getRows().slice(index, index + values.length)
        rows.forEach((r, i) => r.setValues(values[i]))
    }

}


export default Table