UNPKG

protvista-datatable

Version:

[![Published on NPM](https://img.shields.io/npm/v/protvista-datatable.svg)](https://www.npmjs.com/package/protvista-datatable)

350 lines (347 loc) 13.7 kB
import { LitElement, html, css, } from "lit-element"; import { ScrollFilter } from "protvista-utils"; import { isOutside, isWithinRange, parseColumnFilters } from "./utils"; import lightDOMstyles, { ACTIVE, EXPANDED, HIDDEN, OVERLAPPED, TRANSPARENT, } from "./styles"; class ProtvistaDatatable extends LitElement { constructor() { super(); this.height = "25rem"; this.visibleChildren = []; this.noScrollToRow = false; this.noDeselect = false; this.expandTable = false; this.scrollFilter = new ScrollFilter(this); this.wheelListener = (event) => this.scrollFilter.wheel(event); this.eventHandler = this.eventHandler.bind(this); } static get is() { return "protvista-datatable"; } connectedCallback() { super.connectedCallback(); const tbody = this.querySelector("table tbody"); // Return early if no structure if (!tbody) { return; } // The content of the table is dynamically set by the consumer // so we need to lookout for changes this.mutationObserver = new MutationObserver(() => { this.init(); }); // Observe the table body for any changes (e.g. dynamic data) this.mutationObserver.observe(tbody, { characterData: true, childList: true, subtree: true, }); // Add style to light DOM to style slot content const styleTag = document.createElement("style"); styleTag.innerHTML = lightDOMstyles.toString(); document.querySelector("head").appendChild(styleTag); if (this.closest("protvista-manager")) { this.manager = this.closest("protvista-manager"); this.manager.register(this); } if (!this.noDeselect) { document.addEventListener("click", this.eventHandler); } // this makes sure the protvista-zoomable event listener doesn't reset this.classList.add("feature"); if (this.hasAttribute("filter-scroll")) { document.addEventListener("wheel", this.wheelListener); } this.init(); } disconnectedCallback() { super.disconnectedCallback(); if (this.manager) { this.manager.unregister(this); } document.removeEventListener("click", this.eventHandler); document.removeEventListener("wheel", this.wheelListener); this.mutationObserver.disconnect(); } init() { this.columns = this.querySelectorAll("table thead th"); // Add blank column to header for (+/-) if not there alread // Check if added already, otherwise, ∞ loop!! if (!this.querySelector(".pd-group-column-header")) { // Can't use insertCell with "th" const additionalTH = document.createElement("th"); additionalTH.classList.add("pd-group-column-header"); const headerTR = this.querySelector("table thead tr"); headerTR.insertBefore(additionalTH, headerTR.firstChild); } this.rows = this.querySelectorAll("table tbody tr"); this.rows.forEach((row) => { // Add extra (+/-) cell - only if it hasn't got it already!!! if (!row.dataset.groupFor && !row.querySelector(".pd-group-trigger")) { const plusMinusCell = row.insertCell(0); plusMinusCell.classList.add("pd-group-trigger"); if (this.querySelector("[data-group-for]")) { const plusMinusButton = document.createElement("button"); plusMinusButton.dataset.triggerId = row.dataset.id; plusMinusCell.appendChild(plusMinusButton); // Add row click handler plusMinusButton.addEventListener("click", (e) => this.handleGroupToggle(e)); } } // Add row click handler row.addEventListener("click", (e) => this.handleClick(e, row)); }); this.updateRowStyling(); this.selectedFilters = new Map(); this.filterMap = this.parseDataForFilters(); this.addFilterOptions(); } parseDataForFilters() { // Initialise map by looking at Column headers const filterMap = parseColumnFilters(this.columns); // Populate map with values this.rows.forEach((row) => { const tableCells = row.childNodes; tableCells.forEach((cell) => { var _a; if ((_a = cell.dataset) === null || _a === void 0 ? void 0 : _a.filter) { const filterSet = filterMap.get(cell.dataset.filter); filterSet.add(cell.dataset.filterValue); } }); }); return filterMap; } addFilterOptions() { this.columns.forEach((column) => { if (column.dataset.filter) { let select; let wrapper; // Has this column already been modified? if (column.querySelector(".filter-wrap")) { select = column.querySelector("select"); wrapper = column.querySelector(".filter-wrap"); } else { wrapper = document.createElement("span"); wrapper.className = "filter-wrap"; wrapper.innerHTML = column.innerHTML; select = document.createElement("select"); select.dataset.testid = "select"; select.onchange = (e) => this.handleFilterChange(e, column.dataset.filter); } select.innerHTML = "<option selected value>-- Select --</option>"; this.filterMap.get(column.dataset.filter).forEach((optionValue) => { const option = document.createElement("option"); option.value = optionValue; option.label = optionValue; option.dataset.testid = "select-option"; select.appendChild(option); }); // eslint-disable-next-line no-param-reassign column.innerHTML = ""; wrapper.appendChild(select); column.appendChild(wrapper); } }); } eventHandler(e) { const target = e.target; if (!target.closest("protvista-datatable") && !target.closest(".feature")) { this.selectedid = null; this.highlight = null; } } static get properties() { return { highlight: { converter: (value) => { if (value && value !== "null") { try { const splitArray = value.split(":").map((d) => Number(d)); if (splitArray.length !== 2) { throw new Error("Highlight should be only 2 values separated by ':'."); } return [splitArray[0], splitArray[1]]; } catch (e) { console.error("Invalid highlight coordinates:", e); } } return null; }, }, height: { type: String }, displayStart: { type: Number }, displayEnd: { type: Number }, visibleChildren: { type: Array }, selectedid: { type: String }, noScrollToRow: { type: Boolean }, noDeselect: { type: Boolean }, expandTable: { type: Boolean }, }; } static get styles() { return css ` :host { display: block; } .protvista-datatable-container { overflow-y: auto; // Note: overflow-x was set to 'hidden' but changing // to 'auto' doesn't seem to be an issue. overflow-x: auto; scrollbar-gutter: stable; } :host([scrollable="true"]) .protvista-datatable-container { overflow-y: auto; } :host([scrollable="false"]) .protvista-datatable-container { overflow-y: hidden; } `; } handleGroupToggle(e) { const { triggerId } = e.target.dataset; if (this.visibleChildren.includes(triggerId)) { this.visibleChildren = this.visibleChildren.filter((childId) => childId !== triggerId); e.target.classList.remove(EXPANDED.cssText); } else { this.visibleChildren = [...this.visibleChildren, triggerId]; e.target.classList.add(EXPANDED.cssText); } } handleClick(e, row) { // Don't select transparent row if (row.classList.contains("transparent")) { return; } const { id, start, end } = row.dataset; this.selectedid = id; const detail = {}; if (start && end) detail.highlight = `${start}:${end}`; if (this.selectedid) detail.selectedid = this.selectedid; this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true, cancelable: true, })); } handleFilterChange(e, filterName) { const { selectedOptions } = e.target; // Only 1 can be selected const { value } = selectedOptions.item(0); if (value) { this.selectedFilters.set(filterName, value); } else { this.selectedFilters.delete(filterName); } this.updateRowStyling(); } isRowVisible(row) { // Handle show/hide groups const isExpandedGroup = !row.dataset.groupFor || (row.dataset.groupFor && this.visibleChildren.includes(row.dataset.groupFor)); // Handle filters // If no filters are selected, consider it a match if (!this.selectedFilters || this.selectedFilters.size === 0) { return isExpandedGroup; } for (const [filterName, value] of this.selectedFilters) { let column; if (row.dataset.groupFor) { // If group, get group row const groupRow = this.querySelector(`[data-id="${row.dataset.groupFor}"]`); column = groupRow.querySelector(`[data-filter="${filterName}"]`); } else { column = row.querySelector(`[data-filter="${filterName}"]`); } if (column && column.dataset.filterValue !== value) { return false; } } return isExpandedGroup; } updateRowStyling() { var _a; let oddOrEvenCount = 0; (_a = this.rows) === null || _a === void 0 ? void 0 : _a.forEach((row) => { // Filter visibility const isRowVisible = this.isRowVisible(row); if (isRowVisible) { row.classList.remove(HIDDEN.cssText); } else { row.classList.add(HIDDEN.cssText); } // Only increment if non grouped row if (!row.dataset.groupFor) { oddOrEvenCount++; } const { start, end } = row.dataset; row.classList.add(oddOrEvenCount % 2 === 0 ? "even" : "odd"); // Is the row selected? if (this.selectedid && (this.selectedid === row.dataset.id || row.dataset.groupFor === this.selectedid)) { row.classList.add(ACTIVE.cssText); } else { // Note: if too expensive, check before row.classList.remove(ACTIVE.cssText); } // Is the row not within ProtVista track range? if (isOutside(this.displayStart, this.displayEnd, Number(start), Number(end))) { row.classList.add(TRANSPARENT.cssText); } else { // Note: if too expensive, check before row.classList.remove(TRANSPARENT.cssText); } // Is the row part of the selected range? if (this.highlight && isWithinRange(this.highlight[0], this.highlight[1], Number(start), Number(end))) { row.classList.add(OVERLAPPED.cssText); } else { row.classList.remove(OVERLAPPED.cssText); } if (row.dataset.groupFor) { const collSpan = this.columns.length + 1; // Add 1 for the +/- button // eslint-disable-next-line no-param-reassign row.cells[0].colSpan = collSpan - row.cells.length + 1; // Add 1 for column } }); } scrollIntoView() { if (!this.selectedid) { return; } const element = this.querySelector(`[data-id="${this.selectedid}"]`); element === null || element === void 0 ? void 0 : element.scrollIntoView({ behavior: "smooth", block: "center" }); } render() { const style = this.expandTable || this.hasAttribute("expand-table") ? "height: auto" : `max-height:${this.height}`; return html ` <div class="protvista-datatable-container" style=${style}> <slot></slot> </div> `; } updated() { this.updateRowStyling(); if (!this.noScrollToRow) { this.scrollIntoView(); } } } export default ProtvistaDatatable; //# sourceMappingURL=protvista-datatable.js.map