datatable/src/rowmanager.js
phot0n 430d7cfe48 fix: use custom formatter when filtering rows
As datatable renders only some rows at a time and sets each
cell's html property based on that rendering, so when someone
filters things and only looks at some rows without scrolling the whole
grid and tries to filter things (when there is a custom cell formatter),
they won't be able to see the data as the filtering is done based on cell's
content not html (as it's not set at that point)
2022-06-10 11:51:11 +05:30

364 lines
10 KiB
JavaScript

import $ from './dom';
import {
makeDataAttributeString,
nextTick,
ensureArray,
linkProperties,
uniq,
numberSortAsc
} from './utils';
export default class RowManager {
constructor(instance) {
this.instance = instance;
linkProperties(this, this.instance, [
'options',
'fireEvent',
'wrapper',
'bodyScrollable',
'bodyRenderer',
'style'
]);
this.bindEvents();
this.refreshRows = nextTick(this.refreshRows, this);
}
get datamanager() {
return this.instance.datamanager;
}
get cellmanager() {
return this.instance.cellmanager;
}
bindEvents() {
this.bindCheckbox();
}
bindCheckbox() {
if (!this.options.checkboxColumn) return;
// map of checked rows
this.checkMap = [];
$.on(this.wrapper, 'click', '.dt-cell--col-0 [type="checkbox"]', (e, $checkbox) => {
const $cell = $checkbox.closest('.dt-cell');
const {
rowIndex,
isHeader
} = $.data($cell);
const checked = $checkbox.checked;
if (isHeader) {
this.checkAll(checked);
} else {
this.checkRow(rowIndex, checked);
}
});
}
refreshRows() {
this.instance.renderBody();
this.instance.setDimensions();
}
refreshRow(row, rowIndex) {
const _row = this.datamanager.updateRow(row, rowIndex);
_row.forEach(cell => {
this.cellmanager.refreshCell(cell, true);
});
}
getCheckedRows() {
if (!this.checkMap) {
return [];
}
let out = [];
for (let rowIndex in this.checkMap) {
const checked = this.checkMap[rowIndex];
if (checked === 1) {
out.push(rowIndex);
}
}
return out;
}
highlightCheckedRows() {
this.getCheckedRows()
.map(rowIndex => this.checkRow(rowIndex, true));
}
checkRow(rowIndex, toggle) {
const value = toggle ? 1 : 0;
const selector = rowIndex => `.dt-cell--0-${rowIndex} [type="checkbox"]`;
// update internal map
this.checkMap[rowIndex] = value;
// set checkbox value explicitly
$.each(selector(rowIndex), this.bodyScrollable)
.map(input => {
input.checked = toggle;
});
// highlight row
this.highlightRow(rowIndex, toggle);
this.showCheckStatus();
this.fireEvent('onCheckRow', this.datamanager.getRow(rowIndex));
}
checkAll(toggle) {
const value = toggle ? 1 : 0;
// update internal map
if (toggle) {
this.checkMap = Array.from(Array(this.getTotalRows())).map(c => value);
} else {
this.checkMap = [];
}
// set checkbox value
$.each('.dt-cell--col-0 [type="checkbox"]', this.bodyScrollable)
.map(input => {
input.checked = toggle;
});
// highlight all
this.highlightAll(toggle);
this.showCheckStatus();
this.fireEvent('onCheckRow');
}
showCheckStatus() {
if (!this.options.checkedRowStatus) return;
const checkedRows = this.getCheckedRows();
const count = checkedRows.length;
if (count > 0) {
let message = this.instance.translate('{count} rows selected', {
count: count
});
this.bodyRenderer.showToastMessage(message);
} else {
this.bodyRenderer.clearToastMessage();
}
}
highlightRow(rowIndex, toggle = true) {
const $row = this.getRow$(rowIndex);
if (!$row) return;
if (!toggle && this.bodyScrollable.classList.contains('dt-scrollable--highlight-all')) {
$row.classList.add('dt-row--unhighlight');
return;
}
if (toggle && $row.classList.contains('dt-row--unhighlight')) {
$row.classList.remove('dt-row--unhighlight');
}
this._highlightedRows = this._highlightedRows || {};
if (toggle) {
$row.classList.add('dt-row--highlight');
this._highlightedRows[rowIndex] = $row;
} else {
$row.classList.remove('dt-row--highlight');
delete this._highlightedRows[rowIndex];
}
}
highlightAll(toggle = true) {
if (toggle) {
this.bodyScrollable.classList.add('dt-scrollable--highlight-all');
} else {
this.bodyScrollable.classList.remove('dt-scrollable--highlight-all');
for (const rowIndex in this._highlightedRows) {
const $row = this._highlightedRows[rowIndex];
$row.classList.remove('dt-row--highlight');
}
this._highlightedRows = {};
}
}
showRows(rowIndices) {
rowIndices = ensureArray(rowIndices);
const rows = rowIndices.map(rowIndex => this.datamanager.getRow(rowIndex));
this.bodyRenderer.renderRows(rows);
}
showAllRows() {
const rowIndices = this.datamanager.getAllRowIndices();
this.showRows(rowIndices);
}
getChildrenToShowForNode(rowIndex) {
const row = this.datamanager.getRow(rowIndex);
row.meta.isTreeNodeClose = false;
return this.datamanager.getImmediateChildren(rowIndex);
}
openSingleNode(rowIndex) {
const childrenToShow = this.getChildrenToShowForNode(rowIndex);
const visibleRowIndices = this.bodyRenderer.visibleRowIndices;
const rowsToShow = uniq([...childrenToShow, ...visibleRowIndices]).sort(numberSortAsc);
this.showRows(rowsToShow);
}
getChildrenToHideForNode(rowIndex) {
const row = this.datamanager.getRow(rowIndex);
row.meta.isTreeNodeClose = true;
const rowsToHide = this.datamanager.getChildren(rowIndex);
rowsToHide.forEach(rowIndex => {
const row = this.datamanager.getRow(rowIndex);
if (!row.meta.isLeaf) {
row.meta.isTreeNodeClose = true;
}
});
return rowsToHide;
}
closeSingleNode(rowIndex) {
const rowsToHide = this.getChildrenToHideForNode(rowIndex);
const visibleRows = this.bodyRenderer.visibleRowIndices;
const rowsToShow = visibleRows
.filter(rowIndex => !rowsToHide.includes(rowIndex))
.sort(numberSortAsc);
this.showRows(rowsToShow);
}
expandAllNodes() {
let rows = this.datamanager.getRows();
let rootNodes = rows.filter(row => !row.meta.isLeaf);
const childrenToShow = rootNodes.map(row => this.getChildrenToShowForNode(row.meta.rowIndex)).flat();
const visibleRowIndices = this.bodyRenderer.visibleRowIndices;
const rowsToShow = uniq([...childrenToShow, ...visibleRowIndices]).sort(numberSortAsc);
this.showRows(rowsToShow);
}
collapseAllNodes() {
let rows = this.datamanager.getRows();
let rootNodes = rows.filter(row => row.meta.indent === 0);
const rowsToHide = rootNodes.map(row => this.getChildrenToHideForNode(row.meta.rowIndex)).flat();
const visibleRows = this.bodyRenderer.visibleRowIndices;
const rowsToShow = visibleRows
.filter(rowIndex => !rowsToHide.includes(rowIndex))
.sort(numberSortAsc);
this.showRows(rowsToShow);
}
setTreeDepth(depth) {
let rows = this.datamanager.getRows();
const rowsToOpen = rows.filter(row => row.meta.indent < depth);
const rowsToClose = rows.filter(row => row.meta.indent >= depth);
const rowsToHide = rowsToClose.filter(row => row.meta.indent > depth);
rowsToClose.forEach(row => {
if (!row.meta.isLeaf) {
row.meta.isTreeNodeClose = true;
}
});
rowsToOpen.forEach(row => {
if (!row.meta.isLeaf) {
row.meta.isTreeNodeClose = false;
}
});
const rowsToShow = rows
.filter(row => !rowsToHide.includes(row))
.map(row => row.meta.rowIndex)
.sort(numberSortAsc);
this.showRows(rowsToShow);
}
getRow$(rowIndex) {
return $(this.selector(rowIndex), this.bodyScrollable);
}
getTotalRows() {
return this.datamanager.getRowCount();
}
getFirstRowIndex() {
return 0;
}
getLastRowIndex() {
return this.datamanager.getRowCount() - 1;
}
scrollToRow(rowIndex) {
rowIndex = +rowIndex;
this._lastScrollTo = this._lastScrollTo || 0;
const $row = this.getRow$(rowIndex);
if ($.inViewport($row, this.bodyScrollable)) return;
const {
height
} = $row.getBoundingClientRect();
const {
top,
bottom
} = this.bodyScrollable.getBoundingClientRect();
const rowsInView = Math.floor((bottom - top) / height);
let offset = 0;
if (rowIndex > this._lastScrollTo) {
offset = height * ((rowIndex + 1) - rowsInView);
} else {
offset = height * ((rowIndex + 1) - 1);
}
this._lastScrollTo = rowIndex;
$.scrollTop(this.bodyScrollable, offset);
}
getRowHTML(row, props) {
const dataAttr = makeDataAttributeString(props);
let rowIdentifier = props.rowIndex;
if (props.isFilter) {
row = row.map(cell => (Object.assign({}, cell, {
content: this.getFilterInput({
colIndex: cell.colIndex,
name: cell.name
}),
isFilter: 1,
isHeader: undefined,
editable: false
})));
rowIdentifier = 'filter';
}
if (props.isHeader) {
rowIdentifier = 'header';
}
return `
<div class="dt-row dt-row-${rowIdentifier}" ${dataAttr}>
${row.map(cell => this.cellmanager.getCellHTML(cell)).join('')}
</div>
`;
}
getFilterInput(props) {
let title = `title="Filter based on ${props.name || 'Index'}"`;
const dataAttr = makeDataAttributeString(props);
return `<input class="dt-filter dt-input" type="text" ${dataAttr} tabindex="1"
${props.colIndex === 0 ? 'disabled' : title} />`;
}
selector(rowIndex) {
return `.dt-row-${rowIndex}`;
}
}