@siberiaweb/components
Version:
1,676 lines (1,392 loc) • 76.8 kB
text/typescript
/// <reference types="resize-observer-browser" />
import { SelectionType } from "./Types";
import Col from "./Col";
import CSS from "./CSS";
import DataCell from "./DataCell";
import DataRow from "./DataRow";
import HandlerTypes from "./HandlerTypes";
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 Settings from "./Settings";
import WebComponent from "@siberiaweb/webcomponent/lib/WebComponent";
import * as moment from "moment";
import "./CustomGrid.css";
/**
* Настраиваемая сетка.
*
* @template TRecord Тип записи.
*/
export default class CustomGrid< TRecord > extends WebComponent {
/**
* Автоматическая установка фокуса после загрузки страницы.
*/
public static readonly ATTR_AUTOFOCUS: string = "autofocus";
/**
* Автоматический выбор ячейки или строки после установки набора данных.
*/
public static readonly ATTR_AUTOSELECT: string = "autoselect";
/**
* Отключение перемещения столбцов.
*/
public static readonly ATTR_COL_DRAG_DISABLED: string = "col-drag-disabled";
/**
* Ручные настройки.
*/
public static readonly ATTR_CUSTOM_SETTINGS: string = "custom-settings";
/**
* Нет ни одной записи в таблице.
*/
public static readonly ATTR_EMPTY: string = "empty";
/**
* Признак загрузки данных.
*/
private static readonly ATTR_LOADING: string = "loading";
/**
* Локаль.
*/
private static readonly ATTR_LOCALE: string = "locale";
/**
* Сетка готова к выводу на экран.
*/
private static readonly ATTR_READY: string = "ready";
/**
* Высота строки.
*/
private static readonly ATTR_ROW_HEIGHT: string = "row-height";
/**
* Тип выбора.
*/
private static readonly ATTR_SELECTION_TYPE: string = "selection-type";
/**
* Идентификатор для сохранения и загрузки настроек.
*/
private static readonly ATTR_SETTINGS_ID: string = "settings-id";
/**
* Индекс последовательности перехода.
*/
private static readonly ATTR_TAB_INDEX: string = "tabindex";
/**
* Локаль по умолчанию.
*/
public static readonly DEFAULT_LOCALE: string = "ru";
/**
* Высота строки по умолчанию.
*/
public static readonly DEFAULT_ROW_HEIGHT: number = 40;
/**
* Минимальная высота строки.
*/
public static readonly MIN_ROW_HEIGHT: number = 10;
/**
* Тип выбора по умолчанию.
*/
public static readonly DEFAULT_SELECTION_TYPE: SelectionType = "cell";
/**
* Высота строки.
*/
private _rowHeight: number = CustomGrid.DEFAULT_ROW_HEIGHT;
/**
* Таблица.
*/
private readonly table: HTMLTableElement;
/**
* Заголовок таблицы.
*/
private readonly head: HTMLTableSectionElement;
/**
* Основная строка, содержащая заголовки столбцов.
*/
private readonly headMainRow: HTMLTableRowElement;
/**
* Отображаемая граница при изменении ширины столбца.
*/
private readonly colResizeBorder: HTMLDivElement;
/**
* Контейнер изображения при перемещении столбца.
*/
private readonly colDragImage: HTMLDivElement;
/**
* Верхний значок, отображаемый при перемещении столбца.
*/
private readonly colDropIconAbove: Icon;
/**
* Нижний значок, отображаемый при перемещении столбца.
*/
private readonly colDropIconBelow: Icon;
/**
* Индикатор загрузки данных.
*/
private readonly loadIndicator: HTMLDivElement;
/**
* Столбцы.
*/
private cols: Col[] = [];
/**
* Изначальный порядок столбцов.
*/
private colsInitialOrder: Map< Col, number > = new Map();
/**
* Изначально скрытые столбцы.
*/
private colsInitialHidden: Map< Col, boolean > = new Map();
/**
* Столбцы с измененной шириной.
*/
private readonly resizedCols: Set< Col > = new Set();
/**
* Набор данных.
*/
private dataSet: TRecord[] = [];
/**
* Выведенные записи: ( запись, строка ).
*/
private readonly renderedRecords: Map< TRecord, DataRow< TRecord > > = new Map();
/**
* Столбец сортировки.
*/
protected sortCol: Col | null = null;
/**
* Выбранная строка.
*/
private selectedRow: DataRow< TRecord > | null = null;
/**
* Выбранная ячейка.
*/
private selectedCell: DataCell< TRecord > | null = null;
/**
* Индекс первой выведенной записи.
*/
private firstRenderedRecordIndex: number = 0;
/**
* Индекс последней выведенной записи.
*/
private lastRenderedRecordIndex: number = 0;
/**
* Обработчик вывода строки.
*/
public onRowRender: HandlerTypes.RowRender< TRecord > | null = null;
/**
* Обработчик получения значения поля.
*/
public onGetFieldValue: HandlerTypes.GetFieldValue< TRecord > | null = null;
/**
* Обработчик получения значения поля для вывода на экран.
*/
public onGetDisplayValue: HandlerTypes.GetDisplayValue< TRecord > | null = null;
/**
* Обработчик вывода ячейки.
*/
public onCellRender: HandlerTypes.CellRender< TRecord > | null = null;
/**
* Обработчик вывода значения в ячейку.
*/
public onCellOutputValue: HandlerTypes.CellOutputValue< TRecord > | null = null;
/**
* Обработчик настройки ячейки.
*/
public onCellCustomize: HandlerTypes.CellCustomize< TRecord > | null = null;
/**
* Обработчик настройки строки.
*/
public onRowCustomize: HandlerTypes.RowCustomize< TRecord > | null = null;
/**
* Обработчик проверки, что запись может быть выбрана.
*/
public onCheckRecordSelectable: HandlerTypes.CheckRecordSelectable< TRecord > | null = null;
/**
* Обработчик сохранения настроек.
*/
public onSaveSettings: HandlerTypes.SaveSettings | null = null;
/**
* Наблюдаемые атрибуты.
*/
public static get observedAttributes(): string[] {
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
] );
}
/**
* Создание таблицы.
*/
private createTable(): HTMLTableElement {
return document.createElement( "table" );
}
/**
* Создание заголовка таблицы.
*/
private createHead(): HTMLTableSectionElement {
return document.createElement( "thead" );
}
/**
* Создание основной строки, содержащей заголовки столбцов.
*/
private createHeadMainRow(): HTMLTableRowElement {
return document.createElement( "tr" );
}
/**
* Создание отображаемой границы при изменении ширины столбца.
*/
private createColResizeBorder(): HTMLDivElement {
let container: HTMLDivElement = document.createElement( "div" );
container.classList.add( CSS.COL_RESIZE_BORDER );
return container;
}
/**
* Создание контейнера изображения при перемещении столбца.
*/
private createColDragImage(): HTMLDivElement {
let container: HTMLDivElement = document.createElement( "div" );
container.classList.add( CSS.COL_DRAG_IMAGE );
return container;
}
/**
* Создание верхнего значка, отображаемого при перемещении столбца.
*/
private createColDropIconAbove(): Icon {
let icon: Icon = document.createElement( Icon.COMPONENT_NAME ) as Icon;
icon.classList.add( CSS.COL_DROP_ICON_ABOVE );
return icon;
}
/**
* Создание нижнего значка, отображаемого при перемещении столбца.
*/
private createColDropIconBelow(): Icon {
let icon: Icon = document.createElement( Icon.COMPONENT_NAME ) as Icon;
icon.classList.add( CSS.COL_DROP_ICON_BELOW );
return icon;
}
/**
* Создание индикатора загрузки данных.
*/
private createLoadIndicator(): HTMLDivElement {
let container: HTMLDivElement = document.createElement( "div" );
container.classList.add( CSS.LOAD_INDICATOR );
return container;
}
/**
* Инициализация изменения размера столбца.
*
* @param col Столбец.
*/
private initColResize(
col: Col
): void {
col.getResizer().addEventListener( "dblclick", (): void => {
col.setWidth( col.getDefaultWidth() );
this.resizedCols.delete( col );
this.saveSettings();
} );
const ATTR_SW_GRID_COL_RESIZING: string = "sw-grid-col-resizing";
let pageX: number = 0;
let scrollLeft: number = 0;
let width: number = 0;
let colResizerBorderStartOffsetLeft: number = 0;
let scrollListener = (): void => {
this.colResizeBorder.style.left = this.colResizeBorder.offsetLeft + this.scrollLeft - scrollLeft + "px";
scrollLeft = this.scrollLeft;
}
let documentClickListener = (
event: MouseEvent
): void => {
event.stopImmediatePropagation();
}
let documentMouseMoveListener = (
event: MouseEvent
): void => {
this.colResizeBorder.style.left = this.colResizeBorder.offsetLeft + event.pageX - pageX + "px";
pageX = event.pageX;
}
let documentMouseUpListener = (): void => {
document.removeEventListener( "mousemove", documentMouseMoveListener );
document.removeEventListener( "mouseup", documentMouseUpListener );
setTimeout( () => {
document.removeEventListener( "click", documentClickListener, {
capture: true
} )
} );
this.removeEventListener( "scroll", scrollListener );
let newWidth: number = 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: MouseEvent
): void => {
event.stopImmediatePropagation();
} );
col.getResizer().addEventListener( "mousedown", (
event: MouseEvent
): void => {
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 Столбец.
*/
private initColSort(
col: Col
): void {
col.getCell().addEventListener( "click", (
event: MouseEvent
): void => {
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 Столбец.
*/
private initColDrag(
col: Col
): void {
col.getCell().draggable = !this.colDragDisabled;
col.getCell().addEventListener( "dragstart", (
event: DragEvent
): void => {
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: HTMLDivElement = document.createElement( "div" );
imageWrapper.appendChild( col.getCellContentTemplate().content );
this.colDragImage.appendChild( imageWrapper );
col.getCell().appendChild( this.colDragImage );
let cellRect: DOMRect = col.getCell().getBoundingClientRect();
this.colDragImage.style.height = cellRect.height + "px";
this.colDragImage.style.width = cellRect.width + "px";
let dragImageRect: DOMRect = this.colDragImage.getBoundingClientRect();
event.dataTransfer.setDragImage( this.colDragImage, dragImageRect.width / 2, dragImageRect.height / 2 );
} );
col.getCell().addEventListener( "dragover", (
event: DragEvent
): void => {
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: DOMRect = 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: DragEvent
): void => {
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: DragEvent
): void => {
this.colDragImage.remove();
this.colDropIconAbove.remove();
this.colDropIconBelow.remove();
if ( event.dataTransfer === null ) {
return;
}
let dragCol: Col = document.getElementById( event.dataTransfer.getData( "column" ) ) as Col;
if ( col === dragCol ) {
return;
}
let colRect: DOMRect = col.getCell().getBoundingClientRect();
if ( event.pageX < ( colRect.left + colRect.width / 2 ) ) {
this.moveColBefore( dragCol, col );
}
else {
this.moveColAfter( dragCol, col );
}
} );
}
/**
* Инициализация наблюдателя за изменением размера.
*/
private initResizeObserver(): void {
try {
let blockSize: number = this.offsetHeight;
new ResizeObserver( (
entries
): void => {
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 );
}
}
/**
* Инициализация хоста.
*/
private initCustomGridHost(): void {
this.addEventListener(
"scroll",
(): void => {
if ( this.loadIndicator.isConnected ) {
this.loadIndicator.style.transform = "translate(" + this.scrollLeft + "px," + this.scrollTop +
"px)";
}
this.renderBody();
}
);
this.addEventListener( "keydown", (
event: KeyboardEvent
): void => {
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: number = 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: number = 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: TRecord | null = 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: TRecord | null = 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: number = this.getMaxDisplayRowCountInViewport();
if ( skipRecordCount > 1 ) {
skipRecordCount--;
}
let record: TRecord | null = 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: number = this.getMaxDisplayRowCountInViewport();
if ( skipRecordCount > 1 ) {
skipRecordCount--;
}
let record: TRecord | null = 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: TRecord | null = 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: TRecord | null = this.getNearestSelectableRecord( this.selectedRow.record,
this.dataSet.length );
if ( record !== null ) {
this.selectRow( record );
}
}
}
}
} );
}
/**
* Получение максимального низа заголовка таблицы.
*/
protected getHeadMaxBottom(): number {
let maxBottom: number = 0;
for ( const row of this.head.rows ) {
let bottom: number = 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;
}
/**
* Получение высоты порта вывода для тела таблицы.
*/
protected getTableBodyViewportHeight(): number {
let deltaY: number = this.getHeadMaxBottom() - this.getBoundingClientRect().top;
if ( deltaY < 0 ) {
deltaY = 0;
}
return this.clientHeight - deltaY;
}
/**
* Получение максимального количества записей, которое можно отобразить в порте вывода.
*/
protected getMaxDisplayRowCountInViewport(): number {
return Math.ceil( this.getTableBodyViewportHeight() / this._rowHeight );
}
/**
* Получение ячейки данных.
*
* @param col Столбец.
* @param row Строка.
*/
protected getDataCell(
col: Col,
row: DataRow< TRecord >
): DataCell< TRecord > | null {
for ( const cell of row.cells ) {
if ( ( cell instanceof DataCell ) && ( cell.col === col ) ) {
return cell;
}
}
return null;
}
/**
* @override
*/
protected 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".
*/
protected attrAutofocusChange(): void {
if ( this.hasAttribute( CustomGrid.ATTR_AUTOFOCUS ) ) {
window.requestAnimationFrame( () => {
this.focus();
} );
}
}
/**
* Обработка изменения атрибута "col-drag-disabled".
*/
protected attrColDragDisabledChange(): void {
for ( const col of this.cols ) {
col.getCell().draggable = !this.colDragDisabled;
}
}
/**
* Обработка изменения атрибута "loading".
*/
protected attrLoadingChange(): void {
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".
*/
protected attrLocaleChange(): void {
this.renderBody( true );
}
/**
* Обработка изменения атрибута "ready".
*/
protected attrReadyChange(): void {
if ( this.ready ) {
this.renderTable();
}
}
/**
* Обработка изменения атрибута "row-height".
*
* @param newValue Новое значение.
*/
protected attrRowHeightChange(
newValue: string | null
): void {
let value: number = 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".
*/
protected attrSelectionTypeChange(): void {
this.unselectRow();
this.unselectCell();
this.renderBody( true );
}
/**
* @override
*/
protected attributeChangedCallback(
name: string,
oldValue: string | null,
newValue: string | null
): void {
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;
}
}
/**
* Сохранение настроек.
*/
protected saveSettings() {
let settings: Settings = {}
settings.cols = {}
for ( let i: number = 0; i < this.cols.length; i++ ) {
let col: 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 Запись.
*/
protected getFieldValue(
col: Col,
record: TRecord
): any {
let value: any = null;
if ( col.fieldName ) {
value = ( record as any )[ col.fieldName ];
}
if ( this.onGetFieldValue !== null ) {
value = this.onGetFieldValue( col, record, value );
}
return value;
}
/**
* Форматирование логического значения.
*
* @param value Значение.
* @param format Формат вывода.
*/
protected formatBoolean(
value: boolean,
format: string
): string {
let displayValue: string = value ? "true" : "false";
let patterns: string[] = format.split( " " );
if ( patterns.length === 2 ) {
displayValue = value ? patterns[ 0 ] : patterns[ 1 ];
}
return displayValue;
}
/**
* Форматирование даты.
*
* @param value Значение.
* @param format Формат вывода.
*/
protected formatDate(
value: Date,
format: string
): string {
return moment( value ).locale( this.locale ).format( format );
}
/**
* Форматирование даты, представленной в виде строки.
*
* @param value Значение.
* @param pattern Шаблон.
* @param format Формат вывода.
*/
protected formatDateString(
value: string,
pattern: string,
format: string
): string {
return moment( value, pattern ).locale( this.locale ).format( format );
}
/**
* Форматирование числа.
*
* @param value Значение.
*/
protected formatNumber(
value: number
): string {
return value.toLocaleString( this.locale );
}
/**
* Форматирование валюты.
*
* @param value Значение.
*/
protected formatCurrency(
value: number
): string {
return value.toLocaleString( this.locale, {
minimumFractionDigits: 2
} );
}
/**
* Получение значения поля для вывода на экран.
*
* @param col Столбец.
* @param value Значение.
* @param record Запись.
*/
protected getDisplayValue(
col: Col,
value: any,
record: TRecord
): string {
let displayValue: string = 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 Запись.
*/
protected isRecordSelectable(
record: TRecord
): boolean {
return ( this.onCheckRecordSelectable === null ) || this.onCheckRecordSelectable( record );
}
/**
* Прокрутка контейнера сетки, чтобы строка или ячейка данных были в поле видимости.
*
* @param element Строка или ячейка.
* @param options Настройка прокрутки. Опционально.
*/
protected scrollDataIntoView(
element: HTMLTableRowElement | HTMLTableCellElement,
options: ScrollIntoViewOptions = {
block: "nearest",
inline: "nearest",
behavior: "auto"
}
): void {
let hostRect: DOMRect = 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 Настройка прокрутки. Опционально.
*/
public selectRow(
record: TRecord,
scrollIntoViewOptions: ScrollIntoViewOptions = {
block: "nearest",
behavior: "auto",
inline: "nearest"
}
): void {
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: DataRow< TRecord > | undefined = 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< TRecord >( this.selectedRow ) );
}
/**
* Выбор ячейки.
*
* @param col Столбец.
* @param record Запись.
* @param scrollIntoViewOptions Настройка прокрутки. Опционально.
*/
public selectCell(
col: Col,
record: TRecord,
scrollIntoViewOptions: ScrollIntoViewOptions = {
block: "nearest",
behavior: "auto",
inline: "nearest"
}
): void {
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: DataRow< TRecord > | undefined = this.renderedRecords.get( record );
if ( row === undefined ) {
throw new Error( "Ошибка при выводе строк." );
}
let cell: DataCell< TRecord > | null = 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< TRecord >( this.selectedCell ) );
}
/**
* Отмена выбора строки.
*/
public unselectRow(): void {
if ( this.selectedRow === null ) {
return;
}
this.selectedRow.unselect();
this.selectedRow = null;
}
/**
* Отмена выбора ячейки.
*/
public unselectCell(): void {
if ( this.selectedCell === null ) {
return;
}
this.selectedCell.unselect();
this.selectedCell = null;
}
/**
* Создание строки данных.
*
* @param record Запись.
*/
protected createDataRow(
record: TRecord
): DataRow< TRecord > {
let row: DataRow< TRecord > = document.createElement( "tr", { is: DataRow.COMPONENT_NAME } ) as
DataRow< TRecord >
row.record = record;
row.style.height = this._rowHeight + "px";
row.addEventListener( "click", (
event: MouseEvent
): void => {
if ( ( event.button === 0 ) && !( event.altKey || event.ctrlKey || event.shiftKey ) ) {
this.selectRow( record );
}
} );
return row;
}
/**
* Создание ячейки данных.
*
* @param col Столбец.
* @param row Строка.
* @param record Запись.
*/
public createDataCell(
col: Col,
row: DataRow< TRecord >,
record: TRecord,
): DataCell< TRecord > {
let cell: DataCell< TRecord > = document.createElement( "td", { is: DataCell.COMPONENT_NAME } ) as
DataCell< TRecord >;
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: MouseEvent
): void => {
if ( ( event.button === 0 ) && !( event.altKey || event.ctrlKey || event.shiftKey ) ) {
this.selectCell( col, record );
}
} );
return cell;
}
/**
* Вывод строки.
*
* @param row Строка.
*
* @returns Метод возвращает true, если выполнен пользовательский вывод и false в противном случае.
*/
protected rowRender(
row: DataRow< TRecord >
): boolean {
return ( this.onRowRender !== null ) && this.onRowRender( row );
}
/**
* Вывод ячейки.
*
* @param cell Ячейка.
*/
protected cellRender(
cell: DataCell< TRecord >
): void {
if ( this.onCellRender !== null ) {
this.onCellRender( cell );
}
}
/**
* Вывод значения в ячейку.
*
* @param cell Ячейка.
*/
protected cellOutputValue(
cell: DataCell< TRecord >
): void {
if ( ( this.onCellOutputValue !== null ) && this.onCellOutputValue( cell ) ) {
return;
}
let value: any = this.getFieldValue( cell.col, cell.row.record );
cell.getContent().innerHTML = this.getDisplayValue( cell.col, value, cell.row.record );
}
/**
* Настройка ячейки.
*
* @param cell Ячейка.
*/
protected cellCustomize(
cell: DataCell< TRecord >
): void {
if ( this.onCellCustomize !== null ) {
this.onCellCustomize( cell );
}
}
/**
* Настройка строки.
*
* @param row Строка.
*/
protected rowCustomize(
row: DataRow< TRecord >
): void {
if ( this.onRowCustomize !== null ) {
this.onRowCustomize( row );
}
}
/**
* Вывод набора данных.
*
* @param startIndex Индекс записи, с которой необходимо начать вывод.
* @param endIndex Индекс записи, которой необходимо закончить вывод.
*/
protected renderDataSet(
startIndex: number,
endIndex: number
): DocumentFragment {
this.renderedRecords.clear();
let documentFragment: DocumentFragment = document.createDocumentFragment();
for ( let i: number = startIndex; i <= endIndex; i++ ) {
let record = this.dataSet[ i ];
let row: DataRow< TRecord > = this.createDataRow( record );
if ( !this.rowRender( row ) ) {
for ( const col of this.cols ) {
if ( !col.hidden ) {
let cell: DataCell< TRecord > = 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 Индекс первой отображаемой записи. Опционально. По умолчанию рассчитывается
* автоматически.
*/
protected renderBody(
force: boolean = false,
firstVisibleRecordIndex?: number
) {
if ( firstVisibleRecordIndex === undefined ) {
firstVisibleRecordIndex = Math.floor( this.scrollTop / this._rowHeight );
}
let visibleRecordCount: number = Math.ceil( this.clientHeight / this._rowHeight );
let lastVisibleRecordIndex: number = 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: number = visibleRecordCount * 3;
let startRenderRecordIndex: number = 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: number = startRenderRecordIndex * this._rowHeight;
let extraBelowHeight: number = ( this.dataSet.length - 1 - endRenderRecordIndex ) * this._rowHeight;
let extraRowAbove: HTMLTableRowElement = document.createElement( "tr" );
extraRowAbove.style.height = extraAboveHeight + "px";
let extraRowBelow: HTMLTableRowElement = document.createElement( "tr" );
extraRowBelow.style.height = extraBelowHeight + "px";
let body: HTMLTableSectionElement = 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: DataRow< TRecord > | undefined = this.renderedRecords.get( this.selectedRow.record );
if ( row !== undefined ) {
this.selectedRow = row;
this.selectedRow.select();
}
}
if ( this.selectedCell !== null ) {
let row: DataRow< TRecord > | undefined = this.renderedRecords.get( this.selectedCell.row.record );
if ( row !== undefined ) {
let cell: DataCell< TRecord > | null = this.getDataCell( this.selectedCell.col, row );
if ( cell !== null ) {
this.selectedCell = cell;
this.selectedCell.select();
}
}
}
}
/**
* Вывод таблицы.
*/
protected renderTable(): void {
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 Запись, относительно кото