feat(spreadsheet): C 격자 — 행·열 묶어 접기(윤곽 +/- 단추, 계약 더함)
RowInfo·ColumnInfo.묶음{층,접힘} · Command「묶음설정」더함(계약 더함) — 접힘 상태 저장 ·
접으면 숨김처럼 그리되 머리 이중선 표시는 숨김·걸러짐과 갈라 안 보임(따로 표시).
commands.ts 는 기존 setAxis 틀에 「묶음」prop·case 만 최소로 얹음(exhaustive switch 깨짐 막음).
ORCA 시험 틀에서 접기·펴기 dispatch·저장·머리 이동·표시 구분 수치로 확인.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVM3xZ1aUPcUTuW2WZSnwg
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
|
||||
import "./spreadsheet_grid.css";
|
||||
import { toFrac } from "@ui/sheet/ui_template_sheet_frac";
|
||||
import { colIndex, parseA1, parseRange, toA1 } from "./spreadsheet_address";
|
||||
import { colIndex, colName, parseA1, parseRange, toA1 } from "./spreadsheet_address";
|
||||
import {
|
||||
applyCellStyle,
|
||||
cellStyleAt,
|
||||
@@ -22,14 +22,17 @@ import {
|
||||
} from "./spreadsheet_cellstyle";
|
||||
import {
|
||||
buildColGeometry,
|
||||
buildOutlineRuns,
|
||||
buildRowGeometry,
|
||||
commitColWidth,
|
||||
commitRowHeight,
|
||||
isExplicitHidden,
|
||||
makeColHeaderCell,
|
||||
makeRowHeaderCell,
|
||||
startDragResize,
|
||||
visibleIndices,
|
||||
type LineGeometry,
|
||||
type OutlineRun,
|
||||
} from "./spreadsheet_headers";
|
||||
import { formatValue } from "./spreadsheet_numfmt";
|
||||
import { MAX_COLS, MAX_ROWS } from "./spreadsheet_types";
|
||||
@@ -51,6 +54,7 @@ import type {
|
||||
|
||||
const HEAD_ROW_H_BASE = 24;
|
||||
const HEAD_COL_W_BASE = 46;
|
||||
const OUTLINE_GUTTER_BASE = 14;
|
||||
const RESIZE_HOTZONE = 4;
|
||||
const REF_COLORS = ["#1a73e8", "#e8710a", "#0f9d58", "#a142f4", "#d93025"];
|
||||
const ZOOM_MIN = 0.1;
|
||||
@@ -232,8 +236,15 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
let frozenH = 0;
|
||||
let lastSheetId = "";
|
||||
let zoom = 1;
|
||||
const headRowH = (): number => Math.round(HEAD_ROW_H_BASE * zoom);
|
||||
const headColW = (): number => Math.round(HEAD_COL_W_BASE * zoom);
|
||||
let rowOutlineRuns: OutlineRun[] = [];
|
||||
let colOutlineRuns: OutlineRun[] = [];
|
||||
/** 행 묶음 윤곽 띠 — 행 머리 왼쪽(있을 때만) · 열 묶음 띠는 열 머리 위. */
|
||||
const rowGutterW = (): number =>
|
||||
rowOutlineRuns.length ? Math.round(OUTLINE_GUTTER_BASE * zoom) : 0;
|
||||
const colGutterH = (): number =>
|
||||
colOutlineRuns.length ? Math.round(OUTLINE_GUTTER_BASE * zoom) : 0;
|
||||
const headRowH = (): number => Math.round(HEAD_ROW_H_BASE * zoom) + colGutterH();
|
||||
const headColW = (): number => Math.round(HEAD_COL_W_BASE * zoom) + rowGutterW();
|
||||
|
||||
const resize = new ResizeObserver(() => layoutPanes());
|
||||
resize.observe(root);
|
||||
@@ -381,18 +392,33 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
poolSweep(pool, keep);
|
||||
}
|
||||
|
||||
/** 건너뛴 자리(prev+1..cur-1) 안에 사용자 숨김 · 필터 걸러짐이 있나(묶음 접힘은 빼고 — 화면 표시가 다름). */
|
||||
function colHiddenGap(sheet: Sheet, prev: number, cur: number): boolean {
|
||||
for (let i = prev + 1; i < cur; i += 1)
|
||||
if (isExplicitHidden(sheet.열?.[colName(i)])) return true;
|
||||
return false;
|
||||
}
|
||||
function rowHiddenGap(sheet: Sheet, prev: number, cur: number): boolean {
|
||||
for (let i = prev + 1; i < cur; i += 1)
|
||||
if (isExplicitHidden(sheet.행?.[String(i + 1)])) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function layoutColHeaders(
|
||||
cols: number[],
|
||||
pool: CellPool,
|
||||
container: HTMLElement,
|
||||
base: number,
|
||||
): void {
|
||||
const sheet = ctx.sheet();
|
||||
const keep = new Set<string>();
|
||||
const top = colGutterH();
|
||||
const height = Math.round(HEAD_ROW_H_BASE * zoom);
|
||||
cols.forEach((c, i) => {
|
||||
const key = String(c);
|
||||
keep.add(key);
|
||||
const prev = i > 0 ? cols[i - 1] : c - 1;
|
||||
const hiddenBefore = c - prev > 1;
|
||||
const hiddenBefore = c - prev > 1 && colHiddenGap(sheet, prev, c);
|
||||
let cellEl = pool.used.get(key);
|
||||
if (!cellEl) {
|
||||
const built = makeColHeaderCell(c, 0, 0, hiddenBefore);
|
||||
@@ -411,7 +437,9 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
});
|
||||
}
|
||||
cellEl.style.left = `${colGeo.offset(c) - base}px`;
|
||||
cellEl.style.top = `${top}px`;
|
||||
cellEl.style.width = `${colGeo.size(c)}px`;
|
||||
cellEl.style.height = `${height}px`;
|
||||
cellEl.classList.toggle("aislo-grid__colhead--hidden-before", hiddenBefore);
|
||||
});
|
||||
poolSweep(pool, keep);
|
||||
@@ -423,12 +451,15 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
container: HTMLElement,
|
||||
base: number,
|
||||
): void {
|
||||
const sheet = ctx.sheet();
|
||||
const keep = new Set<string>();
|
||||
const left = rowGutterW();
|
||||
const width = Math.round(HEAD_COL_W_BASE * zoom);
|
||||
rows.forEach((r, i) => {
|
||||
const key = String(r);
|
||||
keep.add(key);
|
||||
const prev = i > 0 ? rows[i - 1] : r - 1;
|
||||
const hiddenBefore = r - prev > 1;
|
||||
const hiddenBefore = r - prev > 1 && rowHiddenGap(sheet, prev, r);
|
||||
let cellEl = pool.used.get(key);
|
||||
if (!cellEl) {
|
||||
const built = makeRowHeaderCell(r, 0, 0, hiddenBefore);
|
||||
@@ -447,12 +478,79 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
});
|
||||
}
|
||||
cellEl.style.top = `${rowGeo.offset(r) - base}px`;
|
||||
cellEl.style.left = `${left}px`;
|
||||
cellEl.style.height = `${rowGeo.size(r)}px`;
|
||||
cellEl.style.width = `${width}px`;
|
||||
cellEl.classList.toggle("aislo-grid__rowhead--hidden-before", hiddenBefore);
|
||||
});
|
||||
poolSweep(pool, keep);
|
||||
}
|
||||
|
||||
/** 묶음 접힘 뒤집기(같은 묶음 자리 모두) → `묶음설정` dispatch. */
|
||||
function outlineToggle(axis: "행" | "열", run: OutlineRun): void {
|
||||
const idx: number[] = [];
|
||||
for (let i = run.from; i <= run.to; i += 1) idx.push(i);
|
||||
ctx.dispatch({
|
||||
종류: "묶음설정",
|
||||
시트: ctx.sheet().id,
|
||||
축: axis,
|
||||
번호: idx,
|
||||
묶음: { 층: 1, 접힘: !run.collapsed },
|
||||
});
|
||||
}
|
||||
|
||||
/** 윤곽 +/- 단추 — 행 묶음 마지막 다음 자리(머리 왼쪽 띠) · 열 묶음도 같은 이치(머리 위 띠).
|
||||
* 여러 층(중첩)은 1단계 다음(지금은 늘 층 1). */
|
||||
function renderRowOutline(): void {
|
||||
rowHeadInnerFrozen.querySelectorAll(".aislo-grid__outline-btn").forEach((n) => n.remove());
|
||||
rowHeadInnerScroll.querySelectorAll(".aislo-grid__outline-btn").forEach((n) => n.remove());
|
||||
if (!rowOutlineRuns.length) return;
|
||||
const gw = rowGutterW();
|
||||
for (const run of rowOutlineRuns) {
|
||||
const btnRow = Math.min(run.to + 1, extent.rows - 1);
|
||||
const container = btnRow < frozenRows ? rowHeadInnerFrozen : rowHeadInnerScroll;
|
||||
const base = btnRow < frozenRows ? 0 : rowGeo.offset(frozenRows);
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "aislo-grid__outline-btn";
|
||||
btn.textContent = run.collapsed ? "+" : "−";
|
||||
btn.style.top = `${rowGeo.offset(btnRow) - base}px`;
|
||||
btn.style.left = "0";
|
||||
btn.style.width = `${gw}px`;
|
||||
btn.style.height = `${rowGeo.size(btnRow) || Math.round(HEAD_ROW_H_BASE * zoom)}px`;
|
||||
btn.addEventListener("click", (ev) => {
|
||||
ev.stopPropagation();
|
||||
outlineToggle("행", run);
|
||||
});
|
||||
container.append(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function renderColOutline(): void {
|
||||
colHeadInnerFrozen.querySelectorAll(".aislo-grid__outline-btn").forEach((n) => n.remove());
|
||||
colHeadInnerScroll.querySelectorAll(".aislo-grid__outline-btn").forEach((n) => n.remove());
|
||||
if (!colOutlineRuns.length) return;
|
||||
const gh = colGutterH();
|
||||
for (const run of colOutlineRuns) {
|
||||
const btnCol = Math.min(run.to + 1, extent.cols - 1);
|
||||
const container = btnCol < frozenCols ? colHeadInnerFrozen : colHeadInnerScroll;
|
||||
const base = btnCol < frozenCols ? 0 : colGeo.offset(frozenCols);
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "aislo-grid__outline-btn";
|
||||
btn.textContent = run.collapsed ? "+" : "−";
|
||||
btn.style.left = `${colGeo.offset(btnCol) - base}px`;
|
||||
btn.style.top = "0";
|
||||
btn.style.width = `${colGeo.size(btnCol) || Math.round(HEAD_COL_W_BASE * zoom)}px`;
|
||||
btn.style.height = `${gh}px`;
|
||||
btn.addEventListener("click", (ev) => {
|
||||
ev.stopPropagation();
|
||||
outlineToggle("열", run);
|
||||
});
|
||||
container.append(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function renderVisible(): void {
|
||||
root.classList.toggle("aislo-grid--gridlines", gridlines());
|
||||
const w = root.clientWidth;
|
||||
@@ -490,6 +588,8 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
rowHeadInnerScroll,
|
||||
rowGeo.offset(frozenRows),
|
||||
);
|
||||
renderRowOutline();
|
||||
renderColOutline();
|
||||
|
||||
renderRowOfCells(frozenRowsVisible, frozenColsVisible, pools.tl, paneTLInner, 0, 0);
|
||||
renderRowOfCells(
|
||||
@@ -528,6 +628,8 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
rowGeo = scaledGeometry(baseRowGeo, zoom);
|
||||
extent = computeExtent(sheet);
|
||||
merges = buildMergeMap(sheet);
|
||||
rowOutlineRuns = buildOutlineRuns(sheet.행, (k) => Number(k) - 1);
|
||||
colOutlineRuns = buildOutlineRuns(sheet.열, colIndex);
|
||||
frozenRows = Math.min(sheet.틀고정?.행 ?? 0, extent.rows);
|
||||
frozenCols = Math.min(sheet.틀고정?.열 ?? 0, extent.cols);
|
||||
spacer.style.width = "1px";
|
||||
|
||||
Reference in New Issue
Block a user