- 셀 서식 창 = 부품 모달(40rem) · 탭(32) · 칸 = 부품 입력 · 고르기 28 · 자체 판 · 탭 · 칸 CSS 지움 - 메모 편집 = 공용 확인창 판(.ui-confirm) · 글 칸 = 부품 입력 - 찾기 · 바꾸기 = 떠 있는 판(머리 없음) · 칸 24 · 단추 22 · 닫기 = 아이콘 단추 - 데이터 패널 창(조건부 서식 · 유효성 · 이름) = 떠 있는 판(머리 · 닫기 ✕) - 오류 ⚠ = 아이콘 단추(선 18) · 골라 붙여넣기 · 정렬 칸 · 알약 찾기 칸 = 부품 단추 · 고르기 · 입력 - 옛 보라 별칭 · 날 검정 가림막 → 공통 값(hover · accent · scrim) · 창 닫힘 감시가 모달도 기다림 - 날 단추 23 → 15(남음 = 수식 줄 칸 단추 · 격자 안 ▼ · 알약) · 자체 꼴 규칙 95 → 77 · 격자 색 전후 같음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
4.6 KiB
TypeScript
135 lines
4.6 KiB
TypeScript
/* =============================================================================
|
||
* spreadsheet_find_panel.ts (주인 D)
|
||
* 찾기(Ctrl+F) · 바꾸기(Ctrl+H) 작은 패널 — 부품 오른위에 뜸. 찾기 규칙은 `spreadsheet_find.ts`(sub1).
|
||
* Enter = 다음 · Esc = 닫고 격자로. 바꾸기 = 활성 칸이 맞으면 바꾸고 다음으로(엑셀).
|
||
* ========================================================================== */
|
||
|
||
import { createButton, createIconButton, el, setUiSize } from "@ui/ui_template_elements";
|
||
import { createFloatPanel } from "@ui/ui_template_float_panel";
|
||
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 field = (placeholder: string): HTMLInputElement =>
|
||
setUiSize(el("input", { className: "ui-input ss-find__input", attrs: { placeholder } }), 24);
|
||
const query = field(TEXT.find);
|
||
const repl = field(TEXT.replace);
|
||
const status = el("span", { className: "ss-find__status" });
|
||
const button = (text: string, onClick: () => void): HTMLButtonElement => {
|
||
const b = createButton({ label: text, variant: "ghost", size: 22, onClick });
|
||
b.classList.add("ss-find__btn");
|
||
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 = createIconButton({ glyph: "×", title: TEXT.close, size: 22, onClick: close });
|
||
closeBtn.classList.add("ss-find__btn");
|
||
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)],
|
||
});
|
||
// 떠 있는 판 부품(머리 없음 · 553-6) — 자리는 `.ss-find`
|
||
const { root, body } = createFloatPanel({ popover: true });
|
||
root.classList.add("ss-find");
|
||
root.setAttribute("hidden", "");
|
||
body.append(
|
||
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(),
|
||
};
|
||
}
|