UNPKG

globular-mvc

Version:

Generic template to create web-application that made use of globular as backend and materialize as css (wrap in web-component's)

818 lines (657 loc) 24.2 kB
// Polymer dependencies import { PolymerElement, html } from "@polymer/polymer/polymer-element.js"; import { createElement } from "../element.js"; import { fireResize, isString, exportToCsv, getCoords } from "../utility.js"; import './header.js'; // The maximum allowed number of row for a grid. import { DropdownMenu, DropdownMenuItem} from "../dropdownMenu.js"; var maxRowNumber = 1000; var lastWidth = 0; export class TableElement extends PolymerElement { constructor() { super(); this.rowheight = -1; // in pixels this.index = -1; this.scrollDiv = null; // Some browsers limit the number of rows in a grid, therefore, multiple grids (named tiles) are used to display large tables this.tiles = []; this.sorters = []; this.filters = []; this.sorted = []; this.filtered = {}; this.header = null; this.menu = null; this.hidefilter = true; // Cells are created once then recycled for optimization purposes this.cells = []; // Options for the observer (which mutations to observe) var config = { attributes: true, subtree: true }; // Create an observer instance linked to a resize callback var observer = new MutationObserver((mutation) => { if (mutation[0].target.offsetWidth != lastWidth) { lastWidth = mutation[0].target.offsetWidth; fireResize(); } }); // Start observing the target node for configured mutations observer.observe(this, config); } /** * Internal component properties. */ static get properties() { return { // array of data to display. data: Array, rowheight: Number, order: String, refresh: Function, onexport: Function, ondeleteall: Function, ondeletefiltered: Function, hidemenu: Boolean, width: String }; } static get template() { let template = document.createElement("template") template.innerHTML = ` <style> ::slotted(table-header-element) { } </style> <slot></slot> `; return template } /** * Creates all the grids (tiles) necessary to display the table */ createTiles() { if (this.scrollDiv == null) { return null } this.scrollDiv.element.style.display = ""; var size = 1; // Get the number of tiles necessary to populate the table if (this.size() > maxRowNumber) { size = Math.ceil(this.size() / maxRowNumber); } this.tiles = []; // Create the tiles as divs with a grid display style for (var i = 0; i < size; i++) { this.tiles[i] = this.scrollDiv.appendElement({ "tag": "div", "class": "table-tile", "style": "grid-gap: 0px; display: grid;" }).down(); // Set the number of rows equal to the number of rows in the header. var gridTemplateColumns = ""; for (var j = 0; j < this.header.getSize(); j++) { var headerCell = this.header.getHeaderCell(j) gridTemplateColumns += headerCell.offsetWidth + "px" if (j < this.header.getSize() - 1) { gridTemplateColumns += " "; } } this.tiles[i].element.style.gridTemplateColumns = gridTemplateColumns; // CSS for the table's height if (i < size - 1 || this.size() % maxRowNumber == 0) { this.tiles[i].element.style.gridTemplateRows = "repeat( " + maxRowNumber + ", " + this.rowheight + "px)"; } else { this.tiles[i].element.style.gridTemplateRows = "repeat( " + this.size() % maxRowNumber + ", " + this.rowheight + "px)"; } } var resizeListener = function (tiles, header, table) { return function (entry) { var value = ""; // Set the final header tile's margin to be slightly greater than the other margins so that there is space for the scrollbar var scrollBarWidth = table.scrollDiv.element.offsetWidth - table.scrollDiv.element.clientWidth; if (header.lastChild.style != null) { if (scrollBarWidth > 0) { if (header.children[header.children.length - 2].offsetWidth == header.lastChild.offsetWidth) { header.lastChild.style.marginRight = scrollBarWidth + "px"; } } else { header.lastChild.style.marginRight = ""; } } // Calculate the table's total width using the header as a baseline var totalWidth = 0; for (var i = 0; i < header.children.length; i++) { value += header.children[i].getBoundingClientRect().width + "px"; totalWidth += header.children[i].getBoundingClientRect().width; if (i < header.children.length - 1) { value += " "; } } if (totalWidth == 0) { return; } // Set the table's overall width if (table.width == undefined) { table.style.width = totalWidth + "px"; table.width = totalWidth; } // Set each tiles' width for (var i = 0; i < tiles.length; i++) { tiles[i].element.style.gridTemplateColumns = value; } if (table.menu != undefined) { table.style.marginLeft = table.menu.offsetWidth + 4 + "px"; table.menu.style.left = -1 * (table.menu.offsetWidth + 2) + "px"; } }; }(this.tiles, this.children[0], this); window.addEventListener("resize", resizeListener, true); } /** * Instantiates the cells variable following the Singleton principle. * * Cells are the items within each tile. A tile is a container whereas the cell is the content. */ createCells() { if (this.data.length == 0) { return; } this.cells = [] var max = Math.ceil(this.clientHeight / this.rowheight); if (max == 0 && this.style.maxHeight != undefined) { max = Math.ceil(parseInt(this.style.maxHeight.replace("px", "")) / this.rowheight); } var rowsLength = this.data[0].length; for (var i = 0; i < max; i++) { for (var j = 0; j < rowsLength; j++) { var cell = document.createElement("div"); cell.className = "table-item"; var cellContent = document.createElement("div"); cellContent.className = "table-item-value"; cell.appendChild(cellContent); // keep the cell as element in the buffer. this.cells.push(createElement(cell)); } } } /** * Gets the width of the screen excluding the scrollbar */ getScrollWidth() { var scrollBarWidth = this.scrollDiv.element.offsetWidth - this.scrollDiv.element.clientWidth; return scrollBarWidth } /** * Returns the current row index. */ getIndex() { var index = 0; if (this.scrollDiv != null) { if (this.scrollDiv.element.scrollTop != undefined) { index = parseInt(this.scrollDiv.element.scrollTop / this.rowheight); } } return index; } hasFilter() { return this.getFilters().length > 0; } /** * Render the table */ render() { var index = this.getIndex(); var values = this.getFilteredData(); if (this.index != index) { this.index = index; // Remove the current content within a tile to make sure we have a blank slate. for (var i = 0; i < this.tiles.length; i++) { this.tiles[i].removeAllChilds(); } // Represent the number of visible items to display, I round it to display entire row. var max = Math.ceil(this.clientHeight / this.rowheight); if (max == 0 && this.style.maxHeight != undefined) { max = Math.ceil(parseInt(this.style.maxHeight.replace("px", "")) / this.rowheight); } // create cells once. if (this.cells.length == 0) { this.createCells(); } if (values.length > 0 && this.scrollDiv != undefined) { var scrollBarWidth = this.scrollDiv.element.offsetWidth - this.scrollDiv.element.clientWidth; for (var i = 0; i + this.index < values.length && i < max; i++) { // Get the visible tile. var tileIndex = parseInt((this.index + i) / maxRowNumber); var tile = this.tiles[tileIndex]; // now I will calculate the row index var rowIndex = this.index - maxRowNumber * tileIndex + i; var size = values[i].length; for (var j = 0; j < size; j++) { var renderFct = this.header.getHeaderCell(j).onrender; var cell = this.cells[i * this.getRowData(i).length + j]; cell.element.style.gridRow = rowIndex + 1 + " / span 1"; tile.element.appendChild(cell.element); var div = cell.element.children[0]; var value = values[i + this.index][j]; div.style = ""; div.innerHTML = ""; // Check if there is a valid render function if (renderFct == null) { if (value != undefined) { div.innerHTML = value.toString(); } } else { var r = i + this.index; if (isString(renderFct)) { eval(renderFct + "(div , value, r, j)"); } else { var r = i + this.index; if (isString(renderFct)) { eval(renderFct + "(div , value, r, j)"); } else { renderFct(div, value, r, j); // row, col. } } if (j == size - 1) { if (scrollBarWidth > 0) { cell.element.style.paddingRight = scrollBarWidth + "px"; } else { cell.element.style.paddingRight = ""; } } } } } } else if (this.scrollDiv != undefined) { // hide the scroll div. this.scrollDiv.element.style.display = "none"; } } } /** * Creates the table. * * Called when the table is ready to be displayed. */ ready() { super.ready(); this.style.position = "relative"; // Index the rows. for (var i = 0; i < this.data.length; i++) { // Keep the index in the row itself. this.data[i].index = i; } this.header = this.children[0]; for (var i = 0; i < this.header.children.length; i++) { for (var j = 0; j < this.header.children[i].children.length; j++) { if (this.header.children[i].children[j].tagName == "TABLE-SORTER-ELEMENT") { var sorter = this.header.children[i].children[j]; sorter.childSorter = null; if (sorter.state != undefined) { if (sorter.state != 0) { this.sorters[sorter.order - 1] = sorter; } } sorter.index = this.sorters.length; this.sorters.push(sorter); } else if (this.header.children[i].children[j].tagName == "TABLE-FILTER-ELEMENT") { var filter = this.header.children[i].children[j]; filter.index = this.filters.length; this.filters.push(filter); } } // Set the data type. if (this.header.children[i].typename == undefined) { for (var j = 0; j < this.data.length; j++) { if (this.data[j][i] != null) { this.header.children[i].typename = typeof this.data[j][i]; break; } } } } // Create the table dropdown menu. if (!this.hidemenu) { this.menu = new DropdownMenu("menu") this.menu.innerHTML = ` <globular-dropdown-menu-item id="unorder-menu-item" icon="sort" text="remove all sorter"></globular-dropdown-menu-item> <globular-dropdown-menu-item id="filter-menu-item" icon="filter-list" text="filtering" action=""> <globular-dropdown-menu-item id="unfilter-menu-item" text="remove all filter"></globular-dropdown-menu-item> <globular-dropdown-menu-item id="filter-menu-items" separator="true" text=""; style="display: none"> </globular-dropdown-menu-item> </globular-dropdown-menu-item> <globular-dropdown-menu-item separator="true" id="delete-filtered-menu-item" icon="delete" text="delete filtered"></globular-dropdown-menu-item> <globular-dropdown-menu-item id="delete-all-data-menu-item" icon="delete" text="delete all"></globular-dropdown-menu-item> <globular-dropdown-menu-item id="export-menu-item" icon="file-download" text="export"></globular-dropdown-menu-item> ` // Append the element in the body so it will alway be visible. this.appendChild(this.menu); this.menu.style.position = "absolute"; this.style.marginLeft = this.menu.offsetWidth + 4 + "px"; this.menu.style.left = -1 * (this.menu.offsetWidth + 2) + "px"; // Make the table resize on display. var intersectionObserver = new IntersectionObserver(function (entries) { if (entries[0].isIntersecting) { //entries[0].target.refresh() fireResize(); } }); // Set the observer on the menu itself. intersectionObserver.observe(this); // append in the body. if (this.onexport == undefined) { // export csv file by default. this.menu.querySelector("#export-menu-item").action = () => { // Here I will get the filtered data. if (Object.keys(this.filtered).length > 0) { exportToCsv("data.csv", Object.values(this.filtered)); } else { exportToCsv("data.csv", this.data); } }; } // Remove the ordering this.menu.querySelector("#unorder-menu-item").action = () => { for (var i = 0; i < this.sorters.length; i++) { var sorter = this.sorters[i]; sorter.childSorter = null; sorter.state = undefined; sorter.unset(); } // sort the table. this.sort(); this.refresh(); // refresh the result. } this.menu.querySelector("#unfilter-menu-item").action = () => { // Now I will remove all filters... for (var i = 0; i < this.filters.length; i++) { if (this.filters[i].filter != null) { if (this.filters[i].filter.expressions.length > 0 || this.filters[i].filter.filters.length > 0) { this.filters[i].filter.clear(); } } } this.filter(); this.refresh(); // refresh the result. }; } // Fix the style of the element. this.menu.querySelector("#delete-filtered-menu-item").style.display = "none" this.menu.querySelector("#delete-all-data-menu-item").style.display = "none" this.style.display = "flex"; this.style.flexDirection = "column"; // Create the body of table after the header... this.scrollDiv = createElement(this.insertBefore(document.createElement("div"), this.children[1])); this.scrollDiv.element.style.overflowY = "auto"; // Display scroll as needed. this.scrollDiv.element.style.overflowX = "hidden"; // Display scroll as needed. this.scrollDiv.element.scrollTop = 0; // if now row height are given i will take the header height as default. if (this.rowheight == -1) { this.rowheight = this.children[0].offsetHeight; } // If the header is fixed I will translate it to keep it // at the required position. this.scrollDiv.element.addEventListener("scroll", function (table) { return function (e) { var header = table.children[0]; // If the header is fixe I will if (header.fixed) { if (this.scrollTop != 0) { header.style.boxShadow = "var(--dark-mode-shadow)"; } else { header.style.boxShadow = ""; } } // render the table. table.render(); }; }(this)); this.refresh(); } /** * Redraw all tile and values. */ refresh() { // reset the index. this.index = -1; // remove acutal rows. if (this.scrollDiv != null) { this.scrollDiv.removeAllChilds(); // Recreate tiles this.createTiles(); // Redisplay values. } this.render(); } clear() { this.data = [] this.sorted = []; this.filtered = {}; this.refresh(); } ////////////////////////////////////////////////////////////////////////////////////// // Data access function. ////////////////////////////////////////////////////////////////////////////////////// /** * Return the table data. */ getData() { if (this.data == undefined) { return []; } return this.data; } /** * Return only the filtered data. */ getFilteredData() { if (Object.keys(this.filtered).length == 0) { if (this.getFilters().length > 0) { return []; } return this.data; } // if there are sorters, use this.sorted to store the values. if (this.getSorters().length > 0) { if (this.sorted == 0) { for (var i = 0; i < this.data.length; i++) { if (this.filtered[this.data[i].index] != undefined) { this.sorted.push(this.data[i]); } } } // return the list of sorted and filtered values. return this.sorted; } // Return the list of all filtered values. return Object.values(this.filtered); } /** * Return the data at the given position. * @param {*} row The table row * @param {*} column The table column. */ getDataAt(row, column) { if (this.data == undefined) { return []; } return this.data[row][column]; } /** * Return the data for a given index. * @param {} index */ getRowData(index) { if (this.getData() == undefined) { return []; } return this.data[index]; } /** * Return all data in a given column * @param {*} index The column index. */ getColumnData(index) { var data = []; if (this.getData() != undefined) { for (var i = 0; i < this.getData().length; i++) { data.push({ "value": this.getData()[i][index], "index": this.getData()[i].index }); } } return data; } getFilteredColumnData(index) { let filtered = this.getFilteredData() if (Object.keys(filtered).length == 0) { if (this.getFilters().length > 0) { return []; // all data are filtered. } return this.data; } var data = []; if (this.getData() != undefined) { for (var i in filtered) { data.push({ // push the filtered values. "value": filtered[i][index], "index": filtered[i].index }); } } return data; } /** * Return the visible data size. */ size() { // if filters are applied. let filtered = this.getFilteredData() if (Object.keys(filtered).length > 0) { return Object.keys(filtered).length; } else if (this.hasFilter()) { return 0; // all filtered. } return this.getData().length; } /** * Return the list of sorter. */ getSorters() { var sorters = new Array(); // reset data order. for (var i = 0; i < this.sorters.length; i++) { var sorter = this.sorters[i]; sorter.childSorter = null; if (sorter.state != undefined) { if (sorter.state != 0) { sorters[sorter.order - 1] = sorter; } } } return sorters; } deleteRow(index) { this.data.splice(index, 1) let i = 0 this.data.forEach(d => { d.index = i; i++ }) this.sort() this.filter() this.refresh() } /** * Order a table. * @param {*} side can be asc, desc or nothing. */ sort() { this.data.sort(function (a, b) { var indexA = parseInt(a.index); var indexB = parseInt(b.index); return indexA - indexB; }); // empty the sorted list. this.sorted = []; var sorters = this.getSorters(); // I will copy values of rows to keep the original order... // reset to default... if (sorters.length > 0) { // Link the sorter with each other... for (var i = 0; i < sorters.length - 1; i++) { sorters[i].childSorter = sorters[i + 1]; } // Now I will call sort on the first sorter... if (sorters[0].state != 0) { sorters[0].sortValues(this.data); } } } /** * Return the list of active filters. */ getFilters() { var filters = []; // put all filter in the save array. for (var i = 0; i < this.filters.length; i++) { if (this.filters[i].filter != null) { if (this.filters[i].filter.expressions.length > 0 || this.filters[i].filter.filters.length > 0) { filters.push(this.filters[i].filter); } } } return filters; } /** * Filter table values. */ filter() { // so here I will empty the filtered map. this.filtered = {}; // Get the filters var filters = this.getFilters(); function getData(table, indexs) { var filtered = {}; for (var i = 0; i < indexs.length; i++) { filtered[indexs[i]] = table.getRowData(indexs[i]); } return filtered; } // filters are cumulative from column to columns. if (filters.length > 0) { this.filtered = getData(this, filters[0].evaluate()); if (filters.length > 1) { for (var i = 1; i < filters.length; i++) { for (var i = 1; i < filters.length; i++) { var filtered = getData(this, filters[i].evaluate()); var filtered_ = {}; for (var id in filtered) { if (this.filtered[id] != undefined) { filtered_[id] = this.filtered[id]; } } this.filtered = filtered_; } } } else { this.filtered = getData(this, filters[0].evaluate()); } } // Now I will set the filter in the menu. let filterMenuItems = this.menu.querySelector("#filter-menu-items"); if (filters.length > 0) { filterMenuItems.style.display = "block" filterMenuItems.innerHTML = ""; for (var i = 0; i < filters.length; i++) { // So here I will create the menu item asscoiated with each filter and given. let filter = filters[i]; if (filter.expressions.length > 0 || filter.filters.length > 0) { let text = filter.parent.headerCell.innerText let menuItem = new DropdownMenuItem("icons:close", text) menuItem.slot = "subitems" menuItem.action = () => { menuItem.parentNode.removeChild(menuItem); filter.clearFileterBtn.element.click(); this.filter(); this.refresh(); } filterMenuItems.appendChild(menuItem) } } } else{ filterMenuItems.style.display = "none" }// In that case I will call the onfilter event. for (var i = 0; i < this.filters.length; i++) { if (this.filters[i].filter != null) { if (this.filters[i].onfilter != undefined) { // Call the render function with div and value as parameter. var values = this.filters[i].filter.getFilterdValues(); if (isString(this.filters[i].onfilter)) { eval(this.filters[i].onfilter + "(values)"); } else { this.filters[i].onfilter(values); } } } } } } customElements.define('table-element', TableElement);