UNPKG

cheetah-grid

Version:

Cheetah Grid is a high performance grid engine that works on canvas

1,908 lines (1,831 loc) 108 kB
import * as calc from "../internal/calc"; import * as hiDPI from "../internal/hiDPI"; import * as style from "../internal/style"; import type { AfterSelectedCellEvent, AnyFunction, BeforeSelectedCellEvent, CellAddress, CellContext, CellRange, DrawGridAPI, DrawGridEventHandlersEventMap, DrawGridEventHandlersReturnMap, DrawGridKeyboardOptions, EventListenerId, KeyboardEventListener, KeydownEvent, MousePointerCellEvent, PasteCellEvent, PasteRangeBoxValues, } from "../ts-types"; import { array, browser, event, isDescendantElement, isPromise, } from "../internal/utils"; import { normalizePasteValue, parsePasteRangeBoxValues, } from "../internal/paste-utils"; import { DG_EVENT_TYPE } from "./DG_EVENT_TYPE"; import { EventHandler } from "../internal/EventHandler"; import { EventTarget } from "./EventTarget"; import { NumberMap } from "../internal/NumberMap"; import { Rect } from "../internal/Rect"; import { Scrollable } from "../internal/Scrollable"; import { getFontSize } from "../internal/canvases"; //protected symbol import { getProtectedSymbol } from "../internal/symbolManager"; const { /** @private */ isTouchEvent, /** @private */ getMouseButtons, /** @private */ getKeyCode, /** @private */ cancel: cancelEvent, } = event; /** @private */ const _ = getProtectedSymbol(); /** @private */ function createRootElement(): HTMLElement { const element = document.createElement("div"); element.classList.add("cheetah-grid"); return element; } /** @private */ const KEY_BS = 8; /** @private */ const KEY_TAB = 9; /** @private */ const KEY_ENTER = 13; /** @private */ const KEY_END = 35; /** @private */ const KEY_HOME = 36; /** @private */ const KEY_LEFT = 37; /** @private */ const KEY_UP = 38; /** @private */ const KEY_RIGHT = 39; /** @private */ const KEY_DOWN = 40; /** @private */ const KEY_DEL = 46; /** @private */ const KEY_ALPHA_A = 65; /** @private */ const KEY_ALPHA_C = 67; /** @private */ const KEY_ALPHA_V = 86; //private methods /** @private */ function _vibrate(e: TouchEvent | MouseEvent): void { if (navigator.vibrate && isTouchEvent(e)) { navigator.vibrate(50); } } /** @private */ function _getTargetRowAt( this: DrawGrid, absoluteY: number ): { row: number; top: number } | null { const internal = this.getTargetRowAtInternal(absoluteY); if (internal != null) { return internal; } const findBefore = ( startRow: number, startBottom: number ): { top: number; row: number; } | null => { let bottom = startBottom; for (let row = startRow; row >= 0; row--) { const height = _getRowHeight.call(this, row); const top = bottom - height; if (top <= absoluteY && absoluteY < bottom) { return { top, row, }; } bottom = top; } return null; }; const findAfter = ( startRow: number, startBottom: number ): { top: number; row: number; } | null => { let top = startBottom - _getRowHeight.call(this, startRow); const { rowCount } = this[_]; for (let row = startRow; row < rowCount; row++) { const height = _getRowHeight.call(this, row); const bottom = top + height; if (top <= absoluteY && absoluteY < bottom) { return { top, row, }; } top = bottom; } return null; }; const candidateRow = Math.min( Math.ceil(absoluteY / this[_].defaultRowHeight), this.rowCount - 1 ); const bottom = _getRowsHeight.call(this, 0, candidateRow); if (absoluteY >= bottom) { return findAfter(candidateRow, bottom); } else { return findBefore(candidateRow, bottom); } } /** @private */ function _getTargetColAt( grid: DrawGrid, absoluteX: number ): { left: number; col: number; } | null { let left = 0; const { colCount } = grid[_]; for (let col = 0; col < colCount; col++) { const width = _getColWidth(grid, col); const right = left + width; if (right > absoluteX) { return { left, col, }; } left = right; } return null; } /** @private */ function _getTargetFrozenRowAt( grid: DrawGrid, absoluteY: number ): { top: number; row: number; } | null { if (!grid[_].frozenRowCount) { return null; } let { top } = grid[_].scroll; const rowCount = grid[_].frozenRowCount; for (let row = 0; row < rowCount; row++) { const height = _getRowHeight.call(grid, row); const bottom = top + height; if (bottom > absoluteY) { return { top, row, }; } top = bottom; } return null; } /** @private */ function _getTargetFrozenColAt( grid: DrawGrid, absoluteX: number ): { left: number; col: number; } | null { if (!grid[_].frozenColCount) { return null; } let { left } = grid[_].scroll; const colCount = grid[_].frozenColCount; for (let col = 0; col < colCount; col++) { const width = _getColWidth(grid, col); const right = left + width; if (right > absoluteX) { return { left, col, }; } left = right; } return null; } /** @private */ function _getFrozenRowsRect(grid: DrawGrid): Rect | null { if (!grid[_].frozenRowCount) { return null; } const { top } = grid[_].scroll; let height = 0; const rowCount = grid[_].frozenRowCount; for (let row = 0; row < rowCount; row++) { height += _getRowHeight.call(grid, row); } return new Rect(grid[_].scroll.left, top, grid[_].canvas.width, height); } /** @private */ function _getFrozenColsRect(grid: DrawGrid): Rect | null { if (!grid[_].frozenColCount) { return null; } const { left } = grid[_].scroll; let width = 0; const colCount = grid[_].frozenColCount; for (let col = 0; col < colCount; col++) { width += _getColWidth(grid, col); } return new Rect(left, grid[_].scroll.top, width, grid[_].canvas.height); } /** @private */ function _getCellDrawing( grid: DrawGrid, col: number, row: number ): DrawCellContext | null { if (!grid[_].drawCells[row]) { return null; } return grid[_].drawCells[row][col]; } /** @private */ function _putCellDrawing( grid: DrawGrid, col: number, row: number, context: DrawCellContext ): void { if (!grid[_].drawCells[row]) { grid[_].drawCells[row] = {}; } grid[_].drawCells[row][col] = context; } /** @private */ function _removeCellDrawing(grid: DrawGrid, col: number, row: number): void { if (!grid[_].drawCells[row]) { return; } delete grid[_].drawCells[row][col]; if (Object.keys(grid[_].drawCells[row]).length === 0) { delete grid[_].drawCells[row]; } } /** @private */ function _drawCell( this: DrawGrid, ctx: CanvasRenderingContext2D, col: number, absoluteLeft: number, width: number, row: number, absoluteTop: number, height: number, visibleRect: Rect, skipAbsoluteTop: number, skipAbsoluteLeft: number, drawLayers: DrawLayers ): void { const rect = new Rect( absoluteLeft - visibleRect.left, absoluteTop - visibleRect.top, width, height ); const drawRect = Rect.bounds( Math.max(absoluteLeft, skipAbsoluteLeft) - visibleRect.left, Math.max(absoluteTop, skipAbsoluteTop) - visibleRect.top, rect.right, rect.bottom ); if (drawRect.height > 0 && drawRect.width > 0) { ctx.save(); try { const cellDrawing = _getCellDrawing(this, col, row); if (cellDrawing) { cellDrawing.cancel(); } const dcContext = new DrawCellContext( col, row, ctx, rect, drawRect, !!cellDrawing, this[_].selection, drawLayers ); const p = this.onDrawCell(col, row, dcContext); if (isPromise(p)) { //遅延描画 _putCellDrawing(this, col, row, dcContext); const pCol = col; dcContext._delayMode(this, () => { _removeCellDrawing(this, pCol, row); }); p.then(() => { dcContext.terminate(); }); } } finally { ctx.restore(); } } } /** @private */ function _drawRow( grid: DrawGrid, ctx: CanvasRenderingContext2D, initFrozenCol: { left: number; col: number } | null, initCol: { left: number; col: number }, drawRight: number, row: number, absoluteTop: number, height: number, visibleRect: Rect, skipAbsoluteTop: number, drawLayers: DrawLayers ): void { const { colCount } = grid[_]; const drawOuter = (col: number, absoluteLeft: number): void => { //データ範囲外の描画 if ( col >= colCount - 1 && grid[_].canvas.width > absoluteLeft - visibleRect.left ) { const outerLeft = absoluteLeft - visibleRect.left; ctx.save(); ctx.beginPath(); ctx.fillStyle = grid.underlayBackgroundColor || "#F6F6F6"; ctx.rect( outerLeft, absoluteTop - visibleRect.top, grid[_].canvas.width - outerLeft, height ); ctx.fill(); ctx.restore(); } }; let skipAbsoluteLeft = 0; if (initFrozenCol) { let absoluteLeft = initFrozenCol.left; const count = grid[_].frozenColCount; for (let { col } = initFrozenCol; col < count; col++) { const width = _getColWidth(grid, col); _drawCell.call( grid, ctx, col, absoluteLeft, width, row, absoluteTop, height, visibleRect, skipAbsoluteTop, 0, drawLayers ); absoluteLeft += width; if (drawRight <= absoluteLeft) { //描画範囲外(終了) drawOuter(col, absoluteLeft); return; } } skipAbsoluteLeft = absoluteLeft; } let absoluteLeft = initCol.left; for (let { col } = initCol; col < colCount; col++) { const width = _getColWidth(grid, col); _drawCell.call( grid, ctx, col, absoluteLeft, width, row, absoluteTop, height, visibleRect, skipAbsoluteTop, skipAbsoluteLeft, drawLayers ); absoluteLeft += width; if (drawRight <= absoluteLeft) { //描画範囲外(終了) drawOuter(col, absoluteLeft); return; } } drawOuter(colCount - 1, absoluteLeft); } /** @private */ function _getInitContext(this: DrawGrid): CanvasRenderingContext2D { return this._getInitContext(); } /** @private */ function _invalidateRect(grid: DrawGrid, drawRect: Rect): void { const visibleRect = _getVisibleRect(grid); const { rowCount } = grid[_]; const ctx = _getInitContext.call(grid); const initRow = _getTargetRowAt.call( grid, Math.max(visibleRect.top, drawRect.top) ) || { top: _getRowsHeight.call(grid, 0, rowCount - 1), row: rowCount, }; const initCol = _getTargetColAt( grid, Math.max(visibleRect.left, drawRect.left) ) || { left: _getColsWidth(grid, 0, grid[_].colCount - 1), col: grid[_].colCount, }; const drawBottom = Math.min(visibleRect.bottom, drawRect.bottom); const drawRight = Math.min(visibleRect.right, drawRect.right); const initFrozenRow = _getTargetFrozenRowAt( grid, Math.max(visibleRect.top, drawRect.top) ); const initFrozenCol = _getTargetFrozenColAt( grid, Math.max(visibleRect.left, drawRect.left) ); const drawLayers = new DrawLayers(); const drawOuter = (row: number, absoluteTop: number): void => { //データ範囲外の描画 if ( row >= rowCount - 1 && grid[_].canvas.height > absoluteTop - visibleRect.top ) { const outerTop = absoluteTop - visibleRect.top; ctx.save(); ctx.beginPath(); ctx.fillStyle = grid.underlayBackgroundColor || "#F6F6F6"; ctx.rect( 0, outerTop, grid[_].canvas.width, grid[_].canvas.height - outerTop ); ctx.fill(); ctx.restore(); } }; let skipAbsoluteTop = 0; if (initFrozenRow) { let absoluteTop = initFrozenRow.top; const count = grid[_].frozenRowCount; for (let { row } = initFrozenRow; row < count; row++) { const height = _getRowHeight.call(grid, row); _drawRow( grid, ctx, initFrozenCol, initCol, drawRight, row, absoluteTop, height, visibleRect, 0, drawLayers ); absoluteTop += height; if (drawBottom <= absoluteTop) { //描画範囲外(終了) drawOuter(row, absoluteTop); drawLayers.draw(ctx); return; } } skipAbsoluteTop = absoluteTop; } let absoluteTop = initRow.top; for (let { row } = initRow; row < rowCount; row++) { const height = _getRowHeight.call(grid, row); //行の描画 _drawRow( grid, ctx, initFrozenCol, initCol, drawRight, row, absoluteTop, height, visibleRect, skipAbsoluteTop, drawLayers ); absoluteTop += height; if (drawBottom <= absoluteTop) { //描画範囲外(終了) drawOuter(row, absoluteTop); drawLayers.draw(ctx); return; } } drawOuter(rowCount - 1, absoluteTop); drawLayers.draw(ctx); } /** @private */ function _toPxWidth(grid: DrawGrid, width: string | number): number { return Math.round(calc.toPx(width, grid[_].calcWidthContext)); } /** @private */ function _adjustColWidth( grid: DrawGrid, col: number, orgWidth: number ): number { const limits = _getColWidthLimits(grid, col); return Math.max(_applyColWidthLimits(limits, orgWidth), 0); } /** @private */ function _applyColWidthLimits( limits: { min?: number; max?: number } | void | null, orgWidth: number ): number { if (!limits) { return orgWidth; } if (limits.min) { if (limits.min > orgWidth) { return limits.min; } } if (limits.max) { if (limits.max < orgWidth) { return limits.max; } } return orgWidth; } /** * Gets the definition of the column width. * @param {DrawGrid} grid grid instance * @param {number} col number of column * @returns {string|number} width definition * @private */ function _getColWidthDefine(grid: DrawGrid, col: number): string | number { const width = grid[_].colWidthsMap.get(col); if (width) { return width; } return grid.defaultColWidth; } /** * Gets the column width limits. * @param {DrawGrid} grid grid instance * @param {number} col number of column * @returns {object|null} the column width limits * @private */ function _getColWidthLimits( grid: DrawGrid, col: number ): | { min?: undefined; minDef?: undefined; max?: undefined; maxDef?: undefined; } | { min: number; minDef: string | number; max?: undefined; maxDef?: undefined; } | { min?: undefined; minDef?: undefined; max: number; maxDef: string | number; } | null { const limit = grid[_].colWidthsLimit[col]; if (!limit) { return null; } const result: { min?: number; max?: number; minDef?: string | number; maxDef?: string | number; } = {}; if (limit.min) { result.min = _toPxWidth(grid, limit.min); result.minDef = limit.min; } if (limit.max) { result.max = _toPxWidth(grid, limit.max); result.maxDef = limit.max; } return result as never; } /** * Checks if the given width definition is `auto`. * @param {string|number} width width definition to check * @returns {boolean} `true ` if the given width definition is `auto` * @private */ function isAutoDefine(width: string | number): width is "auto" { return Boolean( width && typeof width === "string" && width.toLowerCase() === "auto" ); } /** * Creates a formula to calculate the auto width. * @param {DrawGrid} grid grid instance * @returns {string} formula * @private */ function _calcAutoColWidthExpr(grid: DrawGrid, shortCircuit = true): string { const fullWidth = grid[_].calcWidthContext.full; let sumMin = 0; const others: (string | number)[] = []; let autoCount = 0; const hasLimitsOnAuto = []; for (let col = 0; col < grid[_].colCount; col++) { const def = _getColWidthDefine(grid, col); const limits = _getColWidthLimits(grid, col); if (isAutoDefine(def)) { if (limits) { hasLimitsOnAuto.push(limits); if (limits.min) { sumMin += limits.min; } } autoCount++; } else { let expr = def; if (limits) { const orgWidth = _toPxWidth(grid, expr); const newWidth = _applyColWidthLimits(limits, orgWidth); if (orgWidth !== newWidth) { expr = `${newWidth}px`; } sumMin += newWidth; } others.push(expr); } if (shortCircuit && sumMin > fullWidth) { // Returns 0px because it has consumed the full width. return "0px"; } } if (hasLimitsOnAuto.length && others.length) { const autoPx = (fullWidth - _toPxWidth( grid, `calc(${others .map((c) => (typeof c === "number" ? `${c}px` : c)) .join(" + ")})` )) / autoCount; hasLimitsOnAuto.forEach((limits) => { if (limits.min && autoPx < limits.min) { others.push(limits.minDef); autoCount--; } else if (limits.max && limits.max < autoPx) { others.push(limits.maxDef); autoCount--; } }); if (shortCircuit && autoCount <= 0) { return `${autoPx}px`; } } if (others.length) { const strDefs: string[] = []; let num = 0; others.forEach((c) => { if (typeof c === "number") { num += c; } else { strDefs.push(c); } }); strDefs.push(`${num}px`); return `calc((100% - (${strDefs.join(" + ")})) / ${autoCount})`; } else { return `${100 / autoCount}%`; } } /** * Calculate the pixels of width from the definition of width. * @param {DrawGrid} grid grid instance * @param {string|number} width width definition * @returns {number} the pixels of width * @private */ function _colWidthDefineToPxWidth( grid: DrawGrid, width: string | number ): number { if (isAutoDefine(width)) { return _toPxWidth(grid, _calcAutoColWidthExpr(grid)); } return _toPxWidth(grid, width); } /** @private */ function _getColWidth(grid: DrawGrid, col: number): number { const width = _getColWidthDefine(grid, col); return _adjustColWidth(grid, col, _colWidthDefineToPxWidth(grid, width)); } /** @private */ function _setColWidth( grid: DrawGrid, col: number, width: string | number | null ): void { if (width != null) { grid[_].colWidthsMap.put(col, width); } else { grid[_].colWidthsMap.remove(col); } } /** * Overwrites the definition of a column whose width is set to `auto` with the current auto width formula. * @param {DrawGrid} grid grid instance * @returns {void} * @private */ function _storeAutoColWidthExprs(grid: DrawGrid): void { let expr: string | null = null; for (let col = 0; col < grid[_].colCount; col++) { const def = _getColWidthDefine(grid, col); if (isAutoDefine(def)) { _setColWidth( grid, col, expr || (expr = _calcAutoColWidthExpr(grid, false)) ); } } } /** @private */ function _getColsWidth( grid: DrawGrid, startCol: number, endCol: number ): number { const defaultColPxWidth = _colWidthDefineToPxWidth( grid, grid.defaultColWidth ); const colCount = endCol - startCol + 1; let w = defaultColPxWidth * colCount; grid[_].colWidthsMap.each(startCol, endCol, (width, col) => { w += _adjustColWidth(grid, col, _colWidthDefineToPxWidth(grid, width)) - defaultColPxWidth; }); for (let col = startCol; col <= endCol; col++) { if (grid[_].colWidthsMap.has(col)) { continue; } const adj = _adjustColWidth(grid, col, defaultColPxWidth); if (adj !== defaultColPxWidth) { w += adj - defaultColPxWidth; } } return w; } /** @private */ function _getRowHeight(this: DrawGrid, row: number): number { const internal = this.getRowHeightInternal(row); if (internal != null) { return internal; } const height = this[_].rowHeightsMap.get(row); if (height) { return height; } return this[_].defaultRowHeight; } /** @private */ function _setRowHeight( grid: DrawGrid, row: number, height: number | null ): void { if (height != null) { grid[_].rowHeightsMap.put(row, height); } else { grid[_].rowHeightsMap.remove(row); } } /** @private */ function _getRowsHeight( this: DrawGrid, startRow: number, endRow: number ): number { const internal = this.getRowsHeightInternal(startRow, endRow); if (internal != null) { return internal; } const rowCount = endRow - startRow + 1; let h = this[_].defaultRowHeight * rowCount; this[_].rowHeightsMap.each(startRow, endRow, (height: number): void => { h += height - this[_].defaultRowHeight; }); return h; } /** @private */ function _getScrollWidth(grid: DrawGrid): number { return _getColsWidth(grid, 0, grid[_].colCount - 1); } /** @private */ function _getScrollHeight(this: DrawGrid, row?: number): number { const internal = this.getScrollHeightInternal(row); if (internal != null) { return internal; } let h = this[_].defaultRowHeight * this[_].rowCount; this[_].rowHeightsMap.each(0, this[_].rowCount - 1, (height) => { h += height - this[_].defaultRowHeight; }); return h; } /** @private */ function _onScroll(grid: DrawGrid, _e: Event): void { const lastLeft = grid[_].scroll.left; const lastTop = grid[_].scroll.top; const moveX = grid[_].scrollable.scrollLeft - lastLeft; const moveY = grid[_].scrollable.scrollTop - lastTop; //次回計算用情報を保持 grid[_].scroll = { left: grid[_].scrollable.scrollLeft, top: grid[_].scrollable.scrollTop, }; // If the focus is on the header, recalculate and move the focus position. const { focus } = grid[_].selection; const isFrozenCell = grid.isFrozenCell(focus.col, focus.row); if ( isFrozenCell && ((isFrozenCell?.col && moveX) || (isFrozenCell?.row && moveY)) ) { grid.setFocusCursor(focus.col, focus.row); } const visibleRect = _getVisibleRect(grid); if ( Math.abs(moveX) >= visibleRect.width || Math.abs(moveY) >= visibleRect.height ) { //全再描画 _invalidateRect(grid, visibleRect); } else { //差分再描画 grid[_].context.drawImage(grid[_].canvas, -moveX, -moveY); if (moveX !== 0) { //横移動の再描画範囲を計算 const redrawRect = visibleRect.copy(); if (moveX < 0) { redrawRect.width = -moveX; if (grid[_].frozenColCount > 0) { //固定列がある場合固定列分描画 const frozenRect = _getFrozenColsRect(grid)!; redrawRect.width += frozenRect.width; } } else if (moveX > 0) { redrawRect.left = redrawRect.right - moveX; } //再描画 _invalidateRect(grid, redrawRect); if (moveX > 0) { if (grid[_].frozenColCount > 0) { //固定列がある場合固定列描画 _invalidateRect(grid, _getFrozenColsRect(grid)!); } } } if (moveY !== 0) { //縦移動の再描画範囲を計算 const redrawRect = visibleRect.copy(); if (moveY < 0) { redrawRect.height = -moveY; if (grid[_].frozenRowCount > 0) { //固定行がある場合固定行分描画 const frozenRect = _getFrozenRowsRect(grid)!; redrawRect.height += frozenRect.height; } } else if (moveY > 0) { redrawRect.top = redrawRect.bottom - moveY; } //再描画 _invalidateRect(grid, redrawRect); if (moveY > 0) { if (grid[_].frozenRowCount > 0) { //固定行がある場合固定行描画 _invalidateRect(grid, _getFrozenRowsRect(grid)!); } } } } } /** @private */ // eslint-disable-next-line complexity function _onKeyDownMove(this: DrawGrid, e: KeyboardEvent): void { const keyCode = getKeyCode(e); const focusCell = e.shiftKey ? this.selection.focus : this.selection.select; const ctrlOrMeta = e.ctrlKey || e.metaKey; if (keyCode === KEY_LEFT) { if (e.altKey) return; // unknown modifier key if (ctrlOrMeta) { move(this, null, "W", e.shiftKey); } else { if (!hMove.call(this, "W", e.shiftKey)) { return; } } cancelEvent(e); } else if (keyCode === KEY_UP) { if (e.altKey) return; // unknown modifier key if (ctrlOrMeta) { move(this, "N", null, e.shiftKey); } else { if (!vMove.call(this, "N", e.shiftKey)) { return; } } cancelEvent(e); } else if (keyCode === KEY_RIGHT) { if (e.altKey) return; // unknown modifier key if (ctrlOrMeta) { move(this, null, "E", e.shiftKey); } else { if (!hMove.call(this, "E", e.shiftKey)) { return; } } cancelEvent(e); } else if (keyCode === KEY_DOWN) { if (e.altKey) return; // unknown modifier key if (ctrlOrMeta) { move(this, "S", null, e.shiftKey); } else { if (!vMove.call(this, "S", e.shiftKey)) { return; } } cancelEvent(e); } else if (keyCode === KEY_HOME) { if (e.altKey) return; // unknown modifier key if (ctrlOrMeta) { move(this, "N", "W", e.shiftKey); } else { move(this, null, "W", e.shiftKey); } cancelEvent(e); } else if (keyCode === KEY_END) { if (e.altKey) return; // unknown modifier key if (ctrlOrMeta) { move(this, "S", "E", e.shiftKey); } else { move(this, null, "E", e.shiftKey); } cancelEvent(e); } else if (this.keyboardOptions?.moveCellOnTab && keyCode === KEY_TAB) { if (e.altKey || ctrlOrMeta) return; // unknown modifier key let newCell: CellAddress | null = null; if (typeof this.keyboardOptions.moveCellOnTab === "function") { newCell = this.keyboardOptions.moveCellOnTab({ cell: focusCell, grid: this, event: e, }); } if (newCell) { _moveFocusCell.call(this, newCell.col, newCell.row, false); } else if (e.shiftKey) { if (!hMove.call(this, "W", false)) { const row = this.getMoveUpRowByKeyDownInternal(focusCell); if (0 > row) { return; } _moveFocusCell.call(this, this.colCount - 1, row, false); } } else { if (!hMove.call(this, "E", false)) { const row = this.getMoveDownRowByKeyDownInternal(focusCell); if (this.rowCount <= row) { return; } _moveFocusCell.call(this, 0, row, false); } } cancelEvent(e); } else if (this.keyboardOptions?.moveCellOnEnter && keyCode === KEY_ENTER) { if (e.altKey || ctrlOrMeta) return; // unknown modifier key let newCell: CellAddress | null = null; if (typeof this.keyboardOptions.moveCellOnEnter === "function") { newCell = this.keyboardOptions.moveCellOnEnter({ cell: focusCell, grid: this, event: e, }); } if (newCell) { _moveFocusCell.call(this, newCell.col, newCell.row, false); } else if (e.shiftKey) { if (!vMove.call(this, "N", false)) { const col = this.getMoveLeftColByKeyDownInternal(focusCell); if (0 > col) { return; } _moveFocusCell.call(this, col, this.rowCount - 1, false); } } else { if (!vMove.call(this, "S", false)) { const col = this.getMoveRightColByKeyDownInternal(focusCell); if (this.colCount <= col) { return; } _moveFocusCell.call( this, col, Math.min(this.frozenRowCount, this.rowCount - 1), false ); } } cancelEvent(e); } else if ( this.keyboardOptions?.selectAllOnCtrlA && keyCode === KEY_ALPHA_A ) { if (e.altKey || e.shiftKey) return; // unknown modifier key if (!ctrlOrMeta) return; this.selection.range = { start: { col: 0, row: 0 }, end: { col: this.colCount - 1, row: this.rowCount - 1 }, }; this.invalidate(); cancelEvent(e); } function move( grid: DrawGrid, vDir: "N" | "S" | null, hDir: "W" | "E" | null, shiftKeyFlg: boolean ): void { const row = vDir === "S" ? grid.rowCount - 1 : vDir === "N" ? 0 : focusCell.row; const col = hDir === "E" ? grid.colCount - 1 : hDir === "W" ? 0 : focusCell.col; _moveFocusCell.call(grid, col, row, shiftKeyFlg); } function vMove( this: DrawGrid, vDir: "N" | "S", shiftKeyFlg: boolean ): boolean { const { col } = focusCell; let row: number; if (vDir === "S") { row = this.getMoveDownRowByKeyDownInternal(focusCell); if (this.rowCount <= row) { // Avoids the problem of the scroll position breaking due to a delayed scrolling event if user hold down the arrow keys. this.makeVisibleCell(col, this.rowCount - 1); return false; } } else { row = this.getMoveUpRowByKeyDownInternal(focusCell); if (row < 0) { // Avoids the problem of the scroll position breaking due to a delayed scrolling event if user hold down the arrow keys. this.makeVisibleCell(col, 0); return false; } } _moveFocusCell.call(this, col, row, shiftKeyFlg); return true; } function hMove( this: DrawGrid, hDir: "W" | "E", shiftKeyFlg: boolean ): boolean { const { row } = focusCell; let col: number; if (hDir === "E") { col = this.getMoveRightColByKeyDownInternal(focusCell); if (this.colCount <= col) { // Avoids the problem of the scroll position breaking due to a delayed scrolling event if user hold down the arrow keys. this.makeVisibleCell(this.colCount - 1, row); return false; } } else { col = this.getMoveLeftColByKeyDownInternal(focusCell); if (col < 0) { // Avoids the problem of the scroll position breaking due to a delayed scrolling event if user hold down the arrow keys. this.makeVisibleCell(0, row); return false; } } _moveFocusCell.call(this, col, row, shiftKeyFlg); return true; } } /** @private */ function _moveFocusCell( this: DrawGrid, col: number, row: number, shiftKey: boolean ): void { const offset = this.getOffsetInvalidateCells(); function extendRange(range: CellRange): CellRange { if (offset > 0) { range.start.col -= offset; range.start.row -= offset; range.end.col += offset; range.end.row += offset; } return range; } const beforeRange = extendRange(this.selection.range); const beforeRect = this.getCellRangeRect(beforeRange); this.selection._setFocusCell(col, row, shiftKey); this.makeVisibleCell(col, row); this.focusCell(col, row); const afterRange = extendRange(this.selection.range); const afterRect = this.getCellRangeRect(afterRange); if (afterRect.intersection(beforeRect)) { const invalidateRect = Rect.max(afterRect, beforeRect); _invalidateRect(this, invalidateRect); } else { _invalidateRect(this, beforeRect); _invalidateRect(this, afterRect); } } /** @private */ function _updatedSelection(this: DrawGrid): void { const { focusControl } = this[_]; const { col: selCol, row: selRow } = this[_].selection.select; const results = this.fireListeners(DG_EVENT_TYPE.EDITABLEINPUT_CELL, { col: selCol, row: selRow, }); const editMode = array.findIndex(results, (v) => !!v) >= 0; focusControl.editMode = editMode; if (editMode) { focusControl.storeInputStatus(); focusControl.setDefaultInputStatus(); this.fireListeners(DG_EVENT_TYPE.MODIFY_STATUS_EDITABLEINPUT_CELL, { col: selCol, row: selRow, input: focusControl.input, }); } } /** @private */ function _getMouseAbstractPoint( grid: DrawGrid, evt: TouchEvent | MouseEvent ): { x: number; y: number } | null { let e: MouseEvent | Touch; if (isTouchEvent(evt)) { e = evt.changedTouches[0]; } else { e = evt; } const clientX = e.clientX || e.pageX + window.scrollX; const clientY = e.clientY || e.pageY + window.scrollY; const rect = grid[_].canvas.getBoundingClientRect(); if (rect.right <= clientX) { return null; } if (rect.bottom <= clientY) { return null; } const x = clientX - rect.left + grid[_].scroll.left; const y = clientY - rect.top + grid[_].scroll.top; return { x, y }; } /** @private */ function _bindEvents(this: DrawGrid): void { // eslint-disable-next-line consistent-this, @typescript-eslint/no-this-alias const grid = this; const { handler, element, scrollable } = grid[_]; const getCellEventArgsSet = <EVT extends TouchEvent | MouseEvent>( e: EVT ): { abstractPos?: { x: number; y: number }; cell?: CellAddress; eventArgs?: CellAddress & { event: EVT }; } => { const abstractPos = _getMouseAbstractPoint(grid, e); if (!abstractPos) { return {}; } const cell = grid.getCellAt(abstractPos.x, abstractPos.y); if (cell.col < 0 || cell.row < 0) { return { abstractPos, cell, }; } const eventArgs = { col: cell.col, row: cell.row, event: e, }; return { abstractPos, cell, eventArgs, }; }; const canResizeColumn = (col: number): boolean => { if (grid[_].disableColumnResize) { return false; } const limit = grid[_].colWidthsLimit[col]; if (!limit || !limit.min || !limit.max) { return true; } return limit.max !== limit.min; }; handler.on(element, "mousedown", (e) => { const eventArgsSet = getCellEventArgsSet(e); const { abstractPos, eventArgs } = eventArgsSet; if (!abstractPos) { return; } if (eventArgs) { const results = grid.fireListeners( DG_EVENT_TYPE.MOUSEDOWN_CELL, eventArgs ); if (array.findIndex(results, (v) => !v) >= 0) { return; } } if ( getMouseButtons(e) !== 1 && // For mobile safari. If we do not post-process here, the keyboard will not start in Mobile Safari. e.buttons !== 0 ) { return; } const resizeCol = _getResizeColAt(grid, abstractPos.x, abstractPos.y); if (resizeCol >= 0 && canResizeColumn(resizeCol)) { //幅変更 grid[_].columnResizer.start(resizeCol, e); } else { //選択 grid[_].cellSelector.start(e); } }); handler.on(element, "mouseup", (e) => { if (!grid.hasListeners(DG_EVENT_TYPE.MOUSEUP_CELL)) { return; } const { eventArgs } = getCellEventArgsSet(e); if (eventArgs) { grid.fireListeners(DG_EVENT_TYPE.MOUSEUP_CELL, eventArgs); } }); let doubleTapBefore: | (CellAddress & { event: TouchEvent | MouseEvent }) | null | undefined = null; let longTouchId: NodeJS.Timeout | null = null; let useTouch: { timeoutId?: NodeJS.Timeout } | null = null; function useTouchStart() { if (useTouch?.timeoutId != null) clearTimeout(useTouch.timeoutId); useTouch = {}; } function useTouchEnd() { if (useTouch) { if (useTouch.timeoutId != null) clearTimeout(useTouch.timeoutId); useTouch.timeoutId = setTimeout(() => { useTouch = null; }, 400); } } handler.on(element, "touchstart", (e) => { // Since it is an environment where touch start can be used, it blocks mousemove that occurs after this. useTouchStart(); const { eventArgs } = getCellEventArgsSet(e); if (eventArgs) { grid.fireListeners(DG_EVENT_TYPE.TOUCHSTART_CELL, eventArgs); } if (!doubleTapBefore) { doubleTapBefore = eventArgs; setTimeout(() => { doubleTapBefore = null; }, 350); } else { if ( eventArgs && eventArgs.col === doubleTapBefore.col && eventArgs.row === doubleTapBefore.row ) { grid.fireListeners(DG_EVENT_TYPE.DBLTAP_CELL, eventArgs); } doubleTapBefore = null; if (e.defaultPrevented) { return; } } if (e.targetTouches.length > 1) { // If touchstart with multiple fingers, // it is not considered as an operation event. return; } longTouchId = setTimeout(() => { //長押しした場合選択モード longTouchId = null; const abstractPos = _getMouseAbstractPoint(grid, e); if (!abstractPos) { return; } const resizeCol = _getResizeColAt(grid, abstractPos.x, abstractPos.y, 15); if (resizeCol >= 0 && canResizeColumn(resizeCol)) { //幅変更 grid[_].columnResizer.start(resizeCol, e); } else { //選択 grid[_].cellSelector.start(e); } }, 500); }); function cancel(_e: Event): void { if (longTouchId) { clearTimeout(longTouchId); longTouchId = null; } } handler.on(element, "touchcancel", (e) => { cancel(e); useTouchEnd(); }); handler.on(element, "touchmove", cancel); handler.on(element, "touchend", (e) => { useTouchEnd(); if (longTouchId) { clearTimeout(longTouchId); grid[_].cellSelector.select(e); longTouchId = null; } }); let isMouseover = false; let mouseEnterCell: CellAddress | null = null; let mouseOverCell: CellAddress | null = null; type MousePointerCellEventInfoProps = Pick< MousePointerCellEvent, "related" | "event" >; function onMouseenterCell( cell: CellAddress, props: MousePointerCellEventInfoProps ): void { grid.fireListeners(DG_EVENT_TYPE.MOUSEENTER_CELL, { ...props, col: cell.col, row: cell.row, }); mouseEnterCell = cell; } function onMouseleaveCell( props: MousePointerCellEventInfoProps ): CellAddress | undefined { const beforeMouseCell = mouseEnterCell; mouseEnterCell = null; if (beforeMouseCell) { grid.fireListeners(DG_EVENT_TYPE.MOUSELEAVE_CELL, { ...props, col: beforeMouseCell.col, row: beforeMouseCell.row, }); } return beforeMouseCell || undefined; } function onMouseoverCell( cell: CellAddress, props: MousePointerCellEventInfoProps ): void { grid.fireListeners(DG_EVENT_TYPE.MOUSEOVER_CELL, { ...props, col: cell.col, row: cell.row, }); mouseOverCell = cell; } function onMouseoutCell( props: MousePointerCellEventInfoProps ): CellAddress | undefined { const beforeMouseCell = mouseOverCell; mouseOverCell = null; if (beforeMouseCell) { grid.fireListeners(DG_EVENT_TYPE.MOUSEOUT_CELL, { ...props, col: beforeMouseCell.col, row: beforeMouseCell.row, }); } return beforeMouseCell || undefined; } const scrollElement = scrollable.getElement(); handler.on(scrollElement, "mouseover", (_e: MouseEvent): void => { isMouseover = true; }); handler.on(scrollElement, "mouseout", (event: MouseEvent): void => { isMouseover = false; onMouseoutCell({ event }); }); handler.on(element, "mouseleave", (event: MouseEvent): void => { onMouseleaveCell({ event }); }); handler.on(element, "mousemove", (e) => { if (useTouch) { // Probably a mousemove event triggered by a touchstart. Therefore, this event is blocked. return; } const eventArgsSet = getCellEventArgsSet(e); const { abstractPos, eventArgs } = eventArgsSet; if (eventArgs) { const beforeMouseCell = mouseEnterCell; if (beforeMouseCell) { grid.fireListeners(DG_EVENT_TYPE.MOUSEMOVE_CELL, eventArgs); if ( beforeMouseCell.col !== eventArgs.col || beforeMouseCell.row !== eventArgs.row ) { const enterCell = { col: eventArgs.col, row: eventArgs.row, }; const outCell = onMouseoutCell({ related: enterCell, event: e }); const leaveCell = onMouseleaveCell({ related: enterCell, event: e }); onMouseenterCell(enterCell, { related: leaveCell, event: e }); if (isMouseover) { onMouseoverCell(enterCell, { related: outCell, event: e }); } } else if (isMouseover && !mouseOverCell) { onMouseoverCell( { col: eventArgs.col, row: eventArgs.row, }, { event: e, } ); } } else { const enterCell = { col: eventArgs.col, row: eventArgs.row, }; onMouseenterCell(enterCell, { event: e, }); if (isMouseover) { onMouseoverCell(enterCell, { event: e, }); } grid.fireListeners(DG_EVENT_TYPE.MOUSEMOVE_CELL, eventArgs); } } else { onMouseoutCell({ event: e, }); onMouseleaveCell({ event: e, }); } if (grid[_].columnResizer.moving(e) || grid[_].cellSelector.moving(e)) { return; } const { style } = element; if (!abstractPos) { if (style.cursor === "col-resize") { style.cursor = ""; } return; } const resizeCol = _getResizeColAt(grid, abstractPos.x, abstractPos.y); if (resizeCol >= 0 && canResizeColumn(resizeCol)) { style.cursor = "col-resize"; } else { if (style.cursor === "col-resize") { style.cursor = ""; } } }); handler.on(element, "click", (e) => { if ( grid[_].columnResizer.lastMoving(e) || grid[_].cellSelector.lastMoving(e) ) { return; } if (!grid.hasListeners(DG_EVENT_TYPE.CLICK_CELL)) { return; } const { eventArgs } = getCellEventArgsSet(e); if (!eventArgs) { return; } grid.fireListeners(DG_EVENT_TYPE.CLICK_CELL, eventArgs); }); handler.on(element, "contextmenu", (e) => { if (!grid.hasListeners(DG_EVENT_TYPE.CONTEXTMENU_CELL)) { return; } const { eventArgs } = getCellEventArgsSet(e); if (!eventArgs) { return; } grid.fireListeners(DG_EVENT_TYPE.CONTEXTMENU_CELL, eventArgs); }); handler.on(element, "dblclick", (e) => { if (!grid.hasListeners(DG_EVENT_TYPE.DBLCLICK_CELL)) { return; } const { eventArgs } = getCellEventArgsSet(e); if (!eventArgs) { return; } grid.fireListeners(DG_EVENT_TYPE.DBLCLICK_CELL, eventArgs); }); grid[_].focusControl.onKeyDown((evt: KeydownEvent) => { grid.fireListeners(DG_EVENT_TYPE.KEYDOWN, evt); }); grid[_].selection.listen(DG_EVENT_TYPE.SELECTED_CELL, (data) => { grid.fireListeners(DG_EVENT_TYPE.SELECTED_CELL, data, data.selected); }); scrollable.onScroll((e) => { _onScroll(grid, e); grid.fireListeners(DG_EVENT_TYPE.SCROLL, { event: e }); }); grid[_].focusControl.onKeyDownMove((e) => { _onKeyDownMove.call(grid, e); }); grid.listen("copydata", (range) => { const copyRange = grid.getCopyRangeInternal(range); const copyLines: string[] = []; for (let { row } = copyRange.start; row <= copyRange.end.row; row++) { let copyLine = ""; for (let { col } = copyRange.start; col <= copyRange.end.col; col++) { const copyCellValue = grid.getCopyCellValue(col, row, copyRange); let strCellValue: string; if (typeof copyCellValue === "string") { strCellValue = copyCellValue; } else if ( copyCellValue == null || // Asynchronous data is treated as empty. (typeof Promise !== "undefined" && copyCellValue instanceof Promise) ) { strCellValue = ""; } else { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions strCellValue = `${copyCellValue}`; if (/^\[object .*\]$/.exec(strCellValue)) { // Ignore maybe object strCellValue = ""; } } copyLine += /[\t\n]/.test(strCellValue) ? // Need quote `"${strCellValue.replace(/"/g, '""')}"` : strCellValue; if (col < copyRange.end.col) { copyLine += "\t"; } } copyLines.push(copyLine); } return copyLines.join("\n"); }); grid[_].focusControl.onCopy((_e: ClipboardEvent): string | void => array.find( grid.fireListeners("copydata", grid[_].selection.range), (r) => r != null ) ); grid[_].focusControl.onPaste( ({ value, event }: { value: string; event: ClipboardEvent }) => { const { trimOnPaste } = grid; const normalizedValue = normalizePasteValue(value); const { col, row } = grid[_].selection.select; const multi = /[\r\n\u2028\u2029\t]/.test(normalizedValue); // is multi cell values let rangeBoxValues: PasteRangeBoxValues | null = null; const pasteCellEvent: PasteCellEvent = { col, row, value, normalizeValue: trimOnPaste ? normalizedValue.trim() : normalizedValue, multi, get rangeBoxValues(): PasteRangeBoxValues { return ( rangeBoxValues ?? (rangeBoxValues = parsePasteRangeBoxValues(normalizedValue, { trimOnPaste, })) ); }, event, }; grid.fireListeners(DG_EVENT_TYPE.PASTE_CELL, pasteCellEvent); } ); grid[_].focusControl.onInput((value) => { const { col, row } = grid[_].selection.select; grid.fireListeners(DG_EVENT_TYPE.INPUT_CELL, { col, row, value }); }); grid[_].focusControl.onDelete((event) => { const { col, row } = grid[_].selection.select; grid.fireListeners(DG_EVENT_TYPE.DELETE_CELL, { col, row, event }); }); grid[_].focusControl.onFocus((e: FocusEvent) => { grid.fireListeners(DG_EVENT_TYPE.FOCUS_GRID, e); grid[_].focusedGrid = true; const { col, row } = grid[_].selection.select; grid.invalidateCell(col, row); }); grid[_].focusControl.onBlur((e) => { grid.fireListeners(DG_EVENT_TYPE.BLUR_GRID, e); grid[_].focusedGrid = false; const { col, row } = grid[_].selection.select; grid.invalidateCell(col, row); }); } /** @private */ function _getResizeColAt( grid: DrawGrid, abstractX: number, abstractY: number, offset = 5 ): number { if (grid[_].frozenRowCount <= 0) { return -1; } const frozenRect = _getFrozenRowsRect(grid)!; if (!frozenRect.inPoint(abstractX, abstractY)) { return -1; } const cell = grid.getCellAt(abstractX, abstractY); const cellRect = grid.getCellRect(cell.col, cell.row); if (abstractX < cellRect.left + offset) { return cell.col - 1; } if (cellRect.right - offset < abstractX) { return cell.col; } return -1; } /** @private */ function _getVisibleRect(grid: DrawGrid): Rect { const { scroll: { left, top }, canvas: { width, height }, } = grid[_]; return new Rect(left, top, width, height); } /** @private */ function _getScrollableVisibleRect(grid: DrawGrid): Rect { let frozenColsWidth = 0; if (grid[_].frozenColCount > 0) { //固定列がある場合固定列分描画 const frozenRect = _getFrozenColsRect(grid)!; frozenColsWidth = frozenRect.width; } let frozenRowsHeight = 0; if (grid[_].frozenRowCount > 0) { //固定列がある場合固定列分描画 const frozenRect = _getFrozenRowsRect(grid)!; frozenRowsHeight = frozenRect.height; } return new Rect( grid[_].scrollable.scrollLeft + frozenColsWidth, grid[_].scrollable.scrollTop + frozenRowsHeight, grid[_].canvas.width - frozenColsWidth, grid[_].canvas.height - frozenRowsHeight ); } /** @private */ function _toRelativeRect(grid: DrawGrid, absoluteRect: Rect): Rect { const rect = absoluteRect.copy(); const visibleRect = _getVisibleRect(grid); rect.offsetLeft(-visibleRect.left); rect.offsetTop(-visibleRect.top); return rect; } //end private methods // // // // /** * managing mouse down moving * @private */ class BaseMouseDownMover { protected _grid: DrawGrid; private _handler: EventHandler; private _events: { mousemove?: EventListenerId; mouseup?: EventListenerId; touchmove?: EventListenerId; touchend?: EventListenerId; touchcancel?: EventListenerId; }; private _started: boolean; private _moved: boolean; private _mouseEndPoint?: { x: number; y: number } | null; constructor(grid: DrawGrid) { this._grid = grid; this._handler = new EventHandler(); this._events = {}; this._started = false; this._moved = false; } moving(_e: MouseEvent | TouchEvent): boolean { return !!this._started; } lastMoving(e: MouseEvent | TouchEvent): boolean { // mouseup後すぐに、clickイベントを反応しないようにする制御要 if (this.moving(e)) { return true; } const last = this._mouseEndPoint; if (!last) { return false; } const pt = _getMouseAbstractPoint(this._grid, e); return pt != null && pt.x === last.x && pt.y === last.y; } protected _bindMoveAndUp(e: MouseEvent | TouchEvent): void { const events = this._events; const handler = this._handler; if (!isTouchEvent(e)) { events.mousemove = handler.on(document.body, "mousemove", (e) => this._mouseMove(e) ); events.mouseup = handler.on(document.body, "mouseup", (e) => this._mouseUp(e) ); } else { events.touchmove = handler.on( document.body, "touchmove", (e) => this._mouseMove(e), { passive: false } ); events.touchend = handler.on(document.body, "touchend", (e) => this._mouseUp(e) ); events.touchcancel = handler.on(document.body, "touchcancel", (e) => this._mouseUp(e) ); } this._started = true; this._moved = false; } private _mouseMove(e: MouseEvent | TouchEvent): void { if (!isTouchEvent(e)) { if (getMouseButtons(e) !== 1) { this._mouseUp(e); return; } } this._moved = this._moveInternal(e) || this._moved /*calculation on after*/; cancelEvent(e); } protected _moveInternal(_e: MouseEvent | TouchEvent): boolean { //protected return false; } private _mouseUp(e: MouseEvent | TouchEvent): void { const events = this._events; const handler =