Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
+1
-1
@@ -55,7 +55,7 @@ function bootstrap(): void {
|
||||
|
||||
// B · M 페이지는 브라우저 기본 오른쪽 클릭 메뉴를 막음 — 우리 메뉴는 그대로(계획서 39-9a)
|
||||
blockNativeMenu(window, () => NATIVE_MENU_ROUTE.test(currentRoute()));
|
||||
// B05~B09 돋보기 — 오른쪽 길게 누름 · Z 누름 동안(계획서 218-1)
|
||||
// B01~B09 · M01 · M02 돋보기 — 오른쪽 길게 누름 · Z 누름 동안(계획서 218-1 · 220)
|
||||
initLoupe(window, () => LOUPE_ROUTE.test(currentRoute()));
|
||||
|
||||
// 3. 셸 렌더 + 현재 라우트 렌더를 함께 수행하는 헬퍼
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* 돋보기(`ui_template/ui_template_loupe.ts` · 계획서 218-1) 단위검증 헬퍼 —
|
||||
/* 돋보기(`ui_template/ui_template_loupe.ts` · 계획서 218-1 · 220-1 휠 배율) 단위검증 헬퍼 —
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌리고 값을 JSON 으로 냄. 가짜 창 · 가짜 시계 · 가짜 요소로 판정. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
@@ -70,6 +70,7 @@ function fakeView() {
|
||||
log,
|
||||
show: (x, y) => log.push(["show", x, y]),
|
||||
move: (x, y) => log.push(["move", x, y]),
|
||||
setZoom: (z) => log.push(["zoom", z]),
|
||||
hide: () => log.push(["hide"]),
|
||||
};
|
||||
}
|
||||
@@ -86,6 +87,9 @@ const r = {};
|
||||
r.consts = {
|
||||
d: m.LOUPE_DIAMETER,
|
||||
zoom: m.LOUPE_ZOOM,
|
||||
zoomMin: m.LOUPE_ZOOM_MIN,
|
||||
zoomMax: m.LOUPE_ZOOM_MAX,
|
||||
zoomStep: m.LOUPE_ZOOM_STEP,
|
||||
hold: m.LOUPE_HOLD_MS,
|
||||
refresh: m.LOUPE_REFRESH_MS,
|
||||
};
|
||||
@@ -93,6 +97,54 @@ r.consts = {
|
||||
const { win } = setup();
|
||||
r.listeners = win.calls.map((c) => [c.type, c.capture]);
|
||||
}
|
||||
{
|
||||
// 220-1 휠 — 켜 있는 동안만 배율 · 스크롤 막음 · 켤 때마다 2배
|
||||
const { win, view } = setup();
|
||||
const wheel = (deltaY) => win.fire("wheel", { deltaY });
|
||||
const zoomLog = () => view.log.filter((l) => l[0] === "zoom").map((l) => l[1]);
|
||||
const offEv = wheel(-100);
|
||||
const offState = { prevented: offEv.prevented, stopped: offEv.stopped, log: view.log.length };
|
||||
win.fire("keydown", { code: "KeyZ" });
|
||||
const up = wheel(-100);
|
||||
const upState = { prevented: up.prevented, stopped: up.stopped, zoom: zoomLog().at(-1) };
|
||||
wheel(100);
|
||||
wheel(100);
|
||||
const down = zoomLog().at(-1);
|
||||
wheel(0); // 가로 굴림 — 배율 그대로 · 스크롤은 막음
|
||||
const flat = zoomLog().at(-1);
|
||||
for (let i = 0; i < 30; i++) wheel(-50);
|
||||
const max = zoomLog().at(-1);
|
||||
for (let i = 0; i < 30; i++) wheel(120);
|
||||
const min = zoomLog().at(-1);
|
||||
win.fire("keyup", { code: "KeyZ" });
|
||||
const n = zoomLog().length;
|
||||
const afterOff = wheel(-100);
|
||||
const afterOffZoomed = zoomLog().length !== n;
|
||||
win.fire("keydown", { code: "KeyZ" });
|
||||
wheel(-100);
|
||||
r.wheel = {
|
||||
off: offState,
|
||||
up: upState,
|
||||
down,
|
||||
flat,
|
||||
max,
|
||||
min,
|
||||
afterOff: { prevented: afterOff.prevented, stopped: afterOff.stopped, zoomed: afterOffZoomed },
|
||||
reopen: zoomLog().at(-1), // 다시 켜면 2배에서 한 칸
|
||||
pure: [
|
||||
m.nextLoupeZoom(2, -1),
|
||||
m.nextLoupeZoom(2, 1),
|
||||
m.nextLoupeZoom(6, -1),
|
||||
m.nextLoupeZoom(1.5, 1),
|
||||
],
|
||||
};
|
||||
// 길게 누름으로 켠 동안도 같음
|
||||
const b = setup();
|
||||
b.win.fire("mousedown", { button: 2, buttons: 2 });
|
||||
b.win.tick(400);
|
||||
const press = b.win.fire("wheel", { deltaY: -1 });
|
||||
r.wheel.press = { prevented: press.prevented, zoom: b.view.log.at(-1) };
|
||||
}
|
||||
{
|
||||
// 길게 누름 — 0.4 초 넘어야 켬 · 떼면 끔 · 뒤따르는 메뉴는 먹음
|
||||
const { win, view } = setup();
|
||||
@@ -194,6 +246,12 @@ r.consts = {
|
||||
}
|
||||
r.routes = {};
|
||||
for (const route of [
|
||||
"a01-home",
|
||||
"a06-login",
|
||||
"a07-register",
|
||||
"b01-account",
|
||||
"b02-proj-register",
|
||||
"b03-file-input",
|
||||
"b04-preprocess",
|
||||
"b05-profile",
|
||||
"b06-section",
|
||||
@@ -201,8 +259,10 @@ for (const route of [
|
||||
"b08-quantity",
|
||||
"b09-estimation",
|
||||
"b10-payment",
|
||||
"a01-home",
|
||||
"b11-status",
|
||||
"b11-loading",
|
||||
"m01-master-data",
|
||||
"m02-master-template",
|
||||
])
|
||||
r.routes[route] = m.LOUPE_ROUTE.test(route);
|
||||
r.origin = {
|
||||
@@ -342,6 +402,7 @@ function fakeDocument(app) {
|
||||
const view = m.createLoupeView(win);
|
||||
view.show(500, 400);
|
||||
const root = doc.body.children[0];
|
||||
const label = doc.body.children[1];
|
||||
const stage = root.children[0];
|
||||
const page = stage.children[0];
|
||||
const copy = page.children[0];
|
||||
@@ -373,6 +434,20 @@ function fakeDocument(app) {
|
||||
observeOpts: observers[0].opts,
|
||||
scrollListen: win.calls.some((c) => c.type === "scroll" && c.capture),
|
||||
};
|
||||
// 220-1 배율 글 · 배율 바꿈
|
||||
r.label = {
|
||||
cls: label.className,
|
||||
ariaHidden: label.attrs["aria-hidden"],
|
||||
text: label.textContent,
|
||||
at: label.style.transform,
|
||||
hidden: label.hidden,
|
||||
};
|
||||
view.setZoom(3);
|
||||
r.label.zoom3 = [label.textContent, stage.style.transform, root.style.transform];
|
||||
view.move(500, 790);
|
||||
r.label.bottom = label.style.transform;
|
||||
view.setZoom(2);
|
||||
view.move(500, 400);
|
||||
// 화면이 바뀜 — 0.5 초 뒤 한 번만 다시 뜸
|
||||
const firstCopy = copy;
|
||||
observers[0].fn();
|
||||
@@ -396,7 +471,11 @@ function fakeDocument(app) {
|
||||
observers[0].fn();
|
||||
win.tick(1000);
|
||||
r.hide.noRecaptureAfterHide = stage.children.length;
|
||||
r.hide.labelHidden = label.hidden;
|
||||
view.setZoom(4);
|
||||
view.hide();
|
||||
view.show(500, 400);
|
||||
r.hide.reopenLabel = [label.textContent, label.hidden, stage.style.transform];
|
||||
r.hide.reuseRoot = doc.body.children.length;
|
||||
}
|
||||
console.log(JSON.stringify(r));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""B05~B09 돋보기(계획서 218-1) 회귀검증 — 오른쪽 길게 누름 · Z 누름 동안 켬 · 크기 · 배율 · 복제 지움 · 페이지 다섯.
|
||||
"""돋보기(계획서 218-1 · 220) 회귀검증 — 오른쪽 길게 누름 · Z 누름 동안 켬 · 크기 · 배율 · 복제 지움 · 붙는 페이지.
|
||||
|
||||
Node 헬퍼(`helper_218_1_loupe.cjs`)가 `ui_template/ui_template_loupe.ts` 를 트랜스파일해
|
||||
가짜 창 · 가짜 시계 · 가짜 요소로 돌리고, 여기서 값을 판정함.
|
||||
@@ -35,17 +35,22 @@ def _read(rel):
|
||||
|
||||
|
||||
def test_크기_배율_상수(r):
|
||||
assert r["consts"] == {"d": 240, "zoom": 2, "hold": 400, "refresh": 500}
|
||||
assert r["capture"]["rootSize"] == ["240px", "240px"]
|
||||
# 커서(500,400)가 원 가운데(120,120)에 2배로 옴
|
||||
assert r["capture"]["stageTransform"] == "translate(-880px, -680px) scale(2)"
|
||||
c = r["consts"]
|
||||
assert (c["d"], c["zoom"], c["hold"], c["refresh"]) == (
|
||||
360,
|
||||
2,
|
||||
400,
|
||||
500,
|
||||
) # 220-1 지름 240 → 360
|
||||
assert r["capture"]["rootSize"] == ["360px", "360px"]
|
||||
# 커서(500,400)가 원 가운데(180,180)에 2배로 옴
|
||||
assert r["capture"]["stageTransform"] == "translate(-820px, -620px) scale(2)"
|
||||
|
||||
|
||||
def test_창_캡처_단계로_들음(r):
|
||||
assert r["listeners"] == [
|
||||
[t, True]
|
||||
for t in ("mousemove", "mousedown", "mouseup", "contextmenu", "keydown", "keyup", "blur")
|
||||
]
|
||||
[t, True] for t in ("mousemove", "mousedown", "mouseup", "contextmenu", "keydown", "keyup")
|
||||
] + [["wheel", {"capture": True, "passive": False}], ["blur", True]]
|
||||
|
||||
|
||||
def test_길게_누름_0_4초_넘어야_켜고_떼면_끔_메뉴는_안_뜸(r):
|
||||
@@ -87,19 +92,25 @@ def test_입력_칸에선_안_켬(r):
|
||||
assert t["checkbox"] and t["button"]
|
||||
|
||||
|
||||
def test_B05_B09_밖이면_안_켬(r):
|
||||
def test_붙일_주소_밖이면_안_켬(r):
|
||||
assert r["off"] == {"log": 0, "menuStopped": False}
|
||||
|
||||
|
||||
def test_페이지_다섯_붙음(r):
|
||||
def test_붙는_페이지_B01_B09_M01_M02(r):
|
||||
on = [k for k, v in r["routes"].items() if v]
|
||||
assert on == [
|
||||
"b01-account",
|
||||
"b02-proj-register",
|
||||
"b03-file-input",
|
||||
"b04-preprocess",
|
||||
"b05-profile",
|
||||
"b06-section",
|
||||
"b07-design-detail",
|
||||
"b08-quantity",
|
||||
"b09-estimation",
|
||||
]
|
||||
"m01-master-data",
|
||||
"m02-master-template",
|
||||
] # 220 — A 영역 · 로그인 · B10 · B11 은 뺌
|
||||
main = _read("A00_Common/main.ts")
|
||||
assert 'from "@ui/ui_template_loupe"' in main
|
||||
assert re.search(r"initLoupe\(window, \(\) => LOUPE_ROUTE\.test\(currentRoute\(\)\)\)", main)
|
||||
@@ -107,9 +118,9 @@ def test_페이지_다섯_붙음(r):
|
||||
|
||||
def test_화면_끝에서_안_잘림(r):
|
||||
o = r["origin"]
|
||||
assert o["mid"] == {"left": 380, "top": 280}
|
||||
assert o["mid"] == {"left": 320, "top": 220}
|
||||
assert o["topLeft"] == {"left": 4, "top": 4}
|
||||
assert o["bottomRight"] == {"left": 1000 - 240 - 4, "top": 800 - 240 - 4}
|
||||
assert o["bottomRight"] == {"left": 1000 - 360 - 4, "top": 800 - 360 - 4}
|
||||
|
||||
|
||||
def test_화면_뜨기(r):
|
||||
@@ -140,7 +151,7 @@ def test_바뀌면_0_5초_뒤_다시_뜸_끄면_복제_지움(r):
|
||||
assert h["stageChildren"] == 0 # 복제 지움
|
||||
assert h["hidden"] and h["disconnected"] and h["scrollListen"] is False
|
||||
assert h["noRecaptureAfterHide"] == 0
|
||||
assert h["reuseRoot"] == 1 # 원은 하나를 다시 씀
|
||||
assert h["reuseRoot"] == 2 # 원 · 배율 글 하나씩을 다시 씀
|
||||
|
||||
|
||||
def test_테두리_그림자_토큰():
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""돋보기 휠 배율(계획서 220-1) 회귀검증 — 지름 360 · 켜진 동안 휠 = 배율 ± 0.25(1.5 ~ 6) ·
|
||||
그동안 휠 기본 동작(스크롤 · 그래프 줌) 막음 · 꺼지면 그대로 · 다음 켤 때 2배 · 원 아래 배율 글.
|
||||
|
||||
218-1 헬퍼(`helper_218_1_loupe.cjs`)의 휠 · 배율 글 장면을 판정함.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_218_1_loupe.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_지름_360_배율_한도(r):
|
||||
c = r["consts"]
|
||||
assert c["d"] == 360 and r["capture"]["rootSize"] == ["360px", "360px"]
|
||||
assert (c["zoom"], c["zoomMin"], c["zoomMax"], c["zoomStep"]) == (2, 1.5, 6, 0.25)
|
||||
|
||||
|
||||
def test_휠_위_크게_아래_작게_한도(r):
|
||||
w = r["wheel"]
|
||||
assert w["up"]["zoom"] == 2.25 # 위로 한 칸
|
||||
assert w["down"] == 1.75 # 아래로 두 칸
|
||||
assert w["flat"] == 1.75 # 가로 굴림은 그대로
|
||||
assert w["max"] == 6 and w["min"] == 1.5
|
||||
assert w["pure"] == [2.25, 1.75, 6, 1.5]
|
||||
|
||||
|
||||
def test_켜진_동안_휠_기본_동작_막음(r):
|
||||
w = r["wheel"]
|
||||
assert w["up"]["prevented"] and w["up"]["stopped"] # 스크롤 · 그래프 휠 줌 안 받음
|
||||
assert w["press"] == {"prevented": True, "zoom": ["zoom", 2.25]} # 우클릭 길게 누름도
|
||||
assert ["wheel", {"capture": True, "passive": False}] in r["listeners"]
|
||||
|
||||
|
||||
def test_꺼지면_스크롤_그대로(r):
|
||||
w = r["wheel"]
|
||||
assert w["off"] == {"prevented": False, "stopped": False, "log": 0}
|
||||
assert w["afterOff"] == {"prevented": False, "stopped": False, "zoomed": False}
|
||||
|
||||
|
||||
def test_다음_켤_때_2배(r):
|
||||
assert r["wheel"]["reopen"] == 2.25 # 1.5 에서 껐다 켜면 2배에서 한 칸
|
||||
h = r["hide"]
|
||||
assert h["reopenLabel"] == ["×2", False, "translate(-820px, -620px) scale(2)"]
|
||||
|
||||
|
||||
def test_배율_글_원_아래(r):
|
||||
lb = r["label"]
|
||||
assert lb["cls"] == "ui-loupe__zoom" and lb["ariaHidden"] == "true" and not lb["hidden"]
|
||||
assert lb["text"] == "×2"
|
||||
# 원(320,220 · 지름 360) 아래 가운데 — 원 끝 580 + 4
|
||||
assert lb["at"] == "translate(500px, 584px) translateX(-50%)"
|
||||
assert lb["zoom3"] == ["×3", "translate(-1320px, -1020px) scale(3)", "translate(320px, 220px)"]
|
||||
assert lb["bottom"] == "translate(500px, 778px) translateX(-50%)" # 화면 아래 끝에서 안 잘림
|
||||
assert r["hide"]["labelHidden"]
|
||||
with open(os.path.join(ROOT, "ui_template", "ui_template_loupe.css"), encoding="utf-8") as f:
|
||||
css = f.read()
|
||||
assert ".ui-loupe__zoom" in css and ".ui-loupe__zoom[hidden]" in css
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* 돋보기 공용 모양(`ui_template_loupe.ts` · 계획서 218-1) — 화면에 하나 · fixed · 원 ·
|
||||
* 테두리 · 그림자 = 테마 토큰(밝음 · 어두움 같이) · 마우스를 안 막음 · 크기 · 배율은 TS 상수
|
||||
* 테두리 · 그림자 = 테마 토큰(밝음 · 어두움 같이) · 마우스를 안 막음 · 크기 · 배율은 TS 상수(220-1 지름 360)
|
||||
* ========================================================================== */
|
||||
|
||||
.ui-loupe {
|
||||
@@ -45,6 +45,27 @@
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* 배율 글(220-1) — 원 아래 가운데 · 작게 · 자리는 TS 가 transform 으로 */
|
||||
.ui-loupe__zoom {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: calc(var(--z-toast) + 10);
|
||||
padding: 0 6px;
|
||||
border-radius: 9px;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-accent);
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
box-shadow: 0 0 0 1px var(--color-accent);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-loupe__zoom[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 가운데 십자 — 작게 */
|
||||
.ui-loupe__cross::before,
|
||||
.ui-loupe__cross::after {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* =============================================================================
|
||||
* ui_template_loupe.ts
|
||||
* 돋보기 — 커서를 따라가는 원 안에 지금 화면을 크게 보여 줌(계획서 218-1) · 공용 진입(`A00_Common/main.ts`)이 B05~B09 에 붙임.
|
||||
* 돋보기 — 커서를 따라가는 원 안에 지금 화면을 크게 보여 줌(계획서 218-1) · 공용 진입(`A00_Common/main.ts`)이 B01~B09 · M01 · M02 에 붙임(220).
|
||||
*
|
||||
* 켜는 법 둘 — 오른쪽 단추를 0.4 초 넘게 누르고 있는 동안 · Z 키를 누르고 있는 동안 · 떼면 꺼짐.
|
||||
* 켜 있는 동안 휠 = 배율(220-1 · 1.5 ~ 6배 · 한 칸 0.25 · 켤 때마다 2배) · 화면 스크롤 · 그래프 휠 줌은 막음.
|
||||
* 길게 누른 우클릭은 뗀 뒤 메뉴(우리 메뉴 · 브라우저 메뉴)를 안 띄움 · 짧게 누르면 지금 우클릭 그대로.
|
||||
* 그리기 — 켤 때 `#app` 을 한 번 떠서(DOM 복제 + 2D canvas 그림 복사 · SVG 그대로) 원 안에 놓고, 움직일 때 transform 만 바꿈.
|
||||
* 화면 밖 상자는 속을 비우고 크기만 남김 · 켜 있는 동안 화면이 바뀌면 0.5 초 뒤 다시 뜸 · 끄면 복제를 지움.
|
||||
@@ -13,17 +14,23 @@ import "./ui_template_loupe.css";
|
||||
import { keyLetter } from "../A00_Common/key_shortcut";
|
||||
|
||||
/** 원 지름(px) */
|
||||
export const LOUPE_DIAMETER = 240;
|
||||
/** 배율 */
|
||||
export const LOUPE_DIAMETER = 360;
|
||||
/** 켤 때 배율 */
|
||||
export const LOUPE_ZOOM = 2;
|
||||
/** 휠 배율 한도 · 한 칸 */
|
||||
export const LOUPE_ZOOM_MIN = 1.5;
|
||||
export const LOUPE_ZOOM_MAX = 6;
|
||||
export const LOUPE_ZOOM_STEP = 0.25;
|
||||
/** 배율 글 높이(px) — 화면 아래 끝에서 안 잘리게 */
|
||||
const LOUPE_LABEL_H = 18;
|
||||
/** 오른쪽 단추를 이만큼(ms) 넘게 누르면 켬 */
|
||||
export const LOUPE_HOLD_MS = 400;
|
||||
/** 켜 있는 동안 화면이 바뀌면 이만큼(ms) 뒤 다시 뜸 */
|
||||
export const LOUPE_REFRESH_MS = 500;
|
||||
/** 화면 끝과 원 사이 여백(px) — 테두리가 안 잘리게 */
|
||||
export const LOUPE_EDGE = 4;
|
||||
/** 붙일 주소 — B05~B09 */
|
||||
export const LOUPE_ROUTE = /^b0[5-9]/;
|
||||
/** 붙일 주소 — B01~B09 · M01 · M02(A 영역 · 로그인 · B10 · B11 은 뺌) */
|
||||
export const LOUPE_ROUTE = /^(b0[1-9]|m0[12])-/;
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const NON_TEXT_INPUT = new Set([
|
||||
@@ -70,6 +77,12 @@ export function isTypingTarget(target: unknown): boolean {
|
||||
return tag === "INPUT" && !NON_TEXT_INPUT.has((el.type || "text").toLowerCase());
|
||||
}
|
||||
|
||||
/** 휠 한 번 뒤 배율 — 위로 굴림(deltaY < 0) = 크게 · 한도 안 */
|
||||
export function nextLoupeZoom(zoom: number, deltaY: number): number {
|
||||
const step = Math.sign(-deltaY) * LOUPE_ZOOM_STEP;
|
||||
return Math.max(LOUPE_ZOOM_MIN, Math.min(LOUPE_ZOOM_MAX, zoom + step));
|
||||
}
|
||||
|
||||
/** 원 왼 위 자리 — 커서 가운데 · 화면 밖으로 안 나감 */
|
||||
export function loupeOrigin(
|
||||
x: number,
|
||||
@@ -88,10 +101,11 @@ export function loupeOrigin(
|
||||
export interface LoupeView {
|
||||
show(x: number, y: number): void;
|
||||
move(x: number, y: number): void;
|
||||
setZoom(zoom: number): void;
|
||||
hide(): void;
|
||||
}
|
||||
|
||||
/** `win` 에 두 켜는 법을 붙임 — `isOn()` 이 거짓이면(B05~B09 밖) 안 켬 */
|
||||
/** `win` 에 두 켜는 법을 붙임 — `isOn()` 이 거짓이면(붙일 주소 밖) 안 켬 */
|
||||
export function initLoupe(
|
||||
win: Window,
|
||||
isOn: () => boolean,
|
||||
@@ -104,11 +118,14 @@ export function initLoupe(
|
||||
let shown = false;
|
||||
let holdTimer = 0;
|
||||
let swallowMenu = false; // 길게 누른 뒤 따라오는 우클릭 메뉴 한 번을 먹음
|
||||
let zoom = LOUPE_ZOOM;
|
||||
|
||||
const update = (): void => {
|
||||
const want = (press || key) && isOn();
|
||||
if (want && !shown) view.show(x, y);
|
||||
else if (!want && shown) view.hide();
|
||||
if (want && !shown) {
|
||||
zoom = LOUPE_ZOOM; // 켤 때마다 2배
|
||||
view.show(x, y);
|
||||
} else if (!want && shown) view.hide();
|
||||
shown = want;
|
||||
};
|
||||
const stopHold = (): void => {
|
||||
@@ -165,6 +182,18 @@ export function initLoupe(
|
||||
key = false;
|
||||
update();
|
||||
});
|
||||
// 켜 있는 동안만 휠을 잡음 — 잡기 단계 첫 자리 · passive:false 여야 스크롤을 막음
|
||||
win.addEventListener(
|
||||
"wheel",
|
||||
(e) => {
|
||||
if (!shown) return;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation(); // 표 · 그래프 휠 줌도 안 받음
|
||||
zoom = nextLoupeZoom(zoom, e.deltaY);
|
||||
view.setZoom(zoom);
|
||||
},
|
||||
{ capture: true, passive: false },
|
||||
);
|
||||
on("blur", (e) => {
|
||||
if (e.target !== win) return; // 칸 초점 빠짐은 무시 — 창이 초점을 잃을 때만(CAD iframe 으로 넘어감 포함)
|
||||
stopHold();
|
||||
@@ -181,6 +210,9 @@ export function createLoupeView(
|
||||
const doc = win.document;
|
||||
let root: HTMLElement | null = null;
|
||||
let stage: HTMLElement | null = null;
|
||||
let label: HTMLElement | null = null;
|
||||
let zoom = LOUPE_ZOOM;
|
||||
let at: [number, number] = [0, 0]; // 마지막 커서 — 배율만 바뀔 때 다시 놓음
|
||||
let observer: MutationObserver | null = null;
|
||||
let refreshTimer = 0;
|
||||
|
||||
@@ -195,7 +227,10 @@ export function createLoupeView(
|
||||
const cross = doc.createElement("div");
|
||||
cross.className = "ui-loupe__cross";
|
||||
root.append(stage, cross);
|
||||
doc.body.append(root);
|
||||
label = doc.createElement("div");
|
||||
label.className = "ui-loupe__zoom";
|
||||
label.setAttribute("aria-hidden", "true");
|
||||
doc.body.append(root, label);
|
||||
};
|
||||
const capture = (): void => {
|
||||
const source = sourceOf();
|
||||
@@ -215,8 +250,9 @@ export function createLoupeView(
|
||||
show(x, y) {
|
||||
if (!root) build();
|
||||
capture();
|
||||
root!.hidden = false;
|
||||
view.move(x, y);
|
||||
root!.hidden = label!.hidden = false;
|
||||
at = [x, y];
|
||||
view.setZoom(LOUPE_ZOOM); // 켤 때마다 2배 · 자리도 놓음
|
||||
const source = sourceOf();
|
||||
const Observer = (win as Window & typeof globalThis).MutationObserver;
|
||||
const watch = new Observer(refresh);
|
||||
@@ -231,11 +267,19 @@ export function createLoupeView(
|
||||
win.addEventListener("scroll", refresh, true);
|
||||
},
|
||||
move(x, y) {
|
||||
if (!root || !stage) return;
|
||||
if (!root || !stage || !label) return;
|
||||
at = [x, y];
|
||||
const { left, top } = loupeOrigin(x, y, win.innerWidth, win.innerHeight);
|
||||
const r = LOUPE_DIAMETER / 2;
|
||||
root.style.transform = `translate(${left}px, ${top}px)`;
|
||||
stage.style.transform = `translate(${r - LOUPE_ZOOM * x}px, ${r - LOUPE_ZOOM * y}px) scale(${LOUPE_ZOOM})`;
|
||||
stage.style.transform = `translate(${r - zoom * x}px, ${r - zoom * y}px) scale(${zoom})`;
|
||||
const labelTop = Math.min(top + LOUPE_DIAMETER + 4, win.innerHeight - LOUPE_LABEL_H - 4);
|
||||
label.style.transform = `translate(${left + r}px, ${labelTop}px) translateX(-50%)`;
|
||||
},
|
||||
setZoom(z) {
|
||||
zoom = z;
|
||||
if (label) label.textContent = `×${z}`;
|
||||
view.move(...at);
|
||||
},
|
||||
hide() {
|
||||
observer?.disconnect();
|
||||
@@ -244,7 +288,7 @@ export function createLoupeView(
|
||||
refreshTimer = 0;
|
||||
win.removeEventListener("scroll", refresh, true);
|
||||
stage?.replaceChildren(); // 복제를 지움
|
||||
if (root) root.hidden = true;
|
||||
if (root) root.hidden = label!.hidden = true;
|
||||
},
|
||||
};
|
||||
return view;
|
||||
|
||||
Reference in New Issue
Block a user