Files
Aislo/A00_Common/spreadsheet/spreadsheet_grid_heads.ts
T
eomsangdonandClaude Opus 5.5 5c0f12a59a refactor(spreadsheet): 격자 1052 줄을 셋으로 나눔 — 칸 그리기 · 머리 · 판(순수 옮기기)
- spreadsheet_grid_cells.ts: 판 하나 칸 그리기 · 값 글 · #### · 긴 글 넘침 붙이기 · 칸 재사용 ·
  자리 재기 도우미
- spreadsheet_grid_heads.ts: 행열 머리 칸 · 숨김 표시 · 묶음 윤곽 단추 · 폭 · 높이 끌기 잇기
- spreadsheet_grid.ts: 틀고정 4 판 · 스크롤 · 확대 · 고름 · 참조 테두리 · 채우기 손잡이 · 누른 자리
  (649 줄) — 자리 재기 · 병합 · 확대는 읽기 전용 `GridView` 로 건넴(끌기 중에도 지금 값)
- 동작 · 겉모양 그대로 · 화면 조각 gzip 33.4 → 33.8 KB(한도 72)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYN9gMie8e6pXzSxJNb6Xt
2026-09-28 23:29:24 +09:00

201 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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<string>();
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<HTMLElement>(".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<string>();
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<HTMLElement>(".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 };
}