- 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
504 lines
18 KiB
TypeScript
504 lines
18 KiB
TypeScript
/* =============================================================================
|
|
* spreadsheet_editor.ts (주인 D)
|
|
* 칸 편집기 · 수식 입력줄 · 주소 상자 · 한글 조합 · 참조 가리키기(식 편집 중 칸 · 범위를 눌러 넣기) ·
|
|
* 참조 칸 색 테두리.
|
|
* 한글: 숨은 textarea(`ta`) 가 늘 활성 칸 자리에서 초점을 쥠 → compositionstart · input 에서 편집을
|
|
* 시작하고 같은 textarea 가 그대로 칸 편집기가 됨(첫 글자 조합이 안 끊김 · keydown 글자 방식 금지).
|
|
* 방식(엑셀): enter = 바로 쳐서 시작(방향키가 확정 · 식이면 가리키기) · edit = F2 · 두 번 누르기 ·
|
|
* 수식 입력줄(방향키가 글자 사이를 옮김). F2 가 둘을 바꿈.
|
|
* ========================================================================== */
|
|
|
|
import { el } from "@ui/ui_template_elements";
|
|
import { moveFormula } from "./spreadsheet_refshift";
|
|
import { colName, parseA1, parseRange, rangeToA1, toA1 } from "./spreadsheet_address";
|
|
import { funcHelp, suggestFunctions, type FuncHelp } from "./spreadsheet_func_help";
|
|
import { tokenize } from "./spreadsheet_parser";
|
|
import { cellsOf, move, selectCell } from "./spreadsheet_selection";
|
|
import type { Cell, CellAddress, CellStyle, Sheet } from "./spreadsheet_types";
|
|
import type {
|
|
CellBox,
|
|
EditorHandle,
|
|
RefHighlight,
|
|
Selection,
|
|
SpreadsheetContext,
|
|
} from "./spreadsheet_view_types";
|
|
|
|
export interface Editor extends EditorHandle {
|
|
/** 숨은 textarea = 칸 편집기(늘 초점) */
|
|
ta: HTMLTextAreaElement;
|
|
/** 주소 상자 + 수식 입력줄 한 줄 */
|
|
bar: HTMLElement;
|
|
formula: HTMLTextAreaElement;
|
|
mode(): "enter" | "edit";
|
|
toggleMode(): void;
|
|
/** 지금 글자 자리에 참조를 넣을 수 있나(식 · 연산자 뒤) */
|
|
pointable(): boolean;
|
|
/** 가리킨 고름을 식 글에 넣음(같은 가리키기면 바꿔 적음) */
|
|
point(sel: Selection): void;
|
|
/** 방향키 가리키기 — 지금 가리킨 곳(없으면 편집 칸)에서 */
|
|
pointMove(dr: number, dc: number, extend: boolean, jump: boolean): void;
|
|
/** 함수 자동 완성 목록이 떠 있으면 고른 이름을 넣음(Tab) */
|
|
accept(): boolean;
|
|
/** 자동 완성 목록 안 위아래(목록이 없으면 false) */
|
|
suggestMove(d: number): boolean;
|
|
/** 고름 칸 전부에 같은 입력(Ctrl+Enter) — 식은 활성 칸 기준 상대 이동 */
|
|
commitAll(): void;
|
|
/** 초점 되찾기(스크롤 없이) */
|
|
focus(): void;
|
|
/** 편집 중이면 칸 자리 다시(스크롤 · 폭 바뀜) */
|
|
place(): void;
|
|
destroy(): void;
|
|
}
|
|
|
|
interface EditState {
|
|
sheet: string;
|
|
at: CellAddress;
|
|
mode: "enter" | "edit";
|
|
original: string;
|
|
/** 가리키기 중 — 식 글 [start, end) 가 가리킨 참조 */
|
|
point: { start: number; end: number; sel: Selection } | null;
|
|
}
|
|
|
|
/** 수 입력 — 1,234 · -1.5e3 · 50% (엑셀 입력 규칙) · 아니면 null */
|
|
export function parseNumber(text: string): number | null {
|
|
let t = text.trim();
|
|
const pct = t.endsWith("%");
|
|
if (pct) t = t.slice(0, -1);
|
|
if (!/\d/.test(t) || !/^[+-]?(\d{1,3}(,\d{3})+|\d*)(\.\d*)?([eE][+-]?\d+)?$/.test(t)) return null;
|
|
const n = Number(t.replace(/,/g, ""));
|
|
if (!Number.isFinite(n)) return null;
|
|
return pct ? n / 100 : n;
|
|
}
|
|
|
|
const looksTyped = (s: string): boolean =>
|
|
(s.length > 1 && s[0] === "=") ||
|
|
s[0] === "'" ||
|
|
parseNumber(s) !== null ||
|
|
/^(true|false)$/i.test(s);
|
|
|
|
/** 친 글 → 칸(서식은 두고) · 지울 칸이면 null */
|
|
export function textToCell(text: string, old?: Cell): Cell | null {
|
|
const base: Cell = old?.서식 !== undefined ? { 서식: old.서식 } : {};
|
|
if (text === "") return old?.서식 !== undefined ? base : null;
|
|
if (text.length > 1 && text[0] === "=") return { ...base, 식: text.slice(1) };
|
|
if (text[0] === "'") return { ...base, 값: text.slice(1) };
|
|
const n = parseNumber(text);
|
|
if (n !== null) return { ...base, 값: n };
|
|
if (/^(true|false)$/i.test(text)) return { ...base, 값: text.toUpperCase() === "TRUE" };
|
|
return { ...base, 값: text };
|
|
}
|
|
|
|
/** 칸 → 편집 글(식은 `=` 붙임 · 수처럼 읽힐 글은 `'` 붙임) */
|
|
export function cellToText(cell: Cell | undefined): string {
|
|
if (!cell) return "";
|
|
if (cell.식 !== undefined) return "=" + cell.식;
|
|
const v = cell.값;
|
|
if (v === undefined) return "";
|
|
if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
|
|
if (typeof v === "number") return String(v);
|
|
return looksTyped(v) ? "'" + v : v;
|
|
}
|
|
|
|
/** 참조 넣을 자리 — 식이고 caret 앞(빈칸 건너)이 연산자 · 괄호 · 쉼표 · `=` */
|
|
export function insertableAt(text: string, pos: number): boolean {
|
|
if (text[0] !== "=") return false;
|
|
if (pos < text.length && /[A-Za-z0-9$_.!']/.test(text[pos])) return false;
|
|
let i = pos - 1;
|
|
while (i >= 0 && text[i] === " ") i--;
|
|
return i >= 0 && "=(,+-*/^&<>:;".includes(text[i]);
|
|
}
|
|
|
|
/** 식 글 참조 → 색 테두리 목록(같은 글이면 같은 색) */
|
|
export function refHighlights(
|
|
text: string,
|
|
host: string,
|
|
sheetId: (name: string) => string | null,
|
|
): RefHighlight[] {
|
|
if (text[0] !== "=") return [];
|
|
const out: RefHighlight[] = [];
|
|
const colors = new Map<string, number>();
|
|
for (const t of tokenize(text.slice(1))) {
|
|
if (t.kind !== "ref") continue;
|
|
const bang = t.text.lastIndexOf("!");
|
|
let sheet: string | null = host;
|
|
if (bang >= 0) {
|
|
const raw = t.text.slice(0, bang);
|
|
const name = raw[0] === "'" ? raw.slice(1, -1).replace(/''/g, "'") : raw;
|
|
sheet = sheetId(name);
|
|
}
|
|
const range = parseRange(t.text.slice(bang + 1).replace(/\$/g, ""));
|
|
if (!sheet || !range) continue;
|
|
const key = t.text.toUpperCase();
|
|
if (!colors.has(key)) colors.set(key, colors.size);
|
|
out.push({ 시트: sheet, 범위: range, 색번호: colors.get(key)! });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 칸 서식(칸 → 행 → 열 → 기본) */
|
|
function styleAt(ctx: SpreadsheetContext, sheet: Sheet, r: number, c: number): CellStyle {
|
|
const idx =
|
|
sheet.칸[toA1(r, c)]?.서식 ??
|
|
sheet.행?.[String(r + 1)]?.서식 ??
|
|
sheet.열?.[colName(c)]?.서식 ??
|
|
0;
|
|
return ctx.book.서식[idx] ?? {};
|
|
}
|
|
|
|
/** 식 글 caret 앞 → 함수 이름 치는 중(prefix) · 함수 인자 안(func · 몇 번째 인자 0 부터) · 아님 null */
|
|
export function formulaHint(
|
|
pre: string,
|
|
): { prefix: string } | { func: string; arg: number } | null {
|
|
if (pre[0] !== "=") return null;
|
|
const m = /(?:^=|[(,+\-*/^&<>:;= ])([A-Za-z][A-Za-z0-9.]*)$/.exec(pre);
|
|
if (m && !/^\$?[A-Za-z]{1,3}\$?\d+$/.test(m[1])) return { prefix: m[1] };
|
|
let depth = 0;
|
|
let arg = 0;
|
|
let inStr = false;
|
|
for (let i = pre.length - 1; i > 0; i--) {
|
|
const ch = pre[i];
|
|
if (ch === '"') inStr = !inStr;
|
|
if (inStr) continue;
|
|
if (ch === ")") depth++;
|
|
else if (ch === "," && depth === 0) arg++;
|
|
else if (ch === "(") {
|
|
if (depth-- > 0) continue;
|
|
const n = /([A-Za-z][A-Za-z0-9.]*)$/.exec(pre.slice(0, i));
|
|
return n ? { func: n[1].toUpperCase(), arg } : null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 편집기 · 미리보기 자리 — C `editorSlot` 의 box 는 격자 뿌리 기준이라 층(layer)이 뿌리 안에서
|
|
* 비켜 놓인 만큼 빼서 층 안 좌표로 맞춤. */
|
|
export function slotOf(
|
|
ctx: SpreadsheetContext,
|
|
r: number,
|
|
c: number,
|
|
): { layer: HTMLElement; box: CellBox } {
|
|
const { layer, box } = ctx.grid.editorSlot(r, c);
|
|
const a = layer.getBoundingClientRect();
|
|
const g = ctx.grid.root.getBoundingClientRect();
|
|
return { layer, box: { ...box, x: box.x - (a.left - g.left), y: box.y - (a.top - g.top) } };
|
|
}
|
|
|
|
/** 칸 자리 → 층 안 자리 */
|
|
export function boxStyle(node: HTMLElement, box: CellBox): void {
|
|
node.style.left = `${box.x}px`;
|
|
node.style.top = `${box.y}px`;
|
|
node.style.minWidth = `${box.w}px`;
|
|
node.style.minHeight = `${box.h}px`;
|
|
}
|
|
|
|
export function createEditor(ctx: SpreadsheetContext): Editor {
|
|
const ta = el("textarea", {
|
|
className: "ss-input ss-input--idle",
|
|
attrs: { spellcheck: "false", autocomplete: "off", rows: "1", "aria-label": "칸 입력" },
|
|
});
|
|
const addr = el("input", {
|
|
className: "ss-addr",
|
|
attrs: { spellcheck: "false", "aria-label": "주소 상자" },
|
|
});
|
|
const formula = el("textarea", {
|
|
className: "ss-formula",
|
|
attrs: { spellcheck: "false", rows: "1", "aria-label": "수식 입력줄" },
|
|
});
|
|
const bar = el("div", {
|
|
className: "ss-bar",
|
|
children: [addr, el("span", { className: "ss-fx", text: "fx" }), formula],
|
|
});
|
|
formula.readOnly = ctx.readOnly;
|
|
ta.readOnly = ctx.readOnly;
|
|
|
|
let st: EditState | null = null;
|
|
|
|
const src = (): HTMLTextAreaElement => (document.activeElement === formula ? formula : ta);
|
|
const text = (): string => src().value;
|
|
const sheetOf = (id: string): Sheet => ctx.book.시트.find((s) => s.id === id) ?? ctx.sheet();
|
|
const sheetId = (name: string): string | null =>
|
|
ctx.book.시트.find((s) => s.이름.toUpperCase() === name.toUpperCase())?.id ?? null;
|
|
|
|
function place(): void {
|
|
const at = st?.at ?? ctx.selection.활성;
|
|
const { layer, box } = slotOf(ctx, at.r, at.c);
|
|
if (ta.parentElement !== layer) {
|
|
const had = document.activeElement === ta;
|
|
layer.append(ta);
|
|
if (had) focus();
|
|
}
|
|
boxStyle(ta, box);
|
|
if (!st) return;
|
|
const s = styleAt(ctx, sheetOf(st.sheet), at.r, at.c);
|
|
ta.style.fontFamily = s.글꼴 ?? "";
|
|
ta.style.fontSize = s.크기 ? `${s.크기}pt` : "";
|
|
ta.style.fontWeight = s.굵게 ? "bold" : "";
|
|
ta.style.fontStyle = s.기울임 ? "italic" : "";
|
|
ta.style.textAlign = s.가로 === "right" || s.가로 === "center" ? s.가로 : "";
|
|
// 글이 길면 오른쪽 · 아래로 늘림(엑셀)
|
|
ta.style.width = "0";
|
|
ta.style.height = "0";
|
|
ta.style.width = `${Math.max(box.w, ta.scrollWidth + 4)}px`;
|
|
ta.style.height = `${Math.max(box.h, ta.scrollHeight)}px`;
|
|
}
|
|
|
|
function focus(): void {
|
|
if (document.activeElement !== ta) ta.focus({ preventScroll: true });
|
|
}
|
|
|
|
function highlight(): void {
|
|
const t = st ? text() : "";
|
|
ctx.grid.setRefHighlights(st ? refHighlights(t, st.sheet, sheetId) : []);
|
|
showHint();
|
|
}
|
|
|
|
// ── 함수 자동 완성 · 인자 도움말 (spreadsheet_func_help · sub1) ────────────────
|
|
const hint = el("div", { className: "ss-hint", attrs: { hidden: "" } });
|
|
let sugg: FuncHelp[] = [];
|
|
let pick = 0;
|
|
let typed = "";
|
|
|
|
function showHint(): void {
|
|
const node = src();
|
|
const h = st ? formulaHint(node.value.slice(0, node.selectionEnd)) : null;
|
|
sugg = h && "prefix" in h ? suggestFunctions(h.prefix).slice(0, 8) : [];
|
|
typed = h && "prefix" in h ? h.prefix : "";
|
|
const help = h && "func" in h ? funcHelp(h.func) : null;
|
|
if (!sugg.length && !help) return void hint.setAttribute("hidden", "");
|
|
pick = Math.min(pick, Math.max(0, sugg.length - 1));
|
|
hint.replaceChildren(
|
|
...(sugg.length
|
|
? sugg.map((f, i) => {
|
|
const row = el("div", {
|
|
className: i === pick ? "ss-hint__item is-on" : "ss-hint__item",
|
|
children: [el("b", { text: f.이름 }), ` ${f.뜻}`],
|
|
});
|
|
row.addEventListener("mousedown", (e) => {
|
|
e.preventDefault();
|
|
pick = i;
|
|
accept();
|
|
});
|
|
return row;
|
|
})
|
|
: [el("div", { className: "ss-hint__help", text: `${help!.꼴} — ${help!.뜻}` })]),
|
|
);
|
|
if (hint.parentElement !== ta.parentElement) ta.parentElement?.append(hint);
|
|
hint.style.left = ta.style.left;
|
|
hint.style.top = `${parseFloat(ta.style.top || "0") + ta.offsetHeight}px`;
|
|
hint.removeAttribute("hidden");
|
|
}
|
|
|
|
function accept(): boolean {
|
|
if (!st || !sugg.length) return false;
|
|
const node = src();
|
|
const pos = node.selectionEnd;
|
|
const name = `${sugg[pick].이름}(`;
|
|
node.setRangeText(name, pos - typed.length, pos, "end");
|
|
(node === ta ? formula : ta).value = node.value;
|
|
pick = 0;
|
|
place();
|
|
highlight();
|
|
return true;
|
|
}
|
|
|
|
/** 편집 시작 — `value` 없으면 textarea 에 이미 든 글(조합 · 바로 친 글자) 그대로 */
|
|
function start(mode: "enter" | "edit", value?: string, from: HTMLTextAreaElement = ta): void {
|
|
if (ctx.readOnly || st) return;
|
|
const sel = ctx.selection;
|
|
const sheet = ctx.sheet();
|
|
const original = cellToText(sheet.칸[toA1(sel.활성.r, sel.활성.c)]);
|
|
st = { sheet: sheet.id, at: { ...sel.활성 }, mode, original, point: null };
|
|
if (value !== undefined) from.value = value;
|
|
(from === ta ? formula : ta).value = from.value;
|
|
ta.classList.remove("ss-input--idle");
|
|
ctx.root.classList.add("ss--editing");
|
|
place();
|
|
highlight();
|
|
if (from === ta && value !== undefined) ta.setSelectionRange(ta.value.length, ta.value.length);
|
|
}
|
|
|
|
function end(): void {
|
|
st = null;
|
|
ta.value = "";
|
|
ta.classList.add("ss-input--idle");
|
|
ta.removeAttribute("style");
|
|
ctx.root.classList.remove("ss--editing");
|
|
ctx.grid.setRefHighlights([]);
|
|
hint.setAttribute("hidden", "");
|
|
sync();
|
|
focus();
|
|
}
|
|
|
|
function write(cells: CellAddress[], value: string, from: CellAddress): void {
|
|
if (!st) return;
|
|
const sheet = sheetOf(st.sheet);
|
|
const out: Record<string, Cell | null> = {};
|
|
for (const a of cells) {
|
|
let v = value;
|
|
if (v.length > 1 && v[0] === "=" && (a.r !== from.r || a.c !== from.c))
|
|
v = "=" + moveFormula(v.slice(1), a.r - from.r, a.c - from.c);
|
|
const key = toA1(a.r, a.c);
|
|
const next = textToCell(v, sheet.칸[key]);
|
|
if (next === null && !sheet.칸[key]) continue;
|
|
out[key] = next;
|
|
}
|
|
if (Object.keys(out).length) ctx.dispatch({ 종류: "칸", 시트: sheet.id, 칸: out });
|
|
}
|
|
|
|
function commit(): void {
|
|
if (!st) return;
|
|
const value = text();
|
|
if (value !== st.original) write([st.at], value, st.at);
|
|
end();
|
|
}
|
|
|
|
function commitAll(): void {
|
|
if (!st) return;
|
|
write(cellsOf(sheetOf(st.sheet), ctx.selection), text(), st.at);
|
|
end();
|
|
}
|
|
|
|
function cancel(): void {
|
|
if (!st) return;
|
|
end();
|
|
}
|
|
|
|
function sync(): void {
|
|
const sel = ctx.selection;
|
|
addr.value = toA1(sel.활성.r, sel.활성.c);
|
|
if (st) return;
|
|
formula.value = cellToText(ctx.sheet().칸[toA1(sel.활성.r, sel.활성.c)]);
|
|
place();
|
|
}
|
|
|
|
function pointable(): boolean {
|
|
if (!st) return false;
|
|
const node = src();
|
|
if (node.selectionStart !== node.selectionEnd) return false;
|
|
const pos = node.selectionEnd;
|
|
if (st.point && st.point.end === pos) return true;
|
|
st.point = null;
|
|
return insertableAt(node.value, pos);
|
|
}
|
|
|
|
function point(sel: Selection): void {
|
|
if (!st) return;
|
|
const node = src();
|
|
const other = node === ta ? formula : ta;
|
|
const pos = node.selectionEnd;
|
|
const p = st.point ?? { start: pos, end: pos, sel };
|
|
const g = sel.범위[0];
|
|
const ref = rangeToA1(g);
|
|
node.value = node.value.slice(0, p.start) + ref + node.value.slice(p.end);
|
|
other.value = node.value;
|
|
st.point = { start: p.start, end: p.start + ref.length, sel };
|
|
node.setSelectionRange(st.point.end, st.point.end);
|
|
place();
|
|
highlight();
|
|
ctx.grid.reveal(sel.활성.r === g.r0 ? g.r1 : g.r0, sel.활성.c === g.c0 ? g.c1 : g.c0);
|
|
}
|
|
|
|
function pointMove(dr: number, dc: number, extend: boolean, jump: boolean): void {
|
|
if (!st) return;
|
|
const sheet = sheetOf(st.sheet);
|
|
const from = st.point?.sel ?? selectCell(sheet, st.at.r, st.at.c);
|
|
point(move(sheet, from, dr, dc, { extend, jump }));
|
|
}
|
|
|
|
// ── 한글 조합 · 바로 치기 → 편집 시작 ─────────────────────────────────────
|
|
ta.addEventListener("compositionstart", () => {
|
|
if (!st) start("enter");
|
|
});
|
|
ta.addEventListener("input", () => {
|
|
if (!st) return start("enter");
|
|
formula.value = ta.value;
|
|
if (st.point && ta.selectionEnd !== st.point.end) st.point = null;
|
|
place();
|
|
highlight();
|
|
});
|
|
// 수식 입력줄 — 누르면 edit 방식으로 시작
|
|
formula.addEventListener("focus", () => {
|
|
if (!st && !ctx.readOnly) start("edit", formula.value, formula);
|
|
});
|
|
formula.addEventListener("input", () => {
|
|
if (!st) return start("edit", undefined, formula);
|
|
ta.value = formula.value;
|
|
if (st.point && formula.selectionEnd !== st.point.end) st.point = null;
|
|
place();
|
|
highlight();
|
|
});
|
|
// 주소 상자 — `B5` · `A1:C3` · `시트!B5` 로 가기
|
|
addr.addEventListener("focus", () => addr.select());
|
|
addr.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
sync();
|
|
focus();
|
|
}
|
|
if (e.key !== "Enter") return;
|
|
e.preventDefault();
|
|
goTo(addr.value.trim());
|
|
focus();
|
|
});
|
|
|
|
function goTo(value: string): void {
|
|
const bang = value.lastIndexOf("!");
|
|
let sheet = ctx.sheet();
|
|
if (bang >= 0) {
|
|
const raw = value.slice(0, bang);
|
|
const id = sheetId(raw[0] === "'" ? raw.slice(1, -1).replace(/''/g, "'") : raw);
|
|
if (!id) return sync();
|
|
if (id !== sheet.id) ctx.showSheet(id);
|
|
sheet = ctx.sheet();
|
|
}
|
|
const body = value.slice(bang + 1).replace(/\$/g, "");
|
|
const one = parseA1(body);
|
|
const g = parseRange(body);
|
|
if (!g) return sync();
|
|
if (one) return ctx.select(selectCell(sheet, one.r, one.c), true);
|
|
const at = { r: g.r0, c: g.c0 };
|
|
ctx.select({ 시트: sheet.id, 범위: [g], 활성: at, 기준: { ...at } }, true);
|
|
}
|
|
|
|
return {
|
|
ta,
|
|
bar,
|
|
formula,
|
|
editing: () => st !== null,
|
|
begin(initial?: string) {
|
|
if (initial === undefined) {
|
|
const sel = ctx.selection;
|
|
start("edit", cellToText(ctx.sheet().칸[toA1(sel.활성.r, sel.활성.c)]));
|
|
} else start("enter", initial);
|
|
focus();
|
|
},
|
|
commit,
|
|
commitAll,
|
|
cancel,
|
|
sync,
|
|
mode: () => st?.mode ?? "enter",
|
|
toggleMode() {
|
|
if (st) ((st.mode = st.mode === "enter" ? "edit" : "enter"), (st.point = null));
|
|
},
|
|
pointable,
|
|
point,
|
|
pointMove,
|
|
accept,
|
|
suggestMove(d: number) {
|
|
if (!st || !sugg.length) return false;
|
|
pick = (pick + d + sugg.length) % sugg.length;
|
|
showHint();
|
|
return true;
|
|
},
|
|
focus,
|
|
place() {
|
|
if (st) place();
|
|
},
|
|
destroy() {
|
|
hint.remove();
|
|
ta.remove();
|
|
bar.remove();
|
|
},
|
|
};
|
|
}
|