- 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
201 lines
7.6 KiB
TypeScript
201 lines
7.6 KiB
TypeScript
/* =============================================================================
|
||
* 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 };
|
||
}
|