frappe-datatable
Version:
A modern datatable library for the web
497 lines (422 loc) • 15.1 kB
JavaScript
import $ from './dom';
import Sortable from 'sortablejs';
import {
linkProperties,
debounce
} from './utils';
export default class ColumnManager {
constructor(instance) {
this.instance = instance;
linkProperties(this, this.instance, [
'options',
'fireEvent',
'header',
'datamanager',
'cellmanager',
'style',
'wrapper',
'rowmanager',
'bodyScrollable',
'bodyRenderer'
]);
this.bindEvents();
}
renderHeader() {
this.header.innerHTML = '<div></div>';
this.refreshHeader();
}
refreshHeader() {
const columns = this.datamanager.getColumns();
// refresh html
$('div', this.header).innerHTML = this.getHeaderHTML(columns);
this.$filterRow = $('.dt-row-filter', this.header);
if (this.$filterRow) {
$.style(this.$filterRow, { display: 'none' });
}
// reset columnMap
this.$columnMap = [];
this.bindMoveColumn();
}
getHeaderHTML(columns) {
let html = this.rowmanager.getRowHTML(columns, {
isHeader: 1
});
if (this.options.inlineFilters) {
html += this.rowmanager.getRowHTML(columns, {
isFilter: 1
});
}
return html;
}
bindEvents() {
this.bindDropdown();
this.bindResizeColumn();
this.bindPerfectColumnWidth();
this.bindFilter();
}
bindDropdown() {
let toggleClass = '.dt-dropdown__toggle';
let dropdownClass = '.dt-dropdown__list';
// attach the dropdown list to container
this.instance.dropdownContainer.innerHTML = this.getDropdownListHTML();
this.$dropdownList = this.instance.dropdownContainer.firstElementChild;
$.on(this.header, 'click', toggleClass, e => {
this.openDropdown(e);
});
const deactivateDropdownOnBodyClick = (e) => {
const selector = [
toggleClass, toggleClass + ' *',
dropdownClass, dropdownClass + ' *'
].join(',');
if (e.target.matches(selector)) return;
deactivateDropdown();
};
$.on(document.body, 'click', deactivateDropdownOnBodyClick);
document.addEventListener('scroll', deactivateDropdown, true);
this.instance.on('onDestroy', () => {
$.off(document.body, 'click', deactivateDropdownOnBodyClick);
$.off(document, 'scroll', deactivateDropdown);
});
$.on(this.$dropdownList, 'click', '.dt-dropdown__list-item', (e, $item) => {
if (!this._dropdownActiveColIndex) return;
const dropdownItems = this.options.headerDropdown;
const { index } = $.data($item);
const colIndex = this._dropdownActiveColIndex;
let callback = dropdownItems[index].action;
callback && callback.call(this.instance, this.getColumn(colIndex));
this.hideDropdown();
});
const _this = this;
function deactivateDropdown(e) {
_this.hideDropdown();
}
this.hideDropdown();
}
openDropdown(e) {
if (!this._dropdownWidth) {
$.style(this.$dropdownList, { display: '' });
this._dropdownWidth = $.style(this.$dropdownList, 'width');
}
$.style(this.$dropdownList, {
display: '',
left: (e.clientX - this._dropdownWidth + 4) + 'px',
top: (e.clientY + 4) + 'px'
});
const $cell = $.closest('.dt-cell', e.target);
const { colIndex } = $.data($cell);
this._dropdownActiveColIndex = colIndex;
}
hideDropdown() {
$.style(this.$dropdownList, {
display: 'none'
});
this._dropdownActiveColIndex = null;
}
bindResizeColumn() {
let isDragging = false;
let $resizingCell, startWidth, startX;
$.on(this.header, 'mousedown', '.dt-cell .dt-cell__resize-handle', (e, $handle) => {
document.body.classList.add('dt-resize');
const $cell = $handle.parentNode.parentNode;
$resizingCell = $cell;
const {
colIndex
} = $.data($resizingCell);
const col = this.getColumn(colIndex);
if (col && col.resizable === false) {
return;
}
isDragging = true;
startWidth = $.style($('.dt-cell__content', $resizingCell), 'width');
startX = e.pageX;
});
const onMouseup = (e) => {
document.body.classList.remove('dt-resize');
if (!$resizingCell) return;
isDragging = false;
const {
colIndex
} = $.data($resizingCell);
this.setColumnWidth(colIndex);
this.style.setBodyStyle();
$resizingCell = null;
};
$.on(document.body, 'mouseup', onMouseup);
this.instance.on('onDestroy', () => {
$.off(document.body, 'mouseup', onMouseup);
});
const onMouseMove = (e) => {
if (!isDragging) return;
let delta = e.pageX - startX;
if (this.options.direction === 'rtl') {
delta = -1 * delta;
}
const finalWidth = startWidth + delta;
const {
colIndex
} = $.data($resizingCell);
let columnMinWidth = this.options.minimumColumnWidth;
if (columnMinWidth > finalWidth) {
// don't resize past 30 pixels
return;
}
this.datamanager.updateColumn(colIndex, {
width: finalWidth
});
this.setColumnHeaderWidth(colIndex);
};
$.on(document.body, 'mousemove', onMouseMove);
this.instance.on('onDestroy', () => {
$.off(document.body, 'mousemove', onMouseMove);
});
}
bindPerfectColumnWidth() {
$.on(this.header, 'dblclick', '.dt-cell .dt-cell__resize-handle', (e, $handle) => {
const $cell = $handle.parentNode.parentNode;
const { colIndex } = $.data($cell);
let longestCell = this.bodyRenderer.visibleRows
.map(d => d[colIndex])
.reduce((acc, curr) => acc.content.length > curr.content.length ? acc : curr);
let $longestCellHTML = this.cellmanager.getCellHTML(longestCell);
let $div = document.createElement('div');
$div.innerHTML = $longestCellHTML;
let cellText = $div.querySelector('.dt-cell__content').textContent;
let {
borderLeftWidth,
borderRightWidth,
paddingLeft,
paddingRight
} = $.getStyle(this.bodyScrollable.querySelector('.dt-cell__content'));
let padding = [borderLeftWidth, borderRightWidth, paddingLeft, paddingRight]
.map(parseFloat)
.reduce((sum, val) => sum + val);
let width = $.measureTextWidth(cellText) + padding;
this.datamanager.updateColumn(colIndex, { width });
this.setColumnHeaderWidth(colIndex);
this.setColumnWidth(colIndex);
});
}
bindMoveColumn() {
if (this.options.disableReorderColumn) return;
const $parent = $('.dt-row', this.header);
this.sortable = Sortable.create($parent, {
onEnd: (e) => {
const {
oldIndex,
newIndex
} = e;
const $draggedCell = e.item;
const {
colIndex
} = $.data($draggedCell);
if (+colIndex === newIndex) return;
this.switchColumn(oldIndex, newIndex);
},
preventOnFilter: false,
filter: '.dt-cell__resize-handle, .dt-dropdown',
chosenClass: 'dt-cell--dragging',
animation: 150
});
}
sortColumn(colIndex, nextSortOrder) {
this.instance.freeze();
this.sortRows(colIndex, nextSortOrder)
.then(() => {
this.refreshHeader();
return this.rowmanager.refreshRows();
})
.then(() => this.instance.unfreeze())
.then(() => {
this.fireEvent('onSortColumn', this.getColumn(colIndex));
this.setSortState();
});
}
saveSorting(colIndex) {
let currentColumn = this.getColumn(colIndex);
let saveSorting = {
[currentColumn.name]: {
colIndex: colIndex,
sortOrder: currentColumn.sortOrder
}
};
this.sortingKey = this.options.sortingKey ? `${this.options.sortingKey}::sortedColumns` : 'sortedColumns' ;
localStorage.setItem(this.sortingKey, JSON.stringify(saveSorting));
}
setSortState(sortOrder) {
if (sortOrder === 'none') {
this.sortState = false;
} else {
this.sortState = true;
}
}
removeColumn(colIndex) {
const removedCol = this.getColumn(colIndex);
this.instance.freeze();
this.datamanager.removeColumn(colIndex)
.then(() => {
this.refreshHeader();
return this.rowmanager.refreshRows();
})
.then(() => this.instance.unfreeze())
.then(() => {
this.fireEvent('onRemoveColumn', removedCol);
});
}
switchColumn(oldIndex, newIndex) {
this.instance.freeze();
this.datamanager.switchColumn(oldIndex, newIndex)
.then(() => {
this.refreshHeader();
return this.rowmanager.refreshRows();
})
.then(() => {
this.setColumnWidth(oldIndex);
this.setColumnWidth(newIndex);
this.instance.unfreeze();
})
.then(() => {
this.fireEvent('onSwitchColumn',
this.getColumn(oldIndex), this.getColumn(newIndex)
);
});
}
toggleFilter(flag) {
if (!this.options.inlineFilters) return;
let showFilter;
if (flag === undefined) {
showFilter = !this.isFilterShown;
} else {
showFilter = flag;
}
if (showFilter) {
$.style(this.$filterRow, { display: '' });
} else {
$.style(this.$filterRow, { display: 'none' });
}
this.isFilterShown = showFilter;
this.style.setBodyStyle();
}
focusFilter(colIndex) {
if (!this.isFilterShown) return;
const $filterInput = $(`.dt-cell--col-${colIndex} .dt-filter`, this.$filterRow);
$filterInput.focus();
}
bindFilter() {
if (!this.options.inlineFilters) return;
const handler = e => {
this.applyFilter(this.getAppliedFilters());
};
$.on(this.header, 'keydown', '.dt-filter', debounce(handler, 300));
}
applyFilter(filters) {
this.datamanager.filterRows(filters)
.then(({
rowsToShow
}) => {
this.rowmanager.showRows(rowsToShow);
});
}
getAppliedFilters() {
const filters = {};
$.each('.dt-filter', this.header).map((input) => {
const value = input.value;
if (value) {
filters[input.dataset.colIndex] = value;
}
});
return filters;
}
applyDefaultSortOrder() {
// sort rows if any 1 column has a default sortOrder set
const columnsToSort = this.getColumns().filter(col => col.sortOrder !== 'none');
if (columnsToSort.length === 1) {
const column = columnsToSort[0];
this.sortColumn(column.colIndex, column.sortOrder);
}
}
applySavedSortOrder() {
let key = this.options.sortingKey ? `${this.options.sortingKey}::sortedColumns` : 'sortedColumns' ;
let sortingConfig = JSON.parse(localStorage.getItem(key));
if (sortingConfig) {
const columnsToSort = Object.values(sortingConfig);
for (let column of columnsToSort) {
this.sortColumn(column.colIndex, column.sortOrder);
this.sortState = true;
}
}
}
sortRows(colIndex, sortOrder) {
return this.datamanager.sortRows(colIndex, sortOrder);
}
getColumn(colIndex) {
return this.datamanager.getColumn(colIndex);
}
getColumns() {
return this.datamanager.getColumns();
}
setColumnWidth(colIndex, width) {
colIndex = +colIndex;
let columnWidth = width || this.getColumn(colIndex).width;
const selector = [
`.dt-cell__content--col-${colIndex}`,
`.dt-cell__edit--col-${colIndex}`
].join(', ');
const styles = {
width: columnWidth + 'px'
};
this.style.setStyle(selector, styles);
}
setColumnHeaderWidth(colIndex) {
colIndex = +colIndex;
this.$columnMap = this.$columnMap || [];
const selector = `.dt-cell__content--header-${colIndex}`;
const {
width
} = this.getColumn(colIndex);
let $column = this.$columnMap[colIndex];
if (!$column) {
$column = this.header.querySelector(selector);
this.$columnMap[colIndex] = $column;
}
$column.style.width = width + 'px';
}
getColumnMinWidth(colIndex) {
colIndex = +colIndex;
return this.getColumn(colIndex).minWidth || 24;
}
getFirstColumnIndex() {
return this.datamanager.getColumnIndexById('_rowIndex') + 1;
}
getHeaderCell$(colIndex) {
return $(`.dt-cell--header-${colIndex}`, this.header);
}
getLastColumnIndex() {
return this.datamanager.getColumnCount() - 1;
}
getDropdownHTML() {
const { dropdownButton } = this.options;
return `
<div class="dt-dropdown">
<div class="dt-dropdown__toggle">${dropdownButton}</div>
</div>
`;
}
getDropdownListHTML() {
const { headerDropdown: dropdownItems } = this.options;
return `
<div class="dt-dropdown__list">
${dropdownItems.map((d, i) => `
<div
class="dt-dropdown__list-item${d.display ? ' dt-hidden' : ''}"
data-index="${i}"
>
${d.label}
</div>
`).join('')}
</div>
`;
}
toggleDropdownItem(index) {
$('.dt-dropdown__list', this.instance.dropdownContainer).children[index].classList.toggle('dt-hidden');
}
}