diff --git a/data-table.js b/data-table.js new file mode 100644 index 0000000..74f1fee --- /dev/null +++ b/data-table.js @@ -0,0 +1,268 @@ +import { IgniteHtml, IgniteProperty } from "../ignite-html/ignite-html.js"; +import { IgniteElement } from "../ignite-html/ignite-element.js"; +import { IgniteTemplate, div, table, thead, tbody, tr, th, td, list, pagination, pager, population, nav, ul, li, button, span, input, select, option } from "../ignite-html/ignite-template.js"; + +class DataColumn { + /** + * The name of the column, this can contain html. + * @type {String|Any} + * */ + name; + + /** + * Whether or not this column can be searched. + * @type {Boolean} + */ + search; + + /** + * + * @param {String} name The name of the column to display + * @param {Boolean} search Whether or not this column is searchable. + */ + constructor(name, search) { + this.name = name; + this.search = search; + } +} + +class DataTable extends IgniteElement { + constructor() { + super(); + } + + get properties() { + return { + columns: [], + items: new IgniteProperty([], { + onChange: (oldValue, newValue) => { + if (newValue) { + this.rows = newValue.map(row => this.converter instanceof IgniteProperty ? this.converter.value(row) : this.converter(row)); + } else { + this.rows = []; + } + }, + onPop: () => this.rows.pop(), + onPush: (array, items) => this.rows.push(...items.map(row => this.converter instanceof IgniteProperty ? this.converter.value(row) : this.converter(row))), + onShift: () => this.rows.shift(), + onSplice: (array, start, deleteCount, items) => this.rows.splice(start, deleteCount, ...items.map(row => this.converter instanceof IgniteProperty ? this.converter.value(row) : this.converter(row))), + onUnshift: (array, items) => this.rows.unshift(...items.map(row => this.converter instanceof IgniteProperty ? this.converter.value(row) : this.converter(row))) + }), + rows: new IgniteProperty([], { + onChange: (oldValue, newValue) => this.filter(), + onPop: () => this.filter(), + onPush: () => this.filter(), + onShift: () => this.filter(), + onSplice: () => this.filter(), + onUnshift: () => this.filter() + }), + results: new IgniteProperty([], { + onChange: (oldValue, newValue) => this.pageCount = newValue && this.pageSize > 0 ? Math.ceil(this.results.length / this.pageSize) : 0, + onPop: () => this.pageCount = this.results && this.pageSize > 0 ? Math.ceil(this.results.length / this.pageSize) : 0, + onPush: () => this.pageCount = this.results && this.pageSize > 0 ? Math.ceil(this.results.length / this.pageSize) : 0, + onShift: () => this.pageCount = this.results && this.pageSize > 0 ? Math.ceil(this.results.length / this.pageSize) : 0, + onSplice: () => this.pageCount = this.results && this.pageSize > 0 ? Math.ceil(this.results.length / this.pageSize) : 0, + onUnshift: () => this.pageCount = this.results && this.pageSize > 0 ? Math.ceil(this.results.length / this.pageSize) : 0 + }), + filterSearch: null, + converter: null, + pageSizeOptions: [15, 25, 50, 100, 500], + pageSize: new IgniteProperty(15, { onChange: (oldValue, newValue) => { + //Calculate the new page count based on the page size and items. + if (!this.items || newValue < 1) { + this.pageCount = 0; + } else { + this.pageCount = Math.ceil(this.items.length / newValue); + } + }}), + pageCount: new IgniteProperty(0, { onChange: (oldValue, newValue) => { + //If the currentPage is greater than the new page count, set it to the last page. + if (this.currentPage > newValue - 1) { + this.currentPage = newValue - 1; + } + }}), + currentPage: 0, + searching: false, + pendingSearch: null, + showPageSize: true, + showSearchBox: true, + showRowCount: true, + showPageJumpTo: true + } + } + + render() { + return this.template.child( + new div().show([this.showPageSize, this.showSearchBox, this.showRowCount], (pageSize, searchBox, rowCount) => pageSize || searchBox || rowCount).class("row mb-3").child( + new div().class("col-12 d-flex flex-row justify-content-between gap-3").child( + new div().class("d-flex flex-row gap-3").child( + //Page size selector + new div().show(this.showPageSize).class("input-group flex-nowrap w-auto").child( + new span().class("input-group-text border-0 bg-white").child(``), + + new select().class("form-select border-0 w-auto").child( + new list(this.pageSizeOptions, size => new option().child(size)) + ).value(this.pageSize, true) + ), + + //Search box + new div().show(this.showSearchBox).class("input-group flex-nowrap").child( + new span().class("input-group-text border-0 bg-white").child(``), + + new input().class("form-control") + .value(this.filterSearch, true) + .onEnter(() => this.filter()) + .onChange(() => this.filter()) + .on("keydown", (event) => { + if (event.key != 'Shift' && event.key != 'Capslock' && event.key != 'Space' && event.key != 'Control') { + this.search(); + } + }) + ) + ), + + //Rows count + new div().show(this.showRowCount).class("bg-white p-2 rounded-2 d-flex align-items-center justify-content-center").innerHTML(this.results, results => results.length == 0 ? "No rows" : `${results.length} ${(results.length < 2 ? "row" : "rows")}`) + ) + ), + + new div().class("table-responsive overflow-hidden rounded-2 mb-3").child( + new table().class("table table-striped align-middle mb-0").child( + //Table columns + new thead().class("table-dark").child( + new tr().child( + new list(this.columns, column => { + if (column instanceof DataColumn) { + return new th().attribute("scope", "col").child(column.name); + } else { + return new th().attribute("scope", "col").child(column); + } + }) + ) + ), + + //Table rows + new tbody().child( + new pagination(this.results, this.pageSize, this.currentPage, row => new tr().child(new list(row, column => new td().child(column)))) + ) + ) + ), + + new div().class("row").child( + new div().class("col-12 d-flex flex-row gap-3 flex-wrap").child( + //Pages + new div().class("btn-group").child( + //Previous page button + new button() + .class("btn btn-secondary") + .child(``) + .onClick(() => { + if (this.currentPage > 0) { + this.currentPage = this.currentPage - 1; + } + }), + + new pager( + this.results, + this.pageSize, + this.currentPage, + 2, + (index, current, filler) => { + if (filler) { + return null; + } + return new button() + .class("btn text-center") + .style("width", "3em") + .class(current, current => current ? "btn-primary" : "btn-secondary") + .innerText(filler ? "..." : index + 1) + .onClick(() => this.currentPage = index) + }), + + //Next page button + new button() + .class("btn btn-secondary") + .child(``) + .onClick(() => { + if (this.currentPage < this.pageCount) { + this.currentPage = this.currentPage + 1; + } + }), + ), + + //Page jump to + new div().show(this.showPageJumpTo).class("input-group flex-nowrap w-auto").child( + new select().class("form-select border-0 w-auto").child( + new population( + this.pageCount, + index => new option().attribute("selected", this.currentPage, currentPage => currentPage == index ? "selected" : null).value(index).child(index + 1) + ) + ).value(this.currentPage, true) + ) + ) + ) + ); + } + + search() { + this.searching = true; + + if (!this.pendingSearch) { + this.pendingSearch = setTimeout(async () => { + this.pendingSearch = null; + + this.filter(); + + this.searching = false; + }, 300); + } + } + + filter() { + var results = this.rows; + + //First filter the results by the search query if we have one. + if (this.filterSearch && this.filterSearch != "" && this.filterSearch != " ") { + var regex = new RegExp(this.filterSearch, 'i'); + + results = results.filter(result => { + var found = false; + + for (var i = 0; i < this.columns.length; i++) { + if (this.columns[i] instanceof DataColumn && this.columns[i].search && result[i].match(regex)) { + found = true; + break; + } + } + + return found; + }); + } + + //Set the results + this.results = results; + } +} + +class Template extends IgniteTemplate { + /** + * + * @param {string|Array} columns The columns of this table + * @param {Array} items The items of this table + * @param {Function|IgniteProperty} converter A converter that converts a row into a series of columns. + */ + constructor(columns, items, converter) { + super("bt-data-table", null); + + this.property("columns", columns); + this.property("items", items); + this.property("converter", converter); + } +} + +IgniteHtml.register("bt-data-table", DataTable); + +export { + Template as DataTable, + DataColumn +} \ No newline at end of file diff --git a/ignite-bootstrap.js b/ignite-bootstrap.js deleted file mode 100644 index 783d436..0000000 --- a/ignite-bootstrap.js +++ /dev/null @@ -1,9 +0,0 @@ -import { DropdownMenu } from "./dropdown-menu.js"; -import { DropdownButton } from "./dropdown-button.js"; -import { Modal } from "./modal.js"; - -export { - DropdownMenu, - DropdownButton, - Modal -} \ No newline at end of file