@siberiaweb/components
Version:
1,358 lines (1,357 loc) • 69.5 kB
JavaScript
import Col from "./Col";
import CSS from "./CSS";
import DataCell from "./DataCell";
import DataRow from "./DataRow";
import HTMLElementUtils from "../utils/HTMLElementUtils";
import Icon from "../icon/Icon";
import LocalStorage from "@siberiaweb/local-storage/lib/LocalStorage";
import SelectCellEvent from "./SelectCellEvent";
import SelectRowEvent from "./SelectRowEvent";
import WebComponent from "@siberiaweb/webcomponent/lib/WebComponent";
import * as moment from "moment";
import "./CustomGrid.css";
/**
* Настраиваемая сетка.
*
* @template TRecord Тип записи.
*/
export default class CustomGrid extends WebComponent {
/**
* Конструктор.
*/
constructor() {
super();
/**
* Высота строки.
*/
this._rowHeight = CustomGrid.DEFAULT_ROW_HEIGHT;
/**
* Столбцы.
*/
this.cols = [];
/**
* Изначальный порядок столбцов.
*/
this.colsInitialOrder = new Map();
/**
* Изначально скрытые столбцы.
*/
this.colsInitialHidden = new Map();
/**
* Столбцы с измененной шириной.
*/
this.resizedCols = new Set();
/**
* Набор данных.
*/
this.dataSet = [];
/**
* Выведенные записи: ( запись, строка ).
*/
this.renderedRecords = new Map();
/**
* Столбец сортировки.
*/
this.sortCol = null;
/**
* Выбранная строка.
*/
this.selectedRow = null;
/**
* Выбранная ячейка.
*/
this.selectedCell = null;
/**
* Индекс первой выведенной записи.
*/
this.firstRenderedRecordIndex = 0;
/**
* Индекс последней выведенной записи.
*/
this.lastRenderedRecordIndex = 0;
/**
* Обработчик вывода строки.
*/
this.onRowRender = null;
/**
* Обработчик получения значения поля.
*/
this.onGetFieldValue = null;
/**
* Обработчик получения значения поля для вывода на экран.
*/
this.onGetDisplayValue = null;
/**
* Обработчик вывода ячейки.
*/
this.onCellRender = null;
/**
* Обработчик вывода значения в ячейку.
*/
this.onCellOutputValue = null;
/**
* Обработчик настройки ячейки.
*/
this.onCellCustomize = null;
/**
* Обработчик настройки строки.
*/
this.onRowCustomize = null;
/**
* Обработчик проверки, что запись может быть выбрана.
*/
this.onCheckRecordSelectable = null;
/**
* Обработчик сохранения настроек.
*/
this.onSaveSettings = null;
this.table = this.createTable();
this.head = this.createHead();
this.headMainRow = this.createHeadMainRow();
this.colResizeBorder = this.createColResizeBorder();
this.colDragImage = this.createColDragImage();
this.colDropIconAbove = this.createColDropIconAbove();
this.colDropIconBelow = this.createColDropIconBelow();
this.loadIndicator = this.createLoadIndicator();
this.importHeadRows(this.querySelector("template[ data-leading-rows ]"));
this.head.appendChild(this.headMainRow);
this.importHeadRows(this.querySelector("template[ data-trailing-rows ]"));
this.table.appendChild(this.head);
let cols = Array.from(this.querySelectorAll(Col.COMPONENT_NAME));
for (const col of cols) {
this.addCol(col);
}
this.lightDOMFragment.appendChild(this.table);
this.initCustomGridHost();
this.initResizeObserver();
}
/**
* Наблюдаемые атрибуты.
*/
static get observedAttributes() {
return WebComponent.observedAttributes.concat([
CustomGrid.ATTR_AUTOFOCUS,
CustomGrid.ATTR_COL_DRAG_DISABLED,
CustomGrid.ATTR_LOADING,
CustomGrid.ATTR_LOCALE,
CustomGrid.ATTR_READY,
CustomGrid.ATTR_ROW_HEIGHT,
CustomGrid.ATTR_SELECTION_TYPE
]);
}
/**
* Создание таблицы.
*/
createTable() {
return document.createElement("table");
}
/**
* Создание заголовка таблицы.
*/
createHead() {
return document.createElement("thead");
}
/**
* Создание основной строки, содержащей заголовки столбцов.
*/
createHeadMainRow() {
return document.createElement("tr");
}
/**
* Создание отображаемой границы при изменении ширины столбца.
*/
createColResizeBorder() {
let container = document.createElement("div");
container.classList.add(CSS.COL_RESIZE_BORDER);
return container;
}
/**
* Создание контейнера изображения при перемещении столбца.
*/
createColDragImage() {
let container = document.createElement("div");
container.classList.add(CSS.COL_DRAG_IMAGE);
return container;
}
/**
* Создание верхнего значка, отображаемого при перемещении столбца.
*/
createColDropIconAbove() {
let icon = document.createElement(Icon.COMPONENT_NAME);
icon.classList.add(CSS.COL_DROP_ICON_ABOVE);
return icon;
}
/**
* Создание нижнего значка, отображаемого при перемещении столбца.
*/
createColDropIconBelow() {
let icon = document.createElement(Icon.COMPONENT_NAME);
icon.classList.add(CSS.COL_DROP_ICON_BELOW);
return icon;
}
/**
* Создание индикатора загрузки данных.
*/
createLoadIndicator() {
let container = document.createElement("div");
container.classList.add(CSS.LOAD_INDICATOR);
return container;
}
/**
* Инициализация изменения размера столбца.
*
* @param col Столбец.
*/
initColResize(col) {
col.getResizer().addEventListener("dblclick", () => {
col.setWidth(col.getDefaultWidth());
this.resizedCols.delete(col);
this.saveSettings();
});
const ATTR_SW_GRID_COL_RESIZING = "sw-grid-col-resizing";
let pageX = 0;
let scrollLeft = 0;
let width = 0;
let colResizerBorderStartOffsetLeft = 0;
let scrollListener = () => {
this.colResizeBorder.style.left = this.colResizeBorder.offsetLeft + this.scrollLeft - scrollLeft + "px";
scrollLeft = this.scrollLeft;
};
let documentClickListener = (event) => {
event.stopImmediatePropagation();
};
let documentMouseMoveListener = (event) => {
this.colResizeBorder.style.left = this.colResizeBorder.offsetLeft + event.pageX - pageX + "px";
pageX = event.pageX;
};
let documentMouseUpListener = () => {
document.removeEventListener("mousemove", documentMouseMoveListener);
document.removeEventListener("mouseup", documentMouseUpListener);
setTimeout(() => {
document.removeEventListener("click", documentClickListener, {
capture: true
});
});
this.removeEventListener("scroll", scrollListener);
let newWidth = width + this.colResizeBorder.offsetLeft - colResizerBorderStartOffsetLeft;
col.setWidth(newWidth);
this.resizedCols.add(col);
this.colResizeBorder.remove();
if (this.selectedCell !== null) {
this.scrollDataIntoView(this.selectedCell);
}
else if (this.selectedRow !== null) {
this.scrollDataIntoView(this.selectedRow);
}
this.scrollLeft = scrollLeft;
document.body.toggleAttribute(ATTR_SW_GRID_COL_RESIZING, false);
this.saveSettings();
};
col.getResizer().addEventListener("click", (event) => {
event.stopImmediatePropagation();
});
col.getResizer().addEventListener("mousedown", (event) => {
if ((event.button === 0) && !(event.altKey || event.ctrlKey || event.shiftKey)) {
event.preventDefault();
event.stopImmediatePropagation();
document.body.toggleAttribute(ATTR_SW_GRID_COL_RESIZING, true);
pageX = event.pageX;
scrollLeft = this.scrollLeft;
width = col.getCell().offsetWidth;
this.colResizeBorder.style.height = this.scrollHeight + "px";
this.colResizeBorder.style.left = col.getResizer().getBoundingClientRect().right -
this.getBoundingClientRect().left + this.scrollLeft + "px";
this.appendChild(this.colResizeBorder);
colResizerBorderStartOffsetLeft = this.colResizeBorder.offsetLeft;
document.addEventListener("mousemove", documentMouseMoveListener);
document.addEventListener("mouseup", documentMouseUpListener);
document.addEventListener("click", documentClickListener, {
capture: true
});
this.addEventListener("scroll", scrollListener);
}
});
}
/**
* Инициализация сортировки столбца.
*
* @param col Столбец.
*/
initColSort(col) {
col.getCell().addEventListener("click", (event) => {
if (col.sortDisabled) {
return;
}
if ((event.button === 0) && !(event.altKey || event.ctrlKey || event.shiftKey)) {
if (col === this.sortCol) {
col.setNextSortOrder();
}
else {
col.sortOrder = "asc";
}
this.sort(col);
}
});
}
/**
* Инициализация перемещения столбца.
*
* @param col Столбец.
*/
initColDrag(col) {
col.getCell().draggable = !this.colDragDisabled;
col.getCell().addEventListener("dragstart", (event) => {
if ((event.target !== col.getCell()) || (event.dataTransfer === null)) {
return;
}
event.dataTransfer.setData("column", col.id);
event.dataTransfer.dropEffect = "move";
event.dataTransfer.effectAllowed = "move";
this.colDragImage.innerHTML = "";
let imageWrapper = document.createElement("div");
imageWrapper.appendChild(col.getCellContentTemplate().content);
this.colDragImage.appendChild(imageWrapper);
col.getCell().appendChild(this.colDragImage);
let cellRect = col.getCell().getBoundingClientRect();
this.colDragImage.style.height = cellRect.height + "px";
this.colDragImage.style.width = cellRect.width + "px";
let dragImageRect = this.colDragImage.getBoundingClientRect();
event.dataTransfer.setDragImage(this.colDragImage, dragImageRect.width / 2, dragImageRect.height / 2);
});
col.getCell().addEventListener("dragover", (event) => {
if ((event.dataTransfer === null) || !event.dataTransfer.types.includes("column")) {
return;
}
event.preventDefault();
if (!this.colDropIconAbove.isConnected) {
this.appendChild(this.colDropIconAbove);
}
if (!this.colDropIconBelow.isConnected) {
this.appendChild(this.colDropIconBelow);
}
let colRect = col.getCell().getBoundingClientRect();
this.colDropIconAbove.style.top = colRect.top + "px";
this.colDropIconBelow.style.top = colRect.bottom + "px";
if (event.pageX < (colRect.left + colRect.width / 2)) {
this.colDropIconAbove.style.left = colRect.left + "px";
this.colDropIconBelow.style.left = colRect.left + "px";
}
else {
this.colDropIconAbove.style.left = colRect.right + "px";
this.colDropIconBelow.style.left = colRect.right + "px";
}
});
col.getCell().addEventListener("dragleave", (event) => {
if ((event.relatedTarget instanceof Node) && this.headMainRow.contains(event.relatedTarget)) {
return;
}
this.colDropIconAbove.remove();
this.colDropIconBelow.remove();
});
col.getCell().addEventListener("dragend", () => {
this.colDragImage.remove();
this.colDropIconAbove.remove();
this.colDropIconBelow.remove();
});
col.getCell().addEventListener("drop", (event) => {
this.colDragImage.remove();
this.colDropIconAbove.remove();
this.colDropIconBelow.remove();
if (event.dataTransfer === null) {
return;
}
let dragCol = document.getElementById(event.dataTransfer.getData("column"));
if (col === dragCol) {
return;
}
let colRect = col.getCell().getBoundingClientRect();
if (event.pageX < (colRect.left + colRect.width / 2)) {
this.moveColBefore(dragCol, col);
}
else {
this.moveColAfter(dragCol, col);
}
});
}
/**
* Инициализация наблюдателя за изменением размера.
*/
initResizeObserver() {
try {
let blockSize = this.offsetHeight;
new ResizeObserver((entries) => {
if ((entries.length > 0) &&
(entries[0].borderBoxSize !== undefined) &&
(entries[0].borderBoxSize.length > 0)) {
if (blockSize !== entries[0].borderBoxSize[0].blockSize) {
blockSize = entries[0].borderBoxSize[0].blockSize;
this.renderBody();
}
}
})
.observe(this, {
box: "border-box"
});
}
catch (e) {
console.warn(e);
}
}
/**
* Инициализация хоста.
*/
initCustomGridHost() {
this.addEventListener("scroll", () => {
if (this.loadIndicator.isConnected) {
this.loadIndicator.style.transform = "translate(" + this.scrollLeft + "px," + this.scrollTop +
"px)";
}
this.renderBody();
});
this.addEventListener("keydown", (event) => {
if (this.selectionType === "cell") {
if ((event.key === "ArrowLeft") && !(event.altKey || event.ctrlKey || event.shiftKey)) {
event.preventDefault();
let cell = this.getNearestSelectableCell(-1, 0);
if (cell !== null) {
this.selectCell(cell.col, cell.record);
}
}
if ((event.key === "ArrowRight") && !(event.altKey || event.ctrlKey || event.shiftKey)) {
event.preventDefault();
let cell = this.getNearestSelectableCell(1, 0);
if (cell !== null) {
this.selectCell(cell.col, cell.record);
}
}
if ((event.key === "ArrowUp") && !(event.altKey || event.ctrlKey || event.shiftKey)) {
event.preventDefault();
let cell = this.getNearestSelectableCell(0, -1);
if (cell !== null) {
this.selectCell(cell.col, cell.record);
}
else {
this.scrollTop -= this._rowHeight;
}
}
if ((event.key === "ArrowDown") && !(event.altKey || event.ctrlKey || event.shiftKey)) {
event.preventDefault();
let cell = this.getNearestSelectableCell(0, 1);
if (cell !== null) {
this.selectCell(cell.col, cell.record);
}
else {
this.scrollTop += this._rowHeight;
}
}
if ((event.key === "PageUp") && !(event.altKey || event.ctrlKey || event.shiftKey)) {
event.preventDefault();
let skipRecordCount = this.getMaxDisplayRowCountInViewport();
if (skipRecordCount > 1) {
skipRecordCount--;
}
let cell = this.getNearestSelectableCell(0, -skipRecordCount);
if (cell !== null) {
this.selectCell(cell.col, cell.record);
}
else {
this.scrollTop -= this.getTableBodyViewportHeight();
}
}
if ((event.key === "PageDown") && !(event.altKey || event.ctrlKey || event.shiftKey)) {
event.preventDefault();
let skipRecordCount = this.getMaxDisplayRowCountInViewport();
if (skipRecordCount > 1) {
skipRecordCount--;
}
let cell = this.getNearestSelectableCell(0, skipRecordCount);
if (cell !== null) {
this.selectCell(cell.col, cell.record);
}
else {
this.scrollTop += this.getTableBodyViewportHeight();
}
}
if ((event.key === "Home") && event.ctrlKey && !(event.altKey || event.shiftKey)) {
event.preventDefault();
let cell = this.getNearestSelectableCell(0, -this.dataSet.length);
if (cell !== null) {
this.selectCell(cell.col, cell.record);
}
}
if ((event.key === "End") && event.ctrlKey && !(event.altKey || event.shiftKey)) {
event.preventDefault();
let cell = this.getNearestSelectableCell(0, this.dataSet.length);
if (cell !== null) {
this.selectCell(cell.col, cell.record);
}
}
if ((event.key === "Home") && !(event.altKey || event.ctrlKey || event.shiftKey)) {
let cell = this.getNearestSelectableCell(-this.getVisibleCols().length, 0);
if (cell !== null) {
this.selectCell(cell.col, cell.record);
}
event.preventDefault();
}
if ((event.key === "End") && !(event.altKey || event.ctrlKey || event.shiftKey)) {
event.preventDefault();
let cell = this.getNearestSelectableCell(this.getVisibleCols().length, 0);
if (cell !== null) {
this.selectCell(cell.col, cell.record);
}
}
}
if (this.selectionType === "row") {
if ((event.key === "ArrowUp") && !(event.altKey || event.ctrlKey || event.shiftKey)) {
event.preventDefault();
if (this.selectedRow !== null) {
let record = this.getNearestSelectableRecord(this.selectedRow.record, -1);
if (record !== null) {
this.selectRow(record);
}
else {
this.scrollTop -= this._rowHeight;
}
}
}
if ((event.key === "ArrowDown") && !(event.altKey || event.ctrlKey || event.shiftKey)) {
event.preventDefault();
if (this.selectedRow !== null) {
let record = this.getNearestSelectableRecord(this.selectedRow.record, 1);
if (record !== null) {
this.selectRow(record);
}
else {
this.scrollTop += this._rowHeight;
}
}
}
if ((event.key === "PageUp") && !(event.altKey || event.ctrlKey || event.shiftKey)) {
event.preventDefault();
if (this.selectedRow !== null) {
let skipRecordCount = this.getMaxDisplayRowCountInViewport();
if (skipRecordCount > 1) {
skipRecordCount--;
}
let record = this.getNearestSelectableRecord(this.selectedRow.record, -skipRecordCount);
if (record !== null) {
this.selectRow(record);
}
else {
this.scrollTop -= this.getTableBodyViewportHeight();
}
}
}
if ((event.key === "PageDown") && !(event.altKey || event.ctrlKey || event.shiftKey)) {
event.preventDefault();
if (this.selectedRow !== null) {
let skipRecordCount = this.getMaxDisplayRowCountInViewport();
if (skipRecordCount > 1) {
skipRecordCount--;
}
let record = this.getNearestSelectableRecord(this.selectedRow.record, skipRecordCount);
if (record !== null) {
this.selectRow(record);
}
else {
this.scrollTop += this.getTableBodyViewportHeight();
}
}
}
if ((event.key === "Home") && !(event.altKey || event.shiftKey)) {
event.preventDefault();
if (this.selectedRow !== null) {
let record = this.getNearestSelectableRecord(this.selectedRow.record, -this.dataSet.length);
if (record !== null) {
this.selectRow(record);
}
}
}
if ((event.key === "End") && !(event.altKey || event.shiftKey)) {
event.preventDefault();
if (this.selectedRow !== null) {
let record = this.getNearestSelectableRecord(this.selectedRow.record, this.dataSet.length);
if (record !== null) {
this.selectRow(record);
}
}
}
}
});
}
/**
* Получение максимального низа заголовка таблицы.
*/
getHeadMaxBottom() {
let maxBottom = 0;
for (const row of this.head.rows) {
let bottom = row.getBoundingClientRect().bottom;
if (bottom > maxBottom) {
maxBottom = bottom;
}
for (const cell of row.cells) {
bottom = cell.getBoundingClientRect().bottom;
if (bottom > maxBottom) {
maxBottom = bottom;
}
}
}
return maxBottom;
}
/**
* Получение высоты порта вывода для тела таблицы.
*/
getTableBodyViewportHeight() {
let deltaY = this.getHeadMaxBottom() - this.getBoundingClientRect().top;
if (deltaY < 0) {
deltaY = 0;
}
return this.clientHeight - deltaY;
}
/**
* Получение максимального количества записей, которое можно отобразить в порте вывода.
*/
getMaxDisplayRowCountInViewport() {
return Math.ceil(this.getTableBodyViewportHeight() / this._rowHeight);
}
/**
* Получение ячейки данных.
*
* @param col Столбец.
* @param row Строка.
*/
getDataCell(col, row) {
for (const cell of row.cells) {
if ((cell instanceof DataCell) && (cell.col === col)) {
return cell;
}
}
return null;
}
/**
* @override
*/
firstConnectedCallback() {
super.firstConnectedCallback();
this.classList.add(CSS.GRID);
this.toggleAttribute(CustomGrid.ATTR_EMPTY, this.dataSet.length === 0);
if (!this.hasAttribute(CustomGrid.ATTR_TAB_INDEX)) {
this.tabIndex = 0;
}
if (!this.customSettings) {
if (this.settingsId) {
this.setSettings(LocalStorage.getObject(this.settingsId, {}));
}
else {
this.setSettings({});
}
}
}
/**
* Обработка изменения атрибута "autofocus".
*/
attrAutofocusChange() {
if (this.hasAttribute(CustomGrid.ATTR_AUTOFOCUS)) {
window.requestAnimationFrame(() => {
this.focus();
});
}
}
/**
* Обработка изменения атрибута "col-drag-disabled".
*/
attrColDragDisabledChange() {
for (const col of this.cols) {
col.getCell().draggable = !this.colDragDisabled;
}
}
/**
* Обработка изменения атрибута "loading".
*/
attrLoadingChange() {
if (this.loading) {
if (!this.loadIndicator.isConnected) {
this.loadIndicator.style.transform = `translate( ${this.scrollLeft}px, ${this.scrollTop}px`;
this.appendChild(this.loadIndicator);
}
}
else {
this.loadIndicator.remove();
}
}
/**
* Обработка изменения атрибута "locale".
*/
attrLocaleChange() {
this.renderBody(true);
}
/**
* Обработка изменения атрибута "ready".
*/
attrReadyChange() {
if (this.ready) {
this.renderTable();
}
}
/**
* Обработка изменения атрибута "row-height".
*
* @param newValue Новое значение.
*/
attrRowHeightChange(newValue) {
let value = newValue === null ? CustomGrid.DEFAULT_ROW_HEIGHT : parseInt(newValue);
if (isNaN(value)) {
value = CustomGrid.DEFAULT_ROW_HEIGHT;
}
else if (value < CustomGrid.MIN_ROW_HEIGHT) {
value = CustomGrid.MIN_ROW_HEIGHT;
}
this._rowHeight = value;
this.renderBody(true);
}
/**
* Обработка изменения атрибута "selection-type".
*/
attrSelectionTypeChange() {
this.unselectRow();
this.unselectCell();
this.renderBody(true);
}
/**
* @override
*/
attributeChangedCallback(name, oldValue, newValue) {
super.attributeChangedCallback(name, oldValue, newValue);
switch (name) {
case CustomGrid.ATTR_AUTOFOCUS:
this.attrAutofocusChange();
break;
case CustomGrid.ATTR_LOCALE:
this.attrLocaleChange();
break;
case CustomGrid.ATTR_LOADING:
this.attrLoadingChange();
break;
case CustomGrid.ATTR_COL_DRAG_DISABLED:
this.attrColDragDisabledChange();
break;
case CustomGrid.ATTR_READY:
this.attrReadyChange();
break;
case CustomGrid.ATTR_ROW_HEIGHT:
this.attrRowHeightChange(newValue);
break;
case CustomGrid.ATTR_SELECTION_TYPE:
this.attrSelectionTypeChange();
break;
}
}
/**
* Сохранение настроек.
*/
saveSettings() {
let settings = {};
settings.cols = {};
for (let i = 0; i < this.cols.length; i++) {
let col = this.cols[i];
settings.cols[col.name] = {
index: i,
width: this.resizedCols.has(col) ? col.getCell().offsetWidth : undefined,
hidden: col.hidden
};
}
if (this.customSettings) {
if (this.onSaveSettings !== null) {
this.onSaveSettings(settings);
}
}
else if (this.settingsId) {
LocalStorage.saveObject(this.settingsId, settings);
}
}
/**
* Получение значения поля.
*
* @param col Столбец.
* @param record Запись.
*/
getFieldValue(col, record) {
let value = null;
if (col.fieldName) {
value = record[col.fieldName];
}
if (this.onGetFieldValue !== null) {
value = this.onGetFieldValue(col, record, value);
}
return value;
}
/**
* Форматирование логического значения.
*
* @param value Значение.
* @param format Формат вывода.
*/
formatBoolean(value, format) {
let displayValue = value ? "true" : "false";
let patterns = format.split(" ");
if (patterns.length === 2) {
displayValue = value ? patterns[0] : patterns[1];
}
return displayValue;
}
/**
* Форматирование даты.
*
* @param value Значение.
* @param format Формат вывода.
*/
formatDate(value, format) {
return moment(value).locale(this.locale).format(format);
}
/**
* Форматирование даты, представленной в виде строки.
*
* @param value Значение.
* @param pattern Шаблон.
* @param format Формат вывода.
*/
formatDateString(value, pattern, format) {
return moment(value, pattern).locale(this.locale).format(format);
}
/**
* Форматирование числа.
*
* @param value Значение.
*/
formatNumber(value) {
return value.toLocaleString(this.locale);
}
/**
* Форматирование валюты.
*
* @param value Значение.
*/
formatCurrency(value) {
return value.toLocaleString(this.locale, {
minimumFractionDigits: 2
});
}
/**
* Получение значения поля для вывода на экран.
*
* @param col Столбец.
* @param value Значение.
* @param record Запись.
*/
getDisplayValue(col, value, record) {
let displayValue = value;
switch (col.fieldType) {
case "boolean":
if (col.format) {
displayValue = this.formatBoolean(value, col.format);
}
break;
case "date":
if (col.format) {
displayValue = this.formatDate(value, col.format);
}
break;
case "date-string":
if (col.pattern && col.format) {
displayValue = this.formatDateString(value, col.pattern, col.format);
}
break;
case "number":
if (col.format === "currency") {
displayValue = this.formatCurrency(parseFloat(value));
}
else if (col.format === "locale") {
displayValue = this.formatNumber(parseFloat(value));
}
break;
}
if (this.onGetDisplayValue !== null) {
displayValue = this.onGetDisplayValue(col, displayValue, record);
}
return displayValue;
}
/**
* Проверка, что запись может быть выбрана.
*
* @param record Запись.
*/
isRecordSelectable(record) {
return (this.onCheckRecordSelectable === null) || this.onCheckRecordSelectable(record);
}
/**
* Прокрутка контейнера сетки, чтобы строка или ячейка данных были в поле видимости.
*
* @param element Строка или ячейка.
* @param options Настройка прокрутки. Опционально.
*/
scrollDataIntoView(element, options = {
block: "nearest",
inline: "nearest",
behavior: "auto"
}) {
let hostRect = this.getBoundingClientRect();
let headMaxBottom = this.getHeadMaxBottom();
if (headMaxBottom < hostRect.top) {
this.style.setProperty(CSS.VAR_DATA_SCROLL_MARGIN_TOP, "0");
}
else {
this.style.setProperty(CSS.VAR_DATA_SCROLL_MARGIN_TOP, (headMaxBottom - hostRect.top) + "px");
}
element.scrollIntoView(options);
}
/**
* Выбор строки.
*
* @param record Запись.
* @param scrollIntoViewOptions Настройка прокрутки. Опционально.
*/
selectRow(record, scrollIntoViewOptions = {
block: "nearest",
behavior: "auto",
inline: "nearest"
}) {
if ((this.selectionType !== "row") ||
!this.isRecordSelectable(record) ||
((this.selectedRow !== null) && (this.selectedRow.record === record))) {
return;
}
this.unselectRow();
if (!this.renderedRecords.has(record)) {
this.renderBody(true, this.dataSet.indexOf(record));
}
let row = this.renderedRecords.get(record);
if (row === undefined) {
throw new Error("Ошибка при выводе строк.");
}
this.selectedRow = row;
this.selectedRow.select();
this.scrollDataIntoView(this.selectedRow, scrollIntoViewOptions);
this.dispatchEvent(new SelectRowEvent(this.selectedRow));
}
/**
* Выбор ячейки.
*
* @param col Столбец.
* @param record Запись.
* @param scrollIntoViewOptions Настройка прокрутки. Опционально.
*/
selectCell(col, record, scrollIntoViewOptions = {
block: "nearest",
behavior: "auto",
inline: "nearest"
}) {
if ((this.selectionType !== "cell") ||
col.hidden ||
!this.isRecordSelectable(record) ||
((this.selectedCell !== null) &&
(this.selectedCell.col === col) &&
(this.selectedCell.row.record === record))) {
return;
}
if (!this.renderedRecords.has(record)) {
this.renderBody(true, this.dataSet.indexOf(record));
}
let row = this.renderedRecords.get(record);
if (row === undefined) {
throw new Error("Ошибка при выводе строк.");
}
let cell = this.getDataCell(col, row);
if (cell === null) {
return;
}
this.unselectCell();
this.selectedCell = cell;
this.selectedCell.select();
this.scrollDataIntoView(this.selectedCell, scrollIntoViewOptions);
this.dispatchEvent(new SelectCellEvent(this.selectedCell));
}
/**
* Отмена выбора строки.
*/
unselectRow() {
if (this.selectedRow === null) {
return;
}
this.selectedRow.unselect();
this.selectedRow = null;
}
/**
* Отмена выбора ячейки.
*/
unselectCell() {
if (this.selectedCell === null) {
return;
}
this.selectedCell.unselect();
this.selectedCell = null;
}
/**
* Создание строки данных.
*
* @param record Запись.
*/
createDataRow(record) {
let row = document.createElement("tr", { is: DataRow.COMPONENT_NAME });
row.record = record;
row.style.height = this._rowHeight + "px";
row.addEventListener("click", (event) => {
if ((event.button === 0) && !(event.altKey || event.ctrlKey || event.shiftKey)) {
this.selectRow(record);
}
});
return row;
}
/**
* Создание ячейки данных.
*
* @param col Столбец.
* @param row Строка.
* @param record Запись.
*/
createDataCell(col, row, record) {
let cell = document.createElement("td", { is: DataCell.COMPONENT_NAME });
cell.col = col;
cell.row = row;
for (const className of col.classList) {
cell.classList.add(className);
}
switch (col.contentAlign) {
case "center":
cell.classList.add(CSS.CELL_CONTENT_ALIGN_CENTER);
break;
case "right":
cell.classList.add(CSS.CELL_CONTENT_ALIGN_RIGHT);
break;
}
cell.addEventListener("click", (event) => {
if ((event.button === 0) && !(event.altKey || event.ctrlKey || event.shiftKey)) {
this.selectCell(col, record);
}
});
return cell;
}
/**
* Вывод строки.
*
* @param row Строка.
*
* @returns Метод возвращает true, если выполнен пользовательский вывод и false в противном случае.
*/
rowRender(row) {
return (this.onRowRender !== null) && this.onRowRender(row);
}
/**
* Вывод ячейки.
*
* @param cell Ячейка.
*/
cellRender(cell) {
if (this.onCellRender !== null) {
this.onCellRender(cell);
}
}
/**
* Вывод значения в ячейку.
*
* @param cell Ячейка.
*/
cellOutputValue(cell) {
if ((this.onCellOutputValue !== null) && this.onCellOutputValue(cell)) {
return;
}
let value = this.getFieldValue(cell.col, cell.row.record);
cell.getContent().innerHTML = this.getDisplayValue(cell.col, value, cell.row.record);
}
/**
* Настройка ячейки.
*
* @param cell Ячейка.
*/
cellCustomize(cell) {
if (this.onCellCustomize !== null) {
this.onCellCustomize(cell);
}
}
/**
* Настройка строки.
*
* @param row Строка.
*/
rowCustomize(row) {
if (this.onRowCustomize !== null) {
this.onRowCustomize(row);
}
}
/**
* Вывод набора данных.
*
* @param startIndex Индекс записи, с которой необходимо начать вывод.
* @param endIndex Индекс записи, которой необходимо закончить вывод.
*/
renderDataSet(startIndex, endIndex) {
this.renderedRecords.clear();
let documentFragment = document.createDocumentFragment();
for (let i = startIndex; i <= endIndex; i++) {
let record = this.dataSet[i];
let row = this.createDataRow(record);
if (!this.rowRender(row)) {
for (const col of this.cols) {
if (!col.hidden) {
let cell = this.createDataCell(col, row, record);
this.cellRender(cell);
this.cellOutputValue(cell);
this.cellCustomize(cell);
row.appendChild(cell);
}
}
}
this.rowCustomize(row);
documentFragment.appendChild(row);
this.renderedRecords.set(record, row);
}
this.firstRenderedRecordIndex = startIndex;
this.lastRenderedRecordIndex = endIndex;
return documentFragment;
}
/**
* Вывод тела таблицы.
*
* @param force Вывод тела таблицы, даже если требуемые для вывода записи уже выведены. Опционально. По умолчанию
* false.
* @param firstVisibleRecordIndex Индекс первой отображаемой записи. Опционально. По умолчанию рассчитывается
* автоматически.
*/
renderBody(force = false, firstVisibleRecordIndex) {
if (firstVisibleRecordIndex === undefined) {
firstVisibleRecordIndex = Math.floor(this.scrollTop / this._rowHeight);
}
let visibleRecordCount = Math.ceil(this.clientHeight / this._rowHeight);
let lastVisibleRecordIndex = firstVisibleRecordIndex + visibleRecordCount;
if (lastVisibleRecordIndex > this.dataSet.length - 1) {
lastVisibleRecordIndex = this.dataSet.length - 1;
}
// Требуемые для вывода записи уже выведены?
if (!force &&
(firstVisibleRecordIndex >= this.firstRenderedRecordIndex) &&
(lastVisibleRecordIndex <= this.lastRenderedRecordIndex)) {
return;
}
if (this.table.tBodies.length > 0) {
this.table.tBodies[0].remove();
}
if (this.dataSet.length === 0) {
return;
}
let renderRecordCount = visibleRecordCount * 3;
let startRenderRecordIndex = firstVisibleRecordIndex - Math.ceil(renderRecordCount / 2);
if (startRenderRecordIndex < 0) {
startRenderRecordIndex = 0;
}
let endRenderRecordIndex = lastVisibleRecordIndex + Math.ceil(renderRecordCount / 2);
if (endRenderRecordIndex > this.dataSet.length - 1) {
endRenderRecordIndex = this.dataSet.length - 1;
}
let extraAboveHeight = startRenderRecordIndex * this._rowHeight;
let extraBelowHeight = (this.dataSet.length - 1 - endRenderRecordIndex) * this._rowHeight;
let extraRowAbove = document.createElement("tr");
extraRowAbove.style.height = extraAboveHeight + "px";
let extraRowBelow = document.createElement("tr");
extraRowBelow.style.height = extraBelowHeight + "px";
let body = document.createElement("tbody");
body.appendChild(extraRowAbove);
body.appendChild(this.renderDataSet(startRenderRecordIndex, endRenderRecordIndex));
body.appendChild(extraRowBelow);
this.table.appendChild(body);
if (this.selectedRow !== null) {
let row = this.renderedRecords.get(this.selectedRow.record);
if (row !== undefined) {
this.selectedRow = row;
this.selectedRow.select();
}
}
if (this.selectedCell !== null) {
let row = this.renderedRecords.get(this.selectedCell.row.record);
if (row !== undefined) {
let cell = this.getDataCell(this.selectedCell.col, row);
if (cell !== null) {
this.selectedCell = cell;
this.selectedCell.select();
}
}
}
}
/**
* Вывод таблицы.
*/
renderTable() {
if (!this.ready) {
return;
}
this.headMainRow.innerHTML = "";
for (const col of this.cols) {
if (!col.hidden) {
this.headMainRow.appendChild(col.getCell());
}
}
this.renderBody(true);
}
/**
* Получение ближайшей выбираемой записи.
*
* @param record Запись, относительно которой будет производиться выбор.
* @param skipRecordCount Количество пропускаемых записей - положительное значение для поиска вниз и отрицательное
* для поиска вверх.
*/
getNearestSelectableRecord(record, skipRecordCount) {
let selectableRecord = null;
let recordIndex = this.dataSet.indexOf(record);
let targetRecordIndex = recordIndex + skipRecordCount;
if (targetRecordIndex < 0) {
targetRecordIndex = 0;
}
if (targetRecordIndex > this.dataSet.length - 1) {
targetRecordIndex = this.dataSet.length - 1;
}
if (targetRecordIndex === recordIndex) {
return null;
}
if (skipRecordCount < 0) {
for (let i = targetRecordIndex; i >= 0; i--) {
let record = this.dataSet[i];
if (this.isRecordSelectable(record)) {
selectableRecord = record;
break;
}
}
if (selectableRecord === null) {
for (let i = targetRecordIndex + 1; i < recordIndex; i++) {
let record = this.dataSet[i];
if (this.isRecordSelectable(record)) {
selectableRecord = record;
break;
}
}
}
}
else {
for (let i = targetRecordIndex; i < this.dataSet.length; i++) {
let record = this.dataSet[i];
if (this.isRecordSelectable(record)) {
selectableRecord = record;
break;
}
}
if (selectableRecord === null) {
for (let i = targetRecordIndex - 1; i > recordIndex; i--) {
let record = this.dataSet[i];
if (this.isRecordSelectable(record)) {
selectableRecord = record;
break;
}
}
}
}
return selectableRecord;
}
/**
* Получение ближайшей выбираемой ячейки, относительно текущей выбранной.
*
* @param skipColCount Количество пропускаемых столбцов - положительное значение для поиска вправо и отрицательное
* для поиска влево.
* @param skipRecordCount Количество пропускаемых записей - положительное значение для поиска вниз и отрицательное
* для поиска вверх.
*/
getNearestSelectableCell(skipColCount, skipRecordCount) {
if (this.selectedCell === null) {
return null;
}
let nearestSelectableRecord = skipRecordCount === 0 ? this.selectedCell.row.record :
this.getNearestSelectableRecord(this.selectedCell.row.record, skipRecordCount);
if (nearestSelectableRecord === null) {
return null;
}
let selectedColIndex = this.cols.indexOf(this.selectedCell.col);
let nearestColIndex = selectedColIndex + skipColCount;
if (nearestColIndex < 0) {
nearestColIndex = 0;
}
if (nearestColIndex > this.cols.length - 1) {
nearestColIndex = this.cols.length - 1;
}
let nearestSelectableCol = null;
if (skipColCount < 0) {
for (let i = nearestColIndex; i >= 0; i--) {
let col = this.cols[i];
if (!col.hidden) {
nearestSelectableCol = col;
break;
}
}
if (nearestSelectableCol === null) {
for (let i = nearestColIndex + 1; i < selectedColIndex; i++) {
let col = this.cols[i];
if (!col.hidden) {
nearestSelectableCol = col;
break;
}
}
}
}
else {
for (let i = nearestColIndex; i < this.cols.length; i++) {
let col = this.cols[i];
if (!col.hidden) {
nearestSelectableCol = col;
break;
}
}
if (nearestSelectableCol === null) {
for (let i = nearestColIndex - 1; i > selectedColIndex; i--) {
let col = this.cols[i];
if (!col.hidden) {
nearestSelectableCol = col;
break;
}
}
}
}
return nearestSelectableCol === null ? null : {
col: nearestSelectableCol,
record: nearestSelectableRecord
};
}
/**
* Добавление столбца.
*
* @param col Столбец.
*/
addCol(col) {
if (this.cols.includes(col)) {
return;
}
if (!this.contains(col)) {
this.appendChild(col);
}
if (col.id.length === 0) {
col.id = HTMLElementUtils.createId();
}
this.cols.push(col);
this.colsInitialOrder.set(col, this.cols.length - 1);
this.colsInitialHidden.set(col, col.hidden);
this.initColResize(col);
this.initColSort(col);
this.initColDrag(col);
let mutationObserver = new MutationObserver((records) => {
for (const record of records) {
if ((record.type === "attributes") && (record.attributeName !== null)) {
if (record.attributeName in [
Col.ATTR_FIELD_NAME,
Col.ATTR_FIELD_TYPE,
Col.ATTR_FORMAT,
Col.ATTR_NAME,
Col.ATTR_PATTERN