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)
364 lines
10 KiB
JavaScript
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}`;
|
|
}
|
|
}
|