- 도구 모음 오른쪽 끝에 흐린 글씨 안내 추가 — 「엑셀 수식까지: 엑셀에서 Ctrl+` → 복사 → 여기 Ctrl+V · 끝나면 엑셀 Ctrl+` 다시」 · 누르면 왜 그런지 두세 줄 설명(엑셀 Ctrl+` = 「수식 표시」 켜기 · 그 상태로 복사해야 수식이 실림 · 그냥 복사하면 값만 옴 · 붙인 뒤 엑셀에서 Ctrl+` 다시 눌러 원래대로) - 도구 모음은 readOnly 면 안 붙는 기존 길을 그대로 써 보기 전용 카드에서는 자동으로 숨음(새 분기 없음) - 엑셀 HTML 붙여넣기(출처: "excel")인데 뭉치 전체에 식이 하나도 없으면 세션에 한 번만 토스트 — 「값만 들어왔음 — 수식까지는 엑셀 Ctrl+` 뒤 복사」 (sessionStorage 로 한 번 표시 기억 · 못 쓰면 매번 뜰 수 있으나 치명적이지 않음) - ORCA 로 확인 — 안내 글 그대로 보임 · 눌러 설명 펼침/접힘 · 식 없는 엑셀 붙여넣기 첫 번은 토스트 뜨고 세션 안 지운 채 두 번째는 안 뜸 · 식이 있으면 (「=A1+B1」) 애초에 안 뜸 · 기존 클립보드 회귀 시험(TSV) 통과 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeeQRLknEpP4SFjVkjDWDs
533 lines
21 KiB
TypeScript
533 lines
21 KiB
TypeScript
/* =============================================================================
|
||
* 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 { 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 ? n : 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", () => {
|
||
const range = ctx.selection.범위[0];
|
||
if (!range) return;
|
||
const merged = (ctx.sheet().병합 ?? []).includes(rangeToA1(range));
|
||
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"),
|
||
},
|
||
}) 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.세로 ?? "top";
|
||
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();
|
||
},
|
||
};
|
||
}
|