Extract table module helpers

This commit is contained in:
Shinuwa 2026-08-14 10:07:26 +02:00
parent df0033739c
commit d4fe3c796a
4 changed files with 353 additions and 299 deletions

View file

@ -0,0 +1,93 @@
// Rôle : rend les en-têtes et lignes de cellules du module Table.
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
import { cellAddress } from "./tableFormulaEngine.js";
import { isCellInBounds, isCellSelected } from "./tableUtils.js";
export function TableRow({ rowIndex, columns, cells, evaluatedCells, editingCell, highlightedCell, textContent, rowLabel, selection, movePreview, onRowLabelChange, onFocus, onBlur, onCellChange, onFormulaSelectionStart, onFormulaSelectionExtend, onStartMoveSelection, onMoveSelection, onSelectCell, onExtendSelection, onPasteSelection, draggingSelectionRef, onCellKeyDown, onStartEditing, registerInput }) {
return (
<>
<HeaderLabelInput
className="table-row-header table-header-input"
value={rowLabel || String(rowIndex + 1)}
fallback={String(rowIndex + 1)}
onSave={(value) => onRowLabelChange(rowIndex, value)}
aria-label={`${textContent.rowLabel || "Intitulé de ligne"} ${rowIndex + 1}`}
title={`${rowIndex + 1} - ${textContent.rowFormulaTitle || "Référence conservée dans les formules"}`}
/>
{Array.from({ length: columns }, (_, columnIndex) => {
const address = cellAddress(rowIndex, columnIndex);
const rawValue = cells[address] || "";
const evaluated = evaluatedCells[address];
const isEditing = editingCell === address;
const isFormula = rawValue.startsWith("=");
const displayValue = isEditing ? rawValue : evaluated?.display || rawValue;
const className = [
"table-cell-input",
isEditing ? "is-editing" : "",
isFormula ? "is-formula" : "",
evaluated?.error && evaluated.error !== "empty" && !isEditing ? "has-error" : "",
isCellSelected(selection, rowIndex, columnIndex) ? "is-selected" : "",
isCellInBounds(movePreview, rowIndex, columnIndex) ? "is-move-target" : "",
highlightedCell === address ? "is-referenced" : ""
].filter(Boolean).join(" ");
return (
<input
key={address}
ref={(element) => registerInput(address, element)}
className={className}
value={displayValue}
onMouseDown={(event) => {
if (onFormulaSelectionStart(rowIndex, columnIndex)) {
event.preventDefault();
} else if (!event.shiftKey && onStartMoveSelection(rowIndex, columnIndex)) {
event.preventDefault();
} else {
draggingSelectionRef.current = true;
onSelectCell(rowIndex, columnIndex, event.shiftKey);
}
}}
onDoubleClick={() => onStartEditing(address, null, { select: true })}
onMouseEnter={() => {
if (onFormulaSelectionExtend(rowIndex, columnIndex)) return;
if (onMoveSelection(rowIndex, columnIndex)) return;
if (draggingSelectionRef.current) onExtendSelection(rowIndex, columnIndex);
}}
onFocus={() => onFocus(address)}
onBlur={() => onBlur(address)}
onChange={(event) => onCellChange(address, event.target.value)}
onKeyDown={(event) => onCellKeyDown(event, rowIndex, columnIndex, address, isEditing)}
onPaste={(event) => onPasteSelection(event, rowIndex, columnIndex, isEditing)}
aria-label={`${textContent.cellLabel || "Cellule"} ${address}`}
title={evaluated?.error && evaluated.error !== "empty" ? textContent.errorTitle || "Formule invalide" : address}
inputMode="text"
readOnly={!isEditing}
spellCheck="false"
/>
);
})}
</>
);
}
export function HeaderLabelInput({ className, value, fallback, onSave, ...props }) {
const inlineEdit = useInlineEdit({
value,
onCommit: onSave,
transform: (draft) => String(draft || ""),
blurOnEscape: true
});
return (
<input
{...inlineEdit.getInputProps({
...props,
className,
onFocus: (event) => {
if (value === fallback) window.requestAnimationFrame(() => event.target.select());
},
spellCheck: "false"
})}
/>
);
}

View file

