oda-framework
Version:
1,491 lines (1,436 loc) • 104 kB
JavaScript
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: [],