@telekom/scale-components
Version:
Scale is the digital design system for Telekom products and experiences.
1,281 lines (1,280 loc) • 45.7 kB
JavaScript
/**
* @license
* Scale https://github.com/telekom/scale
*
* Copyright (c) 2021 Egor Kirpichev and contributors, Deutsche Telekom AG
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Component, Prop, h, Element, Host, Watch, State, Event, } from '@stencil/core';
import { CELL_TYPES, DEFAULT_CELL_TYPE, CELL_DEFAULTS, } from './data-grid-cells';
import classNames from 'classnames';
import { emitEvent } from '../../utils/utils';
// [ ] add options to show nested content without the html column
// [ ] add options to pre-expand all html content
// [ ] Uber cell type where all options are available for user
// [ ] Add custom elipsis for cropped content - with a tooltip that shows full content
// Const for testing auto-width. Will stop re-render.
// Need to manually set opacity and overflow to debug.
const TEST_AUTO_WIDTH_RENDER = false;
/* Reused Private Variables */
let resizeObserver;
const name = 'data-grid';
/* Component Declaration */
export class DataGrid {
/* 6. Lifecycle Events (call order) */
constructor() {
/* 2. State Variables (alphabetical) */
/** Used to force render after sorting/selection */
this.forceRender = 0;
/** Pagination starting index */
this.paginationStart = 0;
/** Table scroll value for frozen header */
this.scrollY = 0;
/** (optional) Freeze header row from scrolling */
this.freezeHeader = false;
/** (optional) Heading string */
this.heading = '';
/** (optional) Set to true to remove border */
this.hideBorder = false;
/** (optional) Set to true to hide header row */
this.hideHeader = false;
/** (optional) Set to true to remove info footer block including pagination and selection status */
this.hideInfo = false;
/** (optional) Set to true to hide settings menu */
this.hideMenu = false;
/** (optional) Set to true to add numbers column */
this.numbered = false;
/** (optional) Set number of rows to display per pagination page */
this.pageSize = Infinity;
/** (optional) Set to true to add selection column */
this.selectable = false;
/** Read-only selection array - populated with raw data from selected rows */
this.selection = [];
/** (optional) Shade every second row darker */
this.shadeAlternate = true;
/** (optional) Set to false to hide table, used for nested tables to re-render upon toggle */
this.visible = true;
/** Stored active sorting column index, for state removal */
this.activeSortingIndex = -1;
/** Track component width to constrict nested content, which is necessary with table layout */
this.contentWidth = 100;
/** Flag to know to check for data completeness */
this.dataNeedsCheck = true;
/** Flag to know if rendering can commence */
this.hasData = false;
/** Flag that is true when width below a certain limit */
this.isMobile = false;
/** Flag that enough data supplied to warrant pagination */
this.isPagination = false;
/** Flag that is true if any fields are sortable */
this.isSortable = false;
/** Track container width to avoid re-calculating column stretching */
this.lastContainerWidth = 100;
/** Index of field to use as mobile title, if any */
this.mobileTitleIndex = -1;
/** Determine if auto-width parsing needed */
this.needsAutoWidthParse = false;
/** Force column resize after render */
this.needsColumnResize = false;
/** Auto-calculated number column width */
this.numberColumnWidth = 0;
/** Selection column width */
this.selectionColumnWidth = 22;
this.handleMenuListClick = (event) => {
const menuItems = ['sortBy', 'toggleVisibility'];
const currentMenuItemsIndex = menuItems.indexOf(event.target.id);
if (currentMenuItemsIndex > -1) {
// check if there is already opened flyout menu list with different id, if opened, close it
const inactiveMenuItem = this.hostElement.shadowRoot.querySelector(`#${menuItems[1 - currentMenuItemsIndex]}List`);
if (inactiveMenuItem) {
inactiveMenuItem.setAttribute('opened', 'false');
}
}
};
// Bind certain callbacks to scope
this.onDividerMove = this.onDividerMove.bind(this);
this.onDividerUp = this.onDividerUp.bind(this);
this.applyResponsiveClasses = this.applyResponsiveClasses.bind(this);
this.updateColumnStretching = this.updateColumnStretching.bind(this);
}
componentWillLoad() {
this.fieldsHandler();
this.rowsHandler();
}
componentWillUpdate() { }
componentDidRender() {
if (this.needsAutoWidthParse) {
this.calculateAutoWidths();
}
// Wait a frame to avoid warning about possible infinite loop
setTimeout(() => {
if (this.needsColumnResize) {
this.updateColumnStretching();
}
});
}
componentDidLoad() {
this.addResizeObserver();
}
componentDidUpdate() { }
disconnectedCallback() {
this.removeResizeObserver();
}
/* 7. Listeners */
fieldsHandler() {
this.parseFields();
this.checkForMobileTitle();
this.checkForSortableFields();
this.dataNeedsCheck = true;
}
rowsHandler() {
// Reset pagination to the last page of the new records if new records are less than previous.
if (this.paginationStart > this.rows.length) {
this.paginationStart =
this.rows.length - (this.rows.length % this.pageSize);
}
this.parseRows();
this.setInitialRowProps();
this.resetSortingToggle();
this.dataNeedsCheck = true;
// Set flag to dirty to redo column width with new data
this.needsAutoWidthParse = true;
this.needsColumnResize = true;
if (
// when we run out of items on the current page
this.rows.length <= this.paginationStart &&
// and we are NOT on the first page
this.paginationStart - this.pageSize > -1) {
// step back one page
this.paginationStart = this.paginationStart - this.pageSize;
}
}
/* 8. Public Methods */
/* 9. Local Methods */
parseFields() {
if (this.fields && typeof this.fields === 'string') {
this.fields = JSON.parse(this.fields);
}
}
parseRows() {
if (this.rows && typeof this.rows === 'string') {
this.rows = JSON.parse(this.rows);
}
}
setInitialRowProps() {
if (!this.rows || !this.rows.length) {
return;
}
this.rows.forEach((row, i) => {
// Store indices of original order on rows for resetting sorting
row.initialIndex = i;
// Set initial selected flag
row.selected = false;
});
// Determine if pagination will be required
this.isPagination = this.pageSize <= this.rows.length - 1;
}
checkHasData() {
// Need both fields and data content in order to populate
if (!this.fields) {
return false;
}
for (let i = 0; i < this.fields.length; i++) {
// Use default type if none set
if (!this.fields[i].type) {
this.fields[i].type = DEFAULT_CELL_TYPE;
}
if (!CELL_TYPES[this.fields[i].type]) {
// tslint:disable-next-line: no-console
console.warn(`Unrecognised field type: "${this.fields[i].type}"`);
return false;
}
}
if (!this.rows || !this.rows.length) {
return false;
}
for (let i = 0; i < this.rows.length; i++) {
if (this.rows[i].length !== this.fields.length) {
// tslint:disable-next-line: no-console
console.warn(`Unable to render ${this.heading && `"${this.heading}" `}table: row data length not equal to supplied fields.`);
return false;
}
}
return true;
}
checkForMobileTitle() {
// Reset for new data
this.mobileTitleIndex = -1;
if (!this.fields) {
return;
}
this.fields.every(({ mobileTitle }, i) => {
if (mobileTitle) {
this.mobileTitleIndex = i;
return false;
}
return true;
});
}
checkForSortableFields() {
this.isSortable = false;
if (!this.fields) {
return;
}
this.fields.forEach(({ sortable }) => {
if (sortable) {
this.isSortable = true;
}
});
}
getCssClassMap() {
return classNames(name, !this.isMobile && `${name}--desktop`, this.isMobile && `${name}--mobile`, this.shadeAlternate && `${name}--shade-alternate`, this.freezeHeader && `${name}--freeze-header`, this.hideBorder && `${name}--hide-border`, this.hideMenu && `${name}--hide-menu`);
}
polyfillMousePosition(e) {
// For touch
if (e.changedTouches && e.changedTouches.length) {
e.x = e.changedTouches[0].pageX;
e.y = e.changedTouches[0].pageY;
}
// For cross browser support
if (e.x === undefined) {
e.x = e.clientX;
e.y = e.clientY;
}
}
getDefaultLongestContent({ rows, columnIndex }) {
let maxLength = 0;
let longestContent;
rows.forEach((row) => {
const length = row[columnIndex].toString().length;
if (length > maxLength) {
longestContent = row[columnIndex];
maxLength = length;
}
});
return longestContent;
}
// Selection handlers
toggleSelectAll() {
if (!this.elToggleSelectAll) {
return;
}
this.rows.forEach((row) => (row.selected = this.elToggleSelectAll.checked));
this.updateReadableSelection();
this.forceRender++;
}
toggleRowSelect({ target }, rowIndex) {
this.rows[rowIndex].selected = target.checked;
this.updateReadableSelection();
this.forceRender++;
}
updateReadableSelection() {
this.selection.length = 0;
this.rows.forEach((row) => row.selected && this.selection.push(row));
// Check header checkbox if any or none are selected
const selectAll = this.hostElement.shadowRoot.querySelector('.thead__cell--selection scale-checkbox');
selectAll.checked = !!this.selection.length;
// selectAll.indeterminate = !!this.selection.length;
}
// Sorting handlers
toggleTableSorting(sortDirection, columnIndex, type) {
// Remove sorting from previous column index
if (this.activeSortingIndex > -1 &&
this.activeSortingIndex !== columnIndex) {
this.fields[this.activeSortingIndex].sortDirection = 'none';
}
// Store new column index
this.activeSortingIndex = columnIndex;
const newSortDirection = sortDirection === 'none'
? 'ascending'
: sortDirection === 'ascending'
? 'descending'
: 'none';
this.fields[columnIndex].sortDirection = newSortDirection;
this.sortTable(newSortDirection, type, columnIndex);
}
sortTable(sortDirection, type, columnIndex) {
if (sortDirection === 'none') {
this.rows.sort((a, b) => {
return a.initialIndex - b.initialIndex;
});
}
else {
switch ((CELL_TYPES[type] &&
CELL_TYPES[type].defaults &&
CELL_TYPES[type].defaults.sortBy) ||
CELL_DEFAULTS.sortBy) {
case 'text':
if (sortDirection === 'ascending') {
this.rows.sort((a, b) => {
const textA = a[columnIndex].toLowerCase();
const textB = b[columnIndex].toLowerCase();
return textA < textB ? -1 : textA > textB ? 1 : 0;
});
}
else {
this.rows.sort((a, b) => {
const textA = a[columnIndex].toLowerCase();
const textB = b[columnIndex].toLowerCase();
return textA > textB ? -1 : textA < textB ? 1 : 0;
});
}
break;
case 'number':
if (sortDirection === 'ascending') {
this.rows.sort((a, b) => {
return Number(a[columnIndex]) - Number(b[columnIndex]);
});
}
else {
this.rows.sort((a, b) => {
return Number(b[columnIndex]) - Number(a[columnIndex]);
});
}
break;
}
}
this.forceRender++;
// Trigger event
this.triggerSortEvent(sortDirection, type, columnIndex);
}
resetSortingToggle() {
if (this.activeSortingIndex > -1) {
this.fields[this.activeSortingIndex].sortDirection = 'none';
}
this.activeSortingIndex = -1;
}
// Column resize handlers
onDividerDown(e) {
this.polyfillMousePosition(e);
// For touch - Prevent mousedown firing, and native scroll
e.preventDefault();
// Store divider elem for use in move and end events
this.activeDivider = e.target;
// Store initial value to calculate change
e.target.downX = e.x;
// Reset to avoid reapplying previous change
this.activeDivider.interactiveWidth = 0;
window.addEventListener('mousemove', this.onDividerMove);
window.addEventListener('touchmove', this.onDividerMove);
window.addEventListener('mouseup', this.onDividerUp);
window.addEventListener('touchend', this.onDividerUp);
}
onDividerMove(e) {
// TODO: calculate width stretchWidth to drop in correct location
this.polyfillMousePosition(e);
const { width, min, max } = this.activeDivider.dataset;
const diff = e.x - this.activeDivider.downX;
const newWidth = Math.min(Number(max), Math.max(Number(min), Number(width) + diff));
const adjustedDiff = newWidth - Number(width);
this.activeDivider.interactiveWidth = newWidth;
// Give immediate visual feedback
this.activeDivider.style.transform = `translateX(${adjustedDiff}px)`;
}
onDividerUp() {
const { index } = this.activeDivider.dataset;
// Store new width on the field data
if (this.activeDivider.interactiveWidth) {
this.fields[Number(index)].width = this.activeDivider.interactiveWidth;
}
// Reset visual feedback
this.activeDivider.style.transform = `translateX(0px)`;
window.removeEventListener('mousemove', this.onDividerMove);
window.removeEventListener('touchmove', this.onDividerMove);
window.removeEventListener('mouseup', this.onDividerUp);
window.removeEventListener('touchend', this.onDividerUp);
// Update column stretching before rendering
this.needsColumnResize = true;
this.updateColumnStretching();
// Render to apply change
this.forceRender++;
}
// Column visibility toggle handlers
toggleVisibilityMenu(e) {
e.preventDefault();
// TODO: replace this with contextual menu component, when available
const visibilityToggle = this.hostElement.shadowRoot.querySelector('.visibility-toggle');
const menu = visibilityToggle.children[1];
// By default
if (visibilityToggle.style.display === 'none') {
visibilityToggle.style.display = 'block';
menu.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`;
}
else {
visibilityToggle.style.display = 'none';
}
}
toggleColumnVisibility(value, columnIndex) {
this.fields[columnIndex].visible = value;
this.forceRender++;
// Update column stretching
this.needsColumnResize = true;
this.updateColumnStretching();
}
// Resize handlers
addResizeObserver() {
if (!resizeObserver) {
// @ts-ignore
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// Skip if table not visible/attached
if (entry.target.offsetParent === null) {
return;
}
entry.target.applyResponsiveClasses(entry);
entry.target.updateColumnStretching();
}
});
}
this.elMmainContainer = this.hostElement.shadowRoot.querySelector(`.${name}`);
// Add this instance's callbacks, as resizeObserver is reused
this.elMmainContainer.applyResponsiveClasses = this.applyResponsiveClasses;
this.elMmainContainer.updateColumnStretching = this.updateColumnStretching;
resizeObserver.observe(this.elMmainContainer);
}
removeResizeObserver() {
if (this.elMmainContainer) {
resizeObserver.unobserve(this.elMmainContainer);
}
}
applyResponsiveClasses() {
// Apply container-scoped media-query-style classes
const newIsMobile = this.elMmainContainer.offsetWidth <= 500;
if (this.isMobile !== newIsMobile) {
this.forceRender++;
}
this.isMobile = newIsMobile;
}
updateColumnStretching() {
// NOTE: any styling padding/margin width changes need to be adjusted here as well
// Ignore auto-width-check content renders
if (this.needsAutoWidthParse) {
return;
}
const container = this.elMmainContainer;
// Minus 2 for border
const containerWidth = container.offsetWidth - 2;
const hasContainerWidthChanged = this.lastContainerWidth !== containerWidth;
// If width hasn't changed, don't re-calculate
if (!hasContainerWidthChanged && !this.needsColumnResize) {
return;
}
this.needsColumnResize = false;
this.lastContainerWidth = containerWidth;
// Don't calculate when mobile layout
if (container.offsetWidth <= 500) {
return;
}
// The theoretical target width - ignoring any previously applied stretching
const targetContentWidth = (() => {
let total = 0;
// Extra margin on first column
total += 8;
if (this.numbered) {
// 32 for padding+margin
total += this.numberColumnWidth + 32;
// this.selectionColumnWidth;
}
if (this.selectable) {
// 32 for padding+margin
total += this.selectionColumnWidth + 32;
// If both selectable and numbered - adjust for reduced margin between
if (this.numbered) {
total -= 16;
}
}
// Add each visible column's target width
this.fields.forEach(({ visible = true, width }) => {
if (visible) {
// 32 for padding+margin
total += width + 32;
}
});
return total;
})();
// Update value passed to nested content to overcome table display layout
this.contentWidth = Math.max(targetContentWidth, containerWidth);
const diff = containerWidth - targetContentWidth;
if (diff <= 0) {
// content larger than container (scrollbar), remove all stretching
this.fields.forEach((field) => (field.stretchWidth = 0));
}
else {
// container larger than content (gap to the right), calculate stretching
// If stretchWeight set, divide value between total to get final weight
// If stretchWeight unset, share remainder of 1 (if any) between all unset cols
let totalSetWeight = 0;
let unsetColsCount = 0;
this.fields.forEach(({ visible = true, stretchWeight }) => {
// Disregard invisible columns
if (!visible) {
return;
}
if (typeof stretchWeight === 'number') {
totalSetWeight += stretchWeight;
}
else {
unsetColsCount++;
}
});
const remainderWeight = Math.max(0, 1 - totalSetWeight);
// Set total to be divided against to be above 1 to keep total set/unset weights equal to 1
totalSetWeight = Math.max(1, totalSetWeight);
this.fields.forEach((field) => {
const { visible = true, stretchWeight } = field;
if (!visible) {
return;
}
// Actual stretch weight, out of a total 1 for all columns
let weight = 0;
if (typeof stretchWeight === 'number') {
weight = stretchWeight / totalSetWeight;
}
else if (remainderWeight > 0) {
weight = remainderWeight / unsetColsCount;
}
// Apply stretching with the weight percentage
field.stretchWidth = diff * weight;
});
}
this.forceRender++;
}
// Auto column width handlers
calculateAutoWidths() {
let isVisible = false;
const columns = this.hostElement.shadowRoot.querySelectorAll(`.${name}__auto-width-check td`);
columns.forEach((cell) => {
// Make sure table is actually rendered (eg not display:none etc)
if (!isVisible && cell.offsetParent !== null) {
isVisible = true;
}
if (!isVisible) {
return;
}
// Update field width with that of largest content
this.fields[cell.dataset.columnindex].width = cell.clientWidth;
});
if (!isVisible) {
return;
}
// Re-render now that widths are updated
if (TEST_AUTO_WIDTH_RENDER) {
return;
}
// Wrap in setTimeout to avoid warning about forcing render within render callback
setTimeout(() => {
this.needsAutoWidthParse = false;
this.forceRender++;
});
}
// Event triggers
triggerSortEvent(sortDirection, type, columnIndex) {
const data = {
rows: this.rows,
type,
sortDirection,
columnIndex,
};
emitEvent(this, 'scaleSort', data);
}
triggerEditEvent(value, rowIndex, columnIndex) {
const data = {
rows: this.rows,
rowIndex,
columnIndex,
value,
};
emitEvent(this, 'scaleEdit', data);
// Force render for checkboxes
this.forceRender++;
}
onTableScroll() {
if (!this.freezeHeader || this.hideHeader) {
return;
}
// Freeze header
const scrollY = this.elScrollContainer.scrollTop;
this.elTableHead.style.transform = `translateY(${scrollY}px)`;
}
renderSettingsMenu() {
var _a, _b, _c;
return (h("scale-menu-flyout", { class: `${name}__settings-menu` },
h("scale-button", { slot: "trigger", variant: "secondary", "icon-only": true, "data-sortable": this.isSortable },
h("scale-icon-service-settings", { accessibilityTitle: "Table options" })),
h("scale-menu-flyout-list", null,
this.isSortable && (h("scale-menu-flyout-item", { id: "sortBy", onClick: this.handleMenuListClick },
h("scale-icon-action-sort", { slot: "prefix" }),
((_a = this.localization) === null || _a === void 0 ? void 0 : _a.sortBy) || 'Sort By',
h("scale-menu-flyout-list", { slot: "sublist", id: "sortByList" }, this.fields.map(({ label, type, sortable, sortDirection = 'none' }, columnIndex) => {
if (!sortable) {
return '';
}
return (h("scale-menu-flyout-item", { "onScale-select": () => this.toggleTableSorting(sortDirection, columnIndex, type) },
sortDirection === 'ascending' && (h("scale-icon-navigation-collapse-up", { size: 16, slot: "prefix" })),
sortDirection === 'descending' && (h("scale-icon-navigation-collapse-down", { size: 16, slot: "prefix" })),
sortDirection === 'none' && (h("scale-icon-navigation-collapse-up", { size: 16, slot: "prefix", style: { opacity: '0' } })),
label || type));
})))),
h("scale-menu-flyout-item", { id: "toggleVisibility", onClick: this.handleMenuListClick },
h("scale-icon-action-hide-password", { slot: "prefix" }),
((_b = this.localization) === null || _b === void 0 ? void 0 : _b.toggle) || 'Toggle Visibility',
h("scale-menu-flyout-list", { slot: "sublist", "close-on-select": "false", id: "toggleVisibilityList" }, this.fields.map(({ label, type, visible = CELL_TYPES[type].defaults.visible !== undefined
? CELL_TYPES[type].defaults.visible
: CELL_DEFAULTS.visible, }, columnIndex) => {
return (h("scale-menu-flyout-item", { checkable: "checkbox", checked: !!visible, "onScale-select": () => this.toggleColumnVisibility(!visible, columnIndex) }, label || type));
}))),
this.selectable && (h("scale-menu-flyout-item", { "onScale-select": () => {
this.elToggleSelectAll.checked =
!this.elToggleSelectAll.checked;
this.toggleSelectAll();
} },
h("scale-icon", { slot: "prefix", path: "M20.9328 10.6668C20.5132 10.6668 20.1731 11.0069 20.1731 11.4265V20.3269H1.5194V1.67309H16.5049C16.9245 1.67309 17.2646 1.33292 17.2646 0.913386C17.2646 0.49385 16.9245 0.153687 16.5049 0.153687H0.759699C0.340163 0.153687 0 0.49385 0 0.913386V21.0866C0 21.5062 0.340163 21.8463 0.759699 21.8463H20.9328C21.3523 21.8463 21.6925 21.5062 21.6925 21.0866V11.4265C21.6925 11.0069 21.3524 10.6668 20.9328 10.6668ZM23.7774 0.653387C23.4807 0.356739 22.9997 0.356739 22.703 0.653387L10.3293 13.0272L7.25501 9.9529C6.9583 9.65625 6.47732 9.65625 6.18061 9.9529C5.88396 10.2496 5.88396 10.7306 6.18061 11.0273L9.7921 14.6388C9.94045 14.7871 10.1349 14.8613 10.3293 14.8613C10.5237 14.8613 10.7181 14.7871 10.8665 14.6388L23.7774 1.72778C24.0741 1.43108 24.0741 0.950095 23.7774 0.653387Z" }),
((_c = this.localization) === null || _c === void 0 ? void 0 : _c.select) || 'Select / Deselect All')),
h("slot", { name: "menu" }))));
}
renderTable() {
if (this.needsAutoWidthParse) {
return this.renderAutoWidthCheck();
}
return (h("div", { ref: (el) => (this.elScrollContainer = el), class: `${name}__scroll-container`, style: { height: this.height || 'auto' }, onScroll: () => this.onTableScroll() },
h("table", { class: `${name}__table` },
this.renderTableHead(),
this.renderTableBody())));
}
renderAutoWidthCheck() {
// Calculate number column width
this.numberColumnWidth = this.rows.length.toString().length * 9;
// Get columns with width option set to 'auto'
const autoCols = [];
this.fields.forEach(({ type, width = CELL_TYPES[type].defaults.width || CELL_DEFAULTS.width, }, columnIndex) => {
if (width === 'auto') {
autoCols.push(columnIndex);
}
});
if (!autoCols.length) {
this.needsAutoWidthParse = false;
return this.renderTable();
}
return (h("table", { class: `${name}__auto-width-check ${name}__table` },
h("tr", { class: `tbody__row` }, autoCols.map((columnIndex) => {
const field = this.fields[columnIndex];
const { type, cell = CELL_TYPES[type] } = field;
// Find largest content of each type. Use custom getter if exists
const getLongestContent = cell.getLongestContent || this.getDefaultLongestContent;
const content = getLongestContent({
rows: this.rows,
columnIndex,
field,
});
return (h("td", { class: `tbody__cell`, style: { width: 'auto' }, "data-columnindex": columnIndex }, cell.render({
field,
content,
component: this,
isAutoWidthCheck: true,
})));
}))));
}
renderTableHead() {
return (h("thead", { ref: (el) => (this.elTableHead = el), class: `thead ${this.hideHeader ? 'sr-only' : ''}` },
h("tr", { class: `thead__row` },
this.numbered && this.renderTableHeadNumberedCell(),
this.selectable && this.renderTableHeadSelectableCell(),
this.fields.map(({ type, label = '',
// Params can be set optionally in the fields options, in the cell type
// descriptor class, or falls back to common defaults
visible = CELL_TYPES[type].defaults.visible !== undefined
? CELL_TYPES[type].defaults.visible
: CELL_DEFAULTS.visible, sortable, sortDirection = 'none', resizable = CELL_TYPES[type].defaults.resizable !== undefined
? CELL_TYPES[type].defaults.resizable
: CELL_DEFAULTS.resizable, width = CELL_TYPES[type].defaults.width || CELL_DEFAULTS.width, minWidth = CELL_TYPES[type].defaults.minWidth ||
CELL_DEFAULTS.minWidth, maxWidth = CELL_TYPES[type].defaults.maxWidth ||
CELL_DEFAULTS.maxWidth, textAlign = CELL_TYPES[type].defaults.textAlign ||
CELL_DEFAULTS.textAlign, stretchWidth = 0, }, columnIndex) => {
if (!visible) {
return;
}
const props = {
class: `thead__cell`,
style: {
width: `calc(${width}px + ${stretchWidth}px)`,
'justify-content': textAlign,
},
'data-type': type,
};
if (sortable) {
props['aria-sort'] = sortDirection;
}
return (h("th", Object.assign({ title: "Activate to sort column" }, props, (sortable
? {
onKeyDown: (event) => {
if (['Enter', ' '].includes(event.key)) {
this.toggleTableSorting(sortDirection, columnIndex, type);
}
},
onClick: () => {
this.toggleTableSorting(sortDirection, columnIndex, type);
},
tabindex: 0,
class: `${props.class} thead-sortable`,
}
: {})),
h("div", { class: `thead__title` },
h("span", { class: `thead__text` },
sortable && (h("scale-icon-content-sort-indicator-up", { size: 16, class: `thead__arrow-top` })),
sortable && (h("scale-icon-content-sort-indicator-down", { size: 16, class: `thead__arrow-bottom` })),
label)),
resizable && (h("div", { class: `thead__divider`, "data-index": columnIndex, "data-width": width, "data-min": minWidth, "data-max": maxWidth, onMouseDown: (e) => this.onDividerDown(e), onTouchStart: (e) => this.onDividerDown(e), "aria-hidden": "true" },
h("div", { class: `thead__divider-line` })))));
}))));
}
renderTableHeadNumberedCell() {
return (h("th", { class: `thead__cell thead__cell--numbered`, style: { width: this.numberColumnWidth + 'px' } },
h("span", { class: "scl-body" }, "#")));
}
renderTableHeadSelectableCell() {
const style = {
width: this.selectionColumnWidth + 'px',
};
// Make selection and numbered cells closer than regular padding
if (this.numbered) {
style.paddingLeft = '0px';
}
return (h("th", { class: `thead__cell thead__cell--selection`, style: style, title: "Select" },
h("scale-checkbox", { ref: (el) => (this.elToggleSelectAll = el), onScaleChange: () => this.toggleSelectAll(), hideLabel: true, "aria-label": "Select" })));
}
renderTableBody() {
return (h("tbody", { class: `tbody` }, (() => {
const rows = [];
// Pagination functionality
const total = this.rows.length;
const start = this.paginationStart;
const end = Math.min(total, this.paginationStart + this.pageSize);
for (let rowIndex = start; rowIndex < end; rowIndex++) {
const rowData = this.rows[rowIndex];
const rowNestedContent = [];
let isNestedExpanded = false;
rows.push(h("tr", { class: `tbody__row` },
this.renderMobileTitle(rowData),
this.numbered && this.renderTableBodyNumberedCell(rowIndex),
this.selectable &&
this.renderTableBodySelectableCell(rowIndex),
rowData.map((cellContent, columnIndex) => {
const field = this.fields[columnIndex];
const visible = field.visible !== undefined
? field.visible
: CELL_TYPES[field.type].defaults.visible !== undefined
? CELL_TYPES[field.type].defaults.visible
: CELL_DEFAULTS.visible;
if (!visible) {
return;
}
// Add rows nested tables to array
if (field.type === 'html') {
if (!cellContent) {
return this.renderTableCell(field, null, rowIndex, columnIndex);
}
if (!!cellContent.isExpanded) {
isNestedExpanded = true;
}
rowNestedContent.push({
content: cellContent,
});
}
return this.renderTableCell(field, cellContent, rowIndex, columnIndex);
})));
// Add second row for nested content if any within the row are expanded
if (rowNestedContent.length) {
rows.push(h("div", { class: `tbody__nested`, style: {
width: this.contentWidth + 'px',
display: isNestedExpanded ? 'block' : 'none',
} },
h("td", { class: `tbody__nested-cell` }, rowNestedContent.map(({ content }) => {
return (content && (h("div", { ref: (el) => {
if (el) {
// Remove content from other pages
let child = el.lastElementChild;
while (child) {
el.removeChild(child);
child = el.lastElementChild;
}
// Append actual content
el.appendChild(content);
}
} })));
}))));
}
}
return rows;
})()));
}
renderMobileTitle(rowData) {
if (this.mobileTitleIndex === -1) {
return h("h5", { class: `tbody__mobile-title scl-h5` }, "\u00A0");
}
return (h("h5", { class: `tbody__mobile-title scl-h5` }, rowData[this.mobileTitleIndex]));
}
renderTableBodyNumberedCell(rowIndex) {
return (h("td", { class: `tbody__cell tbody__cell--numbered`, style: { width: this.numberColumnWidth + 'px' } },
h("p", { class: "scl-body" }, rowIndex + 1)));
}
renderTableBodySelectableCell(rowIndex) {
const style = {
width: this.selectionColumnWidth + 'px',
};
if (this.numbered) {
style.marginLeft = '0px';
style.paddingLeft = '0px';
}
return (h("td", { title: this.rows[rowIndex][0], class: `tbody__cell tbody__cell--selection`, style: style },
h("scale-checkbox", { checked: this.rows[rowIndex].selected, onScaleChange: (e) => this.toggleRowSelect(e, rowIndex), hideLabel: true })));
}
renderTableCell(field, content, rowIndex, columnIndex) {
const cell = CELL_TYPES[field.type];
const { label,
// Use custom field, or default defined in class, or fallback default
width = cell.defaults.width || CELL_DEFAULTS.width, stretchWidth = 0, mobileTitle, // For text cells
} = field;
return (h("td", { class: `tbody__cell${mobileTitle ? ` tbody__cell--used-as-mobile-title` : ``}`, style: { width: `calc(${width}px + ${stretchWidth}px)` } },
h("div", { class: `tbody__mobile-label` }, label),
cell.render({
field,
content,
component: this,
rowIndex,
columnIndex,
})));
}
renderTableInfo() {
return (h("div", { class: `info` },
this.selectable && !!this.selection.length && (h("div", { class: `info__selection` }, `${this.selection.length} row${this.selection.length > 1 ? 's' : ''} selected`)),
this.isPagination && (h("scale-pagination", { class: `info__pagination`, hideBorder: !this.isMobile, startElement: this.paginationStart, totalElements: this.rows.length, pageSize: this.pageSize, onScalePagination: ({ detail }) => (this.paginationStart = detail.startElement) }))));
}
/* 10. Render */
render() {
if (this.dataNeedsCheck) {
this.hasData = this.checkHasData();
}
return (h(Host, { style: {
display: this.visible ? 'block' : 'none',
} },
this.styles && h("style", null, this.styles),
h("div", { class: this.getCssClassMap() },
h("div", { class: `${name}__title-block` },
this.heading && (h("h4", { class: `${name}__heading scl-h5` }, this.heading)),
h("div", null,
h("slot", null)),
this.hasData && this.renderSettingsMenu()),
this.hasData && this.renderTable(),
this.hasData &&
!this.hideInfo &&
!this.needsAutoWidthParse &&
(this.selectable || this.isPagination) &&
this.renderTableInfo())));
}
static get is() { return "scale-data-grid"; }
static get encapsulation() { return "shadow"; }
static get originalStyleUrls() { return {
"$": ["data-grid.css"]
}; }
static get styleUrls() { return {
"$": ["data-grid.css"]
}; }
static get properties() { return {
"fields": {
"type": "any",
"mutable": false,
"complexType": {
"original": "any",
"resolved": "any",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Input fields config array"
},
"attribute": "fields",
"reflect": false
},
"freezeHeader": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Freeze header row from scrolling"
},
"attribute": "freeze-header",
"reflect": false,
"defaultValue": "false"
},
"heading": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Heading string"
},
"attribute": "heading",
"reflect": false,
"defaultValue": "''"
},
"height": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Set static table height, by default will auto-resize"
},
"attribute": "height",
"reflect": false
},
"hideBorder": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Set to true to remove border"
},
"attribute": "hide-border",
"reflect": false,
"defaultValue": "false"
},
"hideHeader": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Set to true to hide header row"
},
"attribute": "hide-header",
"reflect": false,
"defaultValue": "false"
},
"hideInfo": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Set to true to remove info footer block including pagination and selection status"
},
"attribute": "hide-info",
"reflect": false,
"defaultValue": "false"
},
"hideMenu": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Set to true to hide settings menu"
},
"attribute": "hide-menu",
"reflect": false,
"defaultValue": "false"
},
"numbered": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Set to true to add numbers column"
},
"attribute": "numbered",
"reflect": false,
"defaultValue": "false"
},
"pageSize": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Set number of rows to display per pagination page"
},
"attribute": "page-size",
"reflect": false,
"defaultValue": "Infinity"
},
"rows": {
"type": "any",
"mutable": false,
"complexType": {
"original": "any",
"resolved": "any",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Input data array"
},
"attribute": "rows",
"reflect": false
},
"selectable": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Set to true to add selection column"
},
"attribute": "selectable",
"reflect": false,
"defaultValue": "false"
},
"selection": {
"type": "unknown",
"mutable": false,
"complexType": {
"original": "string[]",
"resolved": "string[]",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Read-only selection array - populated with raw data from selected rows"
},
"defaultValue": "[]"
},
"shadeAlternate": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Shade every second row darker"
},
"attribute": "shade-alternate",
"reflect": false,
"defaultValue": "true"
},
"styles": {
"type": "any",
"mutable": false,
"complexType": {
"original": "any",
"resolved": "any",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "(optional) Injected css styles"
},
"attribute": "styles",
"reflect": false
},
"visible": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Set to false to hide table, used for nested tables to re-render upon toggle"
},
"attribute": "visible",
"reflect": false,
"defaultValue": "true"
},
"localization": {
"type": "unknown",
"mutable": false,
"complexType": {
"original": "{\n sortBy: string;\n toggle: string;\n select: string;\n }",
"resolved": "{ sortBy: string; toggle: string; select: string; }",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) set localization for sort, toggle and select/deselect table\nDefault is English."
}
}
}; }
static get states() { return {
"forceRender": {},
"paginationStart": {},
"scrollY": {}
}; }
static get events() { return [{
"method": "scaleEdit",
"name": "scale-edit",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Event triggered every time the editable cells are changed, updating the original rows data"
},
"complexType": {
"original": "DataGridEditEventDetail",
"resolved": "DataGridEditEventDetail",
"references": {
"DataGridEditEventDetail": {
"location": "local"
}
}
}
}, {
"method": "scaleEditLegacy",
"name": "scaleEdit",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [{
"name": "deprecated",
"text": "in v3 in favor of kebab-case event names"
}],
"text": ""
},
"complexType": {
"original": "DataGridEditEventDetail",
"resolved": "DataGridEditEventDetail",
"references": {
"DataGridEditEventDetail": {
"location": "local"
}
}
}
}, {
"method": "scaleSort",
"name": "scale-sort",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Event triggered every time the data is sorted, changing original rows data"
},
"complexType": {
"original": "DataGridSortedEventDetail",
"resolved": "DataGridSortedEventDetail",
"references": {
"DataGridSortedEventDetail": {
"location": "local"
}
}
}
}, {
"method": "scaleSortLegacy",
"name": "scaleSort",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [{
"name": "deprecated",
"text": "in v3 in favor of kebab-case event names"
}],
"text": ""
},
"complexType": {
"original": "DataGridSortedEventDetail",
"resolved": "DataGridSortedEventDetail",
"references": {
"DataGridSortedEventDetail": {
"location": "local"
}
}
}
}]; }
static get elementRef() { return "hostElement"; }
static get watchers() { return [{
"propName": "fields",
"methodName": "fieldsHandler"
}, {
"propName": "rows",
"methodName": "rowsHandler"
}]; }
}