Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVM3xZ1aUPcUTuW2WZSnwg
180 lines
7.1 KiB
TypeScript
180 lines
7.1 KiB
TypeScript
/* =============================================================================
|
|
* spreadsheet_cellstyle.ts (주인 C)
|
|
* 서식 → CSS — 글꼴 · 정렬(균등 분할 · 선택 영역 가운데 · 세로 · 줄 바꿈) · 채움색 ·
|
|
* 테두리(hair · thin · medium · thick · double · dotted · dashed · 색) · 겹친 테두리 고르기.
|
|
* 칸 하나가 위 · 왼 이웃과 테두리를 나눠 가짐(표 `border-collapse` 방식) — 겹쳐 그리지 않게
|
|
* 위 · 왼만 두 이웃 중 굵은 쪽으로 그리고 오른 · 아래는 시트 끝 칸만 그림(`resolveEdgeBorders`).
|
|
* 0 계약 머리 — 몸은 C 가 채움. 계약은 `spreadsheet_types.ts`.
|
|
* ========================================================================== */
|
|
|
|
import { colName, toA1 } from "./spreadsheet_address";
|
|
import type {
|
|
BorderLine,
|
|
BorderSide,
|
|
CellStyle,
|
|
HAlign,
|
|
Sheet,
|
|
VAlign,
|
|
Workbook,
|
|
} from "./spreadsheet_types";
|
|
|
|
/** 선 굵기 차례 — 겹치면 굵은 쪽이 이김(엑셀과 완전히 같지 않음 · 근사). */
|
|
const BORDER_WEIGHT: Record<BorderLine, number> = {
|
|
hair: 0,
|
|
dotted: 1,
|
|
dashed: 2,
|
|
thin: 3,
|
|
double: 4,
|
|
medium: 5,
|
|
thick: 6,
|
|
};
|
|
|
|
/** 두 이웃 칸이 한 자리에 그리려는 테두리 중 굵은 쪽. 없으면 있는 쪽 · 둘 다 없으면 undefined. */
|
|
export function strongerBorder(a?: BorderSide, b?: BorderSide): BorderSide | undefined {
|
|
if (!a) return b;
|
|
if (!b) return a;
|
|
return BORDER_WEIGHT[b.선] > BORDER_WEIGHT[a.선] ? b : a;
|
|
}
|
|
|
|
/** 칸 하나가 실제로 그릴 네 변 — 위 · 왼은 이웃과 겹쳐 고름 · 오른 · 아래는 시트 끝(그 다음 칸이 없음)일 때만. */
|
|
export interface ResolvedBorders {
|
|
위?: BorderSide;
|
|
아래?: BorderSide;
|
|
왼?: BorderSide;
|
|
오른?: BorderSide;
|
|
}
|
|
|
|
/** `서식` 표 번호 — 칸 → 행 → 열 → 시트 `기본.서식` → 0(기본) 차례(칸 안의 계약 주석과 같음). */
|
|
export function styleIndexAt(sheet: Sheet, r: number, c: number, key: string): number {
|
|
const cell = sheet.칸[key];
|
|
if (cell?.서식 !== undefined) return cell.서식;
|
|
const rowFmt = sheet.행?.[String(r + 1)]?.서식;
|
|
if (rowFmt !== undefined) return rowFmt;
|
|
const colFmt = sheet.열?.[colName(c)]?.서식;
|
|
if (colFmt !== undefined) return colFmt;
|
|
if (sheet.기본?.서식 !== undefined) return sheet.기본.서식;
|
|
return 0;
|
|
}
|
|
|
|
/** (r, c) 칸이 쓸 서식 — 서식 표 밖 번호는 기본(빈 `{}`)으로. */
|
|
export function cellStyleAt(book: Workbook, sheet: Sheet, r: number, c: number): CellStyle {
|
|
const key = toA1(r, c);
|
|
const idx = styleIndexAt(sheet, r, c, key);
|
|
return book.서식[idx] ?? {};
|
|
}
|
|
|
|
/** 칸 하나 · 병합 칸 한 덩이(box)의 테두리 — 네 변마다 안쪽 이웃과 겹쳐 굵은 쪽으로 고름.
|
|
* 병합이면 왼위(r0,c0) · 오른아래(r1,c1) 이 다른 칸이라 네 변을 따로 봄(테두리는 칸마다 있음). */
|
|
export function resolveBoxBorders(
|
|
book: Workbook,
|
|
sheet: Sheet,
|
|
r0: number,
|
|
c0: number,
|
|
r1: number,
|
|
c1: number,
|
|
lastRow: number,
|
|
lastCol: number,
|
|
): ResolvedBorders {
|
|
const top = cellStyleAt(book, sheet, r0, c0).테두리?.위;
|
|
const aboveOf = r0 > 0 ? cellStyleAt(book, sheet, r0 - 1, c0).테두리?.아래 : undefined;
|
|
const left = cellStyleAt(book, sheet, r0, c0).테두리?.왼;
|
|
const leftOf = c0 > 0 ? cellStyleAt(book, sheet, r0, c0 - 1).테두리?.오른 : undefined;
|
|
const bottomOwn = cellStyleAt(book, sheet, r1, c0).테두리?.아래;
|
|
const belowOf = r1 < lastRow ? cellStyleAt(book, sheet, r1 + 1, c0).테두리?.위 : undefined;
|
|
const rightOwn = cellStyleAt(book, sheet, r0, c1).테두리?.오른;
|
|
const rightOf = c1 < lastCol ? cellStyleAt(book, sheet, r0, c1 + 1).테두리?.왼 : undefined;
|
|
return {
|
|
위: strongerBorder(top, aboveOf),
|
|
왼: strongerBorder(left, leftOf),
|
|
아래: r1 >= lastRow ? bottomOwn : strongerBorder(bottomOwn, belowOf),
|
|
오른: c1 >= lastCol ? rightOwn : strongerBorder(rightOwn, rightOf),
|
|
};
|
|
}
|
|
|
|
/** 빈 글 = 인라인 테두리를 안 그림(css `aislo-grid--gridlines` 눈금선이 비쳐 보임). `scale` = 확대 · 축소 배율(선 굵기도 같이). */
|
|
function borderCss(side: BorderSide | undefined, scale: number): string {
|
|
if (!side) return "";
|
|
const color = side.색 ?? "#000000";
|
|
const w = (px: number): number => Math.max(1, Math.round(px * scale));
|
|
switch (side.선) {
|
|
case "hair":
|
|
return `${w(1)}px dotted ${color}`;
|
|
case "thin":
|
|
return `${w(1)}px solid ${color}`;
|
|
case "medium":
|
|
return `${w(2)}px solid ${color}`;
|
|
case "thick":
|
|
return `${w(3)}px solid ${color}`;
|
|
case "double":
|
|
return `${w(3)}px double ${color}`;
|
|
case "dotted":
|
|
return `${w(1)}px dotted ${color}`;
|
|
case "dashed":
|
|
return `${w(1)}px dashed ${color}`;
|
|
}
|
|
}
|
|
|
|
/** 가로 정렬 → `text-align`(균등 분할 · 선택 영역 가운데는 근사: `justify` · `center`). */
|
|
const HALIGN_CSS: Record<HAlign, string> = {
|
|
general: "left",
|
|
left: "left",
|
|
right: "right",
|
|
center: "center",
|
|
distributed: "justify",
|
|
centerContinuous: "center",
|
|
};
|
|
|
|
const VALIGN_CSS: Record<VAlign, string> = {
|
|
top: "flex-start",
|
|
center: "center",
|
|
bottom: "flex-end",
|
|
};
|
|
|
|
/** 칸 하나에 서식 · 겹쳐 고른 테두리를 입힘. `hAlign` 은 부른 쪽이 정함(일반 정렬은 값 종류로 갈림 — B `formatValue` 의 `정렬`).
|
|
* `scale` = 확대 · 축소 배율(칸 글자 크기 · 테두리 굵기가 같이 커짐 · 기본 1). */
|
|
export function applyCellStyle(
|
|
cell: HTMLElement,
|
|
style: CellStyle,
|
|
borders: ResolvedBorders,
|
|
hAlign: HAlign,
|
|
scale = 1,
|
|
): void {
|
|
const s = cell.style;
|
|
s.fontFamily = style.글꼴 ?? "";
|
|
s.fontSize = style.크기 ? `${style.크기 * scale}pt` : "";
|
|
s.fontWeight = style.굵게 ? "700" : "400";
|
|
s.fontStyle = style.기울임 ? "italic" : "normal";
|
|
s.textDecorationLine = [style.밑줄 && "underline", style.취소선 && "line-through"]
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
s.color = style.글자색 ?? "";
|
|
s.backgroundColor = style.채움 ?? "";
|
|
s.borderTop = borderCss(borders.위, scale);
|
|
s.borderLeft = borderCss(borders.왼, scale);
|
|
s.borderBottom = borderCss(borders.아래, scale);
|
|
s.borderRight = borderCss(borders.오른, scale);
|
|
s.justifyContent = VALIGN_CSS[style.세로 ?? "bottom"];
|
|
s.whiteSpace = style.줄바꿈 ? "normal" : "nowrap";
|
|
|
|
let text = cell.firstElementChild as HTMLElement | null;
|
|
if (!text || !text.classList.contains("aislo-cell__text")) {
|
|
text = document.createElement("div");
|
|
text.className = "aislo-cell__text";
|
|
cell.replaceChildren(text);
|
|
}
|
|
text.style.textAlign = HALIGN_CSS[hAlign];
|
|
text.style.textAlignLast = hAlign === "distributed" ? "justify" : "";
|
|
}
|
|
|
|
/** 칸 글 — 서식 CSS 와 따로 두어 `invalidate()` 가 값만 바꿀 때 서식을 다시 잴 필요가 없게. */
|
|
export function setCellText(cell: HTMLElement, text: string, title?: string): void {
|
|
const node = cell.firstElementChild as HTMLElement | null;
|
|
if (node && node.classList.contains("aislo-cell__text")) {
|
|
node.textContent = text;
|
|
} else {
|
|
cell.textContent = text;
|
|
}
|
|
if (title) cell.title = title;
|
|
else cell.removeAttribute("title");
|
|
}
|