diff --git a/B09_Estimation/B09_Estimation_UI_Popup.ts b/B09_Estimation/B09_Estimation_UI_Popup.ts index d20b4ad27..eb45a731f 100644 --- a/B09_Estimation/B09_Estimation_UI_Popup.ts +++ b/B09_Estimation/B09_Estimation_UI_Popup.ts @@ -5,7 +5,8 @@ * 창 = 열 제목(머리 여러 줄 그대로) + 강조 범위 줄 전체 · 표 짜기 = `…_UI_Builders`(화면과 같은 옵션) * 강조 범위 = 설계내역서 그 줄 + 자손 · 묶음 탭 호표 구간 · 그 밖 한 줄(하이퍼링크 강조와 같음) * 자리 = 누른 줄 아래(모자라면 위 · 화면 안) · 너비 = 표 너비(최대 화면 90%) · 넘치면 창 안 스크롤 - * 창 안 링크 줄 누름 = 이어진 창(겹쳐 위에 · 그 줄 곁) · Esc · × = 맨 위 하나 · 탭 바꿈 · 주소 바뀜 = 모두 + * 창 안 링크 줄 누름 = 이어진 창(겹쳐 위에 · 그 줄 곁) · × = 그 창부터 위로 · 탭 바꿈 · 주소 바뀜 = 모두 + * Esc · 맨 위 창 밖 누름(아래 창 · 빈 곳 · 톡) = 모두(308) — 그 누름은 닫기만(아래 화면에 안 넘김) * 머리 [탭으로] = 하이퍼링크처럼 옮김(모두 닫음) · 창 안 = 보기 전용(고칠 칸 · 메뉴 없음) * ========================================================================== */ @@ -67,6 +68,8 @@ export function popupPlace( /** 열린 창(아래 → 위) */ const stack: HTMLElement[] = []; let listening = false; +/** 바깥 누름으로 닫은 뒤 따라오는 click · 우클릭 메뉴를 먹음(308) */ +let eat = false; /** from 번째 창부터 위로 모두 닫음(0 = 모두) */ export function closePopups(from = 0): void { @@ -78,17 +81,37 @@ export const popupCount = (): number => stack.length; function listen(): void { if (listening) return; listening = true; - // Esc = 맨 위 하나(뒤 화면의 Esc 풂보다 먼저 · 창 있을 때만 가로챔) + // Esc = 모두(뒤 화면의 Esc 풂보다 먼저 · 창 있을 때만 가로챔) window.addEventListener( "keydown", (event) => { if (event.key !== "Escape" || !stack.length) return; event.preventDefault(); event.stopPropagation(); - closePopups(stack.length - 1); + closePopups(); }, true, ); + // 맨 위 창 밖 누름 = 모두 닫음 · 그 누름은 아래로 안 넘김(308) + window.addEventListener( + "pointerdown", + (event) => { + eat = !!stack.length && !stack[stack.length - 1].contains(event.target as Node); + if (!eat) return; + event.preventDefault(); + event.stopPropagation(); + closePopups(); + }, + true, + ); + const swallow = (event: Event): void => { + if (!eat) return; + eat = false; + event.preventDefault(); + event.stopPropagation(); + }; + window.addEventListener("click", swallow, true); + window.addEventListener("contextmenu", swallow, true); window.addEventListener("hashchange", () => closePopups()); } diff --git a/resources/tester/test_303_b09_row_popup.py b/resources/tester/test_303_b09_row_popup.py index a1bfc3fa6..2012e4161 100644 --- a/resources/tester/test_303_b09_row_popup.py +++ b/resources/tester/test_303_b09_row_popup.py @@ -165,15 +165,25 @@ E.prototype.getBoundingClientRect = function () { return this.rect ?? { left: 0, top: 0, right: 0, bottom: 0, width: 400, height: 300 }; }; document.body = new E("body"); -const winKeys = []; -const hashFns = []; +const winFns = { keydown: [], hashchange: [], pointerdown: [], click: [], contextmenu: [] }; +const winKeys = winFns.keydown; +const hashFns = winFns.hashchange; globalThis.window = { innerWidth: 1200, innerHeight: 800, - addEventListener: (t, f) => (t === "keydown" ? winKeys : hashFns).push(f) }; + addEventListener: (t, f) => winFns[t].push(f) }; const esc = () => { const ev = { key: "Escape", stopped: false, preventDefault() {}, stopPropagation() { ev.stopped = true; } }; winKeys.forEach((f) => f(ev)); return ev.stopped; }; +// 308 창 밖 누름 — pointerdown → click(또는 우클릭 메뉴) · 먹힘 = stopped +const press = (target, type = "click") => { + const ev = (t) => ({ type: t, target, stopped: false, preventDefault() {}, stopPropagation() { this.stopped = true; } }); + const down = ev("pointerdown"); + winFns.pointerdown.forEach((f) => f(down)); + const up = ev(type); + winFns[type].forEach((f) => f(up)); + return [down.stopped, up.stopped]; +}; const tables = []; const createDataTable = (o) => { const root = document.createElement("div"); @@ -259,16 +269,16 @@ def test_이어진_창_겹침_Esc_차례(): "\nconst b = boxes();" "\nconst dg = rows(2);" "\nconst e1 = esc(); const c = boxes();" - "\nconst e2 = esc(); const e3 = esc(); const e4 = esc();" - "\nconsole.log(JSON.stringify({ a, b, dg, top2: second.style.top, c, e: [e1, e2, e3, e4]," + "\nconst e2 = esc();" + "\nconsole.log(JSON.stringify({ a, b, dg, top2: second.style.top, c, e: [e1, e2]," " d: boxes(), gone }));" ) assert out["a"] == ["bill"] assert out["b"] == ["bill", "labor", "dg"] # 겹쳐 위에(뒤 DOM = 위) assert out["dg"] == ["D1", "D1|1", "D1|합계"] # 호표 구간 assert out["top2"] == "362px" # 그 줄(창 안 2번째 줄) 곁 - assert out["c"] == ["bill", "labor"] # Esc = 맨 위 하나 - assert out["e"] == [True, True, True, False] # 창 없으면 Esc 안 가로챔 + assert out["c"] == [] # 308 Esc = 모두 + assert out["e"] == [True, False] # 창 없으면 Esc 안 가로챔 assert out["d"] == [] and out["gone"] == [] @@ -295,6 +305,32 @@ def test_닫기_탭으로_새_누름_주소(): assert out["e"] == [[], 2] +def test_308_맨_위_창_밖_누름_모두_닫힘_누름_먹음(): + out = _popup( + "\nconst outside = document.createElement('td'); document.body.append(outside);" + "\nconst open3 = () => { openRowPopup({ ...base, link: { tab: 'bill', key: 'b1' }, anchor });" + " tables[tables.length - 1].onCellClick(1, 'name');" + " tables[tables.length - 1].onCellClick(0, 'name'); };" + "\nconst pops = () => boxes().filter(Boolean);" + "\nconst top = () => document.body.children[document.body.children.length - 1];" + "\nconst under = () => document.body.children.filter((b) => b.dataset.b09Popup)[1];" + "\nopen3(); const inTop = press(top().children[1]); const a = pops();" # 맨 위 창 안 = 그대로 + "\nconst right0 = press(top().children[0], 'contextmenu'); const a2 = pops();" + "\nconst low = press(under().children[1]); const b = pops();" # 아래 창 누름 + "\nopen3(); const blank = press(outside); const c = pops();" # 쪽 빈 곳 · 톡 + "\nopen3(); const right = press(outside, 'contextmenu'); const d = pops();" # 우클릭 + "\nconst none = press(outside);" # 창 없음 = 안 가로챔 + "\nconsole.log(JSON.stringify({ inTop, a, right0, a2, low, b, blank, c, right, d, none, gone }));" + ) + full = ["bill", "labor", "dg"] + assert out["inTop"] == [False, False] and out["a"] == full # 맨 위 창 안 = 그대로 + assert out["right0"] == [False, False] and out["a2"] == full + assert out["low"] == [True, True] and out["b"] == [] # 모두 닫힘 · 그 누름 먹음 + assert out["blank"] == [True, True] and out["c"] == [] + assert out["right"] == [True, True] and out["d"] == [] + assert out["none"] == [False, False] and out["gone"] == [] + + def test_창_자리_화면_안(): out = _popup( "\nconst v = { width: 1200, height: 800 };"