- spreadsheet.ts — 가짜 부품 자리(parts) 걷고 A · B · C · E 직접 잇기 · 2단계는 spreadsheet_extras.ts 로 - spreadsheet_extras.ts(새) — 메모 풍선(마우스 올림) · Shift+F2 메모 편집 · 서식 붓 단추 · Ctrl+Shift+V 값만 · 서식만 · 수식만 · 도구 모음 뒤 초점 되돌림 - spreadsheet_find_panel.ts(새) — Ctrl+F · Shift+F5 찾기 · Ctrl+H 바꾸기 패널 - spreadsheet_editor.ts — C editorSlot(격자 뿌리 기준) → 층 안 좌표 맞춤(slotOf) · 함수 자동 완성(Tab) · 인자 도움말 - spreadsheet_keys.ts · spreadsheet_mouse.ts — 2단계 잇기 자리 · 열 경계 두 번 누르면 폭 자동 맞춤 - 시험 — 함수 도움말 자리 판정 추가(8 묶음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6KXAabDTenEU7hrDQmCKY
132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
/* =============================================================================
|
||
* spreadsheet_find_panel.ts (주인 D)
|
||
* 찾기(Ctrl+F) · 바꾸기(Ctrl+H) 작은 패널 — 부품 오른위에 뜸. 찾기 규칙은 `spreadsheet_find.ts`(sub1).
|
||
* Enter = 다음 · Esc = 닫고 격자로. 바꾸기 = 활성 칸이 맞으면 바꾸고 다음으로(엑셀).
|
||
* ========================================================================== */
|
||
|
||
import { el } from "@ui/ui_template_elements";
|
||
import { findAll, findNext, replaceAll, replaceOne, type FindOptions } from "./spreadsheet_find";
|
||
import { selectCell } from "./spreadsheet_selection";
|
||
import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types";
|
||
|
||
const TEXT = {
|
||
find: "찾을 내용",
|
||
replace: "바꿀 내용",
|
||
next: "다음",
|
||
one: "바꾸기",
|
||
all: "모두 바꾸기",
|
||
close: "닫기",
|
||
case: "대소문자",
|
||
whole: "전체 칸",
|
||
formula: "수식 안",
|
||
none: "찾을 수 없음",
|
||
replaced: "{n} 개 바꿈",
|
||
};
|
||
|
||
export interface FindPanelHandle extends PartHandle {
|
||
open(replace: boolean): void;
|
||
close(): void;
|
||
}
|
||
|
||
export function mountFindPanel(ctx: SpreadsheetContext, back: () => void): FindPanelHandle {
|
||
const query = el("input", { className: "ss-find__input", attrs: { placeholder: TEXT.find } });
|
||
const repl = el("input", { className: "ss-find__input", attrs: { placeholder: TEXT.replace } });
|
||
const status = el("span", { className: "ss-find__status" });
|
||
const button = (text: string, onClick: () => void): HTMLButtonElement => {
|
||
const b = el("button", { className: "ss-find__btn", text, attrs: { type: "button" } });
|
||
b.addEventListener("click", onClick);
|
||
return b;
|
||
};
|
||
const check = (text: string): HTMLInputElement => {
|
||
const box = el("input", { attrs: { type: "checkbox" } });
|
||
options.append(el("label", { className: "ss-find__opt", children: [box, text] }));
|
||
return box;
|
||
};
|
||
const closeBtn = button("×", close);
|
||
closeBtn.title = TEXT.close;
|
||
const options = el("div", { className: "ss-find__row" });
|
||
const caseBox = check(TEXT.case);
|
||
const wholeBox = check(TEXT.whole);
|
||
const formulaBox = check(TEXT.formula);
|
||
const replaceRow = el("div", {
|
||
className: "ss-find__row",
|
||
children: [repl, button(TEXT.one, one), button(TEXT.all, all)],
|
||
});
|
||
const root = el("div", {
|
||
className: "ss-find",
|
||
attrs: { hidden: "" },
|
||
children: [
|
||
el("div", {
|
||
className: "ss-find__row",
|
||
children: [query, button(TEXT.next, next), closeBtn],
|
||
}),
|
||
replaceRow,
|
||
options,
|
||
status,
|
||
],
|
||
});
|
||
ctx.root.append(root);
|
||
|
||
const opts = (): FindOptions => ({
|
||
대소문자: caseBox.checked,
|
||
전체칸: wholeBox.checked,
|
||
수식안: formulaBox.checked,
|
||
});
|
||
const here = () => ({ 시트: ctx.selection.시트, ...ctx.selection.활성 });
|
||
|
||
function next(): void {
|
||
status.textContent = "";
|
||
if (!query.value) return;
|
||
const hit = findNext(ctx.book, here(), query.value, opts());
|
||
if (!hit) return void (status.textContent = TEXT.none);
|
||
if (hit.시트 !== ctx.selection.시트) ctx.showSheet(hit.시트);
|
||
ctx.select(selectCell(ctx.sheet(), hit.r, hit.c), true);
|
||
}
|
||
|
||
function one(): void {
|
||
if (ctx.readOnly || !query.value) return;
|
||
const at = here();
|
||
const onIt = findAll(ctx.book, query.value, opts(), [at.시트]).some(
|
||
(h) => h.r === at.r && h.c === at.c,
|
||
);
|
||
if (onIt) replaceOne(ctx, at, query.value, repl.value, opts());
|
||
next();
|
||
}
|
||
|
||
function all(): void {
|
||
if (ctx.readOnly || !query.value) return;
|
||
const n = replaceAll(ctx, query.value, repl.value, opts());
|
||
status.textContent = n ? TEXT.replaced.replace("{n}", String(n)) : TEXT.none;
|
||
}
|
||
|
||
function open(replace: boolean): void {
|
||
replaceRow.hidden = !replace || ctx.readOnly;
|
||
root.removeAttribute("hidden");
|
||
status.textContent = "";
|
||
query.focus();
|
||
query.select();
|
||
}
|
||
|
||
function close(): void {
|
||
root.setAttribute("hidden", "");
|
||
back();
|
||
}
|
||
|
||
const onKey = (e: KeyboardEvent): void => {
|
||
if (e.key === "Escape") close();
|
||
else if (e.key === "Enter") e.target === repl ? one() : next();
|
||
else return;
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
};
|
||
root.addEventListener("keydown", onKey);
|
||
|
||
return {
|
||
root,
|
||
open,
|
||
close,
|
||
refresh() {},
|
||
destroy: () => root.remove(),
|
||
};
|
||
}
|