Merge remote-tracking branches 'origin/sub_laptop_2' and 'origin/sub_laptop_8' into sub_laptop_4
This commit is contained in:
@@ -95,6 +95,14 @@ const TEXT = {
|
||||
"병합된 셀의 일부를 바꿀 수 없습니다",
|
||||
"Can't change part of a merged cell",
|
||||
],
|
||||
Unit: ["단위", "Unit"],
|
||||
UnitGroup_area: ["넓이·부피·길이", "Area · volume · length"],
|
||||
UnitGroup_weight: ["무게·부피(액체)", "Weight · liquid volume"],
|
||||
UnitGroup_count: ["개수", "Count"],
|
||||
UnitGroup_rate: ["속도·각도·비율", "Speed · angle · rate"],
|
||||
UnitGroup_op: ["연산·비교", "Operators"],
|
||||
UnitRecent: ["최근 씀", "Recently used"],
|
||||
UnitAsFormat: ["숫자 형식으로 붙이기", "Attach as number format"],
|
||||
} as const;
|
||||
|
||||
export function st(key: keyof typeof TEXT): string {
|
||||
|
||||
@@ -119,6 +119,32 @@
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.ss-toolbar__unit-pop {
|
||||
max-height: 340px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ss-toolbar__unit-section + .ss-toolbar__unit-section {
|
||||
margin-top: var(--spacing-8);
|
||||
}
|
||||
|
||||
.ss-toolbar__unit-label {
|
||||
margin-bottom: var(--spacing-4);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.ss-toolbar__unit-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.ss-toolbar__unit-btn {
|
||||
min-width: 0;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
/* ── 시트 탭 ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
.ss-tabs {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
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";
|
||||
@@ -58,6 +59,43 @@ const BORDER_LINES: BorderLine[] = [
|
||||
"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();
|
||||
@@ -332,6 +370,88 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle {
|
||||
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);
|
||||
|
||||
root.append(
|
||||
group(undoBtn, redoBtn),
|
||||
group(fontInput, sizeInput),
|
||||
@@ -341,6 +461,7 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle {
|
||||
group(mergeBtn),
|
||||
group(borderBtn, borderPop),
|
||||
group(numfmtInput),
|
||||
group(unitBtn, unitPop),
|
||||
);
|
||||
|
||||
function refresh(): void {
|
||||
@@ -383,6 +504,7 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle {
|
||||
refresh,
|
||||
destroy() {
|
||||
document.removeEventListener("click", closeBorderPop);
|
||||
document.removeEventListener("click", closeUnitPop);
|
||||
root.remove();
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user