UNPKG

@siberiaweb/components

Version:
1,358 lines (1,357 loc) 69.5 kB
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