@ -1,210 +1,30 @@
// Rôle : fournit l'outil tableau avec cellules libres, formules simples et échange TSV.
import { useEffect, useMemo, useRef, useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { cellAddress, columnIndexToName, evaluateTable, parseCellAddress } from "./tableFormulaEngine.js";
const DEFAULT_ROWS = 10;
const DEFAULT_COLUMNS = 6;
const MAX_ROWS = 50;
const MAX_COLUMNS = 20;
const MIN_COLUMN_WIDTH = 96;
const MAX_COLUMN_WIDTH = 220;
const MIN_ROW_HEADER_WIDTH = 42;
const MAX_ROW_HEADER_WIDTH = 180;
function cleanCells(cells, rows, columns) {
const nextCells = {};
for (let rowIndex = 0; rowIndex < rows; rowIndex += 1) {
for (let columnIndex = 0; columnIndex < columns; columnIndex += 1) {
const address = cellAddress(rowIndex, columnIndex);
const value = String(cells?.[address] || "");
if (value.trim()) nextCells[address] = value;
}
}
return nextCells;
}
function cleanLabels(labels, length, getFallback) {
const nextLabels = {};
for (let index = 0; index < length; index += 1) {
const value = String(labels?.[index] || "").slice(0, 80);
if (value.trim() && value !== String(getFallback(index))) nextLabels[index] = value;
}
return nextLabels;
}
function estimateTextWidth(value, minWidth, maxWidth) {
const length = String(value || "").length;
return Math.min(maxWidth, Math.max(minWidth, 24 + length * 8));
}
function getColumnWidths(data, evaluatedCells) {
return Array.from({ length: data.columns }, (_, columnIndex) => {
let width = estimateTextWidth(data.columnLabels?.[columnIndex] || columnIndexToName(columnIndex), MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
for (let rowIndex = 0; rowIndex < data.rows; rowIndex += 1) {
const address = cellAddress(rowIndex, columnIndex);
const value = evaluatedCells[address]?.display || data.cells[address] || "";
width = Math.max(width, estimateTextWidth(value, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH));
}
return width;
});
}
function getRowHeaderWidth(data) {
return Array.from({ length: data.rows }, (_, rowIndex) => data.rowLabels?.[rowIndex] || rowIndex + 1)
.reduce((width, label) => Math.max(width, estimateTextWidth(label, MIN_ROW_HEADER_WIDTH, MAX_ROW_HEADER_WIDTH)), MIN_ROW_HEADER_WIDTH);
}
function getSelectionBounds(selection) {
if (!selection?.anchor || !selection?.focus) return null;
return {
startRow: Math.min(selection.anchor.rowIndex, selection.focus.rowIndex),
endRow: Math.max(selection.anchor.rowIndex, selection.focus.rowIndex),
startColumn: Math.min(selection.anchor.columnIndex, selection.focus.columnIndex),
endColumn: Math.max(selection.anchor.columnIndex, selection.focus.columnIndex)
};
}
function isCellSelected(selection, rowIndex, columnIndex) {
const bounds = getSelectionBounds(selection);
return Boolean(bounds && rowIndex >= bounds.startRow && rowIndex <= bounds.endRow && columnIndex >= bounds.startColumn && columnIndex <= bounds.endColumn);
}
function isCellInBounds(bounds, rowIndex, columnIndex) {
return Boolean(bounds && rowIndex >= bounds.startRow && rowIndex <= bounds.endRow && columnIndex >= bounds.startColumn && columnIndex <= bounds.endColumn);
}
function isMultiCellSelection(selection) {
const bounds = getSelectionBounds(selection);
return Boolean(bounds && (bounds.startRow !== bounds.endRow || bounds.startColumn !== bounds.endColumn));
}
function clampCellPosition(rowIndex, columnIndex, rows, columns) {
return {
rowIndex: Math.min(rows - 1, Math.max(0, rowIndex)),
columnIndex: Math.min(columns - 1, Math.max(0, columnIndex))
};
}
function getRangeAddresses(selection) {
const bounds = getSelectionBounds(selection);
if (!bounds) return [];
const addresses = [];
for (let rowIndex = bounds.startRow; rowIndex <= bounds.endRow; rowIndex += 1) {
for (let columnIndex = bounds.startColumn; columnIndex <= bounds.endColumn; columnIndex += 1) {
addresses.push(cellAddress(rowIndex, columnIndex));
}
}
return addresses;
}
function getFormulaRangeText(selection) {
const addresses = getRangeAddresses(selection);
if (!addresses.length) return "";
return addresses.length === 1 ? addresses[0] : `(${addresses.join("+")})`;
}
function getCellBlock(cells, bounds) {
if (!bounds) return null;
const values = [];
for (let rowIndex = bounds.startRow; rowIndex <= bounds.endRow; rowIndex += 1) {
const row = [];
for (let columnIndex = bounds.startColumn; columnIndex <= bounds.endColumn; columnIndex += 1) {
row.push(cells[cellAddress(rowIndex, columnIndex)] || "");
}
values.push(row);
}
return {
height: bounds.endRow - bounds.startRow + 1,
width: bounds.endColumn - bounds.startColumn + 1,
values
};
}
function parseTsvBlock(text) {
const normalizedText = String(text || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const lines = normalizedText.endsWith("\n") ? normalizedText.slice(0, -1).split("\n") : normalizedText.split("\n");
if (!lines.length || (lines.length === 1 && !lines[0])) return null;
const values = lines.map((line) => line.split("\t"));
return {
height: values.length,
width: Math.max(...values.map((row) => row.length)),
values
};
}
function getBoundsFromStartAndSize(rowIndex, columnIndex, height, width) {
return {
startRow: rowIndex,
endRow: rowIndex + height - 1,
startColumn: columnIndex,
endColumn: columnIndex + width - 1
};
}
function isSameBounds(firstBounds, secondBounds) {
return Boolean(firstBounds && secondBounds
&& firstBounds.startRow === secondBounds.startRow
&& firstBounds.endRow === secondBounds.endRow
&& firstBounds.startColumn === secondBounds.startColumn
&& firstBounds.endColumn === secondBounds.endColumn);
}
function moveSelectionCells(cells, sourceBounds, targetBounds) {
const nextCells = { ...cells };
const snapshot = [];
for (let rowIndex = sourceBounds.startRow; rowIndex <= sourceBounds.endRow; rowIndex += 1) {
for (let columnIndex = sourceBounds.startColumn; columnIndex <= sourceBounds.endColumn; columnIndex += 1) {
const value = cells[cellAddress(rowIndex, columnIndex)] || "";
snapshot.push({
rowOffset: rowIndex - sourceBounds.startRow,
columnOffset: columnIndex - sourceBounds.startColumn,
value
});
delete nextCells[cellAddress(rowIndex, columnIndex)];
}
}
for (let rowIndex = targetBounds.startRow; rowIndex <= targetBounds.endRow; rowIndex += 1) {
for (let columnIndex = targetBounds.startColumn; columnIndex <= targetBounds.endColumn; columnIndex += 1) {
delete nextCells[cellAddress(rowIndex, columnIndex)];
}
}
snapshot.forEach(({ rowOffset, columnOffset, value }) => {
if (!String(value || "").trim()) return;
nextCells[cellAddress(targetBounds.startRow + rowOffset, targetBounds.startColumn + columnOffset)] = value;
});
return nextCells;
}
function pasteCellBlock(cells, block, targetRow, targetColumn, rows, columns) {
const clippedHeight = Math.min(block?.height || 0, rows - targetRow);
const clippedWidth = Math.min(block?.width || 0, columns - targetColumn);
if (clippedHeight <= 0 || clippedWidth <= 0) return null;
const nextCells = { ...cells };
for (let rowOffset = 0; rowOffset < clippedHeight; rowOffset += 1) {
for (let columnOffset = 0; columnOffset < clippedWidth; columnOffset += 1) {
const address = cellAddress(targetRow + rowOffset, targetColumn + columnOffset);
const value = String(block.values[rowOffset]?.[columnOffset] || "");
if (value.trim()) {
nextCells[address] = value;
} else {
delete nextCells[address];
}
}
}
return {
cells: nextCells,
bounds: getBoundsFromStartAndSize(targetRow, targetColumn, clippedHeight, clippedWidth)
};
}
import { HeaderLabelInput, TableRow } from "./TableGridParts.jsx";
import {
DEFAULT_COLUMNS,
DEFAULT_ROWS,
MAX_COLUMNS,
MAX_ROWS,
clampCellPosition,
cleanCells,
cleanLabels,
getBoundsFromStartAndSize,
getCellBlock,
getColumnWidths,
getFormulaRangeText,
getRowHeaderWidth,
getSelectionBounds,
isCellSelected,
isMultiCellSelection,
isSameBounds,
moveSelectionCells,
parseTsvBlock,
pasteCellBlock
} from "./tableUtils.js";
export function TableModule({ toolboxId, moduleId, context, editing }) {
const data = context.normalizeTableData(context.getModuleData(toolboxId, moduleId, { rows: DEFAULT_ROWS, columns: DEFAULT_COLUMNS, cells: {} }));
@ -716,92 +536,3 @@ export function TableModule({ toolboxId, moduleId, context, editing }) {
</div>
);
}
function TableRow({ rowIndex, columns, cells, evaluatedCells, editingCell, highlightedCell, textContent, rowLabel, selection, movePreview, onRowLabelChange, onFocus, onBlur, onCellChange, onFormulaSelectionStart, onFormulaSelectionExtend, onStartMoveSelection, onMoveSelection, onSelectCell, onExtendSelection, onPasteSelection, draggingSelectionRef, onCellKeyDown, onStartEditing, registerInput }) {
return (
<>
<HeaderLabelInput
className="table-row-header table-header-input"
value={rowLabel || String(rowIndex + 1)}
fallback={String(rowIndex + 1)}
onSave={(value) => onRowLabelChange(rowIndex, value)}
aria-label={`${textContent.rowLabel || "Intitulé de ligne"} ${rowIndex + 1}`}
title={`${rowIndex + 1} - ${textContent.rowFormulaTitle || "Référence conservée dans les formules"}`}
/>
{Array.from({ length: columns }, (_, columnIndex) => {
const address = cellAddress(rowIndex, columnIndex);
const rawValue = cells[address] || "";
const evaluated = evaluatedCells[address];
const isEditing = editingCell === address;
const isFormula = rawValue.startsWith("=");
const displayValue = isEditing ? rawValue : evaluated?.display || rawValue;
const className = [
"table-cell-input",
isEditing ? "is-editing" : "",
isFormula ? "is-formula" : "",
evaluated?.error && evaluated.error !== "empty" && !isEditing ? "has-error" : "",
isCellSelected(selection, rowIndex, columnIndex) ? "is-selected" : "",
isCellInBounds(movePreview, rowIndex, columnIndex) ? "is-move-target" : "",
highlightedCell === address ? "is-referenced" : ""
].filter(Boolean).join(" ");
return (
<input
key={address}
ref={(element) => registerInput(address, element)}
className={className}
value={displayValue}
onMouseDown={(event) => {
if (onFormulaSelectionStart(rowIndex, columnIndex)) {
event.preventDefault();
} else if (!event.shiftKey && onStartMoveSelection(rowIndex, columnIndex)) {
event.preventDefault();
} else {
draggingSelectionRef.current = true;
onSelectCell(rowIndex, columnIndex, event.shiftKey);
}
}}
onDoubleClick={() => onStartEditing(address, null, { select: true })}
onMouseEnter={() => {
if (onFormulaSelectionExtend(rowIndex, columnIndex)) return;
if (onMoveSelection(rowIndex, columnIndex)) return;
if (draggingSelectionRef.current) onExtendSelection(rowIndex, columnIndex);
}}
onFocus={() => onFocus(address)}
onBlur={() => onBlur(address)}
onChange={(event) => onCellChange(address, event.target.value)}
onKeyDown={(event) => onCellKeyDown(event, rowIndex, columnIndex, address, isEditing)}
onPaste={(event) => onPasteSelection(event, rowIndex, columnIndex, isEditing)}
aria-label={`${textContent.cellLabel || "Cellule"} ${address}`}
title={evaluated?.error && evaluated.error !== "empty" ? textContent.errorTitle || "Formule invalide" : address}
inputMode="text"
readOnly={!isEditing}
spellCheck="false"
/>
);
})}
</>
);
}
function HeaderLabelInput({ className, value, fallback, onSave, ...props }) {
const inlineEdit = useInlineEdit({
value,
onCommit: onSave,
transform: (draft) => String(draft || ""),
blurOnEscape: true
});
return (
<input
{...inlineEdit.getInputProps({
...props,
className,
onFocus: (event) => {
if (value === fallback) window.requestAnimationFrame(() => event.target.select());
},
spellCheck: "false"
})}
/>
);
}

View file

@ -0,0 +1,203 @@
// Rôle : regroupe les limites, dimensions et helpers purs de sélection/copie du tableau.
import { cellAddress, columnIndexToName } from "./tableFormulaEngine.js";
export const DEFAULT_ROWS = 10;
export const DEFAULT_COLUMNS = 6;
export const MAX_ROWS = 50;
export const MAX_COLUMNS = 20;
export const MIN_COLUMN_WIDTH = 96;
export const MAX_COLUMN_WIDTH = 220;
export const MIN_ROW_HEADER_WIDTH = 42;
export const MAX_ROW_HEADER_WIDTH = 180;
export function cleanCells(cells, rows, columns) {
const nextCells = {};
for (let rowIndex = 0; rowIndex < rows; rowIndex += 1) {
for (let columnIndex = 0; columnIndex < columns; columnIndex += 1) {
const address = cellAddress(rowIndex, columnIndex);
const value = String(cells?.[address] || "");
if (value.trim()) nextCells[address] = value;
}
}
return nextCells;
}
export function cleanLabels(labels, length, getFallback) {
const nextLabels = {};
for (let index = 0; index < length; index += 1) {
const value = String(labels?.[index] || "").slice(0, 80);
if (value.trim() && value !== String(getFallback(index))) nextLabels[index] = value;
}
return nextLabels;
}
export function estimateTextWidth(value, minWidth, maxWidth) {
const length = String(value || "").length;
return Math.min(maxWidth, Math.max(minWidth, 24 + length * 8));
}
export function getColumnWidths(data, evaluatedCells) {
return Array.from({ length: data.columns }, (_, columnIndex) => {
let width = estimateTextWidth(data.columnLabels?.[columnIndex] || columnIndexToName(columnIndex), MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
for (let rowIndex = 0; rowIndex < data.rows; rowIndex += 1) {
const address = cellAddress(rowIndex, columnIndex);
const value = evaluatedCells[address]?.display || data.cells[address] || "";
width = Math.max(width, estimateTextWidth(value, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH));
}
return width;
});
}
export function getRowHeaderWidth(data) {
return Array.from({ length: data.rows }, (_, rowIndex) => data.rowLabels?.[rowIndex] || rowIndex + 1)
.reduce((width, label) => Math.max(width, estimateTextWidth(label, MIN_ROW_HEADER_WIDTH, MAX_ROW_HEADER_WIDTH)), MIN_ROW_HEADER_WIDTH);
}
export function getSelectionBounds(selection) {
if (!selection?.anchor || !selection?.focus) return null;
return {
startRow: Math.min(selection.anchor.rowIndex, selection.focus.rowIndex),
endRow: Math.max(selection.anchor.rowIndex, selection.focus.rowIndex),
startColumn: Math.min(selection.anchor.columnIndex, selection.focus.columnIndex),
endColumn: Math.max(selection.anchor.columnIndex, selection.focus.columnIndex)
};
}
export function isCellSelected(selection, rowIndex, columnIndex) {
const bounds = getSelectionBounds(selection);
return Boolean(bounds && rowIndex >= bounds.startRow && rowIndex <= bounds.endRow && columnIndex >= bounds.startColumn && columnIndex <= bounds.endColumn);
}
export function isCellInBounds(bounds, rowIndex, columnIndex) {
return Boolean(bounds && rowIndex >= bounds.startRow && rowIndex <= bounds.endRow && columnIndex >= bounds.startColumn && columnIndex <= bounds.endColumn);
}
export function isMultiCellSelection(selection) {
const bounds = getSelectionBounds(selection);
return Boolean(bounds && (bounds.startRow !== bounds.endRow || bounds.startColumn !== bounds.endColumn));
}
export function clampCellPosition(rowIndex, columnIndex, rows, columns) {
return {
rowIndex: Math.min(rows - 1, Math.max(0, rowIndex)),
columnIndex: Math.min(columns - 1, Math.max(0, columnIndex))
};
}
export function getRangeAddresses(selection) {
const bounds = getSelectionBounds(selection);
if (!bounds) return [];
const addresses = [];
for (let rowIndex = bounds.startRow; rowIndex <= bounds.endRow; rowIndex += 1) {
for (let columnIndex = bounds.startColumn; columnIndex <= bounds.endColumn; columnIndex += 1) {
addresses.push(cellAddress(rowIndex, columnIndex));
}
}
return addresses;
}
export function getFormulaRangeText(selection) {
const addresses = getRangeAddresses(selection);
if (!addresses.length) return "";
return addresses.length === 1 ? addresses[0] : `(${addresses.join("+")})`;
}
export function getCellBlock(cells, bounds) {
if (!bounds) return null;
const values = [];
for (let rowIndex = bounds.startRow; rowIndex <= bounds.endRow; rowIndex += 1) {
const row = [];
for (let columnIndex = bounds.startColumn; columnIndex <= bounds.endColumn; columnIndex += 1) {
row.push(cells[cellAddress(rowIndex, columnIndex)] || "");
}
values.push(row);
}
return {
height: bounds.endRow - bounds.startRow + 1,
width: bounds.endColumn - bounds.startColumn + 1,
values
};
}
export function parseTsvBlock(text) {
const normalizedText = String(text || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const lines = normalizedText.endsWith("\n") ? normalizedText.slice(0, -1).split("\n") : normalizedText.split("\n");
if (!lines.length || (lines.length === 1 && !lines[0])) return null;
const values = lines.map((line) => line.split("\t"));
return {
height: values.length,
width: Math.max(...values.map((row) => row.length)),
values
};
}
export function getBoundsFromStartAndSize(rowIndex, columnIndex, height, width) {
return {
startRow: rowIndex,
endRow: rowIndex + height - 1,
startColumn: columnIndex,
endColumn: columnIndex + width - 1
};
}
export function isSameBounds(firstBounds, secondBounds) {
return Boolean(firstBounds && secondBounds
&& firstBounds.startRow === secondBounds.startRow
&& firstBounds.endRow === secondBounds.endRow
&& firstBounds.startColumn === secondBounds.startColumn
&& firstBounds.endColumn === secondBounds.endColumn);
}
export function moveSelectionCells(cells, sourceBounds, targetBounds) {
const nextCells = { ...cells };
const snapshot = [];
for (let rowIndex = sourceBounds.startRow; rowIndex <= sourceBounds.endRow; rowIndex += 1) {
for (let columnIndex = sourceBounds.startColumn; columnIndex <= sourceBounds.endColumn; columnIndex += 1) {
const value = cells[cellAddress(rowIndex, columnIndex)] || "";
snapshot.push({
rowOffset: rowIndex - sourceBounds.startRow,
columnOffset: columnIndex - sourceBounds.startColumn,
value
});
delete nextCells[cellAddress(rowIndex, columnIndex)];
}
}
for (let rowIndex = targetBounds.startRow; rowIndex <= targetBounds.endRow; rowIndex += 1) {
for (let columnIndex = targetBounds.startColumn; columnIndex <= targetBounds.endColumn; columnIndex += 1) {
delete nextCells[cellAddress(rowIndex, columnIndex)];
}
}
snapshot.forEach(({ rowOffset, columnOffset, value }) => {
if (!String(value || "").trim()) return;
nextCells[cellAddress(targetBounds.startRow + rowOffset, targetBounds.startColumn + columnOffset)] = value;
});
return nextCells;
}
export function pasteCellBlock(cells, block, targetRow, targetColumn, rows, columns) {
const clippedHeight = Math.min(block?.height || 0, rows - targetRow);
const clippedWidth = Math.min(block?.width || 0, columns - targetColumn);
if (clippedHeight <= 0 || clippedWidth <= 0) return null;
const nextCells = { ...cells };
for (let rowOffset = 0; rowOffset < clippedHeight; rowOffset += 1) {
for (let columnOffset = 0; columnOffset < clippedWidth; columnOffset += 1) {
const address = cellAddress(targetRow + rowOffset, targetColumn + columnOffset);
const value = String(block.values[rowOffset]?.[columnOffset] || "");
if (value.trim()) {
nextCells[address] = value;
} else {
delete nextCells[address];
}
}
}
return {
cells: nextCells,
bounds: getBoundsFromStartAndSize(targetRow, targetColumn, clippedHeight, clippedWidth)
};
}