Files
Aislo/A00_Common/spreadsheet/spreadsheet_toolbar.ts
T
eomsangdonandClaude Opus 5.5 7389335ca9 fix(spreadsheet): 16장 도구 막대 · 메뉴 · 대화 창 전수 점검 — 흠 스무 곳 고침
M02 면적산출 SA01010100 에서 도구 막대 · 데이터 메뉴 · 우클릭 · 셀 서식 여섯 탭 · 찾기 · 메모 ·
이름 관리 · 조건부 서식 · 유효성 · 이동 · 틀 고정 · 칸 넣기/지우기 · 데이터 도구 · 윤곽 ·
수식 분석 · 되돌리기/다시 · 저장 안 하고 떠나기 · 다시 열기를 빈 값 · 같은 이름 · 긴 이름 ·
취소 · Esc · Enter · 밝은/어두운 테마 예외까지 돌려 찾은 흠.
- 격자(C): 틀고정이 있으면 본문 판이 그릴 행열을 틀고정 폭만큼 덜 셈 — 아래 · 오른쪽 끝이
  빈 칸으로 남던 것(29 행 고정 때 1 행만 그림) · visibleRange 도 같음 · reveal 은 바로 그림
- 도구 막대: 세로 정렬 기본 표시 「위」 → 「아래」(칸 실제와 맞춤) · 크기 409 넘으면 409 ·
  테두리 · 단위 판이 단추 위로 떠 머리까지 덮던 것 → 단추 아래 · ✕ · 수식 안내 · 확대 글
  대비(흐림 투명도 없앰 · --color-text-secondary · 밝은 5.88 · 어두운 7.91)
- 병합(도구 막대 · 우클릭): 왼위 밖 값이 말없이 지워지던 것 → 엑셀처럼 먼저 물음(confirmMerge)
- 셀 서식 · 이동 · 칸 넣기/지우기 · 텍스트 나누기 · 중복 제거 · 연속 채우기 · 메모 창: 열 때
  초점이 창 밖(BODY)이라 Esc · Enter 가 안 먹던 것 → dialogKeys(창 안 초점 · Esc 닫기 ·
  Enter 실행) · 셀 서식 창 탭마다 높이가 널뛰던 것 → 본문 최소 높이
- 우클릭 메뉴(ui_template 공용): Esc 로 닫힘 · 화면 밖이면 안쪽으로 당김 · 데이터 ▾ 메뉴와 알약
  우클릭 메뉴가 화면 좌표를 부품 기준 자리로 써 오른쪽 아래로 떨어지던 것 고침 · 행 · 열 머리
  메뉴에 잘라내기 · 붙여넣기(안내) 더함
- 자동 필터 ▼ 가 칸 밑에 깔려 눌리지 않던 것(z-index) · 값 목록이 격자 아래 끝에서 잘리면
  위로 뒤집음
- Σ: 여러 칸을 고르면 첫 칸에 =SUM() 편집을 열어 Enter 로 값을 덮던 것 → 엑셀처럼 열마다
  바로 아래(또는 고름 끝 빈 칸)에 합계를 바로 넣음(autoSumCells)
- 조건부 서식: 빈 값이 「보다큼 0」 규칙이 되던 것 · #RRGGBB 아닌 색 · 색 없는 규칙 → 안내 후 막음
- 이름 관리: 255 자 넘는 이름 막음 · 긴 이름에 창이 화면 끝까지 늘던 것 · 시트 id(s1) 대신
  시트 이름 · 「이름 바꾸기」 가 브라우저 prompt(자동 취소됨)라 안 되던 것 → 줄 안에서 고침
- 표시 형식 「@」 를 수에 걸면 빈 칸이 되던 것 → 엑셀처럼 일반으로 보임
- 연속 데이터 채우기 · 텍스트 나누기가 칸 서식을 지우던 것 → 서식 유지
- 틀 고정 「지금 칸 기준」 을 먼 칸(Z1000)에서 하면 고정 판이 격자를 다 덮어 못 움직이던 것 →
  화면 안쪽 칸에서만(안내)

