/* ============================================================================= * spreadsheet_grid_heads.ts * 격자 머리 — 행 · 열 머리 칸(보이는 것만 · 폭 · 높이 끌기) · 숨긴 자리 표시 · 묶음 윤곽 +/- 단추. * `spreadsheet_grid.ts` 가 판을 그릴 때 부름 — 머리 칸 모양 · 끌기는 `spreadsheet_headers.ts`. * ========================================================================== */ import { colName } from "./spreadsheet_address"; import { commitColWidth, commitRowHeight, isExplicitHidden, makeColHeaderCell, makeRowHeaderCell, startDragResize, type OutlineRun, } from "./spreadsheet_headers"; import { poolSweep, type CellPool, type GridView } from "./spreadsheet_grid_cells"; import type { Sheet } from "./spreadsheet_types"; import type { SpreadsheetContext } from "./spreadsheet_view_types"; export const HEAD_ROW_H_BASE = 24; export const HEAD_COL_W_BASE = 46; /** 머리 안쪽 층 넷 — 틀고정 쪽 · 스크롤 쪽. */ export interface HeadLayers { colFrozen: HTMLElement; colScroll: HTMLElement; rowFrozen: HTMLElement; rowScroll: HTMLElement; } export function createHeads(ctx: SpreadsheetContext, view: GridView, layers: HeadLayers) { const { colFrozen: colHeadInnerFrozen, colScroll: colHeadInnerScroll, rowFrozen: rowHeadInnerFrozen, rowScroll: rowHeadInnerScroll, } = layers; /** 건너뛴 자리(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(); const top = view.colGutterH(); const height = Math.round(HEAD_ROW_H_BASE * view.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 && colHiddenGap(sheet, prev, c); let cellEl = pool.used.get(key); if (!cellEl) { const built = makeColHeaderCell(c, 0, 0, hiddenBefore); cellEl = built.el; pool.used.set(key, cellEl); container.append(cellEl); const handle = cellEl.querySelector(".aislo-grid__resize--col")!; startDragResize({ handle, axis: "col", currentPx: () => view.colGeo.size(c), onPreview: (px) => { cellEl!.style.width = `${px}px`; }, onCommit: (px) => commitColWidth(ctx, c, px), }); } cellEl.style.left = `${view.colGeo.offset(c) - base}px`; cellEl.style.top = `${top}px`; cellEl.style.width = `${view.colGeo.size(c)}px`; cellEl.style.height = `${height}px`; cellEl.classList.toggle("aislo-grid__colhead--hidden-before", hiddenBefore); }); poolSweep(pool, keep); } function layoutRowHeaders( rows: number[], pool: CellPool, container: HTMLElement, base: number, ): void { const sheet = ctx.sheet(); const keep = new Set(); const left = view.rowGutterW(); const width = Math.round(HEAD_COL_W_BASE * view.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 && rowHiddenGap(sheet, prev, r); let cellEl = pool.used.get(key); if (!cellEl) { const built = makeRowHeaderCell(r, 0, 0, hiddenBefore); cellEl = built.el; pool.used.set(key, cellEl); container.append(cellEl); const handle = cellEl.querySelector(".aislo-grid__resize--row")!; startDragResize({ handle, axis: "row", currentPx: () => view.rowGeo.size(r), onPreview: (px) => { cellEl!.style.height = `${px}px`; }, onCommit: (px) => commitRowHeight(ctx, r, px), }); } cellEl.style.top = `${view.rowGeo.offset(r) - base}px`; cellEl.style.left = `${left}px`; cellEl.style.height = `${view.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 (!view.rowOutlineRuns.length) return; const gw = view.rowGutterW(); for (const run of view.rowOutlineRuns) { const btnRow = Math.min(run.to + 1, view.extent.rows - 1); const container = btnRow < view.frozenRows ? rowHeadInnerFrozen : rowHeadInnerScroll; const base = btnRow < view.frozenRows ? 0 : view.rowGeo.offset(view.frozenRows); const btn = document.createElement("button"); btn.type = "button"; btn.className = "aislo-grid__outline-btn"; btn.textContent = run.collapsed ? "+" : "−"; btn.style.top = `${view.rowGeo.offset(btnRow) - base}px`; btn.style.left = "0"; btn.style.width = `${gw}px`; btn.style.height = `${view.rowGeo.size(btnRow) || Math.round(HEAD_ROW_H_BASE * view.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 (!view.colOutlineRuns.length) return; const gh = view.colGutterH(); for (const run of view.colOutlineRuns) { const btnCol = Math.min(run.to + 1, view.extent.cols - 1); const container = btnCol < view.frozenCols ? colHeadInnerFrozen : colHeadInnerScroll; const base = btnCol < view.frozenCols ? 0 : view.colGeo.offset(view.frozenCols); const btn = document.createElement("button"); btn.type = "button"; btn.className = "aislo-grid__outline-btn"; btn.textContent = run.collapsed ? "+" : "−"; btn.style.left = `${view.colGeo.offset(btnCol) - base}px`; btn.style.top = "0"; btn.style.width = `${view.colGeo.size(btnCol) || Math.round(HEAD_COL_W_BASE * view.zoom)}px`; btn.style.height = `${gh}px`; btn.addEventListener("click", (ev) => { ev.stopPropagation(); outlineToggle("열", run); }); container.append(btn); } } return { layoutColHeaders, layoutRowHeaders, renderRowOutline, renderColOutline }; }