protvista-datatable
Version:
[](https://www.npmjs.com/package/protvista-datatable)
350 lines (347 loc) • 13.7 kB
JavaScript
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