- 도구 모음: 글꼴 · 크기 · 굵게 · 기울임 · 밑줄 · 취소선 · 글자색 · 채움색 · 가로/세로 정렬 · 줄 바꿈 · 병합 · 테두리 · 숫자 형식 · 되돌리기 - 시트 탭: 더하기 · 이름 바꾸기 · 지우기 · 복사 · 끌어 옮기기 - 우클릭 메뉴: 칸 · 행 머리 · 열 머리별 명령(공용 `ui_template_context_menu` 재사용) - 클립보드: 안 → 안 서식 · 병합 · 수식 상대 이동 왕복 · 밖 → 안(엑셀 값 + 「수식 표시」 · 구글 R1C1) · 안 → 밖(TSV + 인라인 서식 HTML) - E 화면 시험 틀(A · B 몫 가짜 대역) · TSV 파싱 Node 시험 추가
212 lines
6.6 KiB
TypeScript
212 lines
6.6 KiB
TypeScript
/* =============================================================================
|
|
* spreadsheet_menu.ts (주인 E)
|
|
* 우클릭 메뉴 — 칸 · 행 머리 · 열 머리마다: 잘라내기 · 복사 · (붙여넣기 = 「Ctrl+V 를 쓸 것」 안내) ·
|
|
* 행열 넣기 · 지우기 · 숨기기 · 보이기 · 내용 지우기 · 서식 지우기 · 병합 · 풀기.
|
|
* `ui_template_context_menu.ts` 재사용 — 우클릭 밖 자리는 그냥 눌러 선택부터 옮김.
|
|
* ========================================================================== */
|
|
|
|
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
|
|
import { showToast } from "@ui/ui_template_elements";
|
|
import { rangeToA1 } from "./spreadsheet_address";
|
|
import { st } from "./spreadsheet_text";
|
|
import { MAX_COLS, MAX_ROWS, type CellRange } from "./spreadsheet_types";
|
|
import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types";
|
|
|
|
import "./spreadsheet_toolbar.css";
|
|
|
|
function within(range: CellRange, r: number, c: number): boolean {
|
|
return r >= range.r0 && r <= range.r1 && c >= range.c0 && c <= range.c1;
|
|
}
|
|
|
|
/** `ctx.root` 의 contextmenu 를 받음(root null) */
|
|
export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
|
if (ctx.readOnly) return { root: null, refresh() {}, destroy() {} };
|
|
|
|
const menu = createMapContextMenu("ss");
|
|
ctx.root.append(menu.element);
|
|
|
|
function isMerged(range: CellRange): boolean {
|
|
return (ctx.sheet().병합 ?? []).includes(rangeToA1(range));
|
|
}
|
|
|
|
function cutCopy(kind: "cut" | "copy"): void {
|
|
document.execCommand(kind);
|
|
}
|
|
|
|
function cellMenuItems(range: CellRange): MapContextMenuItem[] {
|
|
return [
|
|
[st("MenuCut"), () => cutCopy("cut")],
|
|
[st("MenuCopy"), () => cutCopy("copy")],
|
|
[st("MenuPasteHint"), () => showToast(st("MenuPasteHint"), "info")],
|
|
[
|
|
st("MenuClearContent"),
|
|
() => ctx.dispatch({ 종류: "내용지움", 시트: ctx.selection.시트, 범위: [range] }),
|
|
],
|
|
[
|
|
st("MenuClearFormat"),
|
|
() => ctx.dispatch({ 종류: "서식지움", 시트: ctx.selection.시트, 범위: [range] }),
|
|
],
|
|
[
|
|
isMerged(range) ? st("MenuUnmerge") : st("MenuMerge"),
|
|
() =>
|
|
ctx.dispatch(
|
|
isMerged(range)
|
|
? { 종류: "병합풀기", 시트: ctx.selection.시트, 범위: range }
|
|
: { 종류: "병합", 시트: ctx.selection.시트, 범위: range },
|
|
),
|
|
],
|
|
];
|
|
}
|
|
|
|
function rowMenuItems(rows: number[]): MapContextMenuItem[] {
|
|
const at = Math.min(...rows);
|
|
const 수 = rows.length;
|
|
return [
|
|
[st("MenuCopy"), () => cutCopy("copy")],
|
|
[
|
|
st("MenuInsertRow"),
|
|
() => ctx.dispatch({ 종류: "행넣기", 시트: ctx.selection.시트, at, 수 }),
|
|
],
|
|
[
|
|
st("MenuDeleteRow"),
|
|
() => ctx.dispatch({ 종류: "행지우기", 시트: ctx.selection.시트, at, 수 }),
|
|
],
|
|
[
|
|
st("MenuHide"),
|
|
() =>
|
|
ctx.dispatch({
|
|
종류: "숨김",
|
|
시트: ctx.selection.시트,
|
|
축: "행",
|
|
번호: rows,
|
|
숨김: true,
|
|
}),
|
|
],
|
|
[
|
|
st("MenuUnhide"),
|
|
() =>
|
|
ctx.dispatch({
|
|
종류: "숨김",
|
|
시트: ctx.selection.시트,
|
|
축: "행",
|
|
번호: rows,
|
|
숨김: false,
|
|
}),
|
|
],
|
|
];
|
|
}
|
|
|
|
function colMenuItems(cols: number[]): MapContextMenuItem[] {
|
|
const at = Math.min(...cols);
|
|
const 수 = cols.length;
|
|
return [
|
|
[st("MenuCopy"), () => cutCopy("copy")],
|
|
[
|
|
st("MenuInsertCol"),
|
|
() => ctx.dispatch({ 종류: "열넣기", 시트: ctx.selection.시트, at, 수 }),
|
|
],
|
|
[
|
|
st("MenuDeleteCol"),
|
|
() => ctx.dispatch({ 종류: "열지우기", 시트: ctx.selection.시트, at, 수 }),
|
|
],
|
|
[
|
|
st("MenuHide"),
|
|
() =>
|
|
ctx.dispatch({
|
|
종류: "숨김",
|
|
시트: ctx.selection.시트,
|
|
축: "열",
|
|
번호: cols,
|
|
숨김: true,
|
|
}),
|
|
],
|
|
[
|
|
st("MenuUnhide"),
|
|
() =>
|
|
ctx.dispatch({
|
|
종류: "숨김",
|
|
시트: ctx.selection.시트,
|
|
축: "열",
|
|
번호: cols,
|
|
숨김: false,
|
|
}),
|
|
],
|
|
];
|
|
}
|
|
|
|
function range(r0: number, c0: number, r1: number, c1: number): CellRange {
|
|
return { r0, c0, r1, c1 };
|
|
}
|
|
|
|
function onContextMenu(ev: MouseEvent): void {
|
|
const hit = ctx.grid.hitTest(ev.clientX, ev.clientY);
|
|
if (!hit) return;
|
|
ev.preventDefault();
|
|
const box = ctx.root.getBoundingClientRect();
|
|
const x = ev.clientX - box.left;
|
|
const y = ev.clientY - box.top;
|
|
|
|
if (hit.kind === "rowHead") {
|
|
const covered = ctx.selection.범위.find(
|
|
(rg) => rg.c0 === 0 && rg.c1 >= MAX_COLS - 1 && within(rg, hit.r, 0),
|
|
);
|
|
const rows = covered
|
|
? Array.from({ length: covered.r1 - covered.r0 + 1 }, (_, i) => covered.r0 + i)
|
|
: [hit.r];
|
|
if (!covered)
|
|
ctx.select({
|
|
시트: ctx.selection.시트,
|
|
범위: [range(hit.r, 0, hit.r, MAX_COLS - 1)],
|
|
활성: { r: hit.r, c: 0 },
|
|
기준: { r: hit.r, c: 0 },
|
|
});
|
|
menu.open(x, y, rowMenuItems(rows));
|
|
return;
|
|
}
|
|
|
|
if (hit.kind === "colHead") {
|
|
const covered = ctx.selection.범위.find(
|
|
(rg) => rg.r0 === 0 && rg.r1 >= MAX_ROWS - 1 && within(rg, 0, hit.c),
|
|
);
|
|
const cols = covered
|
|
? Array.from({ length: covered.c1 - covered.c0 + 1 }, (_, i) => covered.c0 + i)
|
|
: [hit.c];
|
|
if (!covered)
|
|
ctx.select({
|
|
시트: ctx.selection.시트,
|
|
범위: [range(0, hit.c, MAX_ROWS - 1, hit.c)],
|
|
활성: { r: 0, c: hit.c },
|
|
기준: { r: 0, c: hit.c },
|
|
});
|
|
menu.open(x, y, colMenuItems(cols));
|
|
return;
|
|
}
|
|
|
|
if (hit.kind === "cell") {
|
|
const covered = ctx.selection.범위.some((rg) => within(rg, hit.r, hit.c));
|
|
if (!covered) {
|
|
ctx.select({
|
|
시트: ctx.selection.시트,
|
|
범위: [range(hit.r, hit.c, hit.r, hit.c)],
|
|
활성: { r: hit.r, c: hit.c },
|
|
기준: { r: hit.r, c: hit.c },
|
|
});
|
|
}
|
|
const active = ctx.selection.범위[0] ?? range(hit.r, hit.c, hit.r, hit.c);
|
|
menu.open(x, y, cellMenuItems(active));
|
|
}
|
|
}
|
|
|
|
ctx.root.addEventListener("contextmenu", onContextMenu);
|
|
|
|
return {
|
|
root: menu.element,
|
|
refresh() {},
|
|
destroy() {
|
|
ctx.root.removeEventListener("contextmenu", onContextMenu);
|
|
menu.close();
|
|
menu.element.remove();
|
|
},
|
|
};
|
|
}
|