|
|
|
@@ -0,0 +1,441 @@
|
|
|
|
|
/* =============================================================================
|
|
|
|
|
* spreadsheet_menu_items.ts (81-3)
|
|
|
|
|
* 우클릭 메뉴에 덧붙이는 항목 — 구글 시트 우클릭 가운데 기능이 이미 있는 것만(조사_A 3 표 (a)).
|
|
|
|
|
* 붙여넣기 · 선택하여 붙여넣기 ▸ · 행 · 열 · 셀 삽입/삭제 ▸ · 행 · 열 크기 · 데이터에 맞추기 ·
|
|
|
|
|
* 시트 정렬 · 필터 · 고정 ▸ · 그룹 ▸ · 셀 작업 더보기 ▸(드롭다운 · 조건부 서식 · 범위 이름).
|
|
|
|
|
* 안 만듦 = 하이퍼링크 · 댓글 · 범위/시트 보호 · 다른 파일로 복사.
|
|
|
|
|
* `spreadsheet_menu.ts` 가 이 목록을 펼쳐 붙임. 몸통 창 · 명령 모듈은 누를 때 `import()`(크기 예산).
|
|
|
|
|
* 붙여넣기는 보안 문맥(localhost · https)에서만 클립보드를 읽어 실제로 붙임 — http 는 Ctrl+V 안내.
|
|
|
|
|
* ========================================================================== */
|
|
|
|
|
|
|
|
|
|
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
|
|
|
|
|
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
|
|
|
|
|
import { dialogKeys, type Act } from "./spreadsheet_actions";
|
|
|
|
|
import { parseA1 } from "./spreadsheet_address";
|
|
|
|
|
import { parseClipboard } from "./spreadsheet_clipboard";
|
|
|
|
|
import type { Editor } from "./spreadsheet_editor";
|
|
|
|
|
import {
|
|
|
|
|
buildColGeometry,
|
|
|
|
|
buildRowGeometry,
|
|
|
|
|
MAX_COL_CHARS,
|
|
|
|
|
pxToColChars,
|
|
|
|
|
pxToPt,
|
|
|
|
|
} from "./spreadsheet_headers";
|
|
|
|
|
import type { PasteSpecialMode } from "./spreadsheet_paste_special";
|
|
|
|
|
import { st } from "./spreadsheet_text";
|
|
|
|
|
import type { CellRange, Command, Sheet } from "./spreadsheet_types";
|
|
|
|
|
import type { SpreadsheetContext } from "./spreadsheet_view_types";
|
|
|
|
|
|
|
|
|
|
const TEXT = {
|
|
|
|
|
paste: "붙여넣기",
|
|
|
|
|
pasteSpecial: "선택하여 붙여넣기",
|
|
|
|
|
pasteValues: "값만 붙여넣기",
|
|
|
|
|
pasteFormat: "서식만 붙여넣기",
|
|
|
|
|
pasteSpecialHint: "선택하여 붙여넣기는 Ctrl+Shift+V 를 쓸 것",
|
|
|
|
|
pasteFailed: "붙여넣을 내용이 없음",
|
|
|
|
|
insertLines: "행 · 열 삽입",
|
|
|
|
|
rowsAbove: (n: number) => `위에 ${n}행 삽입`,
|
|
|
|
|
rowsBelow: (n: number) => `아래에 ${n}행 삽입`,
|
|
|
|
|
colsLeft: (n: number) => `왼쪽에 ${n}열 삽입`,
|
|
|
|
|
colsRight: (n: number) => `오른쪽에 ${n}열 삽입`,
|
|
|
|
|
insertCells: "셀 삽입",
|
|
|
|
|
shiftRight: "셀 삽입 후 오른쪽으로 밀기",
|
|
|
|
|
shiftDown: "셀 삽입 후 아래로 밀기",
|
|
|
|
|
deleteLines: "행 · 열 삭제",
|
|
|
|
|
deleteRows: (n: number) => `${n}행 삭제`,
|
|
|
|
|
deleteCols: (n: number) => `${n}열 삭제`,
|
|
|
|
|
deleteCells: "셀 삭제",
|
|
|
|
|
shiftLeft: "셀 삭제 후 왼쪽으로 당기기",
|
|
|
|
|
shiftUp: "셀 삭제 후 위로 당기기",
|
|
|
|
|
rowSize: "행 크기 조절…",
|
|
|
|
|
colSize: "열 크기 조절…",
|
|
|
|
|
rowSizeLabel: "행 높이(px)",
|
|
|
|
|
colSizeLabel: "열 너비(px)",
|
|
|
|
|
autofit: "데이터에 맞추기",
|
|
|
|
|
sortAsc: "시트 정렬 A → Z",
|
|
|
|
|
sortDesc: "시트 정렬 Z → A",
|
|
|
|
|
filterOn: "필터 만들기",
|
|
|
|
|
filterOff: "필터 삭제",
|
|
|
|
|
freeze: "고정",
|
|
|
|
|
freezeRow: "첫 행 고정",
|
|
|
|
|
freezeCol: "첫 열 고정",
|
|
|
|
|
freezeAt: "현재 칸까지 고정",
|
|
|
|
|
unfreeze: "고정 풀기",
|
|
|
|
|
group: "그룹",
|
|
|
|
|
groupOn: "그룹으로 묶기",
|
|
|
|
|
groupOff: "그룹 풀기",
|
|
|
|
|
more: "셀 작업 더보기",
|
|
|
|
|
dropdown: "드롭다운…",
|
|
|
|
|
condfmt: "조건부 서식…",
|
|
|
|
|
nameRange: "범위 이름 지정…",
|
|
|
|
|
nameLabel: "이름",
|
|
|
|
|
nameEmpty: "이름을 넣을 것",
|
|
|
|
|
ok: "확인",
|
|
|
|
|
cancel: "취소",
|
|
|
|
|
badNumber: "1 이상 숫자를 넣을 것",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type Item = [string, () => void];
|
|
|
|
|
const SEP = "-" as const;
|
|
|
|
|
|
|
|
|
|
/** 메뉴를 닫으면 누른 단추가 지워져 초점이 문서 밖으로 감 — 격자 입력칸으로 되돌림 */
|
|
|
|
|
function back(ctx: SpreadsheetContext): void {
|
|
|
|
|
(ctx.editor as Editor).focus?.();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 잘라내기 · 복사 — 초점을 격자로 되돌린 뒤 불러야 부품의 copy · cut 듣기가 받음(빈 복사 막음) */
|
|
|
|
|
export function copyCut(ctx: SpreadsheetContext, kind: "cut" | "copy"): void {
|
|
|
|
|
back(ctx);
|
|
|
|
|
document.execCommand(kind);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 읽기 전용 · 미리보기 시트 — 「복사」 한 줄 */
|
|
|
|
|
export function copyOnlyItems(ctx: SpreadsheetContext): MapContextMenuItem[] {
|
|
|
|
|
return [[st("MenuCopy"), () => copyCut(ctx, "copy")]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const id = (ctx: SpreadsheetContext): string => ctx.selection.시트;
|
|
|
|
|
|
|
|
|
|
/** 메뉴로 클립보드를 읽을 수 있는가 — 보안 문맥(localhost · https)만 */
|
|
|
|
|
export function canMenuPaste(): boolean {
|
|
|
|
|
return globalThis.isSecureContext === true && typeof navigator?.clipboard?.read === "function";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function readClipboard(): Promise<{ html: string; text: string } | null> {
|
|
|
|
|
if (!canMenuPaste()) return null;
|
|
|
|
|
try {
|
|
|
|
|
let html = "";
|
|
|
|
|
let text = "";
|
|
|
|
|
for (const item of await navigator.clipboard.read()) {
|
|
|
|
|
if (!html && item.types.includes("text/html"))
|
|
|
|
|
html = await (await item.getType("text/html")).text();
|
|
|
|
|
if (!text && item.types.includes("text/plain"))
|
|
|
|
|
text = await (await item.getType("text/plain")).text();
|
|
|
|
|
}
|
|
|
|
|
return { html, text };
|
|
|
|
|
} catch {
|
|
|
|
|
return null; // 권한 거절 — 안내로
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 메뉴 붙여넣기 — 보통은 부품의 paste 듣기로(잘라내기 옮김 · 엑셀 경고 그대로) · 골라 붙이기는 바로 */
|
|
|
|
|
export async function menuPaste(ctx: SpreadsheetContext, mode?: PasteSpecialMode): Promise<void> {
|
|
|
|
|
const clip = await readClipboard();
|
|
|
|
|
if (!clip) return showToast(mode ? TEXT.pasteSpecialHint : st("MenuPasteHint"), "info");
|
|
|
|
|
back(ctx);
|
|
|
|
|
if (!mode) {
|
|
|
|
|
const data = new DataTransfer();
|
|
|
|
|
data.setData("text/html", clip.html);
|
|
|
|
|
data.setData("text/plain", clip.text);
|
|
|
|
|
ctx.root.dispatchEvent(
|
|
|
|
|
new ClipboardEvent("paste", { clipboardData: data, bubbles: true, cancelable: true }),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const at = { ...ctx.selection.활성 };
|
|
|
|
|
const block = parseClipboard(clip.html, clip.text, at);
|
|
|
|
|
if (!block) return showToast(TEXT.pasteFailed, "error");
|
|
|
|
|
const source =
|
|
|
|
|
block.출처 === "aislo" && block.원점 ? { 시트: id(ctx), ...block.원점 } : undefined;
|
|
|
|
|
(await import("./spreadsheet_paste_special")).pasteSpecial(ctx, block, at, mode, source);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function pasteItems(ctx: SpreadsheetContext): MapContextMenuItem[] {
|
|
|
|
|
return [
|
|
|
|
|
[TEXT.paste, () => void menuPaste(ctx)],
|
|
|
|
|
{
|
|
|
|
|
label: TEXT.pasteSpecial,
|
|
|
|
|
children: [
|
|
|
|
|
[TEXT.pasteValues, () => void menuPaste(ctx, "값")],
|
|
|
|
|
[TEXT.pasteFormat, () => void menuPaste(ctx, "서식")],
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 행 · 열 넣기 — 위 · 왼쪽은 `at` · 아래 · 오른쪽은 `at + 수`(고른 줄 수만큼) */
|
|
|
|
|
export function insertLines(
|
|
|
|
|
ctx: SpreadsheetContext,
|
|
|
|
|
rows: boolean,
|
|
|
|
|
at: number,
|
|
|
|
|
수: number,
|
|
|
|
|
after: boolean,
|
|
|
|
|
): void {
|
|
|
|
|
ctx.dispatch({ 종류: rows ? "행넣기" : "열넣기", 시트: id(ctx), at: after ? at + 수 : at, 수 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deleteLines(ctx: SpreadsheetContext, rows: boolean, at: number, 수: number): void {
|
|
|
|
|
ctx.dispatch({ 종류: rows ? "행지우기" : "열지우기", 시트: id(ctx), at, 수 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function rowInsertItems(ctx: SpreadsheetContext, at: number, 수: number): Item[] {
|
|
|
|
|
return [
|
|
|
|
|
[TEXT.rowsAbove(수), () => insertLines(ctx, true, at, 수, false)],
|
|
|
|
|
[TEXT.rowsBelow(수), () => insertLines(ctx, true, at, 수, true)],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function colInsertItems(ctx: SpreadsheetContext, at: number, 수: number): Item[] {
|
|
|
|
|
return [
|
|
|
|
|
[TEXT.colsLeft(수), () => insertLines(ctx, false, at, 수, false)],
|
|
|
|
|
[TEXT.colsRight(수), () => insertLines(ctx, false, at, 수, true)],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const shifts = () => import("./spreadsheet_insert_dialog");
|
|
|
|
|
|
|
|
|
|
/** 칸 메뉴 — 행 · 열 · 셀 삽입/삭제 ▸ */
|
|
|
|
|
export function cellInsertItems(ctx: SpreadsheetContext, g: CellRange): MapContextMenuItem[] {
|
|
|
|
|
const rows = g.r1 - g.r0 + 1;
|
|
|
|
|
const cols = g.c1 - g.c0 + 1;
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
label: TEXT.insertLines,
|
|
|
|
|
children: [...rowInsertItems(ctx, g.r0, rows), ...colInsertItems(ctx, g.c0, cols)],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: TEXT.insertCells,
|
|
|
|
|
children: [
|
|
|
|
|
[TEXT.shiftRight, () => void shifts().then((m) => m.insertShiftRight(ctx, g))],
|
|
|
|
|
[TEXT.shiftDown, () => void shifts().then((m) => m.insertShiftDown(ctx, g))],
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: TEXT.deleteLines,
|
|
|
|
|
children: [
|
|
|
|
|
[TEXT.deleteRows(rows), () => deleteLines(ctx, true, g.r0, rows)],
|
|
|
|
|
[TEXT.deleteCols(cols), () => deleteLines(ctx, false, g.c0, cols)],
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: TEXT.deleteCells,
|
|
|
|
|
children: [
|
|
|
|
|
[TEXT.shiftLeft, () => void shifts().then((m) => m.deleteShiftLeft(ctx, g))],
|
|
|
|
|
[TEXT.shiftUp, () => void shifts().then((m) => m.deleteShiftUp(ctx, g))],
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 몸통은 [데이터 ▾] 와 같은 늦은 길(`spreadsheet_actions_run.ts`) */
|
|
|
|
|
function run(ctx: SpreadsheetContext, act: Act): void {
|
|
|
|
|
void import("./spreadsheet_actions_run").then((m) => m.run(ctx, () => back(ctx), act));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function freezeGroupItems(ctx: SpreadsheetContext): MapContextMenuItem[] {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
label: TEXT.freeze,
|
|
|
|
|
children: [
|
|
|
|
|
[TEXT.freezeRow, () => run(ctx, "freezeRow")],
|
|
|
|
|
[TEXT.freezeCol, () => run(ctx, "freezeCol")],
|
|
|
|
|
[TEXT.freezeAt, () => run(ctx, "freezeAt")],
|
|
|
|
|
[TEXT.unfreeze, () => run(ctx, "unfreeze")],
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: TEXT.group,
|
|
|
|
|
children: [
|
|
|
|
|
[TEXT.groupOn, () => run(ctx, "group")],
|
|
|
|
|
[TEXT.groupOff, () => run(ctx, "ungroup")],
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 쓴 칸을 모두 덮는 범위(없으면 null) */
|
|
|
|
|
export function usedRange(sheet: Sheet): CellRange | null {
|
|
|
|
|
let g: CellRange | null = null;
|
|
|
|
|
for (const key of Object.keys(sheet.칸)) {
|
|
|
|
|
const p = parseA1(key);
|
|
|
|
|
if (!p) continue;
|
|
|
|
|
g = g
|
|
|
|
|
? {
|
|
|
|
|
r0: Math.min(g.r0, p.r),
|
|
|
|
|
c0: Math.min(g.c0, p.c),
|
|
|
|
|
r1: Math.max(g.r1, p.r),
|
|
|
|
|
c1: Math.max(g.c1, p.c),
|
|
|
|
|
}
|
|
|
|
|
: { r0: p.r, c0: p.c, r1: p.r, c1: p.c };
|
|
|
|
|
}
|
|
|
|
|
return g;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 열 머리에서 — 쓴 범위를 잠깐 골라 `job`(정렬 · 필터) 뒤 고름을 되돌림. 빈 열이면 안 함 */
|
|
|
|
|
async function onUsed(
|
|
|
|
|
ctx: SpreadsheetContext,
|
|
|
|
|
c: number,
|
|
|
|
|
job: () => void | Promise<void>,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const used = usedRange(ctx.sheet());
|
|
|
|
|
if (!used || c < used.c0 || c > used.c1) return;
|
|
|
|
|
const keep = ctx.selection;
|
|
|
|
|
const top = { r: used.r0, c };
|
|
|
|
|
ctx.select({ 시트: id(ctx), 범위: [used], 활성: top, 기준: top });
|
|
|
|
|
try {
|
|
|
|
|
await job();
|
|
|
|
|
} finally {
|
|
|
|
|
ctx.select(keep);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 시트 정렬 — 쓴 범위 전체를 이 열 기준으로(열 하나만 섞이지 않게) */
|
|
|
|
|
export function sortByColumn(ctx: SpreadsheetContext, c: number, desc: boolean): Promise<void> {
|
|
|
|
|
return onUsed(ctx, c, async () =>
|
|
|
|
|
(await import("./spreadsheet_sort_dialog")).sortSelection(ctx, desc),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterItem(ctx: SpreadsheetContext, c?: number): Item {
|
|
|
|
|
const on = !!ctx.sheet().필터;
|
|
|
|
|
const go = (): void => ctx.dataMenu?.("filter");
|
|
|
|
|
return [
|
|
|
|
|
on ? TEXT.filterOff : TEXT.filterOn,
|
|
|
|
|
() => (on || c === undefined ? go() : void onUsed(ctx, c, go)),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 숫자 · 글 하나 받는 작은 창 — 확인이 거르면(오류 글) 창이 남음 */
|
|
|
|
|
function askValue(
|
|
|
|
|
ctx: SpreadsheetContext,
|
|
|
|
|
title: string,
|
|
|
|
|
label: string,
|
|
|
|
|
value: string,
|
|
|
|
|
submit: (text: string) => string | null | Promise<string | null>,
|
|
|
|
|
): void {
|
|
|
|
|
const overlay = el("div", { className: "ui-confirm" });
|
|
|
|
|
const close = (): void => {
|
|
|
|
|
overlay.remove();
|
|
|
|
|
back(ctx);
|
|
|
|
|
};
|
|
|
|
|
const field = createInputField({ label, value });
|
|
|
|
|
const error = el("p", { text: "" });
|
|
|
|
|
error.style.cssText = "color:var(--color-danger, red);font-size:0.8rem;min-height:1em;";
|
|
|
|
|
const ok = async (): Promise<void> => {
|
|
|
|
|
const fail = await submit(field.input.value.trim());
|
|
|
|
|
if (fail) error.textContent = fail;
|
|
|
|
|
else close();
|
|
|
|
|
};
|
|
|
|
|
field.input.addEventListener("keydown", (e) => {
|
|
|
|
|
if (e.key === "Enter") void ok();
|
|
|
|
|
});
|
|
|
|
|
overlay.append(
|
|
|
|
|
el("div", {
|
|
|
|
|
className: "ui-confirm__panel",
|
|
|
|
|
children: [
|
|
|
|
|
el("h3", { text: title }),
|
|
|
|
|
field.root,
|
|
|
|
|
error,
|
|
|
|
|
el("div", {
|
|
|
|
|
className: "ui-confirm__actions",
|
|
|
|
|
children: [
|
|
|
|
|
createButton({ label: TEXT.cancel, variant: "ghost", onClick: close }),
|
|
|
|
|
createButton({ label: TEXT.ok, variant: "filled", onClick: () => void ok() }),
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
document.body.append(overlay);
|
|
|
|
|
dialogKeys(overlay, close);
|
|
|
|
|
field.input.focus();
|
|
|
|
|
field.input.select();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 행 높이 · 열 너비 바꾸기(px) — 고른 줄 모두 한 명령(한 번에 되돌림) · 잘못된 값이면 오류 글 */
|
|
|
|
|
export function resizeLines(
|
|
|
|
|
ctx: SpreadsheetContext,
|
|
|
|
|
rows: boolean,
|
|
|
|
|
lines: number[],
|
|
|
|
|
text: string,
|
|
|
|
|
): string | null {
|
|
|
|
|
const px = Number(text);
|
|
|
|
|
if (!Number.isFinite(px) || px < 1) return TEXT.badNumber;
|
|
|
|
|
ctx.dispatch(
|
|
|
|
|
rows
|
|
|
|
|
? { 종류: "행높이", 시트: id(ctx), 행: lines, 높이: Math.min(pxToPt(px), 409) }
|
|
|
|
|
: { 종류: "열폭", 시트: id(ctx), 열: lines, 폭: Math.min(pxToColChars(px), MAX_COL_CHARS) },
|
|
|
|
|
);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sizeItem(ctx: SpreadsheetContext, rows: boolean, lines: number[]): Item {
|
|
|
|
|
return [
|
|
|
|
|
rows ? TEXT.rowSize : TEXT.colSize,
|
|
|
|
|
() => {
|
|
|
|
|
const geo = (rows ? buildRowGeometry : buildColGeometry)(ctx.book, ctx.sheet());
|
|
|
|
|
askValue(
|
|
|
|
|
ctx,
|
|
|
|
|
rows ? TEXT.rowSize : TEXT.colSize,
|
|
|
|
|
rows ? TEXT.rowSizeLabel : TEXT.colSizeLabel,
|
|
|
|
|
String(Math.round(geo.size(lines[0]))),
|
|
|
|
|
(text) => resizeLines(ctx, rows, lines, text),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 열 너비를 데이터에 맞춤 — 고른 열 모두 한 명령 */
|
|
|
|
|
export async function autofitColumns(ctx: SpreadsheetContext, cols: number[]): Promise<void> {
|
|
|
|
|
const { estimateColumnWidth } = await import("./spreadsheet_autofit");
|
|
|
|
|
const 명령: Command[] = cols.map((c) => ({
|
|
|
|
|
종류: "열폭",
|
|
|
|
|
시트: id(ctx),
|
|
|
|
|
열: [c],
|
|
|
|
|
폭: estimateColumnWidth(ctx.book, id(ctx), c),
|
|
|
|
|
}));
|
|
|
|
|
ctx.dispatch({ 종류: "묶음", 명령 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 행 머리 끝 — 크기 · 고정 ▸ · 그룹 ▸ */
|
|
|
|
|
export function rowMoreItems(ctx: SpreadsheetContext, rows: number[]): MapContextMenuItem[] {
|
|
|
|
|
return [SEP, sizeItem(ctx, true, rows), SEP, ...freezeGroupItems(ctx)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 열 머리 끝 — 크기 · 데이터에 맞추기 · 시트 정렬 · 필터 · 고정 ▸ · 그룹 ▸ */
|
|
|
|
|
export function colMoreItems(ctx: SpreadsheetContext, cols: number[]): MapContextMenuItem[] {
|
|
|
|
|
const c = cols[0];
|
|
|
|
|
return [
|
|
|
|
|
SEP,
|
|
|
|
|
sizeItem(ctx, false, cols),
|
|
|
|
|
[TEXT.autofit, () => void autofitColumns(ctx, cols)],
|
|
|
|
|
SEP,
|
|
|
|
|
[TEXT.sortAsc, () => void sortByColumn(ctx, c, false)],
|
|
|
|
|
[TEXT.sortDesc, () => void sortByColumn(ctx, c, true)],
|
|
|
|
|
filterItem(ctx, c),
|
|
|
|
|
SEP,
|
|
|
|
|
...freezeGroupItems(ctx),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 칸 메뉴 끝 — 고정 ▸ · 그룹 ▸ · 셀 작업 더보기 ▸ */
|
|
|
|
|
export function cellMoreItems(ctx: SpreadsheetContext, g: CellRange): MapContextMenuItem[] {
|
|
|
|
|
return [
|
|
|
|
|
SEP,
|
|
|
|
|
...freezeGroupItems(ctx),
|
|
|
|
|
{
|
|
|
|
|
label: TEXT.more,
|
|
|
|
|
children: [
|
|
|
|
|
[TEXT.dropdown, () => ctx.dataMenu?.("validation")],
|
|
|
|
|
[TEXT.condfmt, () => ctx.dataMenu?.("condfmt")],
|
|
|
|
|
[
|
|
|
|
|
TEXT.nameRange,
|
|
|
|
|
() =>
|
|
|
|
|
askValue(ctx, TEXT.nameRange, TEXT.nameLabel, "", (name) => nameRange(ctx, name, g)),
|
|
|
|
|
],
|
|
|
|
|
filterItem(ctx),
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
SEP,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 범위 이름 — `spreadsheet_names.defineName`(오류 글 · 성공 null) */
|
|
|
|
|
export async function nameRange(
|
|
|
|
|
ctx: SpreadsheetContext,
|
|
|
|
|
name: string,
|
|
|
|
|
g: CellRange,
|
|
|
|
|
): Promise<string | null> {
|
|
|
|
|
if (!name) return TEXT.nameEmpty;
|
|
|
|
|
return (await import("./spreadsheet_names")).defineName(ctx, name, g);
|
|
|
|
|
}
|