- 「T1 도면 E7:J17」 + 공용 꼬리표 상태(값 맞음 · 값 다름 · 도면에 표 없음 · 범위 없음) · 테두리 = 시트 위 점선 색 - 상태는 지금 책으로 판정(areaTable → sameValues) · 값이 바뀌면 150ms 모아 다시 - × = 기존 [보내기 취소](화면에서만 · 면적 저장 때 기록) · 시트 위 이름표 그대로 - 보낸 범위 몫을 spreadsheet_link_range.ts 로 나눔(700줄) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
162 lines
6.6 KiB
TypeScript
162 lines
6.6 KiB
TypeScript
/* =============================================================================
|
||
* spreadsheet_link_range.ts (주인 D)
|
||
* 보낸 범위(`LinkOptions.표시범위` · 예: 도면 표) 몫 — `spreadsheet_links` 가 씀.
|
||
* 지금 범위 풀기(104-1 · 이름 있으면 이름이 가리키는 범위) · 색 열쇠 ·
|
||
* 아래 알약 막대 알약(114-3 · 「T1 도면 E7:J17」 + 상태 꼬리표 · 색 = 시트 위 테두리 색 · 누르면 범위 고름 ·
|
||
* × = 보내기 취소) · 상태 모음(값이 바뀌면 150ms 모았다 호스트 `상태` 를 한 번에 부름).
|
||
* ========================================================================== */
|
||
|
||
import { createTag, el } from "@ui/ui_template_elements";
|
||
import { parseRange, quoteSheet, rangeToA1 } from "./spreadsheet_address";
|
||
import { linkHues } from "./spreadsheet_link_color";
|
||
import type { LinkOptions } from "./spreadsheet_links";
|
||
import { resolveName } from "./spreadsheet_names";
|
||
import { selectSpan } from "./spreadsheet_selection";
|
||
import type { CellRange, Workbook } from "./spreadsheet_types";
|
||
import type { SpreadsheetContext } from "./spreadsheet_view_types";
|
||
|
||
export type RangeMark = NonNullable<LinkOptions["표시범위"]>[number];
|
||
|
||
/** 보낸 범위 상태(114-3) — 꼬리표 글 · 색 갈래(공용 `createTag`) · 마우스 올림 글 */
|
||
export interface RangeState {
|
||
글: string;
|
||
종류: "success" | "warning" | "danger";
|
||
설명?: string;
|
||
}
|
||
|
||
const STATE_MS = 150; // 칸 고침이 이어지는 동안 모았다가 한 번에
|
||
|
||
/** 보낸 범위의 지금 범위 — `범위이름` 있으면 그 이름이 지금 가리키는 범위로(행열 넣기 · 지우기를 따라감) ·
|
||
* 이름이 지워졌으면 null(서버가 다음 저장 때 연결을 끊음) · 이름이 없으면(옛 연결) 적힌 `범위` 그대로 */
|
||
export function markRange(book: Workbook, mark: RangeMark): { 시트: string; g: CellRange } | null {
|
||
if (mark.범위이름) {
|
||
const found = resolveName(book, mark.시트, mark.범위이름);
|
||
return found && { 시트: found.시트, g: found.범위 };
|
||
}
|
||
const g = parseRange(mark.범위);
|
||
return g && { 시트: mark.시트, g };
|
||
}
|
||
|
||
/** 색 열쇠 — 범위 이름 없으면 시트!범위(표마다 다른 색 · 알약 색과는 다른 자리) */
|
||
export const markKey = (m: RangeMark): string => m.범위이름 ?? `${m.시트}!${m.범위}`;
|
||
|
||
/** 상태 모음 — `schedule` 마다 150ms 모았다 `상태` 있는 표시마다 부름 · 늦은 답은 버림 · 끝나면 `done` */
|
||
export function createRangeStates(
|
||
marks: () => RangeMark[],
|
||
book: () => Workbook,
|
||
done: () => void,
|
||
): { get: (m: RangeMark) => RangeState | undefined; schedule: () => void; stop: () => void } {
|
||
const got = new Map<RangeMark, RangeState>();
|
||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||
let seq = 0;
|
||
async function run(): Promise<void> {
|
||
const mine = ++seq;
|
||
const list = marks().filter((m) => m.상태);
|
||
const b = book();
|
||
const out = await Promise.all(
|
||
list.map((m) =>
|
||
Promise.resolve()
|
||
.then(() => m.상태!(b))
|
||
.catch(() => undefined),
|
||
),
|
||
);
|
||
if (mine !== seq) return;
|
||
got.clear();
|
||
list.forEach((m, i) => out[i] && got.set(m, out[i]!));
|
||
done();
|
||
}
|
||
return {
|
||
get: (m) => got.get(m),
|
||
schedule: () => {
|
||
if (!marks().some((m) => m.상태)) return;
|
||
clearTimeout(timer);
|
||
timer = setTimeout(() => void run(), STATE_MS);
|
||
},
|
||
stop: () => {
|
||
clearTimeout(timer);
|
||
seq += 1;
|
||
},
|
||
};
|
||
}
|
||
|
||
type Box = { x: number; y: number; w: number; h: number };
|
||
|
||
/** 시트 위 점선 테두리 + 왼위 이름표(104-3) — 겹치면(dup) 테두리는 안쪽으로 · 이름표는 옆으로 · 이벤트는 부른 쪽 */
|
||
export function rangeFrame(
|
||
mark: RangeMark,
|
||
box: Box,
|
||
end: Box,
|
||
dup: number,
|
||
hue: number,
|
||
tip: string,
|
||
): { frame: HTMLElement; tag: HTMLElement } {
|
||
const w = Math.max(box.w, end.x + end.w - box.x);
|
||
const h = Math.max(box.h, end.y + end.h - box.y);
|
||
const inset = dup * 3;
|
||
const tag = el("span", {
|
||
className: "ss-link-range__tag",
|
||
text: mark.이름,
|
||
attrs: { title: mark.설명 ?? tip, role: "button", tabindex: "0" },
|
||
});
|
||
tag.style.left = `${dup * 44}px`;
|
||
const frame = el("div", {
|
||
className: "ss-link-range",
|
||
attrs: { "data-range": mark.범위 },
|
||
children: [tag],
|
||
});
|
||
frame.style.cssText = `left:${box.x + inset}px;top:${box.y + inset}px;width:${Math.max(w - inset * 2, 0)}px;height:${Math.max(h - inset * 2, 0)}px`;
|
||
frame.style.setProperty("--ss-link-h", String(hue));
|
||
return { frame, tag };
|
||
}
|
||
|
||
/** 아래 막대 도면 알약 — 「T1 도면 E7:J17」(다른 시트면 시트!범위) · 상태 꼬리표 · 색 = 시트 위 테두리 ·
|
||
* 누르면 그 범위 고름(다른 시트면 그 시트로) · × = `cancel`(`취소` 있고 고칠 수 있을 때만) */
|
||
export function rangePills(
|
||
ctx: SpreadsheetContext,
|
||
marks: RangeMark[],
|
||
stateOf: (m: RangeMark) => RangeState | undefined,
|
||
back: () => void,
|
||
cancel: (m: RangeMark) => void,
|
||
tips: { pick: string; cancel: string },
|
||
): HTMLButtonElement[] {
|
||
const hues = linkHues(marks.map(markKey));
|
||
return marks.map((mark) => {
|
||
const found = markRange(ctx.book, mark);
|
||
const sheet = found && ctx.book.시트.find((s) => s.id === found.시트);
|
||
const a1 = found && rangeToA1(found.g);
|
||
const where =
|
||
a1 && sheet && found.시트 !== ctx.selection.시트 ? `${quoteSheet(sheet.이름)}!${a1}` : a1;
|
||
const state = stateOf(mark);
|
||
const tag = state && createTag(state.글, state.종류);
|
||
if (tag && state?.설명) tag.title = state.설명;
|
||
const b = el("button", {
|
||
className: "ss-link ss-link--range",
|
||
attrs: { type: "button", title: `${mark.설명 ?? mark.이름} · ${tips.pick}` },
|
||
children: [
|
||
el("span", { text: where ? `${mark.이름} ${where}` : mark.이름 }),
|
||
...(tag ? [tag] : []),
|
||
],
|
||
});
|
||
b.style.setProperty("--ss-link-h", String(hues.get(markKey(mark)) ?? 0));
|
||
b.addEventListener("mousedown", (e) => e.preventDefault()); // 초점은 격자에
|
||
b.addEventListener("click", () => {
|
||
if (found) {
|
||
if (found.시트 !== ctx.selection.시트) ctx.showSheet(found.시트);
|
||
const { g } = found;
|
||
ctx.select(selectSpan(ctx.sheet(), { r: g.r0, c: g.c0 }, { r: g.r1, c: g.c1 }), true);
|
||
}
|
||
back();
|
||
});
|
||
if (mark.취소 && !ctx.readOnly) {
|
||
const x = el("span", {
|
||
className: "ss-link__x",
|
||
text: "×",
|
||
attrs: { role: "button", title: tips.cancel },
|
||
});
|
||
x.addEventListener("click", (e) => (e.stopPropagation(), cancel(mark)));
|
||
b.append(x);
|
||
}
|
||
return b;
|
||
});
|
||
}
|