- 고르기: 누름 = 더함 · Shift = 뺌 · Ctrl = 넣고 빼기 · 상자 = 앞 고름 ∪ 상자 · Shift 상자 = 뺌(H4 · H5) · 명령 안 고르기 단계도 누를 때 첫 점 · 스침 강조(ic/selecting.ts) - 그립: 쉴 때만 집음(N7) · 누르고 떼면 대기 → 다음 누름에 놓음 · 끌어 놓기(H3) · 거리 숫자(N8) · 확정 · 읽기 전용 도면은 그립 · Delete 막음(H2) - 직교: F8 = 기준점에서 가로 · 세로 강제 · Shift 누른 동안 뒤집음 · 객체 스냅 점 · 좌표 · 각도 잠금은 안 덮음 · 클릭 사건 snapped 채움(ic/ortho.ts · H8) · 가이드는 F3 와 뗌 · 가이드 잡는 거리 = 화면 픽셀(H9) - 휠: 기본 밂(앞으로 굴리면 확대 · 오토캐드 · 모든 화면 · ☰ 설정 뒤집기 그대로) · 가운데 단추 두 번 = 범위 줌 · ZOOM · ⛶ 앞 화면 기억(ic/zoom-prompt.ts) - 시험: mouse-acad 새로 · 휠 방향 시험 손질(input-wheel · input-prefs-cad · input_prefs · app_settings · 164 · 220_1 · 221_1 · 244_8 기대값 뒤집음 · 266 pull 박음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
641 lines
22 KiB
JavaScript
641 lines
22 KiB
JavaScript
/* 돋보기(`ui_template/ui_template_loupe.ts` · 계획서 218-1 · 220-1 휠 배율 · 221-1 CAD 빼기 · 221-2 스크롤) 단위검증 헬퍼 —
|
|
* 그 자리에서 트랜스파일해 Node 로 돌리고 값을 JSON 으로 냄. 가짜 창 · 가짜 시계 · 가짜 요소로 판정. */
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
function load(file) {
|
|
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
const req = (p) =>
|
|
p.endsWith(".css")
|
|
? {}
|
|
: p.endsWith("key_shortcut") // 공용 키 판정(214-2)도 그 자리에서 트랜스파일
|
|
? load(path.join(ROOT, "A00_Common", "key_shortcut.ts"))
|
|
: p.endsWith("input_prefs") // 휠 방향(337 · Node = 기본 밂)
|
|
? load(path.join(ROOT, "A00_Common", "input_prefs.ts"))
|
|
: require(p);
|
|
new Function("exports", "module", "require", js)(box.exports, box, req);
|
|
return box.exports;
|
|
}
|
|
const m = load(path.join(ROOT, "ui_template", "ui_template_loupe.ts"));
|
|
|
|
// ---- 가짜 시계 · 가짜 창 ----
|
|
function fakeWindow(doc) {
|
|
const win = {
|
|
innerWidth: 1000,
|
|
innerHeight: 800,
|
|
document: doc,
|
|
now: 0,
|
|
timers: [],
|
|
seq: 0,
|
|
calls: [],
|
|
};
|
|
win.addEventListener = (type, fn, capture) => win.calls.push({ type, fn, capture });
|
|
win.removeEventListener = (type, fn) =>
|
|
(win.calls = win.calls.filter((c) => c.type !== type || c.fn !== fn));
|
|
win.setTimeout = (fn, ms) => (win.timers.push({ id: ++win.seq, at: win.now + ms, fn }), win.seq);
|
|
win.clearTimeout = (id) => (win.timers = win.timers.filter((t) => t.id !== id));
|
|
win.requestAnimationFrame = (fn) => win.setTimeout(fn, 16); // 한 화면 틀
|
|
win.cancelAnimationFrame = win.clearTimeout;
|
|
win.tick = (ms) => {
|
|
win.now += ms;
|
|
for (const t of win.timers.filter((t) => t.at <= win.now)) {
|
|
win.clearTimeout(t.id);
|
|
t.fn();
|
|
}
|
|
};
|
|
win.fire = (type, props = {}) => {
|
|
const ev = {
|
|
type,
|
|
button: 0,
|
|
buttons: 0,
|
|
clientX: 0,
|
|
clientY: 0,
|
|
code: "",
|
|
target: { tagName: "DIV" },
|
|
...props,
|
|
};
|
|
ev.prevented = false;
|
|
ev.stopped = false;
|
|
ev.preventDefault = () => (ev.prevented = true);
|
|
ev.stopImmediatePropagation = () => (ev.stopped = true);
|
|
win.calls.filter((c) => c.type === type).forEach((c) => c.fn(ev));
|
|
return ev;
|
|
};
|
|
return win;
|
|
}
|
|
function fakeView() {
|
|
const log = [];
|
|
return {
|
|
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"]),
|
|
};
|
|
}
|
|
const last = (v) => (v.log.length ? v.log[v.log.length - 1][0] : null);
|
|
|
|
// ---- 켜는 법 ----
|
|
function setup(on = () => true) {
|
|
const win = fakeWindow(null);
|
|
const view = fakeView();
|
|
m.initLoupe(win, on, view);
|
|
return { win, view };
|
|
}
|
|
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,
|
|
};
|
|
{
|
|
const { win } = setup();
|
|
r.listeners = win.calls.map((c) => [c.type, c.capture]);
|
|
}
|
|
{
|
|
// 220-1 휠 — 켜 있는 동안만 배율 · 스크롤 막음 · 켤 때마다 2배 · 확대 = 밂(deltaY < 0 · 337 기본)
|
|
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();
|
|
win.fire("mousedown", { button: 2, buttons: 2, clientX: 300, clientY: 200 });
|
|
win.tick(399);
|
|
const before = view.log.length;
|
|
win.tick(1);
|
|
const on = last(view);
|
|
win.fire("mousemove", { buttons: 2, clientX: 310, clientY: 205 });
|
|
const moved = view.log[view.log.length - 1];
|
|
win.fire("mouseup", { button: 2 });
|
|
const off = last(view);
|
|
const menu = win.fire("contextmenu");
|
|
const nextMenu = win.fire("contextmenu");
|
|
r.long = {
|
|
before,
|
|
on,
|
|
show: view.log[0],
|
|
moved,
|
|
off,
|
|
menuPrevented: menu.prevented,
|
|
menuStopped: menu.stopped,
|
|
nextStopped: nextMenu.stopped,
|
|
};
|
|
}
|
|
{
|
|
// 짧게 누름 — 안 켬 · 메뉴 그대로
|
|
const { win, view } = setup();
|
|
win.fire("mousedown", { button: 2, buttons: 2 });
|
|
win.tick(200);
|
|
win.fire("mouseup", { button: 2 });
|
|
win.tick(1000);
|
|
const menu = win.fire("contextmenu");
|
|
r.short = { log: view.log.length, menuPrevented: menu.prevented, menuStopped: menu.stopped };
|
|
}
|
|
{
|
|
// 왼쪽 단추 · 창 밖에서 뗌
|
|
const { win, view } = setup();
|
|
win.fire("mousedown", { button: 0, buttons: 1 });
|
|
win.tick(1000);
|
|
const left = view.log.length;
|
|
win.fire("mousedown", { button: 2, buttons: 2 });
|
|
win.fire("mousemove", { buttons: 0 });
|
|
win.tick(1000);
|
|
const lostBeforeHold = view.log.length;
|
|
win.fire("mousedown", { button: 2, buttons: 2 });
|
|
win.tick(500);
|
|
win.fire("mousemove", { buttons: 0 });
|
|
r.mouseMisc = { left, lostBeforeHold, lostAfterHold: last(view) };
|
|
}
|
|
{
|
|
// Z 키
|
|
const { win, view } = setup();
|
|
const z = (props) => win.fire("keydown", { code: "KeyZ", ...props });
|
|
z({ key: "ㅋ" });
|
|
const on = last(view);
|
|
win.fire("keydown", { code: "KeyZ", repeat: true });
|
|
const repeatLen = view.log.length;
|
|
win.fire("keyup", { code: "KeyZ" });
|
|
const off = last(view);
|
|
const len = view.log.length;
|
|
z({ ctrlKey: true });
|
|
z({ altKey: true });
|
|
z({ metaKey: true });
|
|
win.fire("keydown", { code: "KeyX" });
|
|
const noCombo = view.log.length === len;
|
|
const typing = {};
|
|
for (const [k, target] of Object.entries({
|
|
input: { tagName: "INPUT", type: "text" },
|
|
inputNoType: { tagName: "INPUT", type: "" },
|
|
number: { tagName: "INPUT", type: "number" },
|
|
textarea: { tagName: "TEXTAREA" },
|
|
select: { tagName: "SELECT" },
|
|
editable: { tagName: "SPAN", isContentEditable: true },
|
|
checkbox: { tagName: "INPUT", type: "checkbox" },
|
|
button: { tagName: "BUTTON" },
|
|
})) {
|
|
const n = view.log.length;
|
|
z({ target });
|
|
typing[k] = view.log.length > n;
|
|
win.fire("keyup", { code: "KeyZ" });
|
|
}
|
|
z({ shiftKey: true });
|
|
const shift = last(view);
|
|
win.fire("blur", { target: { tagName: "INPUT", type: "text" } });
|
|
const fieldBlur = last(view);
|
|
win.fire("blur", { target: win });
|
|
r.key = { on, repeatLen, off, noCombo, typing, shift, fieldBlur, blur: last(view) };
|
|
}
|
|
{
|
|
// B05~B09 밖이면 안 켬
|
|
const { win, view } = setup(() => false);
|
|
win.fire("keydown", { code: "KeyZ" });
|
|
win.fire("mousedown", { button: 2, buttons: 2 });
|
|
win.tick(1000);
|
|
win.fire("mouseup", { button: 2 });
|
|
const menu = win.fire("contextmenu");
|
|
r.off = { log: view.log.length, menuStopped: menu.stopped };
|
|
}
|
|
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",
|
|
"b07-design-detail",
|
|
"b08-quantity",
|
|
"b09-estimation",
|
|
"b10-payment",
|
|
"b11-status",
|
|
"b11-loading",
|
|
"m01-master-data",
|
|
"m02-master-template",
|
|
])
|
|
r.routes[route] = m.LOUPE_ROUTE.test(route);
|
|
r.origin = {
|
|
mid: m.loupeOrigin(500, 400, 1000, 800),
|
|
topLeft: m.loupeOrigin(10, 10, 1000, 800),
|
|
bottomRight: m.loupeOrigin(995, 795, 1000, 800),
|
|
};
|
|
|
|
{
|
|
// 221-1 CAD 영역 — 위에선 안 켬 · 켜진 채 들어가면 숨김 · 나오면 보임 · 휠은 CAD 로
|
|
const cad = { tagName: "IFRAME", closest: (sel) => (sel === m.LOUPE_SKIP ? {} : null) };
|
|
const plain = { tagName: "DIV", closest: () => null };
|
|
const kinds = (v) => v.log.map((l) => l[0]);
|
|
r.cad = {
|
|
isCad: [m.isCadTarget(cad), m.isCadTarget(plain), m.isCadTarget(null), m.isCadTarget({})],
|
|
skip: m.LOUPE_SKIP,
|
|
};
|
|
{
|
|
// Z 를 CAD 위에서 누름 — 안 켬 · 나오면 보임 · 떼면 끔
|
|
const { win, view } = setup();
|
|
win.fire("mouseover", { target: cad });
|
|
win.fire("keydown", { code: "KeyZ", target: cad });
|
|
const onCad = view.log.length;
|
|
const wheel = win.fire("wheel", { deltaY: -100, target: cad });
|
|
win.fire("mouseover", { target: plain, clientX: 50, clientY: 60 });
|
|
const out = view.log[0];
|
|
win.fire("keyup", { code: "KeyZ" });
|
|
r.cad.keyOnCad = { onCad, wheelPrevented: wheel.prevented, out, off: last(view) };
|
|
}
|
|
{
|
|
// 켜진 채 CAD 위로(mouseover · mousemove 둘 다) — 숨김 · 휠 안 막음 · 나오면 배율 그대로 보임
|
|
const { win, view } = setup();
|
|
win.fire("keydown", { code: "KeyZ" });
|
|
win.fire("wheel", { deltaY: -100 }); // 2.25 — 밂 = 확대(337 기본)
|
|
win.fire("mouseover", { target: cad });
|
|
const hidden = last(view);
|
|
const n = view.log.length;
|
|
const wheel = win.fire("wheel", { deltaY: -100, target: cad });
|
|
win.fire("mousemove", { target: cad, clientX: 5, clientY: 5 });
|
|
const quiet = view.log.length === n; // 숨긴 동안 move · 배율 안 부름
|
|
win.fire("mouseover", { target: plain, clientX: 70, clientY: 80 });
|
|
const back = view.log.slice(n);
|
|
win.fire("mousemove", { target: cad, clientX: 6, clientY: 6 });
|
|
const moveHide = last(view);
|
|
win.fire("mousemove", { target: plain, clientX: 7, clientY: 7 });
|
|
r.cad.enter = {
|
|
hidden,
|
|
wheel: { prevented: wheel.prevented, stopped: wheel.stopped },
|
|
quiet,
|
|
back,
|
|
moveHide,
|
|
moveBack: kinds(view).slice(-3),
|
|
};
|
|
}
|
|
{
|
|
// 우클릭 길게 — CAD 위면 안 켜고 메뉴 그대로
|
|
const { win, view } = setup();
|
|
win.fire("mousedown", { button: 2, buttons: 2, target: cad });
|
|
win.tick(1000);
|
|
win.fire("mouseup", { button: 2, target: cad });
|
|
const menu = win.fire("contextmenu", { target: cad });
|
|
r.cad.press = { log: view.log.length, menuPrevented: menu.prevented };
|
|
}
|
|
{
|
|
// 초점이 CAD iframe 으로 넘어감(cad_host) — 끄지 않고 숨김 · Z 떼기는 그쪽 창에서
|
|
const { win, view } = setup();
|
|
const frameCalls = [];
|
|
const frame = {
|
|
...cad,
|
|
contentWindow: { addEventListener: (type, fn, cap) => frameCalls.push({ type, fn, cap }) },
|
|
};
|
|
win.document = { activeElement: frame };
|
|
win.fire("keydown", { code: "KeyZ" });
|
|
win.fire("blur", { target: win });
|
|
const blurHidden = kinds(view);
|
|
win.document = { activeElement: plain };
|
|
const up = frameCalls.find((c) => c.type === "keyup");
|
|
up.fn({ code: "KeyZ" });
|
|
win.fire("mouseover", { target: plain });
|
|
const afterRelease = kinds(view);
|
|
// 우클릭 누른 채 들어감 — 나오면 다시 보임
|
|
const b = setup();
|
|
b.win.fire("mousedown", { button: 2, buttons: 2 });
|
|
b.win.tick(400);
|
|
b.win.document = { activeElement: frame };
|
|
b.win.fire("blur", { target: b.win });
|
|
b.win.document = { activeElement: plain };
|
|
b.win.fire("mousemove", { buttons: 2, target: plain });
|
|
const pressBack = kinds(b.view);
|
|
b.win.fire("mouseup", { button: 2 });
|
|
r.cad.blur = {
|
|
blurHidden,
|
|
frameKeyup: up && up.cap,
|
|
afterRelease,
|
|
pressBack,
|
|
pressOff: last(b.view),
|
|
};
|
|
}
|
|
}
|
|
|
|
// ---- 가짜 요소 ----
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
class El {
|
|
constructor(tagName, opts = {}) {
|
|
this.tagName = tagName;
|
|
this.namespaceURI = opts.ns || "http://www.w3.org/1999/xhtml";
|
|
this.rect = opts.rect || { left: 0, top: 0, right: 100, bottom: 100, width: 100, height: 100 };
|
|
this.children = [];
|
|
this.style = {};
|
|
this.dataset = {};
|
|
this.attrs = {};
|
|
this.st = opts.scrollTop || 0;
|
|
this.scrollLeft = 0;
|
|
this.width = opts.width || 0;
|
|
this.height = opts.height || 0;
|
|
this.value = opts.value || "";
|
|
this.checked = !!opts.checked;
|
|
this.type = opts.type || "";
|
|
this.className = "";
|
|
this.hidden = false;
|
|
this.contentDocument = opts.contentDocument || null;
|
|
this.drawn = [];
|
|
this.parent = null;
|
|
this.mark = opts.mark || "";
|
|
}
|
|
getBoundingClientRect() {
|
|
return this.rect;
|
|
}
|
|
// 실제 DOM 처럼 숨긴(hidden) 조상 아래에선 스크롤을 적어도 안 먹음
|
|
get scrollTop() {
|
|
return this.st;
|
|
}
|
|
set scrollTop(v) {
|
|
for (let e = this; e; e = e.parent) if (e.hidden) return;
|
|
this.st = v;
|
|
}
|
|
get classList() {
|
|
return { contains: (k) => this.className.split(" ").includes(k) };
|
|
}
|
|
setAttribute(k, v) {
|
|
this.attrs[k] = v;
|
|
}
|
|
append(...nodes) {
|
|
for (const n of nodes) ((n.parent = this), this.children.push(n));
|
|
}
|
|
replaceChildren() {
|
|
this.children = [];
|
|
}
|
|
replaceWith(n) {
|
|
const i = this.parent.children.indexOf(this);
|
|
n.parent = this.parent;
|
|
this.parent.children[i] = n;
|
|
}
|
|
cloneNode(deep) {
|
|
const c = new El(this.tagName, {
|
|
ns: this.namespaceURI,
|
|
rect: this.rect,
|
|
width: this.width,
|
|
height: this.height,
|
|
type: this.type,
|
|
mark: this.mark,
|
|
});
|
|
c.cloned = true; // 값 · 스크롤 · 그림은 복제에 안 따라옴(실제 DOM 과 같게)
|
|
if (deep) c.append(...this.children.map((k) => k.cloneNode(true)));
|
|
return c;
|
|
}
|
|
getContext() {
|
|
return { drawImage: (src, x, y, w, h) => this.drawn.push([src.mark, x, y, w, h]) };
|
|
}
|
|
querySelectorAll(sel) {
|
|
const out = [];
|
|
const visit = (e) => (
|
|
e.tagName.toLowerCase() === sel && out.push(e),
|
|
e.children.forEach(visit)
|
|
);
|
|
this.children.forEach(visit);
|
|
return out;
|
|
}
|
|
}
|
|
const far = { left: 0, top: 2000, right: 100, bottom: 2100, width: 100, height: 100 };
|
|
function makePage() {
|
|
const app = new El("DIV", {
|
|
rect: { left: 0, top: 0, right: 1000, bottom: 800, width: 1000, height: 800 },
|
|
});
|
|
const scroller = new El("DIV", { scrollTop: 120, mark: "scroller" });
|
|
const canvas = new El("CANVAS", { width: 50, height: 40, mark: "canvas2d" });
|
|
const blank = new El("CANVAS", { mark: "blank" });
|
|
const input = new El("INPUT", { value: "12.5", type: "text" });
|
|
const check = new El("INPUT", { checked: true, type: "checkbox" });
|
|
const offscreen = new El("SECTION", { rect: far, mark: "off" });
|
|
offscreen.append(new El("P"), new El("P"));
|
|
const table = new El("TABLE");
|
|
const row = new El("TR", { rect: far, mark: "row" });
|
|
row.append(new El("TD", { rect: far }));
|
|
table.append(row);
|
|
const svg = new El("svg", { ns: SVG_NS });
|
|
svg.append(new El("path", { ns: SVG_NS }));
|
|
const frameDoc = new El("HTML");
|
|
frameDoc.append(
|
|
new El("CANVAS", {
|
|
width: 300,
|
|
height: 200,
|
|
mark: "cad",
|
|
rect: { left: 5, top: 6, width: 300, height: 200 },
|
|
}),
|
|
);
|
|
const frame = new El("IFRAME", {
|
|
contentDocument: frameDoc,
|
|
rect: { left: 0, top: 0, right: 320, bottom: 240, width: 320, height: 240 },
|
|
});
|
|
const cadBox = new El("DIV", {
|
|
rect: { left: 0, top: 300, right: 400, bottom: 600, width: 400, height: 300 },
|
|
});
|
|
cadBox.className = "cad-host";
|
|
cadBox.append(new El("IFRAME"), new El("A"));
|
|
scroller.scrollHeight = 900; // 스크롤될 수 있는 칸
|
|
scroller.clientHeight = 300;
|
|
scroller.append(canvas, blank, input, check);
|
|
app.append(scroller, offscreen, table, svg, frame, cadBox);
|
|
return app;
|
|
}
|
|
function fakeDocument(app) {
|
|
const doc = { body: new El("BODY") };
|
|
doc.createElement = (tag) => new El(tag.toUpperCase());
|
|
doc.getElementById = (id) => (id === "app" ? app : null);
|
|
return doc;
|
|
}
|
|
{
|
|
const app = makePage();
|
|
const doc = fakeDocument(app);
|
|
const win = fakeWindow(doc);
|
|
const observers = [];
|
|
win.MutationObserver = class {
|
|
constructor(fn) {
|
|
this.fn = fn;
|
|
this.watching = null;
|
|
observers.push(this);
|
|
}
|
|
observe(target, opts) {
|
|
this.watching = target;
|
|
this.opts = opts;
|
|
}
|
|
disconnect() {
|
|
this.watching = null;
|
|
}
|
|
};
|
|
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];
|
|
const [scroller, offscreen, table, svg, frame, cadBox] = copy.children;
|
|
const [canvas, blank, input, check] = scroller.children;
|
|
r.capture = {
|
|
rootClass: root.className,
|
|
rootSize: [root.style.width, root.style.height],
|
|
inert: root.inert,
|
|
ariaHidden: root.attrs["aria-hidden"],
|
|
hidden: root.hidden,
|
|
rootTransform: root.style.transform,
|
|
stageTransform: stage.style.transform,
|
|
pageBox: [page.style.left, page.style.top, page.style.width, page.style.height],
|
|
isClone: copy.cloned === true && copy !== app,
|
|
canvasDrawn: canvas.drawn.map((d) => d[0]),
|
|
blankDrawn: blank.drawn.length,
|
|
inputValue: input.value,
|
|
checkChecked: check.checked,
|
|
scrollTop: scroller.scrollTop,
|
|
offscreenChildren: offscreen.children.length,
|
|
offscreenSize: [offscreen.style.width, offscreen.style.height],
|
|
rowKept: table.children[0].children.length,
|
|
svgKept: svg.children.length,
|
|
frameTag: frame.tagName,
|
|
frameDrawn: frame.drawn,
|
|
frameSize: [frame.style.width, frame.style.height],
|
|
frameEmpty: frame.children.length,
|
|
cadBoxEmpty: cadBox.children.length,
|
|
cadBoxSize: [cadBox.style.width, cadBox.style.height],
|
|
watching: observers[0].watching === 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();
|
|
observers[0].fn();
|
|
win.tick(499);
|
|
const sameBefore = stage.children[0].children[0] === firstCopy;
|
|
win.tick(1);
|
|
const recaptured = stage.children[0].children[0] !== firstCopy && stage.children.length === 1;
|
|
view.move(10, 10);
|
|
const nearEdge = root.style.transform;
|
|
view.hide();
|
|
r.hide = {
|
|
sameBefore,
|
|
recaptured,
|
|
nearEdge,
|
|
stageChildren: stage.children.length,
|
|
hidden: root.hidden,
|
|
disconnected: observers[0].watching === null,
|
|
scrollListen: win.calls.some((c) => c.type === "scroll"),
|
|
};
|
|
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;
|
|
}
|
|
{
|
|
// 221-2 스크롤 — 스크롤한 뒤 켬 · 켠 채 스크롤(칸 · 문서)
|
|
const app = makePage();
|
|
const scroller = app.children[0];
|
|
const doc = fakeDocument(app);
|
|
const win = fakeWindow(doc);
|
|
win.MutationObserver = class {
|
|
observe() {}
|
|
disconnect() {}
|
|
};
|
|
const view = m.createLoupeView(win);
|
|
const cloneScroller = () => doc.body.children[0].children[0].children[0].children[0].children[0];
|
|
const page = () => doc.body.children[0].children[0].children[0];
|
|
view.show(500, 400);
|
|
view.hide();
|
|
scroller.scrollTop = 0; // 0 에서 켬 → 스크롤 칸으로 잡힘
|
|
view.show(500, 400);
|
|
const zero = cloneScroller().scrollTop;
|
|
view.hide();
|
|
scroller.scrollTop = 260; // 꺼진 동안 스크롤해 둠 → 다시 켬(숨긴 원을 다시 씀)
|
|
view.show(500, 400);
|
|
const before = cloneScroller().scrollTop;
|
|
// 켠 채 스크롤 — 다음 화면 틀에 위치만 · 같은 복제
|
|
const first = cloneScroller();
|
|
scroller.scrollTop = 400;
|
|
app.rect = { ...app.rect, top: -50, bottom: 750 }; // 문서도 50 내려감
|
|
const scroll = () => win.calls.filter((c) => c.type === "scroll").forEach((c) => c.fn({}));
|
|
scroll();
|
|
scroll(); // 한 틀에 한 번으로 묶음
|
|
const rafs = win.timers.filter((t) => t.at === win.now + 16).length;
|
|
win.tick(16);
|
|
const during = {
|
|
top: cloneScroller().scrollTop,
|
|
same: cloneScroller() === first,
|
|
pageTop: page().style.top,
|
|
};
|
|
win.tick(484); // 0.5 초 뒤 다시 뜸 — 비워 둔 칸을 채움 · 스크롤은 그대로
|
|
const recaptured = { same: cloneScroller() === first, top: cloneScroller().scrollTop };
|
|
view.hide();
|
|
const n = win.timers.length;
|
|
r.scroll = { zero, before, rafs, during, recaptured, timersAfterHide: n };
|
|
}
|
|
console.log(JSON.stringify(r));
|