UNPKG

@discoveryjs/discovery

Version:

Frontend framework for rapid data (JSON) analysis, shareable serverless reports and dashboards

323 lines (268 loc) 10.4 kB
/* eslint-env browser */ import usage from './table.usage.js'; import { isArray, isSet } from '../../core/utils/is-type.js'; import { createElement } from '../../core/utils/dom.js'; import { hasOwn } from '../../core/utils/object-utils.js'; function configFromName(name, query) { return { header: name, sorting: `${query} ascN`, view: 'table-cell', data: query }; } function sortingFromConfig(col, host, context) { let prefix = ''; let query = ''; if ('data' in col && col.data !== undefined) { switch (typeof col.data) { case 'string': query = `(${col.data || '$'})`; break; case 'function': prefix = '$dataQuery;'; query = '$dataQuery(#)'; break; default: prefix = '$dataQuery;'; query = '$dataQuery'; } } if (typeof col.content === 'string') { const colonIndex = col.content.indexOf(':'); const viewName = col.content.slice(0, colonIndex); if (colonIndex === -1 || !host.view.has(viewName)) { return; } const viewQuery = col.content.slice(colonIndex + 1); if (viewQuery) { query = query ? `(${query} | ${viewQuery})` : `(${viewQuery})`; } } return query ? host.query(`${prefix} ${query} ascN`, { dataQuery: col.data }, context) : false; } function resolveColConfig(name, config, dataQuery) { if (typeof config === 'string') { config = { content: config }; } return hasOwn(config, 'content') || hasOwn(config, 'data') ? { header: name, view: 'table-cell', ...config } : { ...configFromName(name, dataQuery), ...config }; } function toScalar(value) { return Array.isArray(value) ? value.length : value; } function getOrder(host, data, sorting) { if (typeof sorting !== 'function') { return false; } let order = 0; try { for (let i = 1, prev = toScalar(data[0]); i < data.length; i++) { const current = toScalar(data[i]); const sign = Math.sign(sorting(prev, current)); if (sign) { if (order && sign !== order) { return false; } order = sign; } prev = current; } } catch (e) { host.logger.error('Error on column order detection in table view', e); return 0; } return -order; } export default function(host) { const isNotObject = host.queryFn('is not object'); host.view.define('table', function(el, config, data, context) { let { rows, cols, rowConfig, limit, valueCol = false } = config; let renderRowConfig; if ('rows' in config === false) { rows = data; } if (isSet(rows)) { rows = [...rows]; } if (!isArray(rows)) { rows = rows ? [rows] : []; } const headEl = el.appendChild(createElement('thead')).appendChild(createElement('tr')); const headerCells = []; const footerCells = []; const footerCellIndecies = []; const bodyEl = el.appendChild(createElement('tbody')); const moreEl = el.appendChild(createElement('tbody', { class: 'view-table-more-buttons' })); const moreButtonsEl = moreEl .appendChild(createElement('tr')) .appendChild(createElement('td')); const render = (orderedData) => { bodyEl.innerHTML = ''; moreButtonsEl.innerHTML = ''; for (const headerCell of headerCells) { const order = getOrder(host, orderedData, headerCell.sorting); headerCell.el.classList.toggle('asc', order === 1); headerCell.el.classList.toggle('desc', order === -1); } return host.view.renderList( bodyEl, renderRowConfig, orderedData, { ...context, cols }, 0, { limit: host.view.listLimit(limit, 25), moreContainer: moreButtonsEl, onSliceRender: restCount => restCount === 0 && moreEl.remove() } ); }; async function renderFooter() { const footerEl = el.appendChild(createElement('tfoot')).appendChild(createElement('tr')); // render cells await host.view.render(footerEl, footerCells, data, context); // normalize cells positions by adding empty cells and adjusting colSpans const createEmptyCell = () => createElement('td', { class: 'view-table-footer-cell' }); let colIndex = 0; [...footerEl.childNodes].forEach((cellEl, index) => { const shouldBeIndex = footerCellIndecies[index]; if (cellEl.nodeType !== 1) { return; } if (cellEl.tagName !== 'TD') { cellEl.replaceWith(document.createComment('removed non <td> element')); return; } console.log({colIndex, shouldBeIndex, x: shouldBeIndex - footerCellIndecies[index - 1]}); if (colIndex > shouldBeIndex) { const prevCellEl = cellEl.previousElementSibling; prevCellEl.colSpan = shouldBeIndex - prevCellEl.cellIndex; } else { for (let i = shouldBeIndex - colIndex; i > 0; i--) { cellEl.before(createEmptyCell()); } } colIndex = shouldBeIndex + cellEl.colSpan; }); // pad end with empty cells if needed for (let i = headerCells.length - colIndex; i > 0; i--) { footerEl.append(createEmptyCell()); } } if (Array.isArray(cols)) { cols = cols.map((def, idx) => typeof def === 'string' ? configFromName(def, host.pathToQuery([def])) : { header: 'col' + idx, view: 'table-cell', ...def } ); } else { const colNames = new Set(); const colsMap = cols && typeof cols === 'object' ? cols : {}; cols = []; for (const value of rows) { if (isNotObject(value)) { valueCol = true; } else { for (const key of Object.keys(value)) { colNames.add(key); } } } for (const key of Object.keys(colsMap)) { if (colsMap[key]) { colNames.add(key); } else { colNames.delete(key); } } for (const name of colNames) { cols.push( hasOwn(colsMap, name) ? resolveColConfig(name, colsMap[name], host.pathToQuery([name])) : configFromName(name, host.pathToQuery([name])) ); } } if (valueCol) { cols.unshift({ header: '[value]', view: 'table-cell', sorting: '$ ascN', colSpan: '=is not object ? #.cols.size() : 1', content: '=is not object ? "struct"', details: '=is object ? "struct"' }); } cols = cols.filter(col => !hasOwn(col, 'colWhen') || host.queryBool(col.colWhen, data, context) ); for (const col of cols) { const sorting = hasOwn(col, 'sorting') ? host.query(col.sorting, null, context) : sortingFromConfig(col, host, context); const defaultOrder = typeof sorting === 'function' ? getOrder(host, rows, sorting) // getOrder() returns 0 when all values are equal, it's the same as absence of sorting : 0; const headerCellEl = headEl.appendChild(createElement('th', 'view-table-header-cell')); const headerCell = { el: headerCellEl }; if (hasOwn(col, 'footer') && typeof col.footer !== 'undefined') { const config = typeof col.footer === 'object' && col.footer !== null && typeof col.footer.view !== 'string' ? col.footer : { content: col.footer }; footerCellIndecies.push(headerCells.length); footerCells.push(host.view.composeConfig({ view: 'table-footer-cell' }, config)); } headerCells.push(headerCell); headerCellEl.textContent = col.header; this.applyComputedClassName(headerCellEl, col.headerClassName, data, context); if (defaultOrder !== 0) { headerCell.sorting = sorting; headerCellEl.classList.add('sortable'); headerCellEl.addEventListener('click', () => { if (headerCellEl.classList.contains('asc')) { render(rows.slice().sort(sorting).reverse()); } else if (headerCellEl.classList.contains('desc') && !defaultOrder) { render(rows); } else { render(rows.slice().sort(sorting)); } }); } else { col.sorting = false; } } moreButtonsEl.colSpan = cols.length; renderRowConfig = this.composeConfig({ view: 'table-row', cols: valueCol ? '=is not object ? [#.cols[]] : #.cols' : '=#.cols' }, rowConfig); return footerCells.length === 0 ? render(rows) : Promise.all([ render(rows), renderFooter() ]); }, { tag: 'table', usage }); }