Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -110,7 +110,7 @@ export function createSpreadsheet(
|
||||
): SpreadsheetHandle {
|
||||
const readOnly = !!opts.readOnly;
|
||||
const noTabs = opts.tabs === false;
|
||||
const bare = readOnly || !!opts.preview; // 도구 모음 · 메뉴 없음
|
||||
const bare = readOnly || !!opts.preview; // 도구 모음 없음 · 우클릭은 「복사」 한 줄
|
||||
const root = el("div", {
|
||||
className: `ss${readOnly ? " ss--readonly" : ""}${noTabs ? " ss--notabs" : ""}${opts.preview ? " ss--preview" : ""}`,
|
||||
});
|
||||
@@ -142,7 +142,7 @@ export function createSpreadsheet(
|
||||
ctx.editor = ed;
|
||||
const toolbar: PartHandle | null = bare ? null : mountToolbar(ctx);
|
||||
const tabs: PartHandle | null = noTabs ? null : mountTabs(ctx);
|
||||
const menu: PartHandle | null = bare ? null : attachMenu(ctx);
|
||||
const menu: PartHandle = attachMenu(ctx); // 읽기 전용 · 미리보기는 「복사」 한 줄(81-3)
|
||||
const clip = attachClipboard(ctx);
|
||||
const status = createStatusBar(ctx);
|
||||
const others = [toolbar, tabs, menu, clip, status].filter((p): p is PartHandle => !!p);
|
||||
|
||||
@@ -12,10 +12,21 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { rangeToA1 } from "./spreadsheet_address";
|
||||
import { confirmMerge, dialogKeys } from "./spreadsheet_actions";
|
||||
import { deleteComment, getComment, setComment } from "./spreadsheet_comments";
|
||||
import {
|
||||
cellInsertItems,
|
||||
cellMoreItems,
|
||||
colInsertItems,
|
||||
colMoreItems,
|
||||
copyCut,
|
||||
copyOnlyItems,
|
||||
pasteItems,
|
||||
rowInsertItems,
|
||||
rowMoreItems,
|
||||
} from "./spreadsheet_menu_items";
|
||||
import { st } from "./spreadsheet_text";
|
||||
import { MAX_COLS, MAX_ROWS, type CellRange } from "./spreadsheet_types";
|
||||
import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types";
|
||||
@@ -28,7 +39,8 @@ function within(range: CellRange, r: number, c: number): boolean {
|
||||
|
||||
/** `ctx.root` 의 contextmenu 를 받음(root null) */
|
||||
export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
||||
if (ctx.readOnly) return { root: null, refresh() {}, destroy() {} };
|
||||
// 읽기 전용 · 미리보기 = 「복사」 한 줄(81-3)
|
||||
const bare = ctx.readOnly || ctx.root.classList.contains("ss--preview");
|
||||
|
||||
const menu = createMapContextMenu("ss");
|
||||
ctx.root.append(menu.element);
|
||||
@@ -39,7 +51,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
||||
}
|
||||
|
||||
function cutCopy(kind: "cut" | "copy"): void {
|
||||
document.execCommand(kind);
|
||||
copyCut(ctx, kind);
|
||||
}
|
||||
|
||||
/** 메모 글 입력 — 작은 오버레이(native prompt 안 씀, 자동화 창이 native 창을 자동 취소하는
|
||||
@@ -79,7 +91,10 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
||||
return [
|
||||
[st("MenuCut"), () => cutCopy("cut")],
|
||||
[st("MenuCopy"), () => cutCopy("copy")],
|
||||
[st("MenuPasteHint"), () => showToast(st("MenuPasteHint"), "info")],
|
||||
...pasteItems(ctx),
|
||||
"-",
|
||||
...cellInsertItems(ctx, range),
|
||||
"-",
|
||||
[
|
||||
st("MenuClearContent"),
|
||||
() => ctx.dispatch({ 종류: "내용지움", 시트: ctx.selection.시트, 범위: [range] }),
|
||||
@@ -107,6 +122,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
||||
...(note
|
||||
? ([[st("MenuCommentDelete"), () => deleteComment(ctx, r, c)]] as MapContextMenuItem[])
|
||||
: []),
|
||||
...cellMoreItems(ctx, range),
|
||||
...(ctx.cellMenuExtra?.(r, c) ?? []),
|
||||
];
|
||||
}
|
||||
@@ -117,11 +133,9 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
||||
return [
|
||||
[st("MenuCut"), () => cutCopy("cut")],
|
||||
[st("MenuCopy"), () => cutCopy("copy")],
|
||||
[st("MenuPasteHint"), () => showToast(st("MenuPasteHint"), "info")],
|
||||
[
|
||||
st("MenuInsertRow"),
|
||||
() => ctx.dispatch({ 종류: "행넣기", 시트: ctx.selection.시트, at, 수 }),
|
||||
],
|
||||
...pasteItems(ctx),
|
||||
"-",
|
||||
...rowInsertItems(ctx, at, 수),
|
||||
[
|
||||
st("MenuDeleteRow"),
|
||||
() => ctx.dispatch({ 종류: "행지우기", 시트: ctx.selection.시트, at, 수 }),
|
||||
@@ -148,6 +162,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
||||
숨김: false,
|
||||
}),
|
||||
],
|
||||
...rowMoreItems(ctx, rows),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -157,11 +172,9 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
||||
return [
|
||||
[st("MenuCut"), () => cutCopy("cut")],
|
||||
[st("MenuCopy"), () => cutCopy("copy")],
|
||||
[st("MenuPasteHint"), () => showToast(st("MenuPasteHint"), "info")],
|
||||
[
|
||||
st("MenuInsertCol"),
|
||||
() => ctx.dispatch({ 종류: "열넣기", 시트: ctx.selection.시트, at, 수 }),
|
||||
],
|
||||
...pasteItems(ctx),
|
||||
"-",
|
||||
...colInsertItems(ctx, at, 수),
|
||||
[
|
||||
st("MenuDeleteCol"),
|
||||
() => ctx.dispatch({ 종류: "열지우기", 시트: ctx.selection.시트, at, 수 }),
|
||||
@@ -188,6 +201,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
||||
숨김: false,
|
||||
}),
|
||||
],
|
||||
...colMoreItems(ctx, cols),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -217,7 +231,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
||||
활성: { r: hit.r, c: 0 },
|
||||
기준: { r: hit.r, c: 0 },
|
||||
});
|
||||
menu.open(x, y, rowMenuItems(rows));
|
||||
menu.open(x, y, bare ? copyOnlyItems(ctx) : rowMenuItems(rows));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -235,7 +249,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
||||
활성: { r: 0, c: hit.c },
|
||||
기준: { r: 0, c: hit.c },
|
||||
});
|
||||
menu.open(x, y, colMenuItems(cols));
|
||||
menu.open(x, y, bare ? copyOnlyItems(ctx) : colMenuItems(cols));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -250,7 +264,13 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
||||
});
|
||||
}
|
||||
const active = ctx.selection.범위[0] ?? range(hit.r, hit.c, hit.r, hit.c);
|
||||
menu.open(x, y, cellMenuItems(active, hit.r, hit.c));
|
||||
menu.open(
|
||||
x,
|
||||
y,
|
||||
bare
|
||||
? [...copyOnlyItems(ctx), ...(ctx.cellMenuExtra?.(hit.r, hit.c) ?? [])]
|
||||
: cellMenuItems(active, hit.r, hit.c),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
/* =============================================================================
|
||||
* test_stage4_menu_items.ts — 우클릭 메뉴 덧붙인 항목(81-3) 시험.
|
||||
* 행 위/아래 · 열 좌우 삽입 · 셀 삽입/삭제 · 행열 삭제 · 크기 · 시트 정렬 · 필터 · 고정 · 그룹 ·
|
||||
* 범위 이름 · 붙여넣기 보안 문맥 갈래 · 구분선 · 읽기 전용 복사.
|
||||
* 돌리기: npx tsx resources/tester/spreadsheet/test_stage4_menu_items.ts
|
||||
* ========================================================================== */
|
||||
|
||||
import { register } from "node:module";
|
||||
import type { MapContextMenuItem } from "../../../ui_template/ui_template_context_menu.ts";
|
||||
import { applyCommand } from "../../../A00_Common/spreadsheet/spreadsheet_commands.ts";
|
||||
import { createCalcEngine } from "../../../A00_Common/spreadsheet/spreadsheet_graph.ts";
|
||||
import {
|
||||
canMenuPaste,
|
||||
cellInsertItems,
|
||||
cellMoreItems,
|
||||
colInsertItems,
|
||||
colMoreItems,
|
||||
copyOnlyItems,
|
||||
menuPaste,
|
||||
nameRange,
|
||||
pasteItems,
|
||||
resizeLines,
|
||||
rowInsertItems,
|
||||
rowMoreItems,
|
||||
sortByColumn,
|
||||
usedRange,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_menu_items.ts";
|
||||
import type { Cell, Command, Workbook } from "../../../A00_Common/spreadsheet/spreadsheet_types.ts";
|
||||
import type {
|
||||
Selection,
|
||||
SpreadsheetContext,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_view_types.ts";
|
||||
import { a1, assertEqual, makeBook, makeCtx, makeSheet, v } from "./test_stage2_helpers.ts";
|
||||
|
||||
// 늦게 부르는 창 모듈의 css 는 node 에서 빈 모듈로
|
||||
register(
|
||||
"data:text/javascript,export async function load(u,c,n){return u.endsWith('.css')?{format:'module',source:'',shortCircuit:true}:n(u,c)}",
|
||||
);
|
||||
const tick = () => new Promise((r) => setTimeout(r, 30));
|
||||
|
||||
/** 진짜 명령 · 엔진 · 고름을 쓰는 문맥 */
|
||||
function realCtx(cells: Record<string, Cell>): SpreadsheetContext & { sent: Command[] } {
|
||||
const book: Workbook = makeBook([makeSheet("s1", cells)]);
|
||||
const base = makeCtx(book, "s1");
|
||||
const sent: Command[] = [];
|
||||
const ctx = base as unknown as SpreadsheetContext & { sent: Command[] };
|
||||
ctx.engine = createCalcEngine(book);
|
||||
ctx.sent = sent;
|
||||
ctx.dispatch = (cmd: Command) => {
|
||||
sent.push(cmd);
|
||||
applyCommand(book, cmd);
|
||||
ctx.engine.rebuild(book);
|
||||
};
|
||||
ctx.select = (sel: Selection) => void (ctx.selection = sel);
|
||||
return ctx;
|
||||
}
|
||||
const col = (n: number) =>
|
||||
Object.fromEntries(Array.from({ length: n }, (_, i) => [a1(i, 0), v(i + 1)]));
|
||||
const row = (n: number) =>
|
||||
Object.fromEntries(Array.from({ length: n }, (_, i) => [a1(0, i), v(i + 1)]));
|
||||
const val = (ctx: SpreadsheetContext, r: number, c: number) => ctx.sheet().칸[a1(r, c)]?.값;
|
||||
const leaf = (items: MapContextMenuItem[], label: string): (() => void) => {
|
||||
for (const it of items) {
|
||||
if (it === "-") continue;
|
||||
if (Array.isArray(it)) {
|
||||
if (it[0] === label) return it[1];
|
||||
} else {
|
||||
const hit = it.children.find(([l]) => l === label);
|
||||
if (hit) return hit[1];
|
||||
}
|
||||
}
|
||||
throw new Error(`항목 없음: ${label}`);
|
||||
};
|
||||
const labels = (items: MapContextMenuItem[]) =>
|
||||
items.map((it) => (it === "-" ? "-" : Array.isArray(it) ? it[0] : `${it.label} ▸`));
|
||||
|
||||
// ── 행 위 · 아래 삽입(고른 2행 B2:B3 → at 1 · 수 2) ─────────────────────────
|
||||
{
|
||||
const ctx = realCtx(col(5));
|
||||
leaf(rowInsertItems(ctx, 1, 2), "위에 2행 삽입")();
|
||||
assertEqual(
|
||||
[val(ctx, 0, 0), val(ctx, 1, 0), val(ctx, 2, 0), val(ctx, 3, 0)],
|
||||
[1, undefined, undefined, 2],
|
||||
"위에 2행 — 2행부터 밀림",
|
||||
);
|
||||
}
|
||||
{
|
||||
const ctx = realCtx(col(5));
|
||||
leaf(rowInsertItems(ctx, 1, 2), "아래에 2행 삽입")();
|
||||
assertEqual(
|
||||
[val(ctx, 2, 0), val(ctx, 3, 0), val(ctx, 4, 0), val(ctx, 5, 0)],
|
||||
[3, undefined, undefined, 4],
|
||||
"아래에 2행 — 고른 줄 뒤에 끼움",
|
||||
);
|
||||
}
|
||||
// ── 열 왼 · 오른쪽 삽입 ──────────────────────────────────────────────────
|
||||
{
|
||||
const ctx = realCtx(row(4));
|
||||
leaf(colInsertItems(ctx, 1, 1), "왼쪽에 1열 삽입")();
|
||||
assertEqual([val(ctx, 0, 0), val(ctx, 0, 1), val(ctx, 0, 2)], [1, undefined, 2], "왼쪽에 1열");
|
||||
}
|
||||
{
|
||||
const ctx = realCtx(row(4));
|
||||
leaf(colInsertItems(ctx, 1, 1), "오른쪽에 1열 삽입")();
|
||||
assertEqual([val(ctx, 0, 1), val(ctx, 0, 2), val(ctx, 0, 3)], [2, undefined, 3], "오른쪽에 1열");
|
||||
}
|
||||
// ── 칸 메뉴 — 셀 삽입/삭제 ▸ · 행열 삭제 ▸ ──────────────────────────────
|
||||
{
|
||||
const g = { r0: 0, c0: 1, r1: 0, c1: 1 };
|
||||
const ctx = realCtx(row(3));
|
||||
const items = cellInsertItems(ctx, g);
|
||||
assertEqual(
|
||||
labels(items),
|
||||
["행 · 열 삽입 ▸", "셀 삽입 ▸", "행 · 열 삭제 ▸", "셀 삭제 ▸"],
|
||||
"칸 메뉴 하위 넷",
|
||||
);
|
||||
leaf(items, "셀 삽입 후 오른쪽으로 밀기")();
|
||||
await tick();
|
||||
assertEqual(
|
||||
[val(ctx, 0, 1), val(ctx, 0, 2), val(ctx, 0, 3)],
|
||||
[undefined, 2, 3],
|
||||
"셀 삽입 · 오른쪽으로",
|
||||
);
|
||||
leaf(cellInsertItems(ctx, g), "셀 삭제 후 왼쪽으로 당기기")();
|
||||
await tick();
|
||||
assertEqual([val(ctx, 0, 1), val(ctx, 0, 2)], [2, 3], "셀 삭제 · 왼쪽으로");
|
||||
}
|
||||
{
|
||||
const ctx = realCtx(col(3));
|
||||
leaf(cellInsertItems(ctx, { r0: 1, c0: 0, r1: 1, c1: 0 }), "셀 삽입 후 아래로 밀기")();
|
||||
await tick();
|
||||
assertEqual([val(ctx, 1, 0), val(ctx, 2, 0)], [undefined, 2], "셀 삽입 · 아래로");
|
||||
leaf(cellInsertItems(ctx, { r0: 1, c0: 0, r1: 1, c1: 0 }), "셀 삭제 후 위로 당기기")();
|
||||
await tick();
|
||||
assertEqual([val(ctx, 1, 0), val(ctx, 2, 0)], [2, 3], "셀 삭제 · 위로");
|
||||
leaf(cellInsertItems(ctx, { r0: 0, c0: 0, r1: 1, c1: 0 }), "2행 삭제")();
|
||||
assertEqual(val(ctx, 0, 0), 3, "칸 메뉴 행 삭제 · 고른 2행");
|
||||
}
|
||||
// ── 크기 — px → 열폭(글자) · 행높이(pt) · 잘못된 값 ─────────────────────────
|
||||
{
|
||||
const ctx = realCtx(row(2));
|
||||
assertEqual(
|
||||
resizeLines(ctx, false, [0, 1], "abc"),
|
||||
"1 이상 숫자를 넣을 것",
|
||||
"크기 · 글이면 거름",
|
||||
);
|
||||
assertEqual(resizeLines(ctx, false, [0, 1], "0"), "1 이상 숫자를 넣을 것", "크기 · 0 거름");
|
||||
assertEqual(resizeLines(ctx, false, [0, 1], "100"), null, "열 너비 100px");
|
||||
const w = ctx.sheet().열!;
|
||||
assertEqual(w.A?.폭 === w.B?.폭 && (w.A?.폭 ?? 0) > 10, true, "열 둘 같은 폭(한 명령)");
|
||||
resizeLines(ctx, true, [2], "40");
|
||||
assertEqual(
|
||||
Math.round((ctx.sheet().행?.["3"]?.높이 ?? 0) * 100) / 100,
|
||||
30,
|
||||
"행 높이 40px = 30pt",
|
||||
);
|
||||
}
|
||||
// ── 시트 정렬(열 머리) — 쓴 범위 통째 · 고름 되돌림 · 빈 열은 안 함 ───────────────
|
||||
{
|
||||
const ctx = realCtx({
|
||||
A1: v("이름"),
|
||||
B1: v("값"),
|
||||
A2: v("다"),
|
||||
B2: v(3),
|
||||
A3: v("가"),
|
||||
B3: v(1),
|
||||
A4: v("나"),
|
||||
B4: v(2),
|
||||
});
|
||||
const keep = ctx.selection;
|
||||
await sortByColumn(ctx, 1, false);
|
||||
assertEqual(
|
||||
[val(ctx, 1, 0), val(ctx, 2, 0), val(ctx, 3, 0)],
|
||||
["가", "나", "다"],
|
||||
"B 열 기준 오름 — A 열도 같이",
|
||||
);
|
||||
assertEqual(val(ctx, 0, 0), "이름", "머리 행 그대로");
|
||||
assertEqual(ctx.selection, keep, "정렬 뒤 고름 되돌림");
|
||||
await sortByColumn(ctx, 1, true);
|
||||
assertEqual([val(ctx, 1, 1), val(ctx, 3, 1)], [3, 1], "내림");
|
||||
const before = ctx.sent.length;
|
||||
await sortByColumn(ctx, 5, false);
|
||||
assertEqual(ctx.sent.length, before, "빈 열 정렬은 명령 없음");
|
||||
assertEqual(usedRange(ctx.sheet()), { r0: 0, c0: 0, r1: 3, c1: 1 }, "쓴 범위");
|
||||
}
|
||||
// ── 필터 · 고정 · 그룹 · 더보기 ─────────────────────────────────────────────
|
||||
{
|
||||
const ctx = realCtx({ A1: v("머리"), A2: v(1), B2: v(2) });
|
||||
const calls: [string, unknown][] = [];
|
||||
ctx.dataMenu = (kind) => calls.push([kind, structuredClone(ctx.selection.범위)]);
|
||||
const more = colMoreItems(ctx, [0]);
|
||||
assertEqual(
|
||||
labels(more),
|
||||
[
|
||||
"-",
|
||||
"열 크기 조절…",
|
||||
"데이터에 맞추기",
|
||||
"-",
|
||||
"시트 정렬 A → Z",
|
||||
"시트 정렬 Z → A",
|
||||
"필터 만들기",
|
||||
"-",
|
||||
"고정 ▸",
|
||||
"그룹 ▸",
|
||||
],
|
||||
"열 머리 덧붙임 순서",
|
||||
);
|
||||
leaf(more, "필터 만들기")();
|
||||
await tick();
|
||||
assertEqual(calls[0], ["filter", [{ r0: 0, c0: 0, r1: 1, c1: 1 }]], "열 머리 필터 = 쓴 범위로");
|
||||
leaf(cellMoreItems(ctx, { r0: 0, c0: 0, r1: 0, c1: 0 }), "드롭다운…")();
|
||||
leaf(cellMoreItems(ctx, { r0: 0, c0: 0, r1: 0, c1: 0 }), "조건부 서식…")();
|
||||
assertEqual(
|
||||
calls.slice(1).map((c) => c[0]),
|
||||
["validation", "condfmt"],
|
||||
"더보기 → 데이터 창",
|
||||
);
|
||||
leaf(rowMoreItems(ctx, [0]), "첫 행 고정")();
|
||||
await tick();
|
||||
assertEqual(ctx.sent.at(-1)?.종류, "틀고정", "고정 ▸ 첫 행");
|
||||
ctx.selection = {
|
||||
시트: "s1",
|
||||
범위: [{ r0: 1, c0: 0, r1: 1, c1: 16383 }],
|
||||
활성: { r: 1, c: 0 },
|
||||
기준: { r: 1, c: 0 },
|
||||
};
|
||||
leaf(rowMoreItems(ctx, [1]), "그룹으로 묶기")();
|
||||
await tick();
|
||||
assertEqual(ctx.sheet().행?.["2"]?.묶음, { 층: 1 }, "그룹 ▸ 묶기");
|
||||
assertEqual(
|
||||
await nameRange(ctx, "", { r0: 0, c0: 0, r1: 0, c1: 0 }),
|
||||
"이름을 넣을 것",
|
||||
"범위 이름 · 빈 값",
|
||||
);
|
||||
assertEqual(
|
||||
await nameRange(ctx, "합계칸", { r0: 1, c0: 0, r1: 1, c1: 1 }),
|
||||
null,
|
||||
"범위 이름 지정",
|
||||
);
|
||||
assertEqual(
|
||||
typeof (await nameRange(ctx, "합계칸", { r0: 0, c0: 0, r1: 0, c1: 0 })),
|
||||
"string",
|
||||
"같은 이름은 거름",
|
||||
);
|
||||
}
|
||||
// ── 붙여넣기 — 보안 문맥 갈래 ───────────────────────────────────────────────
|
||||
{
|
||||
const g = globalThis as Record<string, unknown>;
|
||||
g.isSecureContext = false;
|
||||
assertEqual(canMenuPaste(), false, "http(보안 문맥 아님) — 메뉴로 못 읽음 → Ctrl+V 안내");
|
||||
g.isSecureContext = true;
|
||||
assertEqual(canMenuPaste(), false, "클립보드 API 없음 — 안내");
|
||||
const clip = { html: "", text: "7\t8" };
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
value: {
|
||||
read: async () => [
|
||||
{
|
||||
types: ["text/plain"],
|
||||
getType: async () => new Blob([clip.text]),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assertEqual(canMenuPaste(), true, "localhost · https — 메뉴로 읽음");
|
||||
class FakeData {
|
||||
data: Record<string, string> = {};
|
||||
setData(k: string, s: string) {
|
||||
this.data[k] = s;
|
||||
}
|
||||
}
|
||||
class FakePaste {
|
||||
constructor(
|
||||
public type: string,
|
||||
public init: { clipboardData: FakeData },
|
||||
) {}
|
||||
}
|
||||
g.DataTransfer = FakeData;
|
||||
g.ClipboardEvent = FakePaste;
|
||||
const ctx = realCtx({});
|
||||
const events: FakePaste[] = [];
|
||||
ctx.root = { dispatchEvent: (e: FakePaste) => events.push(e) } as unknown as HTMLElement;
|
||||
await menuPaste(ctx);
|
||||
assertEqual(
|
||||
[events[0]?.type, events[0]?.init.clipboardData.data["text/plain"]],
|
||||
["paste", "7\t8"],
|
||||
"보통 붙여넣기 = 부품 paste 길로",
|
||||
);
|
||||
ctx.selection = {
|
||||
시트: "s1",
|
||||
범위: [{ r0: 2, c0: 0, r1: 2, c1: 0 }],
|
||||
활성: { r: 2, c: 0 },
|
||||
기준: { r: 2, c: 0 },
|
||||
};
|
||||
await menuPaste(ctx, "값");
|
||||
assertEqual([val(ctx, 2, 0), val(ctx, 2, 1)], [7, 8], "값만 붙여넣기 — 활성 칸부터");
|
||||
assertEqual(labels(pasteItems(ctx)), ["붙여넣기", "선택하여 붙여넣기 ▸"], "붙여넣기 항목");
|
||||
}
|
||||
// ── 읽기 전용 — 복사 한 줄 ─────────────────────────────────────────────────
|
||||
{
|
||||
const ctx = realCtx({});
|
||||
assertEqual(labels(copyOnlyItems(ctx)), ["복사"], "읽기 전용 · 미리보기 = 복사 한 줄");
|
||||
}
|
||||
console.log("spreadsheet_menu_items 시험 끝");
|
||||
Reference in New Issue
Block a user