cheetah-grid
Version:
Cheetah Grid is a high performance grid engine that works on canvas
1,564 lines (1,514 loc) • 43.7 kB
text/typescript
import * as icons from "./internal/icons";
import * as themes from "./themes";
import { CachedDataSource, DataSource } from "./data";
import type {
CellAddress,
CellContext,
CellRange,
ColorPropertyDefine,
ColorsPropertyDefine,
ColumnActionAPI,
ColumnIconOption,
ColumnStyleOption,
ColumnTypeAPI,
DrawGridAPI,
EventListenerId,
FieldData,
FieldDef,
HeaderValues,
LayoutObjectId,
ListGridAPI,
ListGridEventHandlersEventMap,
ListGridEventHandlersReturnMap,
MaybePromise,
MaybePromiseOrUndef,
Message,
PasteCellEvent,
PasteRejectedValuesEvent,
SelectedCellEvent,
SetPasteValueTestData,
SortState,
ThemeDefine,
} from "./ts-types";
import {
ColumnDefine,
GroupHeaderDefine,
HeaderDefine,
HeadersDefine,
MultiLayoutMap,
SimpleHeaderLayoutMap,
} from "./list-grid/layout-map";
import type {
DrawGridConstructorOptions,
DrawGridProtected,
} from "./core/DrawGrid";
import type { LayoutDefine, LayoutMapAPI } from "./list-grid/layout-map";
import { MessageHandler, hasMessage } from "./columns/message/MessageHandler";
import {
cellEquals,
event,
isPromise,
obj,
omit,
then,
} from "./internal/utils";
import type { BaseColumn } from "./columns/type/BaseColumn";
import { BaseStyle } from "./columns/style";
import type { ColumnData } from "./list-grid/layout-map/api";
import type { DrawCellInfo } from "./ts-types-internal";
import { DrawGrid } from "./core/DrawGrid";
import { GridCanvasHelper } from "./GridCanvasHelper";
import { BaseStyle as HeaderBaseStyle } from "./header/style";
import { LG_EVENT_TYPE } from "./list-grid/LG_EVENT_TYPE";
import { Rect } from "./internal/Rect";
import type { Theme } from "./themes/theme";
import { TooltipHandler } from "./tooltip/TooltipHandler";
//protected symbol
import { getProtectedSymbol } from "./internal/symbolManager";
import { parsePasteRangeBoxValues } from "./internal/paste-utils";
/** @private */
const _ = getProtectedSymbol();
//private methods
/** @private */
function _getCellRange<T>(
grid: ListGrid<T>,
col: number,
row: number
): CellRange {
return grid[_].layoutMap.getCellRange(col, row);
}
/** @private */
function _updateRect<T>(
grid: ListGrid<T>,
col: number,
row: number,
context: CellContext
): void {
context.setRectFilter((rect) => {
let { left, right, top, bottom } = rect;
const {
start: { col: startCol, row: startRow },
end: { col: endCol, row: endRow },
} = _getCellRange(grid, col, row);
for (let c = col - 1; c >= startCol; c--) {
left -= grid.getColWidth(c);
}
for (let c = col + 1; c <= endCol; c++) {
right += grid.getColWidth(c);
}
for (let r = row - 1; r >= startRow; r--) {
top -= grid.getRowHeight(r);
}
for (let r = row + 1; r <= endRow; r++) {
bottom += grid.getRowHeight(r);
}
return Rect.bounds(left, top, right, bottom);
});
}
/** @private */
function _getCellValue<T>(
grid: ListGrid<T>,
col: number,
row: number
): FieldData {
if (row < grid[_].layoutMap.headerRowCount) {
const { caption } = grid[_].layoutMap.getHeader(col, row);
return typeof caption === "function" ? caption() : caption;
} else {
const { field } = grid[_].layoutMap.getBody(col, row);
return _getField(grid, field, row);
}
}
/** @private */
function _setCellValue<T>(
grid: ListGrid<T>,
col: number,
row: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any
): MaybePromise<boolean> {
if (row < grid[_].layoutMap.headerRowCount) {
// nop
return false;
} else {
const { field } = grid[_].layoutMap.getBody(col, row);
if (field == null) {
return false;
}
const index = _getRecordIndexByRow(grid, row);
return grid[_].dataSource.setField(index, field, value);
}
}
/** @private */
function _getCellMessage<T>(
grid: ListGrid<T>,
col: number,
row: number
): FieldData {
if (row < grid[_].layoutMap.headerRowCount) {
return null;
} else {
const { message } = grid[_].layoutMap.getBody(col, row);
if (!message) {
return null;
}
if (!Array.isArray(message)) {
return _getField(grid, message as FieldDef<T>, row);
}
const promises: Promise<Message>[] = [];
for (let index = 0; index < message.length; index++) {
const msg = _getField(grid, message[index] as FieldDef<T>, row);
if (isPromise(msg)) {
promises.push(msg);
} else if (hasMessage(msg)) {
return msg;
}
}
if (!promises.length) {
return null;
}
return new Promise((resolve, reject) => {
promises.forEach((p) => {
p.then((msg) => {
if (hasMessage(msg)) {
resolve(msg);
}
}, reject);
});
});
}
}
/** @private */
function _getCellIcon0<T>(
grid: ListGrid<T>,
icon: ColumnIconOption<T>,
row: number
): ColumnIconOption<never>;
function _getCellIcon0<T>(
grid: ListGrid<T>,
icon: ColumnIconOption<T> | ColumnIconOption<T>[],
row: number
): ColumnIconOption<never> | ColumnIconOption<never>[];
function _getCellIcon0<T>(
grid: ListGrid<T>,
icon: ColumnIconOption<T> | ColumnIconOption<T>[],
row: number
): ColumnIconOption<never> | ColumnIconOption<never>[] {
if (Array.isArray(icon)) {
return icon.map((i) => _getCellIcon0(grid, i, row));
}
if (!obj.isObject(icon) || typeof icon === "function") {
return _getField(grid, icon, row);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const retIcon: any = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const iconOpt: any = icon;
icons.iconPropKeys.forEach((k) => {
if (iconOpt[k]) {
const f = _getField(grid, iconOpt[k], row);
if (f != null) {
retIcon[k] = f;
} else {
if (!_hasField(grid, iconOpt[k], row)) {
retIcon[k] = iconOpt[k];
}
}
}
});
return retIcon;
}
/** @private */
function _getCellIcon<T>(
grid: ListGrid<T>,
col: number,
row: number
): ColumnIconOption<never> | ColumnIconOption<never>[] | null {
if (row < grid[_].layoutMap.headerRowCount) {
const { headerIcon } = grid[_].layoutMap.getHeader(col, row);
if (headerIcon == null) {
return null;
}
return headerIcon;
} else {
const { icon } = grid[_].layoutMap.getBody(col, row);
if (icon == null) {
return null;
}
return _getCellIcon0(grid, icon, row);
}
}
/** @private */
function _getField<T>(
grid: ListGrid<T>,
field: FieldDef<T> | undefined,
row: number
): FieldData {
if (field == null) {
return null;
}
if (row < grid[_].layoutMap.headerRowCount) {
return null;
} else {
const index = _getRecordIndexByRow(grid, row);
return grid[_].dataSource.getField(index, field);
}
}
/** @private */
function _hasField<T>(
grid: ListGrid<T>,
field: FieldDef<T>,
row: number
): boolean {
if (field == null) {
return false;
}
if (row < grid[_].layoutMap.headerRowCount) {
return false;
} else {
const index = _getRecordIndexByRow(grid, row);
return grid[_].dataSource.hasField(index, field);
}
}
/** @private */
function _onDrawValue<T>(
grid: ListGrid<T>,
cellValue: MaybePromise<unknown>,
context: CellContext,
{ col, row }: CellAddress,
style: ColumnStyleOption | null | undefined,
draw: BaseColumn<T>["onDrawCell"]
): MaybePromise<void> {
const helper = grid[_].gridCanvasHelper;
const drawCellBg = ({
bgColor,
}: { bgColor?: ColorPropertyDefine } = {}): void => {
const fillOpt = {
fillColor: bgColor,
};
//cell全体を描画
helper.fillCellWithState(context, fillOpt);
};
const drawCellBorder = (): void => {
if (context.col === grid.frozenColCount - 1) {
//固定列罫線
const rect = context.getRect();
helper.drawWithClip(context, (ctx) => {
const borderColor =
context.row >= grid.frozenRowCount
? helper.theme.borderColor
: helper.theme.frozenRowsBorderColor;
const borderColors = helper.toBoxArray(
helper.getColor(borderColor, context.col, context.row, ctx)
);
if (borderColors[1]) {
ctx.lineWidth = 1;
ctx.strokeStyle = borderColors[1];
ctx.beginPath();
ctx.moveTo(rect.right - 2.5, rect.top);
ctx.lineTo(rect.right - 2.5, rect.bottom);
ctx.stroke();
}
});
}
_borderWithState(grid, helper, context);
};
const drawCellBase = ({
bgColor,
}: { bgColor?: ColorPropertyDefine } = {}): void => {
drawCellBg({ bgColor });
drawCellBorder();
};
const info: DrawCellInfo<T> = {
getRecord: () => grid.getRowRecord(row),
getIcon: () => _getCellIcon(grid, col, row),
getMessage: () => _getCellMessage(grid, col, row),
messageHandler: grid[_].messageHandler,
style,
drawCellBase,
drawCellBg,
drawCellBorder,
};
return draw(cellValue, info, context, grid);
}
/** @private */
function _borderWithState<T>(
grid: ListGrid<T>,
helper: GridCanvasHelper<T>,
context: CellContext
): void {
const { col, row } = context;
const sel = grid.selection.select;
const { layoutMap } = grid[_];
const rect = context.getRect();
const option: { borderColor?: ColorsPropertyDefine; lineWidth?: number } = {};
const selRecordIndex = layoutMap.getRecordIndexByRow(sel.row);
const selId = layoutMap.getCellId(sel.col, sel.row);
function isSelectCell(col: number, row: number): boolean {
if (col === sel.col && row === sel.row) {
return true;
}
return (
selId != null &&
layoutMap.getCellId(col, row) === selId &&
layoutMap.getRecordIndexByRow(row) === selRecordIndex
);
}
//罫線
if (isSelectCell(col, row)) {
option.borderColor = helper.theme.highlightBorderColor;
option.lineWidth = 2;
helper.border(context, option);
} else {
option.lineWidth = 1;
// header color
const isFrozenCell = grid.isFrozenCell(col, row);
if (isFrozenCell?.row) {
option.borderColor = helper.theme.frozenRowsBorderColor;
}
helper.border(context, option);
//追加処理
if (col > 0 && isSelectCell(col - 1, row)) {
//右が選択されている
helper.drawBorderWithClip(context, (ctx) => {
const borderColors = helper.toBoxArray(
helper.getColor(
helper.theme.highlightBorderColor,
sel.col,
sel.row,
ctx
)
);
if (borderColors[1]) {
ctx.lineWidth = 1;
ctx.strokeStyle = borderColors[1];
ctx.beginPath();
ctx.moveTo(rect.left - 0.5, rect.top);
ctx.lineTo(rect.left - 0.5, rect.bottom);
ctx.stroke();
}
});
} else if (row > 0 && isSelectCell(col, row - 1)) {
//上が選択されている
helper.drawBorderWithClip(context, (ctx) => {
const borderColors = helper.toBoxArray(
helper.getColor(
helper.theme.highlightBorderColor,
sel.col,
sel.row,
ctx
)
);
if (borderColors[0]) {
ctx.lineWidth = 1;
ctx.strokeStyle = borderColors[0];
ctx.beginPath();
ctx.moveTo(rect.left, rect.top - 0.5);
ctx.lineTo(rect.right, rect.top - 0.5);
ctx.stroke();
}
});
}
}
}
/** @private */
function _refreshHeader<T>(grid: ListGrid<T>): void {
const protectedSpace = grid[_];
if (protectedSpace.headerEvents) {
protectedSpace.headerEvents.forEach((id) => grid.unlisten(id));
}
const headerEvents: EventListenerId[] = (grid[_].headerEvents = []);
headerEvents.forEach((id) => grid.unlisten(id));
let layoutMap: LayoutMapAPI<T>;
if (
protectedSpace.layout &&
(!Array.isArray(protectedSpace.layout) || protectedSpace.layout.length > 0)
) {
layoutMap = protectedSpace.layoutMap = new MultiLayoutMap(
protectedSpace.layout
);
} else {
layoutMap = protectedSpace.layoutMap = new SimpleHeaderLayoutMap(
protectedSpace.header ?? []
);
}
layoutMap.headerObjects.forEach((cell) => {
const ids = cell.headerType.bindGridEvent(grid, cell.id);
headerEvents.push(...ids);
if (cell.style) {
if (cell.style instanceof HeaderBaseStyle) {
const id = cell.style.listen(
HeaderBaseStyle.EVENT_TYPE.CHANGE_STYLE,
() => {
grid.invalidate();
}
);
headerEvents.push(id);
}
}
if (cell.action) {
const ids = cell.action.bindGridEvent(grid, cell.id);
headerEvents.push(...ids);
}
});
layoutMap.columnObjects.forEach((col) => {
if (col.action) {
const ids = col.action.bindGridEvent(grid, col.id);
headerEvents.push(...ids);
}
if (col.columnType) {
const ids = col.columnType.bindGridEvent(grid, col.id);
headerEvents.push(...ids);
}
if (col.style) {
if (col.style instanceof BaseStyle) {
const id = col.style.listen(BaseStyle.EVENT_TYPE.CHANGE_STYLE, () => {
grid.invalidate();
});
headerEvents.push(id);
}
}
});
for (let col = 0; col < layoutMap.columnWidths.length; col++) {
const column = layoutMap.columnWidths[col];
const { width, minWidth, maxWidth } = column;
if (width && (typeof width === "string" || width > 0)) {
grid.setColWidth(col, width);
} else {
grid.setColWidth(col, null);
}
if (minWidth && (typeof minWidth === "string" || minWidth > 0)) {
grid.setMinColWidth(col, minWidth);
} else {
grid.setMinColWidth(col, null);
}
if (maxWidth && (typeof maxWidth === "string" || maxWidth > 0)) {
grid.setMaxColWidth(col, maxWidth);
} else {
grid.setMaxColWidth(col, null);
}
}
const { headerRowHeight } = grid[_];
for (let row = 0; row < layoutMap.headerRowCount; row++) {
const height = Array.isArray(headerRowHeight)
? headerRowHeight[row]
: headerRowHeight;
if (height && height > 0) {
grid.setRowHeight(row, height);
} else {
grid.setRowHeight(row, null);
}
}
grid.colCount = layoutMap.colCount;
_refreshRowCount(grid);
grid.frozenRowCount = layoutMap.headerRowCount;
}
/** @private */
function _refreshRowCount<T>(grid: ListGrid<T>): void {
const { layoutMap } = grid[_];
grid.rowCount =
grid[_].dataSource.length * layoutMap.bodyRowCount +
layoutMap.headerRowCount;
}
/** @private */
function _tryWithUpdateDataSource<T>(
grid: ListGrid<T>,
fn: (grid: ListGrid<T>) => void
): void {
const { dataSourceEventIds } = grid[_];
if (dataSourceEventIds) {
dataSourceEventIds.forEach((id) => grid[_].handler.off(id));
}
fn(grid);
grid[_].dataSourceEventIds = [
grid[_].handler.on(
grid[_].dataSource,
DataSource.EVENT_TYPE.UPDATED_LENGTH,
() => {
_refreshRowCount(grid);
grid.invalidate();
}
),
grid[_].handler.on(
grid[_].dataSource,
DataSource.EVENT_TYPE.UPDATED_ORDER,
() => {
grid.invalidate();
}
),
];
}
/** @private */
function _setRecords<T>(grid: ListGrid<T>, records: T[] = []): void {
_tryWithUpdateDataSource(grid, () => {
grid[_].records = records;
const newDataSource = (grid[_].dataSource =
CachedDataSource.ofArray(records));
grid.addDisposable(newDataSource);
});
}
/** @private */
function _setDataSource<T>(grid: ListGrid<T>, dataSource: DataSource<T>): void {
_tryWithUpdateDataSource(grid, () => {
if (dataSource) {
if (dataSource instanceof DataSource) {
grid[_].dataSource = dataSource;
} else {
const newDataSource = (grid[_].dataSource = new CachedDataSource(
dataSource
));
grid.addDisposable(newDataSource);
}
} else {
grid[_].dataSource = DataSource.EMPTY;
}
grid[_].records = null;
});
}
/** @private */
function _getRecordIndexByRow<T>(grid: ListGrid<T>, row: number): number {
const { layoutMap } = grid[_];
return layoutMap.getRecordIndexByRow(row);
}
/** @private */
function _onRangePaste<T>(
this: ListGrid<T>,
text: string,
test: (data: SetPasteValueTestData<T>) => boolean = (): boolean => true
): void {
const { layoutMap } = this[_];
const selectionRange = this.selection.range;
const { start } = this.getCellRange(
selectionRange.start.col,
selectionRange.start.row
);
const { end } = this.getCellRange(
selectionRange.end.col,
selectionRange.end.row
);
const values = parsePasteRangeBoxValues(text, {
trimOnPaste: this.trimOnPaste,
});
const pasteRowCount = Math.min(
Math.max(end.row - start.row + 1, values.rowCount),
this.rowCount - start.row
);
const pasteColCount = Math.min(
Math.max(end.col - start.col + 1, values.colCount),
this.colCount - start.col
);
let hasEditable = false;
const actionColumnsBox: ColumnData<T>[][] = [];
for (let bodyRow = 0; bodyRow < layoutMap.bodyRowCount; bodyRow++) {
const actionColumnsRow: ColumnData<T>[] = [];
actionColumnsBox.push(actionColumnsRow);
for (let offsetCol = 0; offsetCol < pasteColCount; offsetCol++) {
const body = layoutMap.getBody(
start.col + offsetCol,
bodyRow + layoutMap.headerRowCount
);
actionColumnsRow[offsetCol] = body;
if (!hasEditable && body.action?.editable) {
hasEditable = true;
}
}
}
if (!hasEditable) {
return;
}
const startRow = layoutMap.getRecordStartRowByRecordIndex(
layoutMap.getRecordIndexByRow(start.row)
);
const startRowOffset = start.row - startRow;
let rejectedDetail: PasteRejectedValuesEvent<T>["detail"] = [];
const addRejectedDetail = (
cell: CellAddress,
record: T | undefined,
define: ColumnDefine<T>,
pasteValue: string
) => {
rejectedDetail.push({
col: cell.col,
row: cell.row,
record,
define,
pasteValue,
});
};
let timeout: NodeJS.Timeout | null = null;
const processRejected = () => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
if (rejectedDetail.length > 0) {
this.fireListeners(LG_EVENT_TYPE.REJECTED_PASTE_VALUES, {
detail: rejectedDetail,
});
rejectedDetail = [];
}
}, 100);
};
let reject = addRejectedDetail;
let duplicate: { [key: number]: boolean } = {};
let actionRow = startRowOffset;
let valuesRow = 0;
for (let offsetRow = 0; offsetRow < pasteRowCount; offsetRow++) {
let valuesCol = 0;
for (let offsetCol = 0; offsetCol < pasteColCount; offsetCol++) {
const { action, id, define } = actionColumnsBox[actionRow][offsetCol];
if (!duplicate[id as number] && action?.editable) {
duplicate[id as number] = true;
const col = start.col + offsetCol;
const row = start.row + offsetRow;
const cellValue = values.getCellValue(valuesCol, valuesRow);
then(this.getRowRecord(row), (record) => {
then(_getCellValue(this, col, row), (oldValue) => {
if (
test({
grid: this,
record: record as T,
col,
row,
value: cellValue,
oldValue,
})
) {
action.onPasteCellRangeBox(this, { col, row }, cellValue, {
reject() {
reject({ col, row }, record, define, cellValue);
},
});
}
});
});
}
valuesCol++;
if (valuesCol >= values.colCount) {
valuesCol = 0;
}
}
actionRow++;
if (actionRow >= layoutMap.bodyRowCount) {
actionRow = 0;
duplicate = {};
}
valuesRow++;
if (valuesRow >= values.rowCount) {
valuesRow = 0;
}
}
const newEnd = {
col: start.col + pasteColCount - 1,
row: start.row + pasteRowCount - 1,
};
this.selection.range = {
start,
end: newEnd,
};
this.invalidateCellRange(this.selection.range);
processRejected();
reject = (cell, record, define, pasteValue) => {
addRejectedDetail(cell, record, define, pasteValue);
processRejected();
};
}
/** @private */
function _onRangeDelete<T>(this: ListGrid<T>): void {
const { layoutMap } = this[_];
const selectionRange = this.selection.range;
const { start } = this.getCellRange(
selectionRange.start.col,
selectionRange.start.row
);
const { end } = this.getCellRange(
selectionRange.end.col,
selectionRange.end.row
);
const deleteRowCount = Math.min(
end.row - start.row + 1,
this.rowCount - start.row
);
const deleteColCount = Math.min(
end.col - start.col + 1,
this.colCount - start.col
);
let hasEditable = false;
const actionColumnsBox: ColumnData<T>[][] = [];
for (let bodyRow = 0; bodyRow < layoutMap.bodyRowCount; bodyRow++) {
const actionColumnsRow: ColumnData<T>[] = [];
actionColumnsBox.push(actionColumnsRow);
for (let offsetCol = 0; offsetCol < deleteColCount; offsetCol++) {
const body = layoutMap.getBody(
start.col + offsetCol,
bodyRow + layoutMap.headerRowCount
);
actionColumnsRow[offsetCol] = body;
if (!hasEditable && body.action?.editable) {
hasEditable = true;
}
}
}
if (!hasEditable) {
return;
}
const startRow = layoutMap.getRecordStartRowByRecordIndex(
layoutMap.getRecordIndexByRow(start.row)
);
const startRowOffset = start.row - startRow;
let duplicate: { [key: number]: boolean } = {};
let actionRow = startRowOffset;
for (let offsetRow = 0; offsetRow < deleteRowCount; offsetRow++) {
for (let offsetCol = 0; offsetCol < deleteColCount; offsetCol++) {
const { action, id } = actionColumnsBox[actionRow][offsetCol];
if (!duplicate[id as number] && action?.editable) {
duplicate[id as number] = true;
const col = start.col + offsetCol;
const row = start.row + offsetRow;
then(this.getRowRecord(row), (_record) => {
then(_getCellValue(this, col, row), (_oldValue) => {
action.onDeleteCellRangeBox(this, { col, row });
});
});
}
}
actionRow++;
if (actionRow >= layoutMap.bodyRowCount) {
actionRow = 0;
duplicate = {};
}
}
this.invalidateCellRange(selectionRange);
}
//end private methods
//
//
//
/** @protected */
interface ListGridProtected<T> extends DrawGridProtected {
dataSourceEventIds?: EventListenerId[];
headerEvents?: EventListenerId[];
layoutMap: LayoutMapAPI<T>;
headerValues?: HeaderValues;
tooltipHandler: TooltipHandler<T>;
messageHandler: MessageHandler<T>;
theme: Theme | null;
headerRowHeight: number[] | number;
header: HeadersDefine<T>;
layout: LayoutDefine<T>;
gridCanvasHelper: GridCanvasHelper<T>;
sortState: SortState;
dataSource: DataSource<T>;
records?: T[] | null;
allowRangePaste: boolean;
}
export { ListGridProtected };
export interface ListGridConstructorOptions<T>
extends DrawGridConstructorOptions {
/**
* Simple header property
*/
header?: HeadersDefine<T>;
/**
* Layout property
*/
layout?: LayoutDefine<T>;
/**
* Header row height(s)
*/
headerRowHeight?: number[] | number;
/**
* Records data source
*/
dataSource?: DataSource<T>;
/**
* Simple records data
*/
records?: T[];
/**
* Theme
*/
theme?: ThemeDefine | string;
/**
* If set to true to allow pasting of ranges. default false
*/
allowRangePaste?: boolean;
/**
* @deprecated Cannot be used with ListGrid.
* @override
*/
rowCount?: undefined;
/**
* @deprecated Cannot be used with ListGrid.
* @override
*/
colCount?: undefined;
/**
* @deprecated Cannot be used with ListGrid.
* @override
*/
frozenRowCount?: undefined;
}
export { HeadersDefine, ColumnDefine, HeaderDefine, GroupHeaderDefine };
/**
* ListGrid
* @classdesc cheetahGrid.ListGrid
* @memberof cheetahGrid
*/
export class ListGrid<T> extends DrawGrid implements ListGridAPI<T> {
protected [_]: ListGridProtected<T> = this[_];
public disabled = false;
public readOnly = false;
static get EVENT_TYPE(): typeof LG_EVENT_TYPE {
return LG_EVENT_TYPE;
}
/**
* constructor
*
* @constructor
* @param options Constructor options
*/
constructor(options: ListGridConstructorOptions<T> = {}) {
super(omit(options, ["colCount", "rowCount", "frozenRowCount"]));
const protectedSpace = this[_];
protectedSpace.header = options.header || [];
protectedSpace.layout = options.layout || [];
protectedSpace.headerRowHeight = options.headerRowHeight || [];
if (options.dataSource) {
_setDataSource(this, options.dataSource);
} else {
_setRecords(this, options.records);
}
protectedSpace.allowRangePaste = options.allowRangePaste ?? false;
_refreshHeader(this);
protectedSpace.sortState = {
col: -1,
row: -1,
order: undefined,
};
protectedSpace.gridCanvasHelper = new GridCanvasHelper(this);
protectedSpace.theme = themes.of(options.theme);
protectedSpace.messageHandler = new MessageHandler(
this,
(col: number, row: number): Message => _getCellMessage(this, col, row)
);
protectedSpace.tooltipHandler = new TooltipHandler(this);
this.invalidate();
protectedSpace.handler.on(window, "resize", () => {
this.updateSize();
this.updateScroll();
this.invalidate();
});
}
/**
* Dispose the grid instance.
* @returns {void}
*/
dispose(): void {
const protectedSpace = this[_];
protectedSpace.messageHandler.dispose();
protectedSpace.tooltipHandler.dispose();
super.dispose();
}
/**
* Gets the define of the header.
*/
get header(): HeadersDefine<T> {
return this[_].header;
}
/**
* Sets the define of the header with the given data.
* <pre>
* column options
* -----
* caption: header caption
* field: field name
* width: column width
* minWidth: column min width
* maxWidth: column max width
* icon: icon definition
* message: message key name
* columnType: column type
* action: column action
* style: column style
* headerType: header type
* headerStyle: header style
* headerAction: header action
* headerField: header field name
* headerIcon: header icon definition
* sort: define sort setting
* -----
*
* multiline header
* -----
* caption: header caption
* columns: columns define
* -----
* </pre>
*/
set header(header: HeadersDefine<T>) {
this[_].header = header;
_refreshHeader(this);
}
/**
* Gets the define of the layout.
*/
get layout(): LayoutDefine<T> {
return this[_].layout;
}
/**
* Sets the define of the layout with the given data.
*/
set layout(layout: LayoutDefine<T>) {
this[_].layout = layout;
_refreshHeader(this);
}
/**
* Gets the define of the headerRowHeight.
*/
get headerRowHeight(): number | number[] {
return this[_].headerRowHeight;
}
/**
* Sets the define of the headerRowHeight with the given data.
*/
set headerRowHeight(headerRowHeight: number | number[]) {
this[_].headerRowHeight = headerRowHeight || [];
_refreshHeader(this);
}
/**
* Get the row count per record
*/
get recordRowCount(): number {
return this[_].layoutMap.bodyRowCount;
}
/**
* Get the records.
*/
get records(): T[] | null {
return this[_].records || null;
}
/**
* Set the records from given
*/
set records(records: T[] | null) {
if (records == null) {
return;
}
_setRecords(this, records);
_refreshRowCount(this);
this.invalidate();
}
/**
* Get the data source.
*/
get dataSource(): DataSource<T> {
return this[_].dataSource;
}
/**
* Set the data source from given
*/
set dataSource(dataSource: DataSource<T>) {
_setDataSource(this, dataSource);
_refreshRowCount(this);
this.invalidate();
}
/**
* Get the theme.
*/
get theme(): Theme | null {
return this[_].theme;
}
/**
* Set the theme from given
*/
set theme(theme: Theme | null) {
this[_].theme = themes.of(theme);
this.invalidate();
}
/**
* If set to true to allow pasting of ranges.
*/
get allowRangePaste(): boolean {
return this[_].allowRangePaste;
}
set allowRangePaste(allowRangePaste: boolean) {
this[_].allowRangePaste = allowRangePaste;
}
/**
* Get the font definition as a string.
* @override
*/
get font(): string {
return super.font || this[_].gridCanvasHelper.theme.font;
}
/**
* Set the font definition with the given string.
* @override
*/
set font(font: string) {
super.font = font;
}
/**
* Get the background color of the underlay.
* @override
*/
get underlayBackgroundColor(): string {
return (
super.underlayBackgroundColor ||
this[_].gridCanvasHelper.theme.underlayBackgroundColor
);
}
/**
* Set the background color of the underlay.
* @override
*/
set underlayBackgroundColor(underlayBackgroundColor: string) {
super.underlayBackgroundColor = underlayBackgroundColor;
}
/**
* Get the sort state.
*/
get sortState(): SortState {
return this[_].sortState;
}
/**
* Sets the sort state.
* If `null` to set, the sort state is initialized.
*/
set sortState(sortState: SortState | null) {
const oldState = this.sortState;
let oldField;
if (oldState.col >= 0 && oldState.row >= 0) {
oldField = this.getHeaderField(oldState.col, oldState.row);
}
const newState = (this[_].sortState =
sortState != null
? sortState
: {
col: -1,
row: -1,
order: undefined,
});
let newField;
if (newState.col >= 0 && newState.row >= 0) {
newField = this.getHeaderField(newState.col, newState.row);
}
// bind header value
if (oldField != null && oldField !== newField) {
this.setHeaderValue(oldState.col, oldState.row, undefined);
}
if (newField != null) {
this.setHeaderValue(newState.col, newState.row, newState.order);
}
}
/**
* Get the header values.
*/
get headerValues(): HeaderValues {
return this[_].headerValues || (this[_].headerValues = new Map());
}
/**
* Sets the header values.
*/
set headerValues(headerValues: HeaderValues) {
this[_].headerValues = headerValues || new Map();
}
/**
* Get the field of the given column index.
* @param {number} col The column index.
* @param {number} row The row index.
* @return {*} The field object.
*/
getField(col: number, row: number): FieldDef<T> | undefined {
return this[_].layoutMap.getBody(
col,
row ?? this[_].layoutMap.headerRowCount
).field;
}
/**
* Get the column define of the given column index.
* @param {number} col The column index.
* @param {number} row The row index.
* @return {*} The column define object.
*/
getColumnDefine(col: number, row: number): ColumnDefine<T> {
return this[_].layoutMap.getBody(
col,
row ?? this[_].layoutMap.headerRowCount
).define;
}
getColumnType(col: number, row: number): ColumnTypeAPI {
return this[_].layoutMap.getBody(col, row).columnType;
}
getColumnAction(col: number, row: number): ColumnActionAPI | undefined {
return this[_].layoutMap.getBody(col, row).action;
}
/**
* Get the header field of the given header cell.
* @param {number} col The column index.
* @param {number} row The header row index.
* @return {*} The field object.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getHeaderField(col: number, row: number): any | undefined {
const hd = this[_].layoutMap.getHeader(col, row);
return hd.field;
}
/**
* Get the header define of the given header cell.
* @param {number} col The column index.
* @param {number} row The header row index.
* @return {*} The header define object.
*/
getHeaderDefine(col: number, row: number): HeaderDefine<T> {
const hd = this[_].layoutMap.getHeader(col, row);
return hd.define;
}
/**
* Get the record of the given row index.
* @param {number} row The row index.
* @return {object} The record.
*/
getRowRecord(row: number): MaybePromiseOrUndef<T> {
if (row < this[_].layoutMap.headerRowCount) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return undefined;
} else {
return this[_].dataSource.get(_getRecordIndexByRow(this, row));
}
}
/**
* Get the record index of the given row index.
* @param {number} row The row index.
*/
getRecordIndexByRow(row: number): number {
return _getRecordIndexByRow(this, row);
}
/**
* Gets the row index starting at the given record index.
* @param {number} index The record index.
*/
getRecordStartRowByRecordIndex(index: number): number {
return this[_].layoutMap.getRecordStartRowByRecordIndex(index);
}
/**
* Get the column index of the given field.
* @param {*} field The field.
* @return {number} The column index.
* @deprecated use `getCellRangeByField` instead
*/
getColumnIndexByField(field: FieldDef<T>): number | null {
const range = this.getCellRangeByField(field, 0);
return range?.start.col ?? null;
}
/**
* Get the column index of the given field.
* @param {*} field The field.
* @param {number} index The record index
* @return {number} The column index.
*/
getCellRangeByField(field: FieldDef<T>, index: number): CellRange | null {
const { layoutMap } = this[_];
const colObj = layoutMap.columnObjects.find((col) => col.field === field);
if (colObj) {
const layoutRange = layoutMap.getBodyLayoutRangeById(colObj.id);
const startRow = layoutMap.getRecordStartRowByRecordIndex(index);
return {
start: {
col: layoutRange.start.col,
row: startRow + layoutRange.start.row,
},
end: {
col: layoutRange.end.col,
row: startRow + layoutRange.end.row,
},
};
}
return null;
}
/**
* Focus the cell.
* @param {*} field The field.
* @param {number} index The record index
* @return {void}
*/
focusGridCell(field: FieldDef<T>, index: number): void {
const {
start: { col: startCol, row: startRow },
end: { col: endCol, row: endRow },
} = this.selection.range;
const newFocus = this.getCellRangeByField(field, index)?.start;
if (newFocus == null) {
return;
}
this.focusCell(newFocus.col, newFocus.row);
this.selection.select = newFocus;
this.invalidateGridRect(startCol, startRow, endCol, endRow);
this.invalidateCell(newFocus.col, newFocus.row);
}
/**
* Scroll to where cell is visible.
* @param {*} field The field.
* @param {number} index The record index
* @return {void}
*/
makeVisibleGridCell(field: FieldDef<T>, index: number): void {
const cell = this.getCellRangeByField(field, index)?.start;
this.makeVisibleCell(
cell?.col ?? 0,
cell?.row ?? this[_].layoutMap.headerRowCount
);
}
getGridCanvasHelper(): GridCanvasHelper<T> {
return this[_].gridCanvasHelper;
}
/**
* Get cell range information for a given cell.
* @param {number} col column index of the cell
* @param {number} row row index of the cell
* @returns {object} cell range info
*/
getCellRange(col: number, row: number): CellRange {
return _getCellRange(this, col, row);
}
/**
* Get header range information for a given cell.
* @param {number} col column index of the cell
* @param {number} row row index of the cell
* @returns {object} cell range info
* @deprecated use `getCellRange` instead
*/
getHeaderCellRange(col: number, row: number): CellRange {
return this.getCellRange(col, row);
}
protected getCopyCellValue(
col: number,
row: number,
range?: CellRange
): unknown {
const cellRange = _getCellRange(this, col, row);
const startCol = range
? Math.max(range.start.col, cellRange.start.col)
: cellRange.start.col;
const startRow = range
? Math.max(range.start.row, cellRange.start.row)
: cellRange.start.row;
if (startCol !== col || startRow !== row) {
return "";
}
const value = _getCellValue(this, col, row);
if (row < this[_].layoutMap.headerRowCount) {
const headerData = this[_].layoutMap.getHeader(col, row);
return headerData.headerType.getCopyCellValue(value, this, { col, row });
}
const columnData = this[_].layoutMap.getBody(col, row);
return columnData.columnType.getCopyCellValue(value, this, { col, row });
}
protected onDrawCell(
col: number,
row: number,
context: CellContext
): MaybePromise<void> {
const { layoutMap } = this[_];
let draw;
let style;
if (row < layoutMap.headerRowCount) {
const hd = layoutMap.getHeader(col, row);
draw = hd.headerType.onDrawCell;
({ style } = hd);
_updateRect(this, col, row, context);
} else {
const column = layoutMap.getBody(col, row);
draw = column.columnType.onDrawCell;
({ style } = column);
_updateRect(this, col, row, context);
}
const cellValue = _getCellValue(this, col, row);
if (this.rowCount <= row) {
// Depending on the FilterDataSource, the rowCount may be reduced.
return undefined;
}
return _onDrawValue(this, cellValue, context, { col, row }, style, draw);
}
doGetCellValue(
col: number,
row: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
valueCallback: (value: any) => void
): boolean {
if (row < this[_].layoutMap.headerRowCount) {
// nop
return false;
} else {
const value = _getCellValue(this, col, row);
if (isPromise(value)) {
//遅延中は無視
return false;
}
valueCallback(value);
}
return true;
}
doChangeValue(
col: number,
row: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
changeValueCallback: (before: any) => any
): MaybePromise<boolean> {
if (row < this[_].layoutMap.headerRowCount) {
// nop
return false;
} else {
const record = this.getRowRecord(row);
if (isPromise(record)) {
//遅延中は無視
return false;
}
const before = _getCellValue(this, col, row);
if (isPromise(before)) {
//遅延中は無視
return false;
}
const after = changeValueCallback(before);
if (after === undefined) {
return false;
}
const { field } = this[_].layoutMap.getBody(col, row);
this.fireListeners(LG_EVENT_TYPE.BEFORE_CHANGE_VALUE, {
col,
row,
record: record as T,
field: field as FieldDef<T>,
value: after,
oldValue: before,
});
return then(_setCellValue(this, col, row, after), (ret) => {
if (ret) {
const { field } = this[_].layoutMap.getBody(col, row);
this.fireListeners(LG_EVENT_TYPE.CHANGED_VALUE, {
col,
row,
record: record as T,
field: field as FieldDef<T>,
value: after,
oldValue: before,
});
}
return ret;
});
}
}
doSetPasteValue(
text: string,
test?: (data: SetPasteValueTestData<T>) => boolean
): void {
_onRangePaste.call<
ListGrid<T>,
[string, (data: SetPasteValueTestData<T>) => boolean],
void
>(this, text, test as (data: SetPasteValueTestData<T>) => boolean);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getHeaderValue(col: number, row: number): any | undefined {
const field = this.getHeaderField(col, row);
return this.headerValues.get(field);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setHeaderValue(col: number, row: number, newValue: any): void {
const field = this.getHeaderField(col, row);
const oldValue = this.headerValues.get(field);
this.headerValues.set(field, newValue);
this.fireListeners(LG_EVENT_TYPE.CHANGED_HEADER_VALUE, {
col,
row,
field,
value: newValue,
oldValue,
});
}
getLayoutCellId(col: number, row: number): LayoutObjectId {
return this[_].layoutMap.getCellId(col, row);
}
protected bindEventsInternal(): void {
const grid: DrawGridAPI = this as DrawGridAPI;
grid.listen(LG_EVENT_TYPE.SELECTED_CELL, (e: SelectedCellEvent) => {
const range = _getCellRange(this, e.col, e.row);
const {
start: { col: startCol, row: startRow },
end: { col: endCol, row: endRow },
} = range;
if (startCol !== endCol || startRow !== endRow) {
this.invalidateCellRange(range);
}
});
grid.listen(LG_EVENT_TYPE.PASTE_CELL, (e: PasteCellEvent) => {
if (!this[_].allowRangePaste) {
return;
}
const { start, end } = this.selection.range;
if (!e.multi && cellEquals(start, end)) {
return;
}
const { layoutMap } = this[_];
if (start.row < layoutMap.headerRowCount) {
return;
}
event.cancel(e.event);
_onRangePaste.call<ListGrid<T>, [string], void>(this, e.normalizeValue);
});
grid.listen(LG_EVENT_TYPE.DELETE_CELL, (e) => {
const { start } = this.selection.range;
const { layoutMap } = this[_];
if (start.row < layoutMap.headerRowCount) {
return;
}
event.cancel(e.event);
_onRangeDelete.call<ListGrid<T>, [], void>(this);
});
}
protected getMoveLeftColByKeyDownInternal({ col, row }: CellAddress): number {
const {
start: { col: startCol },
} = _getCellRange(this, col, row);
col = startCol;
return super.getMoveLeftColByKeyDownInternal({ col, row });
}
protected getMoveRightColByKeyDownInternal({
col,
row,
}: CellAddress): number {
const {
end: { col: endCol },
} = _getCellRange(this, col, row);
col = endCol;
return super.getMoveRightColByKeyDownInternal({ col, row });
}
protected getMoveUpRowByKeyDownInternal({ col, row }: CellAddress): number {
const {
start: { row: startRow },
} = _getCellRange(this, col, row);
row = startRow;
return super.getMoveUpRowByKeyDownInternal({ col, row });
}
protected getMoveDownRowByKeyDownInternal({ col, row }: CellAddress): number {
const {
end: { row: endRow },
} = _getCellRange(this, col, row);
row = endRow;
return super.getMoveDownRowByKeyDownInternal({ col, row });
}
protected getOffsetInvalidateCells(): number {
return 1;
}
protected getCopyRangeInternal(range: CellRange): CellRange {
const { start } = this.getCellRange(range.start.col, range.start.row);
const { end } = this.getCellRange(range.end.col, range.end.row);
return { start, end };
}
fireListeners<TYPE extends keyof ListGridEventHandlersEventMap<T>>(
type: TYPE,
...event: ListGridEventHandlersEventMap<T>[TYPE]
): ListGridEventHandlersReturnMap[TYPE][] {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return super.fireListeners(type as any, ...event);
}
}