UNPKG

oda-framework

Version:

It's an ES Progressive Framework based on the technology of Web Components and designed especially for creating custom UI/UX of any complexity for web and cross-platform PWA mobile applications.

1,491 lines (1,436 loc) 104 kB
const PROP_PREFIX = 'table-prop:'; ODA({is: 'oda-table', imports: '@oda/button, @oda/checkbox, @oda/icon, @oda/splitter, @oda/menu', template: /*html*/` <style> :host { @apply --flex; @apply --vertical; overflow: hidden; position: relative; } :host([show-borders]) { border: 1px solid gray; } </style> <oda-table-group-panel ~if="showGroupingPanel" :groups></oda-table-group-panel> <oda-table-header :columns="headerColumns" ~if="showHeader"></oda-table-header> <oda-table-body class="flex" ::over-height :rows tabindex="0" :scroll-top="$scrollTop" :scroll-left="$scrollLeft" :even-odd :col-lines :row-lines ></oda-table-body> <oda-table-footer :columns="rowColumns" ~if="showFooter" class="dark"></oda-table-footer> `, hostAttributes: { tabindex: 0 }, onTapEditMode: { $pdp: true, $def: false, }, overHeight: false, /** * @this {Table} * @param {MouseEvent & {path: (HTMLElement & {row: TableRow})[]}} e */ onDblclick(e) { const el = e.path.find(p => p.row); if (el) { this.table.fire('cell-dblclick', el.row); } }, arrowLeft(e) { this.body.$keys.arrowLeft(e); }, arrowRight(e) { this.body.$keys.arrowRight(e); }, arrowUp(e) { this.body.$keys.arrowUp(e); }, arrowDown(e) { this.body.$keys.arrowDown(e); }, home(e) { this.body.$keys.home(e); }, end(e) { this.body.$keys.end(e); }, pageUp(e) { this.body.$keys.pageUp(e); }, pageDown(e) { this.body.$keys.pageDown(e); }, enter(e) { this.body.$keys.enter(e); }, // async openSettings(parent) { // this.showSettings = !this.showSettings; // // await ODA.import('@tools/containers'); // // await ODA.showDropdown( // // 'oda-table-settings', // // { table: this.table }, // // { parent, align: 'left', minHeight: '100%', title: 'Settings', hideCancelButton: true } // // ); // }, /** @this {Table} */ get storage() { return ODA.LocalStorage.create(this.$savePath); }, screenLength: { $pdp: true, /** @this {Table} */ get() { return (Math.round(this.$height / this.rowHeight) || 0) + 1; }, }, columns: [], dataSet: [], focusedCellEnd: {}, /** @this {Table} */ get _fixWidth() { return this.headerColumns.filter(i => { return i.fix; }).reduce((res, i) => { return res += i.width || i.$width; }, 0); }, templates: { $public: true, $pdp: true, footerTemplate: 'oda-table-footer-cell', groupTemplate: 'oda-table-cell-group', headerTemplate: 'oda-table-header-cell', cellTemplate: 'oda-table-cell', checkTemplate: 'oda-table-check' }, $public: { $pdp: true, showSettings: false, selectByCheck: false, allowFocusCell: true, allowFocusCellZone: { $def: 'data', $list: ['data', 'tree', 'left', 'right'], $multiSelect: true }, autoFixRows: false, noLazy: false, pivotMode: { $def: false, $save: true }, rowFixLimit: { $def: 10, $save: true, }, allowSettings: false, allowCheck: { $def: 'none', $list: ['none', 'single', 'down', 'up', 'double', 'clear-down', 'clear-up', 'clear-double'] }, allowDrag: false, allowDrop: false, allowFocus: false, allowSelection: { $attr: true, $list: ['none', 'all', 'level', 'type', 'by-check'], $def: 'none', set(n, o) { if (o) this.clearSelection(); } }, allowSort: false, autoRowHeight: false, autoWidth: { $def: false, $attr: true }, colLines: { $def: false, $attr: true }, columnId: 'name', size: { $type: Number, get() { return this.items?.length || 0; } }, evenOdd: { $def: false, $attr: true }, groupExpandingMode: { $def: 'none', $list: ['none', 'first', 'auto', 'all'] }, hideRoot: false, hideTop: false, icon: 'odant:grid', iconChecked: 'icons:check-box', iconUnchecked: 'icons:check-box-outline-blank', iconCollapsed: 'icons:chevron-right', iconExpanded: 'icons:chevron-right:90', iconExpanding: 'odant:spin', iconIntermediate: 'icons:check-box-indeterminate', iconSize: 24, rowLines: { $def: false, $attr: true, $save: true }, showBorders: { $type: Boolean, $attr: true }, showFilter: { $type: Boolean, $def: false, $save: true, set(n) { } }, showFooter: false, showGroupingPanel: { $def: false, $save: true }, showGroupFooter: false, showHeader: false, showTreeLines: { $type: Boolean, $attr: true }, treeLineStyle: { $type: Object, $def: { width: 1, color: 'rgba(0, 0, 0, 0.25)' } }, treeStep: 24, get scrollBoxWidth() { const div = document.createElement('div'); div.style.setProperty('overflow-y', 'scroll'); div.style.setProperty('overflow-x', 'hidden'); div.style.setProperty('min-height', '1px'); div.style.setProperty('position', 'fixed'); div.style.setProperty('visibility', 'hidden'); document.body.appendChild(div); requestAnimationFrame(() => { div.remove(); }); return div.offsetWidth; }, activeCell: null, fillingNewLineMode: false, /** @this {Table} */ get screenFrom() { return Math.round(this.$scrollTop / this.rowHeight); }, }, pointerRow: Object, expandLevel: -1, expandAll: false, filter: { $def: '', $public: true }, $pdp: { /** * @this {Table} * @param {TableCellInfo} v */ set focusedCell(v) { if (!v || v.column.$flex || !this.body) return; const elem = this.body.findCellByCoordinates(v); if (!v.column.fix) { const { left: leftFixColsWidth, right: rightFixColsWidth } = this.activeCols .reduce((res, c) => (res[c.fix] += c.$width, res), { left: 0, right: 0 }); const bodyRect = this.body.getBoundingClientRect(); const cellRect = elem.getBoundingClientRect(); if (cellRect.x + cellRect.width - bodyRect.x > bodyRect.width - rightFixColsWidth) { this.body.scrollLeft += (cellRect.x + cellRect.width + rightFixColsWidth) - bodyRect.width; } else if (cellRect.x - bodyRect.x < leftFixColsWidth) { this.body.scrollLeft -= leftFixColsWidth - (cellRect.x - bodyRect.x); } } const rowIndex = this.rows.findIndex(r => this.compareRows(r, v.row)); if ((rowIndex + .8) * this.rowHeight > this.$height) { this.$scrollTop += this.rowHeight; } if (this.activeCell) { this.activateCell(elem); } }, /**@this {Table} */ focusCell(row, column) { if (!row || !column || column.$flex) return; if (this.compareRows(this.focusedCell?.row, row) && this.focusedCell?.column === column) return; if (this.focusedCell && (!this.compareRows(this.focusedCell.row, row) || column === this.activeCols.at(-1)) && this.activeCell && this.fillingNewLineMode) { this.fillingNewLineMode = false; } this.focusedCell = { row, column }; }, get activeCols() { return this.rowColumns.filter(i => { return !i.$flex && !i.$hidden; }).sort((a, b) => a.$order - b.$order); }, set focusedRow(n) { if (n) { this.debounce('focusedRow', () => { this.scrollToRow(n); }); } }, get body() { return this.$('oda-table-body'); }, compareRows(row1, row2) { // вынесено в функцию для возможности переопределения if (!row1 || !row2) return false; return Object.equal(row1, row2); }, isSelectedRow(row) { // вынесено в функцию для возможности переопределения return this.selectedRows.some(sr => this.compareRows(row, sr)); }, isFocusedRow(row) { // вынесено в функцию для возможности переопределения return this.compareRows(row, this.focusedRow); }, get table() { return this; }, $scrollLeft: 0, $scrollTop: { $def: 0, // set(n) { // this.$scrollTop = Math.ceil(n / this.rowHeight) * this.rowHeight; // } }, get rowHeight() { return Math.round(this.iconSize * 4 / 3); }, get $scrollHeight() { return (this.size + this.fixedRows.length) * this.rowHeight; }, $width: 0, $height: 0, get visibleRows() { const rows = this.rows; const raisedRows = this.raisedRows; const fixedRows = this.fixedRows; return [...fixedRows, ...raisedRows, ...rows]; }, draggedRows: [], selectedRows: [], raisedRows: [], checkedRows: [], get rows() { if (this.noLazy) return this.sortedItems; const rows = this.sortedItems.slice(this.screenFrom, this.screenFrom + this.screenLength + this.raisedRows.length); if (this.autoFixRows) { const raised = []; let top = rows[0]; // Если hideTop не подниматься до самого верхнего __parent__ const getParent = (row) => { return this.hideTop && !row?.__parent__?.__parent__ ? null : row?.__parent__; }; let __parent__ = getParent(top); while (__parent__ || top?.__expanded__ && top?.items?.length) { if (top?.__expanded__ && top?.items?.length) { raised.add(top); rows.shift(); top = rows[0]; } else { raised.unshift(__parent__); if (raised && (rows[0]?.__parent__ === raised.at(-1))) { rows.shift(); top = rows[0]; } else { raised.pop(); } __parent__ = getParent(__parent__); } } this.raisedRows = raised; } else this.raisedRows ??= []; return rows; }, set rows(v) { this.async(() => { this.$render(); }, 100); }, disableColumnsSave: false, fixedRows: [], modifyColumn(col) { if (!this.disableColumnsSave) modifyColumn(this.table, col); }, get rowColumns() { const convert = (cols) => { return cols.filter(i => !i.$hidden).reduce((res, col) => { this.modifyColumn(col); if (col.__expanded__ && col.items?.length) { const items = col.items.filter(i => !i.$hidden).map((c, i) => { c.id = `${col.id}-${i}`; c.__parent__ ??= col; return c; }); res.push(...convert(items)); } else { res.push(col); } return res; }, []); }; return convert(this.headerColumns); }, get sorts() { const find_sorts = (col = []) => { this.modifyColumn(col); return col.reduce((res, i) => { res.add(i); let items = i.items; if (items) { if (items?.then) { items?.then(r => { if (r?.length) res.add(...r); }); } else { items = find_sorts(items); res.add(...items); } } return res; }, []); }; let result = find_sorts(this.columns); result = result.filter(i => { return i.$sort; }); return result.sort((a, b) => { return Math.abs(a.$sort) > Math.abs(b.$sort) ? 1 : -1; }); }, groupColPaths: { $def: [], $save: true }, get groups() { if (!this.columns?.length) return []; const getColByPath = (columns, path) => { const steps = path.split('/'); const step = steps.pop(); const col = columns.find(c => c.name === step); if (col) { if (steps.length) { return getColByPath(col.items, steps.join('/')); } return col; } }; return this.groupColPaths.map(p => getColByPath(this.columns, p)).filter(Boolean); }, pivotLabels: [], get filters() { return this.columns?.filter(i => i.$filter) || []; }, $scrollWidth: 0, _selectedAll: false, get items() { if (!this.dataSet?.length) return emptyRows; let array = Object.assign([], this.dataSet); array = extract.call(this, array, this.hideRoot || this.hideTop ? -1 : 0); return array; }, get filteredItems() { if (this.items?.length) { const items = [...this.items]; this._useColumnFilters(items); this._applyFilter(items); return items; } return emptyRows; }, get groupedItems() { if (this.filteredItems?.length) { const array = [...this.filteredItems]; if (this.groups.length) this._group(array); return array; } return emptyRows; }, sortedItems: { $type: Array, get() { if (this.groupedItems?.length) return [...this.groupedItems]; return emptyRows; } }, get footer() { const obj = {}; this.rowColumns.forEach(c => { switch (c.summary) { case 'count': { obj[c[this.columnId]] = this.size; } break; case 'sum': { obj[c[this.columnId]] = this.dataSet.reduce((res, r) => { const v = r[c.name]; if (v) res += parseFloat(v); return res; }, 0); } break; default: { if (c.treeMode) { let d = this.size; if (this.screenFrom !== undefined && this.screenLength !== undefined) { let to = this.screenFrom + this.screenLength; if (to > d) to = d; if (d) d = `${d.toLocaleString()} [ ${(this.screenFrom).toLocaleString()} ... ${to.toLocaleString()} ]`; } obj[c[this.columnId]] = d || ''; } } } }); return obj; }, }, get headerColumns() { this.columns?.forEach?.((col, i) => { this.modifyColumn(col); let order = i; if (col.treeMode) order -= 500; switch (col.fix) { case 'left': order -= 1000; break; case 'right': order += 1000; break; } const id = col[this.columnId]; if (!id) order -= 1000; order *= maxColsCount; if (id) col.$order ??= order; else col.$order = order; col.index = col.order = col.$order; }); const cols = this.columns?.filter?.(i => !i.$hidden) || []; if (!this.autoWidth) cols.push({ $flex: true, $order: 999 * maxColsCount, cellTemplate: 'div', headerTemplate: 'div', footerTemplate: 'div' }); if (this.allowCheck !== 'none' && !this.columns.some(i => i.treeMode)) { cols.push({ width: 32, $order: -999 * maxColsCount, cellTemplate: this.checkTemplate, headerTemplate: 'div', fix: 'left' }); } cols.filter(i => i.fix === 'left' || i.treeMode).reduce((res, i) => { i.left = res; res += i.width || i.$width || 0; return res; }, 0); cols.filter(i => i.fix === 'right').toReversed().reduce((res, i) => { i.right = res; res += i.width ?? i.$width ?? 0; return res; }, 0); cols.forEach((c, i) => { c.id = i; }); return cols; }, colStyles: { $pdp: true, get() { const result = this.rowColumns.map(col => { if (col.id === undefined) return ''; col.className = `col-${col.id}`; let style = `.${col.className}{/*${col[this.columnId]}*/\n\t\n\torder: ${col.$order};`; if (col.__parent__) style += '\n\tbackground-color: whitesmoke;'; if (col.$flex) style += '\n\tflex: 1;\n\tflex-basis: "100%";'; else { style += '\n\tposition: sticky;'; if (col.width) { style += `\n\tmin-width: ${col.width}px; \n\tmax-width: ${col.width}px;\n\tflex: 0;`; } else { if (this.autoWidth && this.rowColumns.last === col) style += `\n\tflex: 1 !important;`; style += `\n\tmin-width: 16px;`; } col.$width = col.$width || col.width || 150; style += `\n\twidth: ${col.$width}px;`; const min = (this.autoWidth && !col.fix) ? '10px' : (col.width + 'px'); const max = col.$width + 'px'; if (col.fix) { style += `\n\tz-index: 1;`; if (col.fix === 'left') { style += `\n\tleft: ${col.left}px;`; } else if (col.fix === 'right') { style += `\n\tright: ${col.right}px;`; } } } style += '\n}\n'; return style; }).join('\n'); return result; } }, focus() { this.body.focus?.(); }, $listeners: { dragend: 'onDragEnd', // dragleave: 'onDragEnd', }, onTapRows(e) { if (e.button) return; const evt = e.sourceEvent || e; if (evt.which !== 1) return; this.onSelectRow(evt); }, expand(row, force, old) { const items = this._beforeExpand(row, force); if (items?.then) { const id = setTimeout(() => { row.$loading = true; this.$render(); }); return items.then(async items => { clearTimeout(id); row.$loading = false; if (this.sorts.length) this._sort(items); const node = old || row; if ((node.items && node.__expanded__)) { for (const i in items) { const n = items[i]; const o = (this.idName ? node.items.find(i => i[this.idName] === n[this.idName]) : node.items[i]) || node.items[i]; n.__expanded__ = !!o?.__expanded__; if (n.__expanded__) { this.expand(n, false, o); } } } if (items?.length) { row.items = items; } else if (row.items?.length > 0) { items = row.items = []; } else { items = row.items; } return items; }).catch((err) => { clearTimeout(id); row.$loading = false; }); } row.items = items; return items; }, _beforeExpand(item) { return item.items; }, _checkChildren(node) { const items = this._beforeExpand(node); if (items?.then) { return items.then(res => (res?.length > 0)); } return (items?.length > 0); }, _useColumnFilters(array) { this.filters?.forEach(col => { const name = col[this.columnId]; let filter = String(col.$filter).toLowerCase().replace('&&', '&').replace('||', '|'); filter = filter.replaceAll(' and ', '&').replaceAll('&&', '&').replaceAll(' or ', '|').replaceAll('||', '|'); filter = filter.split('&').reduce((res, and) => { const or = and.split('|').reduce((res, or) => { const space = or.split(' ').reduce((res, space) => { if (space.trim()) res.push(`String(val).toLowerCase().includes('${space.trim()}')`); return res; }, []).join(' || '); if (space) res.push(`(${space})`); return res; }, []).join(' || '); if (or) res.push(`(${or})`); return res; }, []).join(' && '); const func = new Function('val', `return (${filter})`); // array.splice(0, array.length, ...array.filter(item => { return func(item[name]) })); array.splice(0, array.length, ...array.filter(item => { return (func(item[name]) || item.items?.find(subItem => { return func(subItem[name]); })); })); }); }, /** * @param {TableRow[]} items * @this {Table} */ _applyFilter(items) { if (!this.filter) return; try { const filter = new RegExp(this.filter); items.splice(0, items.length, ...items.filter(item => { return this.rowColumns.some(col => { const name = col[this.columnId]; return (filter.test(item[name]) || item.items?.find(subItem => { return filter.test(subItem[name]); })); }) })); } catch (err) { console.warn(err); } }, _groups: [], _group(array) { const grouping = (items, __level__ = 0, __parent__) => { const column = this.groups[__level__]; const name = column[this.columnId]; const label = column.label; const oldGroups = [...(__parent__ || this)._groups]; const groups = (__parent__ || this)._groups; groups.splice(0, groups.length); const result = items.reduce((res, i) => { if (!i.__group__ && i.__level__ !== 0) return res; const value = i[name]; let group = res.find(r => r.value === value); if (!group) { group = oldGroups.find(r => r.value === value); if (group) { group.items = []; groups.push(group); res.push(group); } } if (!group) { group = { __group__: true, value, name, label, __level__, items: [], __parent__, _groups: [], hideCheckbox: column.hideGroupCheckbox, hideExpander: column.__expanded__ }; groups.push(group); res.push(group); } i.__parent__ = group; group.items.push(i); return res; }, []); // if (newGroups.length > 0) { // groups.splice(0, groups.length, ...newGroups) // } if (this.groups[0].$sortGroups) { this._sortGroups(result, this.groups[0].$sortGroups); } if (__level__ < this.groups.length - 1) { for (const group of groups) { group.items = grouping(group.items, __level__ + 1, group); } } for (let i = 0; i < groups.length; i++) { if ([true, false].includes(groups[i].__expanded__)) { continue; } let expanded = column.__expanded__; if (this.groupExpandingMode === 'auto' && (true)) { expanded = true; } else if (this.groupExpandingMode === 'all') { expanded = true; } else if (this.groupExpandingMode === 'first' && i === 0) { expanded = true; } groups[i].__expanded__ = expanded; } return result; }; const expanding = (items) => { return items.reduce((res, group) => { res.push(group); if (group.__expanded__ && group.items?.length) { // if (this.allowSort) this._sort(group.items); const subItems = expanding(group.items); res.push(...subItems); } return res; }, []); }; let result = grouping(array); result = expanding(result); array.splice(0, array.length, ...result); return array; }, _sort(array = []) { if (!this.sorts.length) return; array.sort((a, b) => { let res = 0; this.sorts.some(col => { const _a = a[col[this.columnId]]; const _b = b[col[this.columnId]]; res = (String(_a)).localeCompare(String(_b)) * col.$sort; if (res) return true; }); return res; }); }, _sortGroups(array = [], sort) { array.sort((a, b) => { const _a = a.value || ''; const _b = b.value || ''; return (String(_a)).localeCompare(String(_b)) * sort; }); }, onfocusRow(e, d) { if (e.ctrlKey || e.shiftKey) return; const row = d?.value || e?.target?.item || d.value; if (!row) return; this.focusRow(row); }, /**@this {Table} */ focusRow(row) { if (this.allowFocus && row && !row.__group__ && (!row.disabled && row.$allowFocus !== false)) { this.focusedRow = row; } if (row.disabled) { row.__expanded__ = !row.__expanded__; this.table.expand(row); } }, onSelectRow(e, d) { const row = d?.value || e.target.item; if (!row) return; this.selectRow(row, { range: e.shiftKey, add: e.ctrlKey }); }, /**@this {Table} */ selectRow(row, { range = false, add = false } = {}) { if (this.allowSelection === 'none') { this.focusRow(row); return; } if (this.selectByCheck) { if (!row.disabled) { if (this.selectedRows.includes(row)) { const idx = this.selectedRows.indexOf(row); this.selectedRows.splice(idx, 1); } else { this.selectedRows.push(row); } this.selectedRows = [...this.selectedRows]; } return; } if (!~this.selectionStartIndex) { this.selectionStartIndex = this.getRowIndex(this.selectedRows[0] || row); } if (range) { let from = this.selectionStartIndex; const to = this.getRowIndex(row); this.clearSelection(); if (from <= to) { while (from <= to) { this.addSelection(this.rows[from]); from++; } } else { while (from >= to) { this.addSelection(this.rows[from]); from--; } } return; } if (add) { this._selectedAll = false; const idx = this.selectedRows.indexOf(row); if (idx < 0) this.addSelection(row); else { this.selectedRows.splice(idx, 1); // this.fire('selected-rows-changed', this.selectedRows); if (row === this.selectionStartRow) { this.selectionStartRow = this.selectedRows[0] || null; } this.selectedRows = [...this.selectedRows]; } return; } if (row.disabled) { return; } this.focusRow(row); this.selectionStartIndex = -1; this.selectedRows.clear(); this.addSelection(row); }, /**@this {Table} */ moveCellPointer(h = 0, v = 0) { if (!this.allowFocusCell) return; const maxRowIndex = Math.min(this.visibleRows.length, Math.trunc(this.$height/this.rowHeight)); if (!this.focusedCell) { this.focusedCell = { row: this.focusedRow || this.visibleRows[0], column: this.activeCols[0] }; } const rowIndex = this.visibleRows.findIndex(r => this.compareRows(this.focusedCell.row, r)); if (rowIndex === -1) { const globalRowIndex = this.items.findIndex(r => this.compareRows(this.focusedCell.row, r)); const scrollToItem = globalRowIndex * this.rowHeight; const halfScreenTop = Math.trunc(maxRowIndex / 2) * this.rowHeight; this.$scrollTop = Math.max(0, Math.min(scrollToItem - halfScreenTop, this.scrollHeight - this.$height)); return; } const columnIndex = this.activeCols.indexOf(this.focusedCell.column); const newPos = { row: rowIndex + v, col: columnIndex + h }; //row if (v < 0) { if (newPos.row < 0) { if (this.$scrollTop > 0) { this.$scrollTop += this.rowHeight * v; this.$scrollTop = Math.max(0, this.$scrollTop); } newPos.row = rowIndex; } } else if (v > 0) { if ((newPos.row >= maxRowIndex)) { if (this.$scrollTop < this.scrollHeight - this.$height) { this.$scrollTop += this.rowHeight * v; } newPos.row = Math.min(this.visibleRows.length - 1, rowIndex); } } //col if(!this.activeCols[newPos.col]){ newPos.col = columnIndex; } this.debounce('moveCellPointer', () => { this.focusCell(this.visibleRows[newPos.row], this.activeCols[newPos.col]); }); }, getRowIndex(row) { return this.rows.findIndex(r => Object.equal(r, row)); }, /**@this {Table} */ addSelection(item) { if (!item || item.__group__ || item.$allowSelection === false) return; switch (this.allowSelection) { case 'all': break; case 'level': if (this.selectedRows.length) { if (Object.equal(item.__parent__, this.selectedRows[0].__parent__)) break; else return; } else break; case 'type': if (this.selectedRows.length) { if (item.type === this.selectedRows[0].type) break; else return; } else break; case 'none': default: return; } this.selectedRows.push(item); this.selectedRows = [...this.selectedRows]; // this.fire('selected-rows-changed', this.selectedRows); }, clearSelection() { this._selectedAll = false; this.selectedRows = []; // this.fire('selected-rows-changed', this.selectedRows); }, /** @this {Table} */ scrollToRowIndex(index) { if (this.style.getPropertyValue('visibility') === 'hidden') { return this.async(() => this.scrollToRowIndex(index), 100); } if (index <= -1) { return; } this.throttle('changeScrollTop', () => { // for complete of rendering if (!this.body) return; const pos = index * this.rowHeight; const shift = this.rowHeight * Math.floor(this.body.offsetHeight / (3 * this.rowHeight)); if ((this.body.scrollTop + 0.8 * this.rowHeight > pos) || (this.body.offsetHeight + this.body.scrollTop - 1.5 * this.rowHeight < pos)) { this.body.scrollTop = (pos - shift < 0) ? 0 : pos - shift; } }, 100); }, scrollToRow(item) { if (this.style.getPropertyValue('visibility') === 'hidden') { return this.async(() => this.scrollToRow(item), 100); } item ??= this.focusedRow; if (this.rows.some(r => r === item)) return; const index = this.items.findIndex(i => { return Object.equal(i, item); }); this.scrollToRowIndex(index); }, selectAndFocusRow(item) { this.clearSelection(); const items = this.items; if (item && items) { if (item.disabled || item.isGroup) { item.__expanded__ = !item.__expanded__; } else { this.selectedRows.push(item); // this.fire('selected-rows-changed', this.selectedRows); // this.focusedRow = item; this.focusRow(item); this.selectedRows = [...this.selectedRows]; // this.scrollToRow(item); } } }, selectAll() { if (!this._selectedAll) this._selectedAll = true; const items = this.items; if (items.length && this.allowSelection !== 'none') { for (const item of items) { this.addSelection(item); } } this.render(); }, getColPath(col) { if (col.$parent) return `${this.getColPath(col.__parent__)}/${col.name}`; return col.name; }, addGroup(col) { const path = this.getColPath(col); if (this.groupColPaths.includes(path)) { return; } this.groupColPaths.push(path); this.groupColPaths = Array.from(this.groupColPaths); this.showGroupingPanel = true; }, removeGroup(col) { const path = this.getColPath(col); const idx = this.groupColPaths.indexOf(path); if (idx > -1) { this.groupColPaths.splice(idx, 1); this.groupColPaths = Array.from(this.groupColPaths); } if (!this.groupColPaths.length) this.showGroupingPanel = false; }, _getColumns(row) { if (row.__group__) return [row.$col]; return this.rowColumns; }, _swapColumns(col1, col2) { const ord = col1.order; col1.order = col2.order; col2.order = ord; }, insertBeforeRow(row, rows) { if (!Array.isArray(rows)) rows = [rows]; const items = (row.__parent__?.items || this.dataSet); items.splice(items.indexOf(row), 0, ...rows); }, insertAfterRow(row, rows) { if (!Array.isArray(rows)) rows = [rows]; const items = (row.__parent__?.items || this.dataSet); items.splice(items.indexOf(row) + 1, 0, ...rows); }, appendChildRows(target, rows) { if (!Array.isArray(rows)) rows = [rows]; if (!target.items) target.items = rows; else target.items.push(...rows); target.__expanded__ = true; }, removeRows(rows) { if (!Array.isArray(rows)) rows = [rows]; rows.forEach(row => { const items = (row.__parent__?.items || this.dataSet); items.splice(items.indexOf(row), 1); }); }, deleteItems(callback, once = false) { const items = once ? [this._find(callback)] : this._filter(callback); items.forEach(i => { const array = i.__parent__?.items || this.dataSet; const idx = array.indexOf(i); if (~idx) { array.splice(idx, 1); } }); }, //#region drag & drop _onDragStart(e) { const el = e.path.find(p => p.row); if (!(el && (this.allowDrag || el.row.drag))) { return; } e.dataTransfer.clearData(); this._setDragImage(e); this.draggedRows = this.selectedRows.includes(el.row) ? this.selectedRows : [el.row]; this._getDragData(this.draggedRows).forEach(data => { e.dataTransfer.setData(data.mime, data.data); }); }, _setDragImage(e) { try { const node = e.target.querySelector('.cell'); e.dataTransfer.setDragImage(node || new Image(), 0, 0); } catch (err) { e.dataTransfer.setDragImage(new Image(), 0, 0); } }, _getDragData(rows) { return rows.map(r => { return { mime: 'application/json', data: r }; }); }, _onDragLeave(e) { const el = e.path.find(p => p.row); if (el) el.row.$dropMode = ''; clearTimeout(this._expandTimer); this._expandTimer = null; }, _checkDropWait: null, _onDragOver(e) { if (!this.allowDrop) return; e.stopPropagation(); if (this._draggableColumn) return; const target = e.path.find(p => p.row); if (!target) return; const row = target.row; if (this.draggedRows?.length) { let r = row; while (r) { if (this.draggedRows.includes(r)) return; r = r.__parent__; } if (this.draggedRows.some(i => i.__parent__ === row)) return; } e.preventDefault(); if (!this._expandTimer) { this._expandTimer = setTimeout(() => { clearTimeout(this._expandTimer); this._expandTimer = null; row.__expanded__ = true; }, 1000); } if (this.sorts.length) row.$dropMode = 'in'; else { let rect = target.getBoundingClientRect(); rect = (e.y - rect.y) / rect.height; if (rect < .25) row.$dropMode = 'top'; else if (rect < .75) row.$dropMode = 'in'; else row.$dropMode = 'bottom'; } e.dataTransfer.dropEffect = this._getDropEffect(this.draggedRows, row, e); if (!e.dataTransfer.dropEffect || e.dataTransfer.dropEffect === 'none') row.$dropMode = ''; }, _getDropEffect(source, target, event) { return event.ctrlKey ? 'copy' : 'move'; }, _onDrop(e) { e.stopPropagation(); if (this._draggableColumn) return; const el = e.path.find(p => p.row); if (!el) return; const row = el.row; e.preventDefault(); try { this._doDrop(this.draggedRows, row, e); } catch (err) { console.error(err); } finally { row.$dropMode = ''; this['#items'] = undefined; } }, _doDrop(source, target, event) { if (source?.length > 0) { if (!event.ctrlKey) { this.deleteItems(i => source.includes(i)); } switch (target.$dropMode) { case 'top': { this.insertBeforeRow(target, source); } break; case 'in': { this.appendChildRows(target, source); } break; case 'bottom': { this.insertAfterRow(target, source); } break; } } }, onDragEnd(e) { this.draggedRows = []; this._checkDropWait = null; e.dataTransfer.clearData(); }, _onDropToEmptySpace() { }, _onDragOverToEmptySpace() { }, //#endregion & drop _find(callback) { const find = (items) => { let res = items.find(callback); if (!res) { res = items.find(i => { return i.items?.length && find(i.items); }); if (res) return find(res.items); } return res; }; return find(this.dataSet); }, _filter(callback) { const find = (items) => { const res = items.filter(i => i.items).reduce((res, item) => { res.push(...find(item.items)); return res; }, []); res.push(...items.filter(callback)); return res; }; return find(this.dataSet); }, // _onRowContextMenu(e) { // const el = e.path.find(p => p.row); // if (el) // this.fire('row-contextmenu', el.row); // }, /**@this {Table}*/ _onDownToEmptySpace(e) { if (e.button) return; this.focusedRow = null; this.clearSelection(); }, /**@this {Table}*/ activateCell(cellElement) { if (this.activeCell) { this.deactivateCell(this.activeCell); if (this.activeCell === cellElement) { return this.deactivateCell(cellElement); } } if (typeof cellElement?.activate === 'function' && (cellElement.column?.treeMode || !(cellElement.readOnly || cellElement.readonly))) { this.listen('deactivate', () => { if (this.activeCell === cellElement) { if (this.fillingNewLineMode) { this.focus(); this.moveCellPointer(1, 0); } else { this.activeCell = null; } } }, { target: cellElement, once: true }); this.activeCell = cellElement; return cellElement.activate(); } const row = cellElement.cellCoordinates.row; if (row && this.compareRows(row, this.focusedRow)) { const treeColumn = this.activeCols.find(c => c.treeMode); if (treeColumn) { const treeCell = this.body.findCellByCoordinates({ row, column: treeColumn }); if (treeCell) { return this.activateCell(treeCell); } } } this.activeCell = null; }, /**@this {Table}*/ deactivateCell(cellElement) { if (typeof cellElement.deactivate === 'function') { cellElement.deactivate(); } else { cellElement.fire('deactivate'); } }, /**@this {Table}*/ onCellPointerDown(row, col, cell) { this.table.focusCell(row, col); if (this.onTapEditMode && !this.activeCell) { this.activateCell(cell) } }, /**@this {Table}*/ onCellDoubleClick(e, cell) { this.onDblclick(e); if (!this.onTapEditMode) { this.activateCell(cell) } }, }); ODA({is: 'oda-table-group-panel', imports: '@oda/icon', template: /*html*/` <style> :host { @apply --header; @apply --horizontal; font-size: small; } .item { max-width: 120px; margin: 0px 4px; padding-left: 4px; align-items: center; border-radius: 4px; } .closer { cursor: pointer; } .panel { margin: 2px; min-width: 50%; } label { @apply --flex; @apply --disabled; } :host > div > div{ align-items: center; padding: 0px 4px; } oda-icon { transform: scale(.7); } </style> <div class="horizontal border flex panel"> <oda-icon disabled :icon-size icon="icons:dns"></oda-icon> <div class="flex horizontal"> <label ~if="!groups.length">Drag here to set row groups</label> <div class="no-flex horizontal"> <div class="item shadow content no-flex horizontal" ~for="groups"> <label class="label flex" ~text="$for.item.$saveKey || $for.item.label"></label> <oda-icon class="closer" icon="icons:close" :icon-size @tap="_close($event, $for.item)"></oda-icon> </div> </div> </div> </div> <div ~show="pivotMode" class="horizontal border flex panel"> <oda-icon disabled :icon-size icon="icons:dns:90"></oda-icon> <div class="flex horizontal"> <label ~if="!pivotLabels.length">Drag here to set column labels</label> <div class="no-flex horizontal"> <div class="item shadow content no-flex horizontal" ~for="pivotLabels"> <label class="label flex" ~text="$for.item.$saveKey || $for.item.name"></label> <oda-icon class="closer" icon="icons:close" :icon-size @tap="_close($event, $for.item)"></oda-icon> </div> </div> </div> </div> `, $listeners: { dragover: '_dragover', drop: '_drop' }, _close(e, column) { e.stopPropagation(); this.table.removeGroup(column); }, _dragover(e) { if (this.table._draggableColumn && !this.groups.includes(this.table._draggableColumn)) { e.preventDefault(); } }, _drop(e) { this.table.addGroup(this.table._draggableColumn); } }); ODA({is: 'oda-table-part', template: ` <style>{{colStyles}}</style> ` }); ODA({is: 'oda-table-hide-column', imports: '@oda/checkbox', template: /*html*/` <style> :host { flex: 1; } .list { padding: 5px; } </style> <div class="horizontal no-flex header list"> <oda-checkbox id="checker" :value="items.every(i=> !i.col.$hidden)" @tap="_onSelectAll"></oda-checkbox> <label class="flex label center" style="font-size: 10pt; padding: 0px 8px;">(show all)</label> </div> <div> <div ~for="items" class="list no-flex horizontal"> <oda-checkbox :value="!$for.item.col.$hidden" @value-changed="_onChange($event, $for.item)"></oda-checkbox> <label class="label center" style="font-size: 10pt; padding: 0px 8px;">{{getLabel(item)}}</label> </div> </div> `, items: [],