@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
1,706 lines (1,550 loc) • 67.7 kB
JavaScript
/**
* Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact Volker Schukai.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import { instanceSymbol } from "../../constants.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import {
ATTRIBUTE_ERRORMESSAGE,
ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import { CustomControl } from "../../dom/customcontrol.mjs";
import {
assembleMethodSymbol,
registerCustomElement,
} from "../../dom/customelement.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { Observer } from "../../types/observer.mjs";
import { isArray, isObject, isString } from "../../types/is.mjs";
import { ID } from "../../types/id.mjs";
import { clone } from "../../util/clone.mjs";
import { SheetStyleSheet } from "./stylesheet/sheet.mjs";
export { Sheet };
const gridElementSymbol = Symbol("sheetGrid");
const gridWrapperSymbol = Symbol("sheetGridWrapper");
const spacerElementSymbol = Symbol("sheetGridSpacer");
const addRowButtonSymbol = Symbol("sheetAddRowButton");
const addColumnButtonSymbol = Symbol("sheetAddColumnButton");
const lastSnapshotSymbol = Symbol("sheetLastSnapshot");
const resizeStateSymbol = Symbol("sheetResizeState");
const skipRenderSymbol = Symbol("sheetSkipRender");
const lastViewportSymbol = Symbol("sheetLastViewport");
const scrollFrameSymbol = Symbol("sheetScrollFrame");
const forceRenderSymbol = Symbol("sheetForceRender");
const resizeFrameSymbol = Symbol("sheetResizeFrame");
const selectionSymbol = Symbol("sheetSelection");
const selectionBoxSymbol = Symbol("sheetSelectionBox");
const fillHandleSymbol = Symbol("sheetFillHandle");
const statusSymbol = Symbol("sheetStatus");
const statusTimeoutSymbol = Symbol("sheetStatusTimeout");
const contextMenuSymbol = Symbol("sheetContextMenu");
const menuStateSymbol = Symbol("sheetMenuState");
const dragFillSymbol = Symbol("sheetDragFill");
const lastCopySymbol = Symbol("sheetLastCopy");
const dragSelectSymbol = Symbol("sheetDragSelect");
class Sheet extends CustomControl {
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/form/sheet@@instance");
}
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initControlReferences.call(this);
initEventHandler.call(this);
initOptionObserver.call(this);
updateControl.call(this);
return this;
}
get defaults() {
return Object.assign({}, super.defaults, {
templates: { main: getTemplate() },
labels: getTranslations(),
classes: {
button: "monster-button-outline-primary",
},
features: {
addRows: false,
addColumns: false,
editable: true,
resizeRows: true,
resizeColumns: true,
virtualize: false,
},
virtualization: {
rowBuffer: 4,
columnBuffer: 2,
},
columns: defaultColumns(3),
rows: defaultRows(3),
sizes: {
columns: {},
rows: {},
rowHeaderWidth: 56,
headerHeight: 32,
},
constraints: {
minColumnWidth: 64,
maxColumnWidth: 360,
minRowHeight: 28,
maxRowHeight: 120,
},
value: { cells: {}, formulas: {} },
disabled: false,
cell: {
placeholder: "",
},
});
}
static getTag() {
return "monster-sheet";
}
static getCSSStyleSheet() {
return [SheetStyleSheet];
}
get value() {
return this.getOption("value");
}
set value(value) {
this.setOption("value", value);
this[forceRenderSymbol] = true;
setFormValueSafe.call(this);
updateControl.call(this);
}
}
function initControlReferences() {
const root = this.shadowRoot;
this[gridElementSymbol] = root.querySelector(`[${ATTRIBUTE_ROLE}=grid]`);
this[gridWrapperSymbol] = root.querySelector(
`[${ATTRIBUTE_ROLE}=grid-wrapper]`,
);
if (this[gridWrapperSymbol]) {
let spacer = this[gridWrapperSymbol].querySelector(
`[${ATTRIBUTE_ROLE}=spacer]`,
);
if (!spacer) {
spacer = document.createElement("div");
spacer.setAttribute(ATTRIBUTE_ROLE, "spacer");
spacer.setAttribute("part", "spacer");
this[gridWrapperSymbol].prepend(spacer);
}
this[spacerElementSymbol] = spacer;
}
this[addRowButtonSymbol] = root.querySelector(`[${ATTRIBUTE_ROLE}=add-row]`);
this[addColumnButtonSymbol] = root.querySelector(
`[${ATTRIBUTE_ROLE}=add-column]`,
);
this[selectionBoxSymbol] = root.querySelector(
`[${ATTRIBUTE_ROLE}=selection]`,
);
this[fillHandleSymbol] = root.querySelector(
`[${ATTRIBUTE_ROLE}=fill-handle]`,
);
this[statusSymbol] = root.querySelector(`[${ATTRIBUTE_ROLE}=status]`);
this[contextMenuSymbol] = root.querySelector(
`[${ATTRIBUTE_ROLE}=context-menu]`,
);
}
function initEventHandler() {
this[addRowButtonSymbol].addEventListener("click", () => {
if (!canAddRows.call(this)) return;
addRow.call(this);
});
this[addColumnButtonSymbol].addEventListener("click", () => {
if (!canAddColumns.call(this)) return;
addColumn.call(this);
});
this[gridElementSymbol].addEventListener("input", (event) => {
const input = event.target;
if (!(input instanceof HTMLInputElement)) return;
const cell = input.closest(`[${ATTRIBUTE_ROLE}=cell]`);
if (!cell) return;
const rowId = cell.dataset.rowId;
const colId = cell.dataset.colId;
if (!rowId || !colId) return;
setCellValue.call(this, rowId, colId, input.value);
});
this[gridElementSymbol].addEventListener("focusin", (event) => {
const input = event.target;
if (!(input instanceof HTMLInputElement)) return;
const cell = input.closest(`[${ATTRIBUTE_ROLE}=cell]`);
if (!cell) return;
const rowId = cell.dataset.rowId;
const colId = cell.dataset.colId;
if (!rowId || !colId) return;
const data = normalizeValue.call(this);
const formula = data.formulas?.[rowId]?.[colId];
if (isString(formula)) {
input.value = formula;
}
const selection = this[selectionSymbol];
const isMulti =
selection?.anchor &&
selection?.focus &&
(selection.anchor.rowId !== selection.focus.rowId ||
selection.anchor.colId !== selection.focus.colId);
if (!event.shiftKey && isMulti) return;
setSelectionFromCell.call(this, rowId, colId, event.shiftKey);
});
this[gridElementSymbol].addEventListener("focusout", (event) => {
const input = event.target;
if (!(input instanceof HTMLInputElement)) return;
const cell = input.closest(`[${ATTRIBUTE_ROLE}=cell]`);
if (!cell) return;
const rowId = cell.dataset.rowId;
const colId = cell.dataset.colId;
if (!rowId || !colId) return;
refreshDisplayValues.call(this);
});
this[gridElementSymbol].addEventListener("pointerdown", (event) => {
if (event.button !== 0) return;
const columnHandle = event.target.closest(
`[${ATTRIBUTE_ROLE}=column-resize]`,
);
if (columnHandle) {
if (!this.getOption("features.resizeColumns", true)) return;
startColumnResize.call(this, event, columnHandle);
return;
}
const rowHandle = event.target.closest(`[${ATTRIBUTE_ROLE}=row-resize]`);
if (rowHandle) {
if (!this.getOption("features.resizeRows", true)) return;
startRowResize.call(this, event, rowHandle);
return;
}
const cell = event.target.closest(`[${ATTRIBUTE_ROLE}=cell]`);
if (!cell) return;
const rowId = cell.dataset.rowId;
const colId = cell.dataset.colId;
if (!rowId || !colId) return;
setSelectionFromCell.call(this, rowId, colId, event.shiftKey);
startSelectionDrag.call(this, event);
});
this[gridElementSymbol].addEventListener("keydown", (event) => {
if (!(event.ctrlKey || event.metaKey)) return;
if (event.key === "c" || event.key === "C") {
event.preventDefault();
void copySelectionToClipboard.call(this);
return;
}
if (event.key === "x" || event.key === "X") {
event.preventDefault();
void cutSelectionToClipboard.call(this);
return;
}
if (event.key === "v" || event.key === "V") {
event.preventDefault();
void pasteFromClipboard.call(this);
}
});
this[gridElementSymbol].addEventListener("contextmenu", (event) => {
const cell = event.target.closest(`[${ATTRIBUTE_ROLE}=cell]`);
if (!cell) return;
event.preventDefault();
const rowId = cell.dataset.rowId;
const colId = cell.dataset.colId;
if (rowId && colId) {
setSelectionFromCell.call(this, rowId, colId, false);
}
showContextMenu.call(this, event.clientX, event.clientY);
});
this.shadowRoot.addEventListener("pointerdown", (event) => {
const menu = this[contextMenuSymbol];
if (!menu || menu.hidden) return;
if (menu.contains(event.target)) return;
hideContextMenu.call(this);
});
if (this[gridWrapperSymbol]) {
this[gridWrapperSymbol].addEventListener("scroll", () => {
if (!this.getOption("features.virtualize", false)) return;
if (this[scrollFrameSymbol]) return;
this[scrollFrameSymbol] = requestAnimationFrame(() => {
this[scrollFrameSymbol] = null;
updateControl.call(this);
});
});
}
if (this[gridWrapperSymbol]) {
this[gridWrapperSymbol].addEventListener("scroll", () => {
hideContextMenu.call(this);
});
}
if (this[fillHandleSymbol]) {
this[fillHandleSymbol].addEventListener("pointerdown", (event) => {
startFillDrag.call(this, event);
});
}
wireContextMenuActions.call(this);
}
function initOptionObserver() {
this[lastSnapshotSymbol] = "";
this.attachObserver(
new Observer(() => {
const snapshot = JSON.stringify({
value: this.getOption("value"),
columns: this.getOption("columns"),
rows: this.getOption("rows"),
sizes: this.getOption("sizes"),
features: this.getOption("features"),
disabled: this.getOption("disabled"),
classes: this.getOption("classes"),
labels: this.getOption("labels"),
cell: this.getOption("cell"),
});
if (this[skipRenderSymbol]) {
this[lastSnapshotSymbol] = snapshot;
this[skipRenderSymbol] = false;
return;
}
if (snapshot === this[lastSnapshotSymbol]) return;
this[lastSnapshotSymbol] = snapshot;
updateControl.call(this);
}),
);
}
function updateControl() {
const normalized = normalizeValue.call(this);
const current = this.getOption("value");
if (!isSameValue(current, normalized)) {
this.setOption("value", normalized);
setFormValueSafe.call(this);
}
const virtualize = this.getOption("features.virtualize", false) === true;
if (virtualize) {
this.setAttribute("data-virtualized", "");
if (this[forceRenderSymbol]) {
this[lastViewportSymbol] = null;
this[forceRenderSymbol] = false;
}
renderVirtual.call(this, normalized);
} else {
if (this.hasAttribute("data-virtualized")) {
this.removeAttribute("data-virtualized");
}
if (this[spacerElementSymbol]) {
this[spacerElementSymbol].style.width = "0px";
this[spacerElementSymbol].style.height = "0px";
}
this[lastViewportSymbol] = null;
const displayCells = buildDisplayCells.call(this, normalized);
renderGrid.call(this, normalized, displayCells);
}
syncToolbarState.call(this);
applyDisabledState.call(this);
updateSelectionDisplay.call(this);
}
function syncToolbarState() {
const addRow = canAddRows.call(this);
const addCol = canAddColumns.call(this);
const labels = this.getOption("labels", {});
const buttonClass = this.getOption("classes.button", "");
this[addRowButtonSymbol].hidden = !addRow;
this[addColumnButtonSymbol].hidden = !addCol;
this[addRowButtonSymbol].disabled =
this.getOption("disabled", false) || !addRow;
this[addColumnButtonSymbol].disabled =
this.getOption("disabled", false) || !addCol;
this[addRowButtonSymbol].className = buttonClass;
this[addColumnButtonSymbol].className = buttonClass;
if (isString(labels.addRow)) {
this[addRowButtonSymbol].textContent = labels.addRow;
}
if (isString(labels.addColumn)) {
this[addColumnButtonSymbol].textContent = labels.addColumn;
}
syncContextMenuLabels.call(this);
}
function applyDisabledState() {
const disabled = this.getOption("disabled", false);
const editable = this.getOption("features.editable", true);
const inputs = this.shadowRoot.querySelectorAll(
`[${ATTRIBUTE_ROLE}=cell-input]`,
);
inputs.forEach((input) => {
input.disabled = disabled || !editable;
});
}
function renderGrid(value, displayCells) {
const grid = this[gridElementSymbol];
if (!grid) return;
grid.style.position = "";
grid.style.left = "";
grid.style.top = "";
grid.style.transform = "";
grid.textContent = "";
const fragment = document.createDocumentFragment();
fragment.appendChild(buildCornerCell.call(this));
const lastColumnId =
value.columns.length > 0
? value.columns[value.columns.length - 1].id
: null;
for (const col of value.columns) {
fragment.appendChild(
buildColumnHeader.call(this, col, col.id === lastColumnId),
);
}
for (const row of value.rows) {
fragment.appendChild(buildRowHeader.call(this, row));
for (const col of value.columns) {
fragment.appendChild(
buildCell.call(this, row, col, displayCells, col.id === lastColumnId),
);
}
}
grid.appendChild(fragment);
applyGridTemplate.call(this, value);
}
function renderVirtual(value) {
const grid = this[gridElementSymbol];
const wrapper = this[gridWrapperSymbol];
const spacer = this[spacerElementSymbol];
if (!grid || !wrapper || !spacer) return;
const sizes = getVirtualSizes.call(this, value);
const viewportWidth = wrapper.clientWidth;
const viewportHeight = wrapper.clientHeight;
if (viewportWidth === 0 || viewportHeight === 0) {
if (!this[scrollFrameSymbol]) {
this[scrollFrameSymbol] = requestAnimationFrame(() => {
this[scrollFrameSymbol] = null;
updateControl.call(this);
});
}
return;
}
const scrollLeft = wrapper.scrollLeft;
const scrollTop = wrapper.scrollTop;
const bufferCols = getSizeNumber(
this.getOption("virtualization.columnBuffer"),
2,
);
const bufferRows = getSizeNumber(
this.getOption("virtualization.rowBuffer"),
4,
);
const visible = getVisibleRange(
sizes.columnOffsets,
sizes.rowOffsets,
scrollLeft - sizes.rowHeaderWidth,
scrollTop - sizes.headerHeight,
viewportWidth,
viewportHeight,
);
const colStart = Math.max(0, visible.colStart - bufferCols);
const colEnd = Math.min(
value.columns.length - 1,
visible.colEnd + bufferCols,
);
const rowStart = Math.max(0, visible.rowStart - bufferRows);
const rowEnd = Math.min(value.rows.length - 1, visible.rowEnd + bufferRows);
const viewportKey = `${colStart}-${colEnd}-${rowStart}-${rowEnd}-${sizes.totalWidth}-${sizes.totalHeight}`;
if (this[lastViewportSymbol] === viewportKey) return;
this[lastViewportSymbol] = viewportKey;
spacer.style.width = `${sizes.totalWidth}px`;
spacer.style.height = `${sizes.totalHeight}px`;
const offsetX = sizes.rowHeaderWidth + sizes.columnOffsets[colStart];
const offsetY = sizes.headerHeight + sizes.rowOffsets[rowStart];
grid.style.position = "absolute";
grid.style.left = "0";
grid.style.top = "0";
grid.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
const visibleColumns = value.columns.slice(colStart, colEnd + 1);
const visibleRows = value.rows.slice(rowStart, rowEnd + 1);
const visibleWidths = visibleColumns.map(
(col, index) => `${sizes.columnWidths[colStart + index]}px`,
);
const visibleHeights = visibleRows.map(
(row, index) => `${sizes.rowHeights[rowStart + index]}px`,
);
grid.style.gridTemplateColumns = `${sizes.rowHeaderWidth}px ${visibleWidths.join(" ")}`;
grid.style.gridTemplateRows = `${sizes.headerHeight}px ${visibleHeights.join(" ")}`;
grid.textContent = "";
const fragment = document.createDocumentFragment();
fragment.appendChild(buildCornerCell.call(this));
const lastVisibleColumnId =
visibleColumns.length > 0
? visibleColumns[visibleColumns.length - 1].id
: null;
for (const col of visibleColumns) {
fragment.appendChild(
buildColumnHeader.call(this, col, col.id === lastVisibleColumnId),
);
}
const errors = [];
const getDisplayValue = (rowId, colId) => {
const formula = value.formulas?.[rowId]?.[colId];
if (isString(formula)) {
const evaluated = evaluateFormula.call(this, value, formula);
if (Number.isFinite(evaluated)) return evaluated;
errors.push({ rowId, colId, formula });
return "#ERR";
}
const raw = value.cells?.[rowId]?.[colId];
return raw === undefined || raw === null ? "" : raw;
};
for (const row of visibleRows) {
fragment.appendChild(buildRowHeader.call(this, row));
for (const col of visibleColumns) {
fragment.appendChild(
buildCell.call(
this,
row,
col,
getDisplayValue,
col.id === lastVisibleColumnId,
),
);
}
}
if (errors.length > 0) {
fireCustomEvent(this, "monster-sheet-formula-error", { errors });
}
grid.appendChild(fragment);
}
function buildCornerCell() {
const labels = this.getOption("labels", {});
const cell = document.createElement("div");
cell.setAttribute(ATTRIBUTE_ROLE, "corner");
cell.setAttribute("part", "corner");
cell.textContent = isString(labels.corner) ? labels.corner : "";
return cell;
}
function buildColumnHeader(column, isLastColumn) {
const cell = document.createElement("div");
cell.setAttribute(ATTRIBUTE_ROLE, "column-header");
cell.setAttribute("part", "column-header");
cell.dataset.colId = column.id;
if (isLastColumn) {
cell.dataset.lastColumn = "true";
}
cell.textContent = column.label ?? column.id;
if (this.getOption("features.resizeColumns", true)) {
const handle = document.createElement("span");
handle.setAttribute(ATTRIBUTE_ROLE, "column-resize");
handle.setAttribute("part", "column-resize");
handle.dataset.colId = column.id;
cell.appendChild(handle);
}
return cell;
}
function buildRowHeader(row) {
const cell = document.createElement("div");
cell.setAttribute(ATTRIBUTE_ROLE, "row-header");
cell.setAttribute("part", "row-header");
cell.dataset.rowId = row.id;
cell.textContent = row.label ?? row.id;
if (this.getOption("features.resizeRows", true)) {
const handle = document.createElement("span");
handle.setAttribute(ATTRIBUTE_ROLE, "row-resize");
handle.setAttribute("part", "row-resize");
handle.dataset.rowId = row.id;
cell.appendChild(handle);
}
return cell;
}
function buildCell(row, column, cells, isLastColumn) {
const wrapper = document.createElement("div");
wrapper.setAttribute(ATTRIBUTE_ROLE, "cell");
wrapper.setAttribute("part", "cell");
wrapper.dataset.rowId = row.id;
wrapper.dataset.colId = column.id;
if (isLastColumn) {
wrapper.dataset.lastColumn = "true";
}
const input = document.createElement("input");
input.setAttribute(ATTRIBUTE_ROLE, "cell-input");
input.setAttribute("part", "cell-input");
input.type = "text";
input.autocomplete = "off";
input.inputMode = "text";
input.placeholder = this.getOption("cell.placeholder", "");
const value =
typeof cells === "function"
? cells(row.id, column.id)
: cells?.[row.id]?.[column.id];
input.value = value === undefined || value === null ? "" : String(value);
input.disabled =
this.getOption("disabled", false) ||
!this.getOption("features.editable", true);
wrapper.appendChild(input);
return wrapper;
}
function applyGridTemplate(value) {
const sizes = this.getOption("sizes", {});
const rowHeaderWidth = getRowHeaderWidthNumber.call(
this,
sizes.rowHeaderWidth,
value.rows,
);
const headerHeight = getSizeNumber(sizes.headerHeight, 32);
const columnSizes = value.columns.map((col) =>
getColumnWidth.call(this, col.id),
);
const rowSizes = value.rows.map((row) => getRowHeight.call(this, row.id));
this[gridElementSymbol].style.gridTemplateColumns =
`${rowHeaderWidth}px ${columnSizes.join(" ")}`;
this[gridElementSymbol].style.gridTemplateRows =
`${headerHeight}px ${rowSizes.join(" ")}`;
}
function getVirtualSizes(value) {
const sizes = this.getOption("sizes", {});
const rowHeaderWidth = getRowHeaderWidthNumber.call(
this,
sizes.rowHeaderWidth,
value.rows,
);
const headerHeight = getSizeNumber(sizes.headerHeight, 32);
const columnWidths = value.columns.map((col) =>
getColumnWidthNumber.call(this, col.id),
);
const rowHeights = value.rows.map((row) =>
getRowHeightNumber.call(this, row.id),
);
const columnOffsets = buildOffsets(columnWidths);
const rowOffsets = buildOffsets(rowHeights);
const totalWidth =
rowHeaderWidth +
(columnWidths.length > 0
? columnOffsets[columnOffsets.length - 1] +
columnWidths[columnWidths.length - 1]
: 0);
const totalHeight =
headerHeight +
(rowHeights.length > 0
? rowOffsets[rowOffsets.length - 1] + rowHeights[rowHeights.length - 1]
: 0);
return {
rowHeaderWidth,
headerHeight,
columnWidths,
rowHeights,
columnOffsets,
rowOffsets,
totalWidth,
totalHeight,
};
}
function buildOffsets(values) {
const offsets = [];
let current = 0;
for (const value of values) {
offsets.push(current);
current += value;
}
return offsets;
}
function getVisibleRange(
columnOffsets,
rowOffsets,
scrollLeft,
scrollTop,
viewportWidth,
viewportHeight,
) {
const colStart = findStartIndex(columnOffsets, Math.max(0, scrollLeft));
const colEnd = findEndIndex(
columnOffsets,
Math.max(0, scrollLeft) + viewportWidth,
);
const rowStart = findStartIndex(rowOffsets, Math.max(0, scrollTop));
const rowEnd = findEndIndex(
rowOffsets,
Math.max(0, scrollTop) + viewportHeight,
);
return { colStart, colEnd, rowStart, rowEnd };
}
function findStartIndex(offsets, value) {
let low = 0;
let high = offsets.length - 1;
let result = 0;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if (offsets[mid] <= value) {
result = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
return result;
}
function findEndIndex(offsets, value) {
let low = 0;
let high = offsets.length - 1;
let result = offsets.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if (offsets[mid] < value) {
low = mid + 1;
} else {
result = mid;
high = mid - 1;
}
}
return Math.max(0, result);
}
function normalizeValue() {
const optionsValue = this.getOption("value");
const columns = normalizeColumns(
optionsValue?.columns ?? this.getOption("columns"),
);
const rows = normalizeRows(optionsValue?.rows ?? this.getOption("rows"));
const cells = isObject(optionsValue?.cells) ? clone(optionsValue.cells) : {};
const formulas = isObject(optionsValue?.formulas)
? clone(optionsValue.formulas)
: {};
return { columns, rows, cells, formulas };
}
function normalizeColumns(columns) {
const list = isArray(columns) ? columns : [];
return list.map((col, index) => {
if (isString(col)) {
return { id: col, label: col };
}
if (isObject(col)) {
const id = isString(col.id) ? col.id : nextColumnLabel(index);
return { id, label: col.label ?? id };
}
const id = nextColumnLabel(index);
return { id, label: id };
});
}
function normalizeRows(rows) {
const list = isArray(rows) ? rows : [];
return list.map((row, index) => {
if (isString(row)) {
return { id: row, label: row };
}
if (isObject(row)) {
const id = isString(row.id) ? row.id : nextRowLabel(index);
return { id, label: row.label ?? id };
}
const id = nextRowLabel(index);
return { id, label: id };
});
}
function setCellValue(rowId, colId, value) {
const data = normalizeValue.call(this);
if (!data.cells[rowId]) data.cells[rowId] = {};
if (!data.formulas[rowId]) data.formulas[rowId] = {};
const next = isString(value) ? value : String(value);
if (next.trim().startsWith("=")) {
data.formulas[rowId][colId] = next.trim();
delete data.cells[rowId][colId];
} else {
delete data.formulas[rowId][colId];
data.cells[rowId][colId] = value;
}
this.setOption("value", data);
this[skipRenderSymbol] = true;
this[forceRenderSymbol] = true;
setFormValueSafe.call(this);
fireCustomEvent(this, "monster-sheet-change", {
value: data,
cell: { rowId, colId, value },
});
}
function addRow() {
const data = normalizeValue.call(this);
const newRow = createRow(data.rows.length, data.rows);
data.rows.push(newRow);
this.setOption("value", data);
this[skipRenderSymbol] = true;
setFormValueSafe.call(this);
fireCustomEvent(this, "monster-sheet-add-row", { row: newRow, value: data });
updateControl.call(this);
}
function addColumn() {
const data = normalizeValue.call(this);
const newColumn = createColumn(data.columns.length, data.columns);
data.columns.push(newColumn);
this.setOption("value", data);
this[skipRenderSymbol] = true;
setFormValueSafe.call(this);
fireCustomEvent(this, "monster-sheet-add-column", {
column: newColumn,
value: data,
});
updateControl.call(this);
}
function canAddRows() {
return this.getOption("features.addRows", false) === true;
}
function canAddColumns() {
return this.getOption("features.addColumns", false) === true;
}
function createColumn(index, columns) {
const label = nextColumnLabel(index);
const existing = new Set(columns.map((col) => col.id));
const id = existing.has(label) ? new ID("column-").toString() : label;
return { id, label };
}
function createRow(index, rows) {
const label = nextRowLabel(index);
const existing = new Set(rows.map((row) => row.id));
const id = existing.has(label) ? new ID("row-").toString() : label;
return { id, label };
}
function nextColumnLabel(index) {
let value = index + 1;
let label = "";
while (value > 0) {
const mod = (value - 1) % 26;
label = String.fromCharCode(65 + mod) + label;
value = Math.floor((value - 1) / 26);
}
return label;
}
function nextRowLabel(index) {
return String(index + 1);
}
function defaultColumns(count) {
return Array.from({ length: count }, (_, i) => {
const label = nextColumnLabel(i);
return { id: label, label };
});
}
function defaultRows(count) {
return Array.from({ length: count }, (_, i) => {
const label = nextRowLabel(i);
return { id: label, label };
});
}
function getSizeNumber(value, fallback) {
const n = Number(value);
return Number.isFinite(n) ? n : fallback;
}
function getColumnWidth(columnId) {
const sizes = this.getOption("sizes.columns", {});
const width = getSizeNumber(sizes?.[columnId], 120);
const min = getSizeNumber(this.getOption("constraints.minColumnWidth"), 64);
const max = getSizeNumber(this.getOption("constraints.maxColumnWidth"), 360);
return `${clamp(width, min, max)}px`;
}
function getColumnWidthNumber(columnId) {
const sizes = this.getOption("sizes.columns", {});
const width = getSizeNumber(sizes?.[columnId], 120);
const min = getSizeNumber(this.getOption("constraints.minColumnWidth"), 64);
const max = getSizeNumber(this.getOption("constraints.maxColumnWidth"), 360);
return clamp(width, min, max);
}
function getRowHeight(rowId) {
const sizes = this.getOption("sizes.rows", {});
const height = getSizeNumber(sizes?.[rowId], 32);
const min = getSizeNumber(this.getOption("constraints.minRowHeight"), 28);
const max = getSizeNumber(this.getOption("constraints.maxRowHeight"), 120);
return `${clamp(height, min, max)}px`;
}
function getRowHeightNumber(rowId) {
const sizes = this.getOption("sizes.rows", {});
const height = getSizeNumber(sizes?.[rowId], 32);
const min = getSizeNumber(this.getOption("constraints.minRowHeight"), 28);
const max = getSizeNumber(this.getOption("constraints.maxRowHeight"), 120);
return clamp(height, min, max);
}
function getRowHeaderWidthNumber(value, rows) {
if (value === "auto" || value === null || value === undefined) {
const maxLen = getMaxRowLabelLength(rows);
const base = 16;
const charWidth = 8;
return Math.max(56, base + maxLen * charWidth);
}
return getSizeNumber(value, 56);
}
function getMaxRowLabelLength(rows) {
if (!isArray(rows) || rows.length === 0) return 1;
const last = rows[rows.length - 1];
const label = isString(last?.label) ? last.label : last?.id;
const text = label === undefined || label === null ? "" : String(label);
if (/^\d+$/.test(text)) {
return String(rows.length).length;
}
return text.length;
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function startColumnResize(event, handle) {
const colId = handle.dataset.colId;
if (!colId) return;
event.preventDefault();
event.stopPropagation();
const grid = this[gridElementSymbol];
const headerCell = handle.parentElement;
if (!headerCell) return;
const startWidth = headerCell.getBoundingClientRect().width;
this.setAttribute("data-resizing", "");
this[resizeStateSymbol] = {
kind: "column",
id: colId,
start: event.clientX,
startSize: startWidth,
};
grid.setPointerCapture(event.pointerId);
grid.addEventListener("pointermove", handleResizeMove);
grid.addEventListener("pointerup", handleResizeEnd);
grid.addEventListener("pointercancel", handleResizeEnd);
}
function startRowResize(event, handle) {
const rowId = handle.dataset.rowId;
if (!rowId) return;
event.preventDefault();
event.stopPropagation();
const headerCell = handle.parentElement;
if (!headerCell) return;
const startHeight = headerCell.getBoundingClientRect().height;
const grid = this[gridElementSymbol];
this.setAttribute("data-resizing", "");
this[resizeStateSymbol] = {
kind: "row",
id: rowId,
start: event.clientY,
startSize: startHeight,
};
grid.setPointerCapture(event.pointerId);
grid.addEventListener("pointermove", handleResizeMove);
grid.addEventListener("pointerup", handleResizeEnd);
grid.addEventListener("pointercancel", handleResizeEnd);
}
function handleResizeMove(event) {
const grid = event.currentTarget;
const sheet = grid.getRootNode().host;
if (!sheet || !sheet[resizeStateSymbol]) return;
const state = sheet[resizeStateSymbol];
if (state.kind === "column") {
const delta = event.clientX - state.start;
const size = state.startSize + delta;
setColumnSize.call(sheet, state.id, size);
} else if (state.kind === "row") {
const delta = event.clientY - state.start;
const size = state.startSize + delta;
setRowSize.call(sheet, state.id, size);
}
}
function handleResizeEnd(event) {
const grid = event.currentTarget;
const sheet = grid.getRootNode().host;
if (!sheet) return;
if (sheet.hasAttribute("data-resizing")) {
sheet.removeAttribute("data-resizing");
}
grid.releasePointerCapture(event.pointerId);
grid.removeEventListener("pointermove", handleResizeMove);
grid.removeEventListener("pointerup", handleResizeEnd);
grid.removeEventListener("pointercancel", handleResizeEnd);
sheet[resizeStateSymbol] = null;
}
function setColumnSize(columnId, size) {
const min = getSizeNumber(this.getOption("constraints.minColumnWidth"), 64);
const max = getSizeNumber(this.getOption("constraints.maxColumnWidth"), 360);
const next = clamp(size, min, max);
this.setOption(`sizes.columns.${columnId}`, next);
this[skipRenderSymbol] = true;
if (this.getOption("features.virtualize", false) === true) {
scheduleVirtualResizeRender.call(this);
} else {
applyGridTemplate.call(this, normalizeValue.call(this));
}
fireCustomEvent(this, "monster-sheet-resize-column", {
columnId,
width: next,
});
}
function setRowSize(rowId, size) {
const min = getSizeNumber(this.getOption("constraints.minRowHeight"), 28);
const max = getSizeNumber(this.getOption("constraints.maxRowHeight"), 120);
const next = clamp(size, min, max);
this.setOption(`sizes.rows.${rowId}`, next);
this[skipRenderSymbol] = true;
if (this.getOption("features.virtualize", false) === true) {
scheduleVirtualResizeRender.call(this);
} else {
applyGridTemplate.call(this, normalizeValue.call(this));
}
fireCustomEvent(this, "monster-sheet-resize-row", {
rowId,
height: next,
});
}
function setFormValueSafe() {
try {
this.setFormValue(this.value);
} catch (e) {
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
}
}
function isSameValue(a, b) {
try {
return JSON.stringify(a) === JSON.stringify(b);
} catch (e) {
return false;
}
}
function refreshDisplayValues() {
const data = normalizeValue.call(this);
const virtualize = this.getOption("features.virtualize", false) === true;
const displayCells = virtualize ? null : buildDisplayCells.call(this, data);
const active = this.shadowRoot?.activeElement;
const inputs = this.shadowRoot.querySelectorAll(
`[${ATTRIBUTE_ROLE}=cell-input]`,
);
inputs.forEach((input) => {
if (!(input instanceof HTMLInputElement)) return;
if (input === active) return;
const cell = input.closest(`[${ATTRIBUTE_ROLE}=cell]`);
if (!cell) return;
const rowId = cell.dataset.rowId;
const colId = cell.dataset.colId;
if (!rowId || !colId) return;
const value = virtualize
? getCellDisplayValue.call(this, data, rowId, colId)
: displayCells?.[rowId]?.[colId];
input.value = value === undefined || value === null ? "" : String(value);
});
}
function setSelectionFromCell(rowId, colId, expand) {
hideContextMenu.call(this);
const selection = this[selectionSymbol] || {};
if (expand && selection.anchor) {
selection.focus = { rowId, colId };
} else {
selection.anchor = { rowId, colId };
selection.focus = { rowId, colId };
}
this[selectionSymbol] = selection;
updateSelectionDisplay.call(this);
}
function setSelectionFocus(rowId, colId) {
const selection = this[selectionSymbol] || {};
if (!selection.anchor) {
selection.anchor = { rowId, colId };
}
selection.focus = { rowId, colId };
this[selectionSymbol] = selection;
updateSelectionDisplay.call(this);
}
function startSelectionDrag(event) {
if (event.button !== 0) return;
const cell = event.target.closest(`[${ATTRIBUTE_ROLE}=cell]`);
if (!cell) return;
const state = {
active: true,
startX: event.clientX,
startY: event.clientY,
moved: false,
};
this[dragSelectSymbol] = state;
this.setAttribute("data-selecting", "");
const move = (moveEvent) => {
handleSelectionDragMove.call(this, moveEvent);
};
const end = () => {
handleSelectionDragEnd.call(this);
};
window.addEventListener("pointermove", move);
window.addEventListener("pointerup", end, { once: true });
window.addEventListener("pointercancel", end, { once: true });
state.cleanup = () => {
window.removeEventListener("pointermove", move);
};
}
function handleSelectionDragMove(event) {
const state = this[dragSelectSymbol];
if (!state?.active) return;
if (!state.moved) {
const dx = event.clientX - state.startX;
const dy = event.clientY - state.startY;
if (Math.abs(dx) < 3 && Math.abs(dy) < 3) return;
state.moved = true;
}
const cell = getCellFromPoint.call(this, event.clientX, event.clientY);
if (!cell) return;
const rowId = cell.dataset.rowId;
const colId = cell.dataset.colId;
if (!rowId || !colId) return;
setSelectionFocus.call(this, rowId, colId);
}
function handleSelectionDragEnd() {
const state = this[dragSelectSymbol];
if (!state) return;
if (state.cleanup) state.cleanup();
this[dragSelectSymbol] = null;
this.removeAttribute("data-selecting");
}
function updateSelectionDisplay() {
const grid = this[gridElementSymbol];
if (!grid) return;
const data = normalizeValue.call(this);
const selection = this[selectionSymbol];
const range = selection ? getSelectionRange(selection, data) : null;
const virtualize = this.getOption("features.virtualize", false) === true;
const cells = grid.querySelectorAll(`[${ATTRIBUTE_ROLE}=cell]`);
let overlay = null;
const wrapper = this[gridWrapperSymbol];
const wrapperRect = wrapper?.getBoundingClientRect() ?? null;
let minLeft = Infinity;
let minTop = Infinity;
let maxRight = -Infinity;
let maxBottom = -Infinity;
cells.forEach((cell) => {
const rowId = cell.dataset.rowId;
const colId = cell.dataset.colId;
let selected = false;
if (range && rowId && colId) {
const { rowIndexById, colIndexById } = range;
const rowIndex = rowIndexById.get(rowId);
const colIndex = colIndexById.get(colId);
if (
rowIndex !== undefined &&
colIndex !== undefined &&
rowIndex >= range.rowStart &&
rowIndex <= range.rowEnd &&
colIndex >= range.colStart &&
colIndex <= range.colEnd
) {
selected = true;
}
}
if (selected) {
cell.dataset.selected = "true";
if (wrapperRect) {
const rect = cell.getBoundingClientRect();
minLeft = Math.min(minLeft, rect.left);
minTop = Math.min(minTop, rect.top);
maxRight = Math.max(maxRight, rect.right);
maxBottom = Math.max(maxBottom, rect.bottom);
}
} else {
delete cell.dataset.selected;
}
if (
selection?.focus?.rowId === rowId &&
selection?.focus?.colId === colId
) {
cell.dataset.active = "true";
} else {
delete cell.dataset.active;
}
});
if (Number.isFinite(minLeft) && Number.isFinite(maxRight) && wrapperRect) {
overlay = {
left: minLeft - wrapperRect.left + wrapper.scrollLeft,
top: minTop - wrapperRect.top + wrapper.scrollTop,
width: Math.max(0, maxRight - minLeft),
height: Math.max(0, maxBottom - minTop),
};
} else if (range && virtualize) {
const sizes = getVirtualSizes.call(this, data);
const colStart = range.colStart;
const colEnd = range.colEnd;
const rowStart = range.rowStart;
const rowEnd = range.rowEnd;
const left = sizes.rowHeaderWidth + sizes.columnOffsets[colStart];
const top = sizes.headerHeight + sizes.rowOffsets[rowStart];
const width =
sizes.columnOffsets[colEnd] +
sizes.columnWidths[colEnd] -
sizes.columnOffsets[colStart];
const height =
sizes.rowOffsets[rowEnd] +
sizes.rowHeights[rowEnd] -
sizes.rowOffsets[rowStart];
overlay = { left, top, width, height };
}
updateSelectionOverlay.call(this, overlay);
}
function updateSelectionOverlay(bounds) {
const box = this[selectionBoxSymbol];
const wrapper = this[gridWrapperSymbol];
const handle = this[fillHandleSymbol];
if (!box || !wrapper || !handle) return;
if (!bounds) {
box.style.display = "none";
handle.style.display = "none";
return;
}
box.style.display = "block";
box.style.left = `${bounds.left}px`;
box.style.top = `${bounds.top}px`;
box.style.width = `${bounds.width}px`;
box.style.height = `${bounds.height}px`;
handle.style.display = "block";
handle.style.left = `${bounds.left + bounds.width - 4}px`;
handle.style.top = `${bounds.top + bounds.height - 4}px`;
}
function getSelectionRange(selection, data) {
if (!selection?.anchor || !selection?.focus) return null;
const { rowIndexById, colIndexById } = getIndexMaps(data);
const startRow = rowIndexById.get(selection.anchor.rowId);
const endRow = rowIndexById.get(selection.focus.rowId);
const startCol = colIndexById.get(selection.anchor.colId);
const endCol = colIndexById.get(selection.focus.colId);
if (
startRow === undefined ||
endRow === undefined ||
startCol === undefined ||
endCol === undefined
) {
return null;
}
return {
rowStart: Math.min(startRow, endRow),
rowEnd: Math.max(startRow, endRow),
colStart: Math.min(startCol, endCol),
colEnd: Math.max(startCol, endCol),
rowIndexById,
colIndexById,
};
}
function getIndexMaps(data) {
const rowIndexById = new Map();
const colIndexById = new Map();
data.rows.forEach((row, index) => {
if (row?.id) rowIndexById.set(row.id, index);
});
data.columns.forEach((col, index) => {
if (col?.id) colIndexById.set(col.id, index);
});
return { rowIndexById, colIndexById };
}
function getActiveCell() {
const active = this.shadowRoot?.activeElement;
if (!(active instanceof HTMLInputElement)) return null;
const cell = active.closest(`[${ATTRIBUTE_ROLE}=cell]`);
if (!cell) return null;
const rowId = cell.dataset.rowId;
const colId = cell.dataset.colId;
if (!rowId || !colId) return null;
return { rowId, colId };
}
async function copySelectionToClipboard() {
const data = normalizeValue.call(this);
const range = getSelectionRange.call(this, this[selectionSymbol], data);
const resolvedRange = range ?? getRangeFromActiveCell.call(this, data);
if (!resolvedRange) return;
const text = buildClipboardText(data, resolvedRange);
this[lastCopySymbol] = {
text,
range: {
rowStart: resolvedRange.rowStart,
rowEnd: resolvedRange.rowEnd,
colStart: resolvedRange.colStart,
colEnd: resolvedRange.colEnd,
},
values: buildClipboardMatrix(data, resolvedRange),
};
const success = await writeClipboardText(text);
if (success) {
showStatus.call(this, this.getOption("labels.copied") || "Copied");
}
}
async function cutSelectionToClipboard() {
const data = normalizeValue.call(this);
const range = getSelectionRange.call(this, this[selectionSymbol], data);
const resolvedRange = range ?? getRangeFromActiveCell.call(this, data);
if (!resolvedRange) return;
const text = buildClipboardText(data, resolvedRange);
const success = await writeClipboardText(text);
if (!success) return;
const next = clearRange(data, resolvedRange);
this.value = next;
showStatus.call(this, this.getOption("labels.cutDone") || "Cut");
}
async function pasteFromClipboard() {
const text = await readClipboardText();
if (text === null) return;
const data = normalizeValue.call(this);
const range = getSelectionRange.call(this, this[selectionSymbol], data);
const startRange = range ?? getRangeFromActiveCell.call(this, data);
if (!startRange) return;
const { startRow, startCol } = {
startRow: startRange.rowStart,
startCol: startRange.colStart,
};
const cached = this[lastCopySymbol];
const next =
cached && cached.text === text
? applyPasteCached(data, cached, startRow, startCol)
: applyPasteText(data, text, startRow, startCol);
this.value = next.value;
setSelectionByRange.call(this, next.range, next.value);
showStatus.call(this, this.getOption("labels.pasted") || "Pasted");
}
function buildClipboardText(data, range) {
const rows = [];
for (let r = range.rowStart; r <= range.rowEnd; r += 1) {
const rowId = data.rows[r]?.id;
if (!rowId) continue;
const cols = [];
for (let c = range.colStart; c <= range.colEnd; c += 1) {
const colId = data.columns[c]?.id;
if (!colId) continue;
const value = getCellRawValue(data, rowId, colId);
cols.push(value);
}
rows.push(cols.join("\t"));
}
return rows.join("\n");
}
function buildClipboardMatrix(data, range) {
const rows = [];
for (let r = range.rowStart; r <= range.rowEnd; r += 1) {
const rowId = data.rows[r]?.id;
if (!rowId) continue;
const cols = [];
for (let c = range.colStart; c <= range.colEnd; c += 1) {
const colId = data.columns[c]?.id;
if (!colId) continue;
cols.push(getCellRawValue(data, rowId, colId));
}
rows.push(cols);
}
return rows;
}
function getCellRawValue(data, rowId, colId) {
const formula = data.formulas?.[rowId]?.[colId];
if (isString(formula)) return formula;
const raw = data.cells?.[rowId]?.[colId];
return raw === undefined || raw === null ? "" : String(raw);
}
function clearRange(data, range) {
const next = {
...data,
cells: data.cells ? { ...data.cells } : {},
formulas: data.formulas ? { ...data.formulas } : {},
};
for (let r = range.rowStart; r <= range.rowEnd; r += 1) {
const rowId = data.rows[r]?.id;
if (!rowId) continue;
for (let c = range.colStart; c <= range.colEnd; c += 1) {
const colId = data.columns[c]?.id;
if (!colId) continue;
if (next.cells[rowId]) delete next.cells[rowId][colId];
if (next.formulas[rowId]) delete next.formulas[rowId][colId];
}
}
return next;
}
function getRangeFromActiveCell(data) {
const active = getActiveCell.call(this);
if (!active) return null;
const { rowIndexById, colIndexById } = getIndexMaps(data);
const rowIndex = rowIndexById.get(active.rowId);
const colIndex = colIndexById.get(active.colId);
if (rowIndex === undefined || colIndex === undefined) return null;
return {
rowStart: rowIndex,
rowEnd: rowIndex,
colStart: colIndex,
colEnd: colIndex,
rowIndexById,
colIndexById,
};
}
function applyPasteText(data, text, startRowIndex, startColIndex) {
const rows = normalizeClipboardRows(text);
const next = {
...data,
cells: data.cells ? { ...data.cells } : {},
formulas: data.formulas ? { ...data.formulas } : {},
};
let rowEnd = startRowIndex;
let colEnd = startColIndex;
rows.forEach((rowText, rowOffset) => {
const rowIndex = startRowIndex + rowOffset;
if (rowIndex >= data.rows.length) return;
const rowId = data.rows[rowIndex]?.id;
if (!rowId) return;
const cols = rowText.split("\t");
cols.forEach((cellText, colOffset) => {
const colIndex = startColIndex + colOffset;
if (colIndex >= data.columns.length) return;
const colId = data.columns[colIndex]?.id;
if (!colId) return;
setCellData(next, rowId, colId, cellText);
rowEnd = Math.max(rowEnd, rowIndex);
colEnd = Math.max(colEnd, colIndex);
});
});
return {
value: next,
range: {
rowStart: startRowIndex,
rowEnd,
colStart: startColIndex,
colEnd,
},
};
}
function applyPasteCached(data, cached, startRowIndex, startColIndex) {
const next = {
...data,
cells: data.cells ? { ...data.cells } : {},
formulas: data.formulas ? { ...data.formulas } : {},
};
const deltaRow = startRowIndex - cached.range.rowStart;
const deltaCol = startColIndex - cached.range.colStart;
let rowEnd = startRowIndex;
let colEnd = startColIndex;
cached.values.forEach((rowValues, rowOffset) => {
const rowIndex = startRowIndex + rowOffset;
if (rowIndex >= data.rows.length) return;
const rowId = data.rows[rowIndex]?.id;
if (!rowId) return;
rowValues.forEach((cellValue, colOffset) => {
const colIndex = startColIndex + colOffset;
if (colIndex >= data.columns.length) return;
const colId = data.columns[colIndex]?.id;
if (!colId) return;
let nextValue = cellValue;
if (isFormulaString(cellValue)) {
nextValue = adjustFormulaReferences(cellValue, deltaRow, deltaCol);
}
setCellData(next, rowId, colId, nextValue);
rowEnd = Math.max(rowEnd, rowIndex);
colEnd = Math.max(colEnd, colIndex);
});
});
return {
value: next,
range: {
rowStart: startRowIndex,
rowEnd,
colStart: startColIndex,
colEnd,
},
};
}
function normalizeClipboardRows(text) {
const normalized = String(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const rows = normalized.split("\n");
while (rows.length > 1 && rows[rows.length - 1] === "") {
rows.pop();
}
return rows;
}
function setCellData(data, rowId, colId, value) {
if (!data.cells) data.cells = {};
if (!data.formulas) data.formulas = {};
if (!data.cells[rowId]) data.cells[rowId] = {};
if (!data.formulas[rowId]) data.formulas[rowId] = {};
const next = String(value ?? "");
if (next.trim().startsWith("=")) {
data.formulas[rowId][colId] = next.trim();
delete data.cells[rowId][colId];
} else {
delete data.formulas[rowId][colId];
data.cells[rowId][colId] = next;
}
}
function isFormulaString(value) {
return isString(value) && value.trim().startsWith("=");
}
function adjustFormulaReferences(formula, deltaRow, deltaCol) {
const expr = formula.trim();
if (!expr.startsWith("=")) return formula;
const body = expr.slice(1);
const adjusted = body.replace(/([A-Za-z]+)([0-9]+)/g, (match, col, row) => {
const colIndex = columnLabelToIndex(col);
if (colIndex === null) return match;
const rowIndex = Number(row) - 1;
if (!Number.isFinite(rowIndex)) return match;
const nextCol = colIndex + deltaCol;
const nextRow = rowIndex + deltaRow;
if (nextCol < 0 || nextRow < 0) return match;
return `${indexToColumnLabel(nextCol)}${nextRow + 1}`;
});
return `=${adjusted}`;
}
function columnLabelToIndex(label) {
const text = String(label || "").toUpperCase();
if (!/^[A-Z]+$/.test(text)) return null;
let index = 0;
for (let i = 0; i < text.length; i += 1) {
index = index * 26 + (text.charCodeAt(i) - 64);
}
return index - 1;
}
function indexToColumnLabel(index) {
if (!Number.isFinite(index) || index < 0) return "A";
return nextColumnLabel(index);
}
function setSelectionByRange(range, data) {
if (!range) return;
const rows = data.rows;
const cols = data.columns;
if (
range.rowStart < 0 ||
range.colStart < 0 ||
range.rowEnd >= rows.length ||
range.colEnd >= cols.length
) {
return;
}
this[selectionSymbol] = {
anchor: {
rowId: rows[range.rowStart].id,
colId: cols[range.colStart].id,
},
focus: {
rowId: rows[range.rowEnd].id,
colId: cols[range.colEnd].id,
},
};
updateSelectionDisplay.call(this);
}
async function writeClipboardText(text) {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch (e) {
return false;
}
return false;
}
async function readClipboardText() {
try {
if (navigator.clipboard?.readText) {
return await navigator.clipboard.readText();
}
} catch (e) {
return null;
}
return null;
}
function showStatus(message) {
const status = this[statusSymbol];
if (!status) return;
status.textContent = message;
status.dataset.show = "true";
if (this[statusTimeoutSymbol]) {
clearTimeout(this[statusTimeoutSymbol]);
}
this[statusTimeoutSymbol] = setTimeout(() => {
status.dataset.show = "false";
}, 1200);
}
function syncContextMenuLabels() {
const menu = this[contextMenuSymbol];
if (!menu) return;
const labels = this.getOption("labels", {});
const copy = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-copy]`);
const paste = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-paste]`);
const cut = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-cut]`);
if (copy && isString(labels.copy)) copy.textContent = labels.copy;
if (paste && isString(labels.paste)) paste.textContent = labels.paste;
if (cut && isString(labels.cut)) cut.textContent = labels.cut;
}
function wireContextMenuActions() {
const menu = this[contextMenuSymbol];
if (!menu) return;
const copy = menu.querySelector(`[${ATT