ORCA(IP http://100.72.32.27:5181 · 메인 폴더 탭 · 판정은 수치): 목록 80 항목 밝은 80/80 ·
어두운 80/80 · 사각형 겹침 · 잘림 · 화면 밖 · 대비 3 미만 0 건. 시험 — test_stage3_autosum
(여러 칸 넷 더함) · test_stage2_data_tools · test_stage2_format_code · spreadsheet_b_test_entry
197/197(@ 하나 더함) · test_spreadsheet_size(css gzip 3121 · 합 67403) 통과. 시험으로 바꾼 칸은
저장 안 하고 「다시 열기」 로 버림.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W9YEkMnYQnNnRZRPXoxFsw
2026-09-28 22:57:48 +09:00

536 lines
21 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_toolbar.ts (주인 E)
* 서식 도구 모음 — 되돌리기 · 다시 · 글꼴 · 크기 · 굵게 · 기울임 · 밑줄 · 취소선 ·
* 글자색 · 채움색 · 가로/세로 정렬 · 줄 바꿈 · 병합 · 테두리 · 숫자 형식.
* 모두 `ctx.dispatch` 명령으로 — 이 파일은 문서를 직접 안 건드림.
* 활성 칸 서식을 `refresh()` 때 단추 상태로. readOnly 면 안 붙임(root null).
*
* 이음(D `spreadsheet.ts` 잇는 자리) — `mountToolbar(ctx)` 한 번 부르고 `root`(null 이면 안 붙임)를
* 도구 모음 자리에 붙임. 고름 · 문서가 바뀔 때마다(자기 dispatch 뒤 포함) `refresh()` 를 부름 —
* 스스로 구독하지 않음. `destroy()` 는 부품을 뗄 때.
* ========================================================================== */
import { createButton, el } from "@ui/ui_template_elements";
import { confirmMerge } from "./spreadsheet_actions";
import { colName, rangeToA1, toA1 } from "./spreadsheet_address";
import { cellToText, textToCell } from "./spreadsheet_editor";
import { st } from "./spreadsheet_text";
import type { BorderLine, BorderSide, CellRange, CellStyle, Command } from "./spreadsheet_types";
import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types";
import "./spreadsheet_toolbar.css";
type Side = "위" | "아래" | "왼" | "오른";
type BorderPreset = "none" | "all" | "outer" | "top" | "bottom" | "left" | "right";
/** 앱 기본 글꼴 · 크기 — `spreadsheet_grid.ts`(C) 의 `fontCss` 가 쓰는 기본과 같은 값(거울) ·
* 칸 · 행 · 열 · 시트 기본 어디에도 없을 때 이 값을 흐리게(placeholder) 보임. */
const APP_DEFAULT_FONT = "굴림";
const APP_DEFAULT_SIZE = 10;
const FONTS = [
"굴림",
"굴림체",
"맑은 고딕",
"나눔고딕",
"돋움",
"Arial",
"Calibri",
"Times New Roman",
];
const NUMFMT_PRESETS: [label: string, code: string][] = [
[st("NumberFormat_general"), ""],
[st("NumberFormat_integer"), "0"],
[st("NumberFormat_decimal2"), "0.00"],
[st("NumberFormat_thousands"), "#,##0"],
[st("NumberFormat_thousands2"), "#,##0.00"],
[st("NumberFormat_percent"), "0%"],
[st("NumberFormat_accounting"), '_-* #,##0.00_-;-* #,##0.00_-;_-* "-"_-;_-@_-'],
[st("NumberFormat_text"), "@"],
];
const BORDER_LINES: BorderLine[] = [
"hair",
"thin",
"medium",
"thick",
"double",
"dotted",
"dashed",
];
type TextKey = Parameters<typeof st>[0];
const UNIT_GROUPS: [label: TextKey, items: string[]][] = [
["UnitGroup_area", ["㎡", "㎥", "m", "㎝", "㎜", "㎞"]],
["UnitGroup_weight", ["kg", "t", "ℓ", "㎖"]],
["UnitGroup_count", ["개", "본", "식", "EA", "매"]],
["UnitGroup_rate", ["m/s", "°", "%", "‰"]],
["UnitGroup_op", ["×", "÷", "±", "≤", "≥", "Σ"]],
];
const UNIT_RECENT_MAX = 8;
const UNIT_RECENT_KEY = "aislo-ss-recent-units";
function loadRecentUnits(): string[] {
try {
const raw = localStorage.getItem(UNIT_RECENT_KEY);
return raw ? (JSON.parse(raw) as string[]) : [];
} catch {
return [];
}
}
function pushRecentUnit(unit: string): string[] {
const list = [unit, ...loadRecentUnits().filter((u) => u !== unit)].slice(0, UNIT_RECENT_MAX);
try {
localStorage.setItem(UNIT_RECENT_KEY, JSON.stringify(list));
} catch {
/* 저장 못 해도 이번 넣기는 그대로 진행 */
}
return list;
}
/** 숫자 형식 코드 끝 따옴표 글자(전에 붙인 단위)를 떼고 새 단위를 붙임 — 없으면 "0" 에서 시작. */
function attachUnitToFormat(code: string, unit: string): string {
const base = (code || "0").replace(/"[^"]*"\s*$/, "");
return base + `"${unit}"`;
}
/** 활성 칸에 지금 걸린 서식 — 칸 자기 것 없으면 열 · 행 · 시트 기본(`Sheet.기본.서식`) · 그다음 0. */
function styleAt(ctx: SpreadsheetContext, r: number, c: number): CellStyle {
const sheet = ctx.sheet();
const cell = sheet.칸[toA1(r, c)];
const idx =
cell?.서식 ??
sheet.열?.[colName(c)]?.서식 ??
sheet.행?.[String(r + 1)]?.서식 ??
sheet.기본?.서식 ??
0;
return ctx.book.서식[idx] ?? {};
}
export function mountToolbar(ctx: SpreadsheetContext): PartHandle {
if (ctx.readOnly) return { root: null, refresh() {}, destroy() {} };
const root = el("div", { className: "ss-toolbar" });
const group = (...children: HTMLElement[]): HTMLElement =>
el("div", { className: "ss-toolbar__group", children });
const patch = (change: { [K in keyof CellStyle]?: CellStyle[K] | null }): void => {
ctx.dispatch({
종류: "서식",
시트: ctx.selection.시트,
범위: ctx.selection.범위,
바꿀: change,
});
};
// ── 되돌리기 · 다시 ────────────────────────────────────────────────────
const undoBtn = createButton({ label: "↶", variant: "ghost", onClick: () => ctx.undo() });
undoBtn.title = st("Undo");
const redoBtn = createButton({ label: "↷", variant: "ghost", onClick: () => ctx.redo() });
redoBtn.title = st("Redo");
// ── 글꼴 · 크기 ────────────────────────────────────────────────────────
const fontInput = el("input", {
className: "ss-toolbar__input ss-toolbar__input--font",
attrs: { list: "ss-toolbar-fonts", "aria-label": st("FontName") },
}) as HTMLInputElement;
const fontList = el("datalist", {
attrs: { id: "ss-toolbar-fonts" },
children: FONTS.map((name) => el("option", { attrs: { value: name } })),
});
fontInput.append(fontList);
const commitFont = (): void => patch({ 글꼴: fontInput.value.trim() || null });
fontInput.addEventListener("change", commitFont);
const sizeInput = el("input", {
className: "ss-toolbar__input ss-toolbar__input--size",
attrs: { type: "number", min: "1", max: "409", step: "0.5", "aria-label": st("FontSize") },
}) as HTMLInputElement;
sizeInput.addEventListener("change", () => {
const n = Number(sizeInput.value);
patch({ 크기: sizeInput.value && Number.isFinite(n) && n > 0 ? Math.min(n, 409) : null });
});
// ── 굵게 · 기울임 · 밑줄 · 취소선 ──────────────────────────────────────
const boldBtn = createButton({ label: "B", variant: "ghost" });
boldBtn.title = st("Bold");
const italicBtn = createButton({ label: "I", variant: "ghost" });
italicBtn.title = st("Italic");
const underlineBtn = createButton({ label: "U", variant: "ghost" });
underlineBtn.title = st("Underline");
const strikeBtn = createButton({ label: "S", variant: "ghost" });
strikeBtn.title = st("Strike");
const toggles: [HTMLButtonElement, keyof CellStyle][] = [
[boldBtn, "굵게"],
[italicBtn, "기울임"],
[underlineBtn, "밑줄"],
[strikeBtn, "취소선"],
];
for (const [btn, key] of toggles) {
btn.addEventListener("click", () => patch({ [key]: !btn.classList.contains("is-on") }));
}
// ── 글자색 · 채움색 ────────────────────────────────────────────────────
const textColor = el("input", {
className: "ss-toolbar__color",
attrs: { type: "color", "aria-label": st("TextColor"), value: "#000000" },
}) as HTMLInputElement;
textColor.addEventListener("change", () => patch({ 글자색: textColor.value }));
const textColorClear = el("button", {
className: "ss-toolbar__clear",
text: "✕",
attrs: { type: "button", title: st("ColorClear") },
});
textColorClear.addEventListener("click", () => patch({ 글자색: null }));
const fillColor = el("input", {
className: "ss-toolbar__color",
attrs: { type: "color", "aria-label": st("FillColor"), value: "#ffffff" },
}) as HTMLInputElement;
fillColor.addEventListener("change", () => patch({ 채움: fillColor.value }));
const fillColorClear = el("button", {
className: "ss-toolbar__clear",
text: "✕",
attrs: { type: "button", title: st("ColorClear") },
});
fillColorClear.addEventListener("click", () => patch({ 채움: null }));
// ── 가로 · 세로 정렬 · 줄 바꿈 ─────────────────────────────────────────
const HALIGN_LABEL = {
general: st("Align_general"),
left: st("Align_left"),
center: st("Align_center"),
right: st("Align_right"),
distributed: st("Align_distributed"),
centerContinuous: st("Align_centerContinuous"),
} as const;
const VALIGN_LABEL = {
top: st("VAlign_top"),
center: st("VAlign_center"),
bottom: st("VAlign_bottom"),
} as const;
const hAlignSelect = el("select", {
className: "ss-toolbar__select",
attrs: { "aria-label": st("HAlign") },
children: (Object.keys(HALIGN_LABEL) as (keyof typeof HALIGN_LABEL)[]).map((v) =>
el("option", { attrs: { value: v }, text: HALIGN_LABEL[v] }),
),
}) as HTMLSelectElement;
hAlignSelect.addEventListener("change", () =>
patch({ 가로: hAlignSelect.value as CellStyle["가로"] }),
);
const vAlignSelect = el("select", {
className: "ss-toolbar__select",
attrs: { "aria-label": st("VAlign") },
children: (Object.keys(VALIGN_LABEL) as (keyof typeof VALIGN_LABEL)[]).map((v) =>
el("option", { attrs: { value: v }, text: VALIGN_LABEL[v] }),
),
}) as HTMLSelectElement;
vAlignSelect.addEventListener("change", () =>
patch({ 세로: vAlignSelect.value as CellStyle["세로"] }),
);
const wrapBtn = createButton({ label: st("Wrap"), variant: "ghost" });
wrapBtn.addEventListener("click", () => patch({ 줄바꿈: !wrapBtn.classList.contains("is-on") }));
// ── 병합 ───────────────────────────────────────────────────────────────
const mergeBtn = createButton({ label: st("Merge"), variant: "ghost" });
mergeBtn.addEventListener("click", async () => {
const range = ctx.selection.범위[0];
if (!range) return;
const merged = (ctx.sheet().병합 ?? []).includes(rangeToA1(range));
if (!merged && !(await confirmMerge(ctx, range))) return;
ctx.dispatch({ 종류: merged ? "병합풀기" : "병합", 시트: ctx.selection.시트, 범위: range });
});
// ── 테두리 ───────────────────────────────────────────────────────────
const borderBtn = createButton({ label: st("Border"), variant: "ghost" });
const borderPop = el("div", { className: "ss-toolbar__popover", attrs: { hidden: "true" } });
const lineSelect = el("select", {
className: "ss-toolbar__select",
children: BORDER_LINES.map((v) => el("option", { attrs: { value: v }, text: v })),
}) as HTMLSelectElement;
const lineColor = el("input", {
className: "ss-toolbar__color",
attrs: { type: "color", value: "#000000" },
}) as HTMLInputElement;
const presetsRow = el("div", { className: "ss-toolbar__popover-presets" });
const presets: [BorderPreset, string][] = [
["none", st("Border_none")],
["all", st("Border_all")],
["outer", st("Border_outer")],
["top", st("Border_top")],
["bottom", st("Border_bottom")],
["left", st("Border_left")],
["right", st("Border_right")],
];
for (const [preset, label] of presets) {
const b = createButton({
label,
variant: "ghost",
onClick: () => {
applyBorderPreset(preset);
borderPop.hidden = true;
},
});
presetsRow.append(b);
}
borderPop.append(
el("div", {
className: "ss-toolbar__popover-row",
children: [el("span", { text: st("BorderLine") }), lineSelect],
}),
el("div", {
className: "ss-toolbar__popover-row",
children: [el("span", { text: st("BorderColor") }), lineColor],
}),
presetsRow,
);
borderBtn.addEventListener("click", (ev) => {
ev.stopPropagation();
borderPop.hidden = !borderPop.hidden;
});
borderPop.addEventListener("click", (ev) => ev.stopPropagation());
const closeBorderPop = (): void => {
borderPop.hidden = true;
};
document.addEventListener("click", closeBorderPop);
function cellsFor(
range: CellRange,
edge: BorderPreset,
): { r: number; c: number; sides: Side[] }[] {
const { r0, c0, r1, c1 } = range;
const out: { r: number; c: number; sides: Side[] }[] = [];
for (let r = r0; r <= r1; r++) {
for (let c = c0; c <= c1; c++) {
const sides: Side[] = [];
if (edge === "all") sides.push("위", "아래", "왼", "오른");
else {
if ((edge === "outer" || edge === "top") && r === r0) sides.push("위");
if ((edge === "outer" || edge === "bottom") && r === r1) sides.push("아래");
if ((edge === "outer" || edge === "left") && c === c0) sides.push("왼");
if ((edge === "outer" || edge === "right") && c === c1) sides.push("오른");
}
if (sides.length) out.push({ r, c, sides });
}
}
return out;
}
function applyBorderPreset(preset: BorderPreset): void {
const range = ctx.selection.범위[0];
if (!range) return;
if (preset === "none") {
ctx.dispatch({
종류: "서식",
시트: ctx.selection.시트,
범위: ctx.selection.범위,
바꿀: { 테두리: null },
});
return;
}
const style: BorderSide = { 선: lineSelect.value as BorderLine, 색: lineColor.value };
const cmds: Command[] = cellsFor(range, preset).map(({ r, c, sides }) => {
const cur = { ...(styleAt(ctx, r, c).테두리 ?? {}) };
for (const s of sides) cur[s] = style;
return {
종류: "서식",
시트: ctx.selection.시트,
범위: [{ r0: r, c0: c, r1: r, c1: c }],
바꿀: { 테두리: cur },
};
});
if (cmds.length === 1) ctx.dispatch(cmds[0]);
else if (cmds.length > 1) ctx.dispatch({ 종류: "묶음", 명령: cmds });
}
// ── 숫자 형식 ─────────────────────────────────────────────────────────
const numfmtInput = el("input", {
className: "ss-toolbar__input ss-toolbar__input--numfmt",
attrs: {
list: "ss-toolbar-numfmt",
"aria-label": st("NumberFormat"),
title: st("NumberFormat_hint"),
placeholder: st("NumberFormat_placeholder"),
},
}) as HTMLInputElement;
const numfmtList = el("datalist", {
attrs: { id: "ss-toolbar-numfmt" },
children: NUMFMT_PRESETS.map(([label, code]) =>
el("option", {
attrs: { value: code },
text: `${label} (${code || st("NumberFormat_general")})`,
}),
),
});
numfmtInput.append(numfmtList);
numfmtInput.addEventListener("change", () => patch({ 형식: numfmtInput.value || null }));
// ── 단위 기호 ─────────────────────────────────────────────────────────
const unitBtn = createButton({ label: `${st("Unit")} ▾`, variant: "ghost" });
const unitPop = el("div", {
className: "ss-toolbar__popover ss-toolbar__unit-pop",
attrs: { hidden: "true" },
});
const unitAsFormat = el("input", { attrs: { type: "checkbox", id: "ss-unit-as-format" } });
const unitAsFormatRow = el("label", {
className: "ss-toolbar__popover-row",
attrs: { for: "ss-unit-as-format" },
children: [unitAsFormat, el("span", { text: st("UnitAsFormat") })],
});
const recentGrid = el("div", { className: "ss-toolbar__unit-grid" });
const recentSection = el("div", {
className: "ss-toolbar__unit-section",
attrs: { hidden: "true" },
children: [
el("div", { className: "ss-toolbar__unit-label", text: st("UnitRecent") }),
recentGrid,
],
});
function insertUnit(unit: string): void {
if (ctx.editor.editing()) {
const active = document.activeElement;
if (active instanceof HTMLTextAreaElement && ctx.root.contains(active)) {
const s = active.selectionStart ?? active.value.length;
const e = active.selectionEnd ?? active.value.length;
active.value = active.value.slice(0, s) + unit + active.value.slice(e);
active.setSelectionRange(s + unit.length, s + unit.length);
active.dispatchEvent(new Event("input", { bubbles: true }));
}
} else if (unitAsFormat.checked) {
const { r, c } = ctx.selection.활성;
patch({ 형식: attachUnitToFormat(styleAt(ctx, r, c).형식 ?? "", unit) });
} else {
const { r, c } = ctx.selection.활성;
const sheet = ctx.sheet();
const key = toA1(r, c);
const text = cellToText(sheet.칸[key]) + unit;
ctx.dispatch({ 종류: "칸", 시트: sheet.id, 칸: { [key]: textToCell(text, sheet.칸[key]) } });
}
renderRecent(pushRecentUnit(unit));
unitPop.hidden = true;
}
function renderRecent(list: string[]): void {
recentGrid.textContent = "";
recentSection.hidden = list.length === 0;
for (const u of list) {
const b = createButton({ label: u, variant: "ghost", onClick: () => insertUnit(u) });
b.className += " ss-toolbar__unit-btn";
recentGrid.append(b);
}
}
renderRecent(loadRecentUnits());
unitPop.append(unitAsFormatRow, recentSection);
for (const [labelKey, items] of UNIT_GROUPS) {
const grid = el("div", { className: "ss-toolbar__unit-grid" });
for (const u of items) {
const b = createButton({ label: u, variant: "ghost", onClick: () => insertUnit(u) });
b.className += " ss-toolbar__unit-btn";
grid.append(b);
}
unitPop.append(
el("div", {
className: "ss-toolbar__unit-section",
children: [el("div", { className: "ss-toolbar__unit-label", text: st(labelKey) }), grid],
}),
);
}
unitBtn.addEventListener("click", (ev) => {
ev.stopPropagation();
unitPop.hidden = !unitPop.hidden;
});
unitPop.addEventListener("click", (ev) => ev.stopPropagation());
const closeUnitPop = (): void => {
unitPop.hidden = true;
};
document.addEventListener("click", closeUnitPop);
// ── 엑셀 수식 복사 안내(오른쪽 위 흐린 글씨 · 누르면 두세 줄 설명) ─────────
const formulaHintBtn = el("button", {
className: "ss-toolbar__formula-hint",
attrs: { type: "button", title: st("FormulaHint_title") },
text: st("FormulaHint_label"),
});
const formulaHintDetail = el("div", {
className: "ss-toolbar__formula-hint-detail",
attrs: { hidden: "true" },
text: st("FormulaHint_detail"),
});
formulaHintBtn.addEventListener("click", (ev) => {
ev.stopPropagation();
formulaHintDetail.hidden = !formulaHintDetail.hidden;
});
const formulaHint = el("div", {
className: "ss-toolbar__hint",
children: [formulaHintBtn, formulaHintDetail],
});
root.append(
group(undoBtn, redoBtn),
group(fontInput, sizeInput),
group(boldBtn, italicBtn, underlineBtn, strikeBtn),
group(textColor, textColorClear, fillColor, fillColorClear),
group(hAlignSelect, vAlignSelect, wrapBtn),
group(mergeBtn),
group(borderBtn, borderPop),
group(numfmtInput),
group(unitBtn, unitPop),
formulaHint,
);
function refresh(): void {
undoBtn.disabled = !ctx.history.canUndo();
redoBtn.disabled = !ctx.history.canRedo();
const { r, c } = ctx.selection.활성;
const style = styleAt(ctx, r, c);
fontInput.value = style.글꼴 ?? "";
fontInput.placeholder = style.글꼴 ? "" : (ctx.book.기본?.글꼴 ?? APP_DEFAULT_FONT);
sizeInput.value = style.크기 != null ? String(style.크기) : "";
sizeInput.placeholder =
style.크기 != null ? "" : String(ctx.book.기본?.크기 ?? APP_DEFAULT_SIZE);
boldBtn.classList.toggle("is-on", !!style.굵게);
italicBtn.classList.toggle("is-on", !!style.기울임);
underlineBtn.classList.toggle("is-on", !!style.밑줄);
strikeBtn.classList.toggle("is-on", !!style.취소선);
textColor.value = style.글자색 ?? "#000000";
fillColor.value = style.채움 ?? "#ffffff";
hAlignSelect.value = style.가로 ?? "general";
vAlignSelect.value = style.세로 ?? "bottom";
wrapBtn.classList.toggle("is-on", !!style.줄바꿈);
numfmtInput.value = style.형식 ?? "";
const range = ctx.selection.범위[0];
mergeBtn.textContent = "";
mergeBtn.append(
el("span", {
className: "ui-btn__label",
text:
range && (ctx.sheet().병합 ?? []).includes(rangeToA1(range))
? st("Unmerge")
: st("Merge"),
}),
);
}
refresh();
return {
root,
refresh,
destroy() {
document.removeEventListener("click", closeBorderPop);
document.removeEventListener("click", closeUnitPop);
root.remove();
},
};
}