Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
747 lines
21 KiB
TypeScript
747 lines
21 KiB
TypeScript
import { computed, ref, shallowRef, watch, type Ref } from 'vue';
|
|
|
|
import { createFillRangeCommand, iterateRange, normalizeRange } from '../commands/FillRangeCommand';
|
|
import { createPasteRangeCommand } from '../commands/PasteRangeCommand';
|
|
import { createSetCellValueCommand } from '../commands/SetCellValueCommand';
|
|
import {
|
|
computeFixedWidth,
|
|
getContentSize,
|
|
getFixedColIndices,
|
|
renderSelectionImage,
|
|
} from '../canvas/SpreadsheetRenderer';
|
|
import { applyViewState, isColumnFilterActive } from './filterSort';
|
|
import {
|
|
getRowHeight,
|
|
hitTestCell,
|
|
hitTestHeaderCol,
|
|
measureColumnWidths,
|
|
} from '../composables/useAutoMeasure';
|
|
import { useClipboard } from '../composables/useClipboard';
|
|
import { useEditHistory } from '../composables/useEditHistory';
|
|
import { useSelection } from '../composables/useSelection';
|
|
import { useUndoRedo } from '../composables/useUndoRedo';
|
|
import type {
|
|
CellEditContext,
|
|
CellRange,
|
|
SpreadsheetBatchCellChange,
|
|
SpreadsheetColumnConfig,
|
|
SpreadsheetEngineExpose,
|
|
SpreadsheetHeaderConfig,
|
|
SpreadsheetHistoryEntry,
|
|
SpreadsheetTableConfig,
|
|
SpreadsheetViewState,
|
|
SortDirection,
|
|
} from '../types';
|
|
import {
|
|
createDefaultViewState,
|
|
DEFAULT_COL_WIDTH,
|
|
DEFAULT_ROW_HEIGHT,
|
|
HEADER_HEIGHT,
|
|
ROW_NUMBER_WIDTH,
|
|
} from '../types';
|
|
import {
|
|
evaluateFormula,
|
|
formatFormulaResult,
|
|
type CellValueReader,
|
|
type FormulaResult,
|
|
} from '../formula/evaluator';
|
|
import {
|
|
formatCellWithSuffix,
|
|
resolveConditionalStyle,
|
|
} from '../composables/cellDisplay';
|
|
|
|
export function useSpreadsheetEngine(options: {
|
|
tableConfig: Ref<SpreadsheetTableConfig>;
|
|
mergedHeaders: Ref<SpreadsheetHeaderConfig[]>;
|
|
columns: Ref<SpreadsheetColumnConfig[]>;
|
|
data: Ref<Record<string, unknown>[]>;
|
|
rowKeyField: Ref<string>;
|
|
layoutColWidths: Ref<Record<number, number>>;
|
|
scrollViewportHeight: Ref<number>;
|
|
isDark: Ref<boolean>;
|
|
onChange: (data: Record<string, unknown>[]) => void;
|
|
}) {
|
|
const sortedColumns = computed(() =>
|
|
[...options.columns.value].sort((a, b) => a.col - b.col),
|
|
);
|
|
|
|
const colWidths = ref<number[]>([]);
|
|
const rowHeight = ref(DEFAULT_ROW_HEIGHT);
|
|
const scrollTop = ref(0);
|
|
const scrollLeft = ref(0);
|
|
const viewportWidth = ref(800);
|
|
const viewportHeight = ref(600);
|
|
const filterValues = ref<Record<number, string>>({});
|
|
const viewState = ref<SpreadsheetViewState>(createDefaultViewState());
|
|
const initialSnapshot = shallowRef<Map<string, unknown>>(new Map());
|
|
const dirtyKeys = ref<Set<string>>(new Set());
|
|
const editing = ref<{ row: number; col: number } | null>(null);
|
|
const measureCanvas = ref<HTMLCanvasElement | null>(null);
|
|
|
|
const selectionApi = useSelection();
|
|
const undoRedo = useUndoRedo();
|
|
const clipboard = useClipboard();
|
|
const history = useEditHistory(
|
|
options.tableConfig.value.tableName,
|
|
options.tableConfig.value.sheetKey,
|
|
options.tableConfig.value.enableHistory !== false,
|
|
);
|
|
|
|
const filteredRowIndices = computed(() =>
|
|
applyViewState(
|
|
options.data.value,
|
|
sortedColumns.value,
|
|
viewState.value,
|
|
(dataRowIndex, colCfg, row) =>
|
|
resolveDisplayValueForDataRow(dataRowIndex, colCfg, row),
|
|
),
|
|
);
|
|
|
|
function resolveDisplayValueForDataRow(
|
|
dataRowIndex: number,
|
|
colCfg: SpreadsheetColumnConfig,
|
|
row?: Record<string, unknown>,
|
|
): unknown {
|
|
if (colCfg.cellType === 'formula' && colCfg.formula) {
|
|
const result = evaluateFormula(
|
|
colCfg.formula,
|
|
dataRowIndex,
|
|
(viewRow, col) => {
|
|
const cfg = colConfig(col);
|
|
if (!cfg || cfg.cellType === 'formula') return 0;
|
|
const dr = options.data.value[viewRow];
|
|
return dr ? getCellValue(viewRow, cfg.fieldId) : 0;
|
|
},
|
|
);
|
|
return formatFormulaResult(result);
|
|
}
|
|
const rowData = row ?? options.data.value[dataRowIndex];
|
|
if (!rowData) return undefined;
|
|
return rowData[colCfg.fieldId];
|
|
}
|
|
|
|
const fixedCols = computed(() =>
|
|
getFixedColIndices(options.mergedHeaders.value),
|
|
);
|
|
|
|
const fixedWidth = computed(() =>
|
|
computeFixedWidth(colWidths.value, fixedCols.value),
|
|
);
|
|
|
|
const displayRowCount = computed(() => {
|
|
const minRows = Math.max(
|
|
1,
|
|
Math.ceil(options.scrollViewportHeight.value / rowHeight.value),
|
|
);
|
|
return Math.max(filteredRowIndices.value.length, minRows);
|
|
});
|
|
|
|
const colCount = computed(
|
|
() => Math.max(...sortedColumns.value.map((c) => c.col), 0) + 1,
|
|
);
|
|
|
|
const defaultColWidth = computed(
|
|
() => options.tableConfig.value.defaultColWidth ?? DEFAULT_COL_WIDTH,
|
|
);
|
|
|
|
const dataContentWidth = computed(
|
|
() => ROW_NUMBER_WIDTH + colWidths.value.reduce((s, w) => s + w, 0),
|
|
);
|
|
|
|
const displayContentWidth = computed(() =>
|
|
Math.max(dataContentWidth.value, viewportWidth.value),
|
|
);
|
|
|
|
const fillerColCount = computed(() => {
|
|
const remaining = displayContentWidth.value - dataContentWidth.value;
|
|
if (remaining <= 0) return 0;
|
|
return Math.ceil(remaining / defaultColWidth.value);
|
|
});
|
|
|
|
const displayColCount = computed(
|
|
() => colCount.value + fillerColCount.value,
|
|
);
|
|
|
|
const paddedColWidths = computed(() => {
|
|
const defaultW = defaultColWidth.value;
|
|
const result: number[] = [];
|
|
for (let c = 0; c < displayColCount.value; c += 1) {
|
|
result.push(colWidths.value[c] ?? defaultW);
|
|
}
|
|
return result;
|
|
});
|
|
|
|
function colConfig(col: number) {
|
|
return sortedColumns.value.find((c) => c.col === col);
|
|
}
|
|
|
|
function dataRowFromViewRow(viewRow: number) {
|
|
return filteredRowIndices.value[viewRow] ?? viewRow;
|
|
}
|
|
|
|
function viewRowFromDataRow(dataRow: number) {
|
|
return filteredRowIndices.value.indexOf(dataRow);
|
|
}
|
|
|
|
function isEditable(colCfg?: SpreadsheetColumnConfig) {
|
|
if (!colCfg) return false;
|
|
if (colCfg.cellType === 'readonly' || colCfg.cellType === 'formula') {
|
|
return false;
|
|
}
|
|
return colCfg.editable !== false;
|
|
}
|
|
|
|
const readCellForFormula: CellValueReader = (viewRow, col) => {
|
|
const cfg = colConfig(col);
|
|
if (!cfg || cfg.cellType === 'formula') return 0;
|
|
const dataRow = dataRowFromViewRow(viewRow);
|
|
return getCellValue(dataRow, cfg.fieldId);
|
|
};
|
|
|
|
function resolveNumericValue(
|
|
viewRow: number,
|
|
colCfg: SpreadsheetColumnConfig,
|
|
row?: Record<string, unknown>,
|
|
): number | null {
|
|
if (colCfg.cellType === 'formula' && colCfg.formula) {
|
|
const result: FormulaResult = evaluateFormula(
|
|
colCfg.formula,
|
|
viewRow,
|
|
readCellForFormula,
|
|
);
|
|
if (result === 'DIV_ZERO' || result == null) return null;
|
|
return result;
|
|
}
|
|
if (colCfg.cellType === 'number' && row) {
|
|
const n = Number(row[colCfg.fieldId]);
|
|
return Number.isFinite(n) ? n : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function resolveCellStyle(
|
|
viewRow: number,
|
|
colCfg: SpreadsheetColumnConfig,
|
|
row?: Record<string, unknown>,
|
|
) {
|
|
return resolveConditionalStyle(
|
|
colCfg,
|
|
resolveNumericValue(viewRow, colCfg, row),
|
|
);
|
|
}
|
|
|
|
function resolveDisplayValue(
|
|
viewRow: number,
|
|
colCfg: SpreadsheetColumnConfig,
|
|
row?: Record<string, unknown>,
|
|
): unknown {
|
|
if (colCfg.cellType === 'formula' && colCfg.formula) {
|
|
const result = evaluateFormula(
|
|
colCfg.formula,
|
|
viewRow,
|
|
readCellForFormula,
|
|
);
|
|
return formatFormulaResult(result);
|
|
}
|
|
if (!row) return undefined;
|
|
return row[colCfg.fieldId];
|
|
}
|
|
|
|
function getCellValue(dataRow: number, fieldId: string) {
|
|
return options.data.value[dataRow]?.[fieldId];
|
|
}
|
|
|
|
function buildCellContext(viewRow: number, col: number): CellEditContext | null {
|
|
const colCfg = colConfig(col);
|
|
if (!colCfg) return null;
|
|
const dataRow = dataRowFromViewRow(viewRow);
|
|
const rowData = options.data.value[dataRow];
|
|
if (!rowData) return null;
|
|
return {
|
|
fieldId: colCfg.fieldId,
|
|
fieldValue: rowData[colCfg.fieldId],
|
|
rowIndex: viewRow,
|
|
colIndex: col,
|
|
rowKey: rowData[options.rowKeyField.value] as string | number,
|
|
rowData,
|
|
editParams: colCfg.editParams ?? {},
|
|
selection: selectionApi.getNormalizedSelection() ?? undefined,
|
|
};
|
|
}
|
|
|
|
function snapshotKey(dataRow: number, fieldId: string) {
|
|
const row = options.data.value[dataRow];
|
|
const rowKey = row?.[options.rowKeyField.value];
|
|
return `${rowKey}:${fieldId}`;
|
|
}
|
|
|
|
function markDirty(dataRow: number, fieldId: string, value: unknown) {
|
|
const key = snapshotKey(dataRow, fieldId);
|
|
const initial = initialSnapshot.value.get(key);
|
|
const next = new Set(dirtyKeys.value);
|
|
if (initial !== undefined && initial !== value) {
|
|
next.add(`${dataRow}:${fieldId}`);
|
|
} else {
|
|
next.delete(`${dataRow}:${fieldId}`);
|
|
}
|
|
dirtyKeys.value = next;
|
|
}
|
|
|
|
function applyLocalValue(dataRow: number, fieldId: string, value: unknown) {
|
|
const rows = options.data.value.map((r, i) =>
|
|
i === dataRow ? { ...r, [fieldId]: value } : r,
|
|
);
|
|
options.onChange(rows);
|
|
markDirty(dataRow, fieldId, value);
|
|
}
|
|
|
|
async function persistChanges(
|
|
items: Array<{ viewRow: number; col: number; value: unknown }>,
|
|
opts: {
|
|
recordHistory?: boolean;
|
|
action?: SpreadsheetHistoryEntry['action'];
|
|
} = {},
|
|
) {
|
|
const batchChanges: SpreadsheetBatchCellChange[] = [];
|
|
|
|
for (const item of items) {
|
|
const colCfg = colConfig(item.col);
|
|
if (!colCfg || !isEditable(colCfg)) continue;
|
|
|
|
const dataRow = dataRowFromViewRow(item.viewRow);
|
|
const ctx = buildCellContext(item.viewRow, item.col);
|
|
if (!ctx) continue;
|
|
|
|
const before = getCellValue(dataRow, colCfg.fieldId);
|
|
applyLocalValue(dataRow, colCfg.fieldId, item.value);
|
|
batchChanges.push({
|
|
ctx: { ...ctx, fieldValue: item.value },
|
|
before,
|
|
after: item.value,
|
|
col: item.col,
|
|
viewRow: item.viewRow,
|
|
});
|
|
}
|
|
|
|
if (batchChanges.length === 0) return;
|
|
|
|
const onBatchEdit = options.tableConfig.value.onBatchEdit;
|
|
if (onBatchEdit) {
|
|
await onBatchEdit(batchChanges);
|
|
} else {
|
|
for (const ch of batchChanges) {
|
|
const cfg = colConfig(ch.col);
|
|
if (cfg?.onEdit) {
|
|
await cfg.onEdit({ ...ch.ctx, fieldValue: ch.after });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
opts.recordHistory !== false &&
|
|
options.tableConfig.value.enableHistory !== false
|
|
) {
|
|
for (const ch of batchChanges) {
|
|
await history.recordChange({
|
|
action: opts.action ?? 'edit',
|
|
rowKey: ch.ctx.rowKey,
|
|
fieldId: ch.ctx.fieldId,
|
|
beforeSnapshot: ch.before,
|
|
afterSnapshot: ch.after,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function toPersistItems(
|
|
batch: Array<{ row: number; col: number; value: unknown }>,
|
|
) {
|
|
return batch.map((ch) => ({
|
|
viewRow: ch.row,
|
|
col: ch.col,
|
|
value: ch.value,
|
|
}));
|
|
}
|
|
|
|
async function commitCellChange(
|
|
viewRow: number,
|
|
col: number,
|
|
newValue: unknown,
|
|
recordHistory = true,
|
|
action: SpreadsheetHistoryEntry['action'] = 'edit',
|
|
) {
|
|
const colCfg = colConfig(col);
|
|
if (!colCfg || !isEditable(colCfg)) return;
|
|
|
|
const dataRow = dataRowFromViewRow(viewRow);
|
|
const before = getCellValue(dataRow, colCfg.fieldId);
|
|
if (before === newValue) return;
|
|
|
|
await persistChanges([{ viewRow, col, value: newValue }], {
|
|
recordHistory,
|
|
action,
|
|
});
|
|
|
|
undoRedo.push(
|
|
createSetCellValueCommand(
|
|
() =>
|
|
persistChanges([{ viewRow, col, value: before }], {
|
|
recordHistory: false,
|
|
}),
|
|
() =>
|
|
persistChanges([{ viewRow, col, value: newValue }], {
|
|
recordHistory: false,
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
async function setCellValue(
|
|
viewRow: number,
|
|
col: number,
|
|
value: unknown,
|
|
recordHistory = true,
|
|
action: SpreadsheetHistoryEntry['action'] = 'edit',
|
|
) {
|
|
await commitCellChange(viewRow, col, value, recordHistory, action);
|
|
}
|
|
|
|
async function fillRange(source: CellRange, target: CellRange) {
|
|
const src = normalizeRange(source);
|
|
const tgt = normalizeRange(target);
|
|
const before: Array<{ row: number; col: number; value: unknown }> = [];
|
|
const after: Array<{ row: number; col: number; value: unknown }> = [];
|
|
|
|
const srcRows = src.endRow - src.startRow + 1;
|
|
const srcCols = src.endCol - src.startCol + 1;
|
|
|
|
iterateRange(tgt, (row, col) => {
|
|
const colCfg = colConfig(col);
|
|
if (!colCfg || !isEditable(colCfg)) return;
|
|
const relRow = (row - tgt.startRow) % srcRows;
|
|
const relCol = (col - tgt.startCol) % srcCols;
|
|
const srcRow = src.startRow + relRow;
|
|
const srcCol = src.startCol + relCol;
|
|
const srcColCfg = colConfig(srcCol);
|
|
if (!srcColCfg) return;
|
|
|
|
const dataRow = dataRowFromViewRow(row);
|
|
const beforeVal = getCellValue(dataRow, colCfg.fieldId);
|
|
|
|
const afterVal = getCellValue(
|
|
dataRowFromViewRow(srcRow),
|
|
srcColCfg.fieldId,
|
|
);
|
|
|
|
before.push({ row, col, value: beforeVal });
|
|
after.push({ row, col, value: afterVal });
|
|
});
|
|
|
|
await persistChanges(toPersistItems(after), {
|
|
recordHistory: true,
|
|
action: 'fill',
|
|
});
|
|
|
|
undoRedo.push(
|
|
createFillRangeCommand(
|
|
(batch) => persistChanges(toPersistItems(batch), { recordHistory: false }),
|
|
before,
|
|
after,
|
|
),
|
|
);
|
|
}
|
|
|
|
function copy(selectionOverride?: CellRange) {
|
|
const sel = selectionOverride ?? selectionApi.getNormalizedSelection();
|
|
if (!sel) return;
|
|
const rows: unknown[][] = [];
|
|
iterateRange(sel, (viewRow, col) => {
|
|
const relRow = viewRow - sel.startRow;
|
|
if (!rows[relRow]) rows[relRow] = [];
|
|
const colCfg = colConfig(col);
|
|
if (!colCfg) {
|
|
rows[relRow]![col - sel.startCol] = '';
|
|
return;
|
|
}
|
|
const dataRow = dataRowFromViewRow(viewRow);
|
|
const row = options.data.value[dataRow];
|
|
const raw = resolveDisplayValue(viewRow, colCfg, row);
|
|
rows[relRow]![col - sel.startCol] = formatCellWithSuffix(raw, colCfg);
|
|
});
|
|
const matrix = {
|
|
rows,
|
|
startRow: sel.startRow,
|
|
startCol: sel.startCol,
|
|
};
|
|
clipboard.setMatrix(matrix);
|
|
void clipboard.writeSystemClipboard(matrix);
|
|
}
|
|
|
|
async function copyAsImage(selectionOverride?: CellRange) {
|
|
const sel = selectionOverride ?? selectionApi.getNormalizedSelection();
|
|
if (!sel) return;
|
|
const blob = await renderSelectionImage({
|
|
selection: sel,
|
|
headers: options.mergedHeaders.value,
|
|
columns: sortedColumns.value,
|
|
colWidths: paddedColWidths.value,
|
|
rowHeight: rowHeight.value,
|
|
defaultColWidth: defaultColWidth.value,
|
|
defaultTextAlign: options.tableConfig.value.defaultTextAlign ?? 'left',
|
|
isDark: options.isDark.value,
|
|
filteredRowIndices: filteredRowIndices.value,
|
|
rows: options.data.value,
|
|
dirtyKeys: dirtyKeys.value,
|
|
resolveDisplayValue,
|
|
resolveCellStyle,
|
|
});
|
|
await navigator.clipboard.write([
|
|
new ClipboardItem({ 'image/png': blob }),
|
|
]);
|
|
}
|
|
|
|
async function paste() {
|
|
let matrix = clipboard.getMatrix();
|
|
if (!matrix) {
|
|
const text = await clipboard.readSystemClipboard();
|
|
if (!text) return;
|
|
matrix = {
|
|
rows: clipboard.tsvToMatrix(text),
|
|
startRow: 0,
|
|
startCol: 0,
|
|
};
|
|
}
|
|
|
|
const start = selectionApi.activeCell.value ?? {
|
|
row: 0,
|
|
col: sortedColumns.value[0]?.col ?? 0,
|
|
};
|
|
|
|
const before: Array<{ row: number; col: number; value: unknown }> = [];
|
|
const after: Array<{ row: number; col: number; value: unknown }> = [];
|
|
|
|
matrix.rows.forEach((rowVals, rOff) => {
|
|
rowVals.forEach((val, cOff) => {
|
|
const viewRow = start.row + rOff;
|
|
const col = start.col + cOff;
|
|
const colCfg = colConfig(col);
|
|
if (!colCfg || !isEditable(colCfg)) return;
|
|
const dataRow = dataRowFromViewRow(viewRow);
|
|
if (dataRow >= options.data.value.length) return;
|
|
before.push({
|
|
row: viewRow,
|
|
col,
|
|
value: getCellValue(dataRow, colCfg.fieldId),
|
|
});
|
|
after.push({ row: viewRow, col, value: val });
|
|
});
|
|
});
|
|
|
|
const applyBatch = (
|
|
changes: Array<{ row: number; col: number; value: unknown }>,
|
|
) => persistChanges(toPersistItems(changes), { recordHistory: false });
|
|
|
|
await persistChanges(toPersistItems(after), {
|
|
recordHistory: true,
|
|
action: 'paste',
|
|
});
|
|
|
|
undoRedo.push(createPasteRangeCommand(applyBatch, before, after));
|
|
}
|
|
|
|
function remeasure() {
|
|
if (!measureCanvas.value) return;
|
|
const defaultW =
|
|
options.tableConfig.value.defaultColWidth ?? DEFAULT_COL_WIDTH;
|
|
const measured = measureColumnWidths(
|
|
measureCanvas.value,
|
|
options.mergedHeaders.value,
|
|
sortedColumns.value,
|
|
options.data.value,
|
|
defaultW,
|
|
(dataRowIdx, colCfg, row) =>
|
|
resolveDisplayValue(dataRowIdx, colCfg, row),
|
|
formatCellWithSuffix,
|
|
);
|
|
const overrides = options.layoutColWidths.value;
|
|
colWidths.value = measured.map((w, i) => overrides[i] ?? w);
|
|
const firstType = sortedColumns.value[0]?.cellType ?? 'text';
|
|
rowHeight.value = getRowHeight(
|
|
firstType,
|
|
options.tableConfig.value.rowHeight,
|
|
);
|
|
}
|
|
|
|
function initSnapshot() {
|
|
const map = new Map<string, unknown>();
|
|
options.data.value.forEach((row, dataRow) => {
|
|
sortedColumns.value.forEach((col) => {
|
|
map.set(snapshotKey(dataRow, col.fieldId), row[col.fieldId]);
|
|
});
|
|
});
|
|
initialSnapshot.value = map;
|
|
dirtyKeys.value = new Set();
|
|
}
|
|
|
|
watch(
|
|
() => options.data.value,
|
|
() => {
|
|
if (initialSnapshot.value.size === 0) initSnapshot();
|
|
remeasure();
|
|
},
|
|
{ deep: true, immediate: true },
|
|
);
|
|
|
|
watch(
|
|
() => options.mergedHeaders.value,
|
|
() => remeasure(),
|
|
{ deep: true },
|
|
);
|
|
|
|
watch(
|
|
() => options.layoutColWidths.value,
|
|
() => remeasure(),
|
|
{ deep: true },
|
|
);
|
|
|
|
const contentSize = computed(() =>
|
|
getContentSize(
|
|
colWidths.value,
|
|
displayRowCount.value,
|
|
rowHeight.value,
|
|
displayContentWidth.value,
|
|
),
|
|
);
|
|
|
|
function setColumnFilter(col: number, checkedValues: string[] | undefined) {
|
|
const filters = { ...viewState.value.filters };
|
|
if (checkedValues === undefined) {
|
|
delete filters[col];
|
|
} else {
|
|
filters[col] = { checkedValues };
|
|
}
|
|
viewState.value = { ...viewState.value, filters };
|
|
}
|
|
|
|
function clearColumnFilter(col: number) {
|
|
const next = { ...viewState.value.filters };
|
|
delete next[col];
|
|
viewState.value = { ...viewState.value, filters: next };
|
|
}
|
|
|
|
function setSort(col: number, direction: SortDirection) {
|
|
viewState.value = { ...viewState.value, sort: { col, direction } };
|
|
}
|
|
|
|
function clearSort() {
|
|
viewState.value = { ...viewState.value, sort: null };
|
|
}
|
|
|
|
const engine: SpreadsheetEngineExpose = {
|
|
copy,
|
|
copyAsImage,
|
|
paste,
|
|
undo: undoRedo.undo,
|
|
redo: undoRedo.redo,
|
|
canUndo: undoRedo.canUndo,
|
|
canRedo: undoRedo.canRedo,
|
|
canCopy: () => !!selectionApi.getNormalizedSelection(),
|
|
canCopyAsImage: () => !!selectionApi.getNormalizedSelection(),
|
|
canPaste: () => clipboard.hasContent() || true,
|
|
canEdit: (_row, col) => {
|
|
const cfg = colConfig(col);
|
|
return !!cfg && isEditable(cfg);
|
|
},
|
|
getSelection: selectionApi.getNormalizedSelection,
|
|
getCellEditContext: (row, col) => buildCellContext(row, col),
|
|
getHeaderContext: (col) => {
|
|
const header =
|
|
options.mergedHeaders.value.find((h) => h.col === col) ?? {
|
|
title: '',
|
|
col,
|
|
};
|
|
return {
|
|
colIndex: col,
|
|
fieldId: header.fieldId ?? colConfig(col)?.fieldId,
|
|
headerTitle: header.title,
|
|
filterValue: isColumnFilterActive(viewState.value.filters[col])
|
|
? 'active'
|
|
: '',
|
|
};
|
|
},
|
|
setCellValue,
|
|
fillRange,
|
|
getColumnConfig: colConfig,
|
|
getFilterValue: (col) =>
|
|
isColumnFilterActive(viewState.value.filters[col]) ? 'active' : '',
|
|
};
|
|
|
|
return {
|
|
engine,
|
|
sortedColumns,
|
|
colWidths,
|
|
rowHeight,
|
|
scrollTop,
|
|
scrollLeft,
|
|
viewportWidth,
|
|
viewportHeight,
|
|
filterValues,
|
|
viewState,
|
|
setColumnFilter,
|
|
clearColumnFilter,
|
|
setSort,
|
|
clearSort,
|
|
dirtyKeys,
|
|
editing,
|
|
measureCanvas,
|
|
filteredRowIndices,
|
|
contentSize,
|
|
displayRowCount,
|
|
displayColCount,
|
|
displayContentWidth,
|
|
defaultColWidth,
|
|
paddedColWidths,
|
|
fixedCols,
|
|
fixedWidth,
|
|
colCount,
|
|
selectionApi,
|
|
undoRedo,
|
|
dataRowFromViewRow,
|
|
viewRowFromDataRow,
|
|
colConfig,
|
|
isEditable,
|
|
resolveDisplayValue,
|
|
resolveCellStyle,
|
|
buildCellContext,
|
|
commitCellChange,
|
|
remeasure,
|
|
initSnapshot,
|
|
hitTestCell: (x: number, y: number) =>
|
|
hitTestCell(
|
|
x,
|
|
y,
|
|
scrollTop.value,
|
|
scrollLeft.value,
|
|
paddedColWidths.value,
|
|
rowHeight.value,
|
|
displayRowCount.value,
|
|
colCount.value,
|
|
fixedCols.value,
|
|
fixedWidth.value,
|
|
),
|
|
hitTestHeaderCol: (x: number) =>
|
|
hitTestHeaderCol(
|
|
x,
|
|
scrollLeft.value,
|
|
paddedColWidths.value,
|
|
colCount.value,
|
|
fixedCols.value,
|
|
fixedWidth.value,
|
|
),
|
|
HEADER_HEIGHT,
|
|
ROW_NUMBER_WIDTH,
|
|
};
|
|
}
|
|
|
|
export type SpreadsheetEngine = ReturnType<typeof useSpreadsheetEngine>;
|