for-humanity 0.4.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +6 -5
- package/dist/browser.js +39 -0
- package/dist/cli.css +350 -262
- package/dist/cli.js +1174 -364
- package/package.json +8 -11
- package/src/component/widget/{shell → cursor-face}/_constant/cursor-face.ts +5 -0
- package/src/component/widget/cursor-face/wg-cursor-face.css +15 -0
- package/src/component/widget/cursor-face/wg-cursor-face.tsx +153 -0
- package/src/component/widget/navigation/_constant/navigation-data-schema.ts +35 -0
- package/src/component/widget/navigation/_constant/navigation.ts +9 -0
- package/src/component/widget/navigation/_function/restore-navigation-scroll.ts +59 -0
- package/src/{content → component/widget/navigation/_function}/to-doc-groups/_to-doc-branches.ts +2 -2
- package/src/{content → component/widget/navigation/_function}/to-doc-groups/_to-doc-group-branches.ts +3 -3
- package/src/{content → component/widget/navigation/_function}/to-doc-groups/to-doc-groups.ts +1 -1
- package/src/component/widget/navigation/_function/to-navigation-branch-id.ts +6 -0
- package/src/component/widget/navigation/_function/to-navigation-data/_to-navigation-document.ts +14 -0
- package/src/component/widget/navigation/_function/to-navigation-data/_to-navigation-group.ts +14 -0
- package/src/component/widget/navigation/_function/to-navigation-data/to-navigation-data.ts +25 -0
- package/src/component/widget/navigation/_function/to-navigation-json.ts +8 -0
- package/src/component/widget/{shell → navigation}/_function/to-toc-groups.ts +2 -2
- package/src/{type → component/widget/navigation/_type}/doc-group.ts +1 -1
- package/src/component/widget/navigation/_type/navigation-data.ts +83 -0
- package/src/component/widget/navigation/_type/navigation-focus-snapshot.ts +21 -0
- package/src/component/widget/navigation/_wg-navigation-branch-toggle.css +28 -0
- package/src/component/widget/navigation/_wg-navigation-branch-toggle.tsx +48 -0
- package/src/component/widget/{shell/_wg-shell-nav-list.css → navigation/_wg-navigation-nav-list.css} +24 -16
- package/src/component/widget/navigation/_wg-navigation-nav-list.tsx +106 -0
- package/src/component/widget/{shell/_wg-shell-nav.css → navigation/_wg-navigation-nav.css} +9 -3
- package/src/component/widget/navigation/_wg-navigation-nav.tsx +43 -0
- package/src/component/widget/navigation/_wg-navigation-scroll.tsx +117 -0
- package/src/component/widget/{shell/_wg-shell-toc.css → navigation/_wg-navigation-toc.css} +39 -24
- package/src/component/widget/navigation/_wg-navigation-toc.tsx +155 -0
- package/src/component/widget/navigation/wg-navigation.css +282 -0
- package/src/component/widget/navigation/wg-navigation.tsx +479 -0
- package/src/component/widget/prose/_function/rehype-sections/rehype-sections.ts +1 -1
- package/src/component/widget/shell/wg-shell.css +13 -269
- package/src/component/widget/shell/wg-shell.tsx +16 -97
- package/src/component/widget/shell-controls/wg-shell-controls.css +4 -0
- package/src/component/widget/shell-controls/wg-shell-controls.tsx +170 -0
- package/src/constant/asset.ts +0 -5
- package/src/constant/copy.ts +5 -0
- package/src/entry/browser/browser.tsx +22 -0
- package/src/entry/cli/_constant/crawling.ts +14 -0
- package/src/{app.tsx → entry/cli/_function/create-app.tsx} +6 -6
- package/src/{dev.ts → entry/cli/_function/create-dev-app.ts} +9 -1
- package/src/{content → entry/cli/_function/create-processor}/create-processor.ts +3 -3
- package/src/{content → entry/cli/_function}/read-docs.ts +21 -2
- package/src/{content → entry/cli/_function}/read-home.ts +1 -1
- package/src/entry/cli/_function/to-crawling-app.ts +42 -0
- package/src/{content/read-docs.test.ts → entry/cli/cli.test.ts} +211 -31
- package/src/{cli.ts → entry/cli/cli.ts} +31 -15
- package/src/store/navigation/_constant/tree.ts +6 -0
- package/src/store/navigation/create-navigation-stores.test.ts +92 -0
- package/src/store/navigation/create-navigation-stores.ts +73 -0
- package/src/store/navigation/navigation-state.ts +46 -0
- package/src/store/navigation/navigation-storage.ts +14 -0
- package/src/store/navigation/to-navigation-scroll-state.ts +42 -0
- package/src/store/navigation/to-navigation-tree-state.ts +19 -0
- package/src/store/navigation/use-navigation-tree-store.ts +12 -0
- package/src/style/token.css +3 -2
- package/src/type/doc-content.ts +1 -1
- package/src/type/doc-file-data.ts +1 -1
- package/src/{component/widget/shell/_type → type}/doc-outline.ts +1 -1
- package/src/type/site-assets.ts +2 -2
- package/src/type/site-config.ts +10 -1
- package/dist/client.js +0 -7
- package/src/client.ts +0 -335
- package/src/component/widget/shell/_constant/navigation.ts +0 -9
- package/src/component/widget/shell/_function/bind-mobile-navigation.ts +0 -147
- package/src/component/widget/shell/_function/bind-navigation-overflow.ts +0 -47
- package/src/component/widget/shell/_function/bind-navigation-scroll.ts +0 -84
- package/src/component/widget/shell/_function/restore-navigation-scroll.ts +0 -47
- package/src/component/widget/shell/_wg-shell-nav-list.tsx +0 -87
- package/src/component/widget/shell/_wg-shell-nav.tsx +0 -49
- package/src/component/widget/shell/_wg-shell-toc.tsx +0 -57
- /package/src/{constant → component/widget/navigation/_constant}/locale.ts +0 -0
- /package/src/component/widget/{shell → navigation}/_constant/reading-line.ts +0 -0
- /package/src/{type → component/widget/navigation/_type}/doc-branch.ts +0 -0
- /package/src/component/widget/{shell → navigation}/_type/toc-group.ts +0 -0
- /package/src/{constant → entry/cli/_constant}/cli.ts +0 -0
- /package/src/{content/rehype-assets.ts → entry/cli/_function/create-processor/_rehype-assets.ts} +0 -0
- /package/src/{content/rehype-heading-ids.ts → entry/cli/_function/create-processor/_rehype-heading-ids.ts} +0 -0
- /package/src/{content/remark-report.ts → entry/cli/_function/create-processor/_remark-report.ts} +0 -0
- /package/src/{component/widget/prose/_type → type}/doc-section.ts +0 -0
package/src/client.ts
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* 브라우저 동작 · 모바일 탐색, 커서 장식, 읽는 절, hash 위치 보정
|
|
3
|
-
* 서버 HTML의 data-* 연결 · React hydration 없음
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import {heading_highlight_hold_ms} from "@/component/widget/prose/_constant/heading-highlight";
|
|
7
|
-
import {cursor_face_offset_px, cursor_face_storage_key} from "@/component/widget/shell/_constant/cursor-face";
|
|
8
|
-
import {reading_line_slack_px} from "@/component/widget/shell/_constant/reading-line";
|
|
9
|
-
import {bindMobileNavigation} from "@/component/widget/shell/_function/bind-mobile-navigation";
|
|
10
|
-
import {bindNavigationScroll} from "@/component/widget/shell/_function/bind-navigation-scroll";
|
|
11
|
-
import {findHashTarget} from "@/util/dom/find-hash-target";
|
|
12
|
-
import {revealHashTarget} from "@/util/dom/reveal-hash-target";
|
|
13
|
-
|
|
14
|
-
bindMobileNavigation();
|
|
15
|
-
bindNavigationScroll();
|
|
16
|
-
|
|
17
|
-
const cursorFace = document.querySelector<HTMLImageElement>("[data-cursor-face]");
|
|
18
|
-
|
|
19
|
-
if (cursorFace) {
|
|
20
|
-
const cursorMedia = matchMedia("(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)");
|
|
21
|
-
const position = {x: 0, y: 0, frame: 0};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 터치 전환과 화면 이탈 시 예약된 이동도 취소해 장식이 다시 나타나지 않게 함
|
|
25
|
-
*/
|
|
26
|
-
const handleCursorHide = () => {
|
|
27
|
-
cancelAnimationFrame(position.frame);
|
|
28
|
-
position.frame = 0;
|
|
29
|
-
cursorFace.classList.remove("wg_shell__cursorFace--visible");
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 비활성 전환에서만 숨김 · 활성화 알림이 새 마우스 이동을 취소하지 않도록 함
|
|
34
|
-
*/
|
|
35
|
-
const handleCursorMediaChange = () => {
|
|
36
|
-
if (!cursorMedia.matches) {
|
|
37
|
-
handleCursorHide();
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 이동·화면 크기 변경·문서 진입에 같은 위치 계산 적용 · 화면 갱신마다 한 번만 반영
|
|
43
|
-
*/
|
|
44
|
-
const renderCursorFace = () => {
|
|
45
|
-
if (position.frame !== 0) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
position.frame = requestAnimationFrame(() => {
|
|
50
|
-
position.frame = 0;
|
|
51
|
-
|
|
52
|
-
// 포인터 좌표는 실행 중에만 결정 · transform으로 문서 재배치 방지
|
|
53
|
-
cursorFace.style.transform = `translate3d(
|
|
54
|
-
${Math.max(0, Math.min(position.x + cursor_face_offset_px, document.documentElement.clientWidth - cursorFace.width - cursor_face_offset_px))}px,
|
|
55
|
-
${Math.max(0, Math.min(position.y + cursor_face_offset_px, document.documentElement.clientHeight - cursorFace.height - cursor_face_offset_px))}px,
|
|
56
|
-
0
|
|
57
|
-
)`;
|
|
58
|
-
cursorFace.classList.add("wg_shell__cursorFace--visible");
|
|
59
|
-
});
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 누른 상태도 마우스 입력으로 처리 · 클릭·텍스트 선택 중에도 얼굴 유지
|
|
64
|
-
*/
|
|
65
|
-
const handleDocumentPointer = (event: PointerEvent) => {
|
|
66
|
-
const isMouse = cursorMedia.matches && event.pointerType === "mouse";
|
|
67
|
-
|
|
68
|
-
if (!isMouse) {
|
|
69
|
-
handleCursorHide();
|
|
70
|
-
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
position.x = event.clientX;
|
|
75
|
-
position.y = event.clientY;
|
|
76
|
-
renderCursorFace();
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* 이미 보이는 얼굴을 새 화면 크기에 맞춰 이동 · 추가 마우스 이동 불필요
|
|
81
|
-
*/
|
|
82
|
-
const handleCursorResize = () => {
|
|
83
|
-
if (cursorFace.classList.contains("wg_shell__cursorFace--visible")) {
|
|
84
|
-
renderCursorFace();
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 전체 페이지 이동에서도 마지막 좌표 유지 · 저장 실패는 기본 마우스 동작에 영향 없음
|
|
90
|
-
*/
|
|
91
|
-
const handlePageHide = () => {
|
|
92
|
-
if (!cursorFace.classList.contains("wg_shell__cursorFace--visible")) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
sessionStorage.setItem(cursor_face_storage_key, JSON.stringify({x: position.x, y: position.y}));
|
|
98
|
-
} catch {
|
|
99
|
-
// 저장소가 막힌 환경에서도 클릭·선택과 현재 페이지의 장식 유지
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 첫 진입과 BFCache 복귀에서 좌표를 한 번 소비 · 이전 문서의 위치가 남지 않도록 삭제
|
|
105
|
-
*/
|
|
106
|
-
const handlePageShow = () => {
|
|
107
|
-
try {
|
|
108
|
-
const stored = sessionStorage.getItem(cursor_face_storage_key);
|
|
109
|
-
|
|
110
|
-
sessionStorage.removeItem(cursor_face_storage_key);
|
|
111
|
-
|
|
112
|
-
if (stored !== null && cursorMedia.matches) {
|
|
113
|
-
const point: unknown = JSON.parse(stored);
|
|
114
|
-
// 속성 타입의 좁히기를 유지하도록 저장소 검증과 사용을 같은 분기에 둠
|
|
115
|
-
if (
|
|
116
|
-
typeof point === "object" &&
|
|
117
|
-
point !== null &&
|
|
118
|
-
"x" in point &&
|
|
119
|
-
"y" in point &&
|
|
120
|
-
typeof point.x === "number" &&
|
|
121
|
-
typeof point.y === "number" &&
|
|
122
|
-
Number.isFinite(point.x) &&
|
|
123
|
-
Number.isFinite(point.y)
|
|
124
|
-
) {
|
|
125
|
-
position.x = point.x;
|
|
126
|
-
position.y = point.y;
|
|
127
|
-
renderCursorFace();
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
} catch {
|
|
131
|
-
// 저장 실패나 잘못된 좌표는 무시하고 다음 실제 마우스 입력에서 표시
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
handlePageShow();
|
|
136
|
-
|
|
137
|
-
document.addEventListener("pointermove", handleDocumentPointer, {passive: true});
|
|
138
|
-
document.addEventListener("pointerdown", handleDocumentPointer, {passive: true});
|
|
139
|
-
document.documentElement.addEventListener("pointerleave", handleCursorHide);
|
|
140
|
-
addEventListener("blur", handleCursorHide);
|
|
141
|
-
addEventListener("resize", handleCursorResize);
|
|
142
|
-
addEventListener("pagehide", handlePageHide);
|
|
143
|
-
addEventListener("pageshow", handlePageShow);
|
|
144
|
-
cursorMedia.addEventListener("change", handleCursorMediaChange);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const hashTarget = revealHashTarget(location.hash);
|
|
148
|
-
|
|
149
|
-
// 값 조립이 아닌 현재 강조의 수명 상태 · 새 이동은 이전 타이머를 취소
|
|
150
|
-
let highlightedHeading: HTMLElement | null = null;
|
|
151
|
-
let highlightTimer = 0;
|
|
152
|
-
let highlightScroll: AbortController | null = null;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* 강조 해제와 도착 감시 정리 · 다음 앵커의 오래된 타이머 개입 방지
|
|
156
|
-
*/
|
|
157
|
-
const clearHeadingHighlight = () => {
|
|
158
|
-
clearTimeout(highlightTimer);
|
|
159
|
-
highlightScroll?.abort();
|
|
160
|
-
highlightScroll = null;
|
|
161
|
-
highlightedHeading?.classList.remove("wg_prose__headingText--highlighted");
|
|
162
|
-
highlightedHeading = null;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* 도착한 제목만 잠시 강조 · 같은 대상 재클릭도 유지 시간 재시작
|
|
167
|
-
*/
|
|
168
|
-
const highlightHeading = (target: HTMLElement | null) => {
|
|
169
|
-
clearHeadingHighlight();
|
|
170
|
-
highlightedHeading = target === null ? null : target.querySelector<HTMLElement>(".wg_prose__headingText");
|
|
171
|
-
|
|
172
|
-
if (highlightedHeading === null) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
highlightedHeading.classList.add("wg_prose__headingText--highlighted");
|
|
177
|
-
highlightTimer = window.setTimeout(clearHeadingHighlight, heading_highlight_hold_ms);
|
|
178
|
-
highlightScroll = new AbortController();
|
|
179
|
-
|
|
180
|
-
// 부드러운 스크롤이 끝난 뒤에도 3초 확보 · 움직이지 않거나 scrollend가 없으면 위 타이머 사용
|
|
181
|
-
document.addEventListener("scrollend", handleHighlightArrival, {signal: highlightScroll.signal});
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* 본문 스크롤의 첫 도착에서 유지 시간 갱신 · 사이드바 스크롤은 제외
|
|
186
|
-
*/
|
|
187
|
-
const handleHighlightArrival = (event: Event) => {
|
|
188
|
-
if (event.target !== document) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
clearTimeout(highlightTimer);
|
|
193
|
-
highlightScroll?.abort();
|
|
194
|
-
highlightScroll = null;
|
|
195
|
-
highlightTimer = window.setTimeout(clearHeadingHighlight, heading_highlight_hold_ms);
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* 같은 hash를 다시 눌러도 접힌 대상 공개 · 기본 링크 이동 유지
|
|
200
|
-
*/
|
|
201
|
-
const handleHashClick = (event: MouseEvent) => {
|
|
202
|
-
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (
|
|
207
|
-
event.currentTarget instanceof HTMLAnchorElement &&
|
|
208
|
-
(event.currentTarget.target === "" || event.currentTarget.target === "_self") &&
|
|
209
|
-
!event.currentTarget.hasAttribute("download") &&
|
|
210
|
-
event.currentTarget.origin === location.origin &&
|
|
211
|
-
event.currentTarget.pathname === location.pathname &&
|
|
212
|
-
event.currentTarget.search === location.search &&
|
|
213
|
-
event.currentTarget.hash
|
|
214
|
-
) {
|
|
215
|
-
const target = revealHashTarget(event.currentTarget.hash);
|
|
216
|
-
|
|
217
|
-
if (event.currentTarget.hash === location.hash) {
|
|
218
|
-
highlightHeading(target);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* hash 변경의 숨은 대상 공개와 위치 보정
|
|
225
|
-
*/
|
|
226
|
-
const handleHashChange = () => {
|
|
227
|
-
const target = revealHashTarget(location.hash);
|
|
228
|
-
|
|
229
|
-
target?.scrollIntoView();
|
|
230
|
-
highlightHeading(target);
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
for (const link of document.querySelectorAll<HTMLAnchorElement>("a[href]")) {
|
|
234
|
-
link.addEventListener("click", handleHashClick);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
addEventListener("hashchange", handleHashChange);
|
|
238
|
-
|
|
239
|
-
// 목차 링크와 실제 제목 연결
|
|
240
|
-
const tocSections = [...document.querySelectorAll<HTMLAnchorElement>("[data-toc-link]")].flatMap((link) => {
|
|
241
|
-
const heading = findHashTarget(link.hash);
|
|
242
|
-
const item = link.parentElement;
|
|
243
|
-
|
|
244
|
-
if (!heading || !item) {
|
|
245
|
-
return [];
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return [
|
|
249
|
-
{
|
|
250
|
-
heading,
|
|
251
|
-
link,
|
|
252
|
-
item,
|
|
253
|
-
list: item.parentElement,
|
|
254
|
-
subs: [...item.querySelectorAll<HTMLAnchorElement>("[data-toc-sub-link]")].flatMap((sub) => {
|
|
255
|
-
const subHeading = findHashTarget(sub.hash);
|
|
256
|
-
const subItem = sub.parentElement;
|
|
257
|
-
|
|
258
|
-
return subHeading && subItem ? [{heading: subHeading, link: sub, item: subItem}] : [];
|
|
259
|
-
}),
|
|
260
|
-
},
|
|
261
|
-
];
|
|
262
|
-
});
|
|
263
|
-
const firstSection = tocSections.at(0);
|
|
264
|
-
|
|
265
|
-
if (firstSection) {
|
|
266
|
-
/**
|
|
267
|
-
* 읽는 선 기준 절·소제목 표시
|
|
268
|
-
* 기준: 제목의 scroll-margin-top + reading_line_slack_px
|
|
269
|
-
*/
|
|
270
|
-
const markActive = () => {
|
|
271
|
-
// scroll-margin-top 계산값: CSS px
|
|
272
|
-
const line = Number.parseFloat(getComputedStyle(firstSection.heading).scrollMarginTop) + reading_line_slack_px;
|
|
273
|
-
// 접힌 Details 안 제목은 좌표가 남아도 현재 위치에서 제외
|
|
274
|
-
const current = tocSections.findLast((section) => section.heading.checkVisibility() && section.heading.getBoundingClientRect().top <= line) ?? firstSection;
|
|
275
|
-
const sub = current.subs.findLast((item) => item.heading.checkVisibility() && item.heading.getBoundingClientRect().top <= line);
|
|
276
|
-
const currentIndex = tocSections.indexOf(current);
|
|
277
|
-
const currentSubIndex = sub === undefined ? -1 : current.subs.indexOf(sub);
|
|
278
|
-
|
|
279
|
-
for (const [index, section] of tocSections.entries()) {
|
|
280
|
-
const active = section === current && sub === undefined;
|
|
281
|
-
section.link.classList.toggle("wg_shellToc__link--active", active);
|
|
282
|
-
if (active) {
|
|
283
|
-
section.link.setAttribute("aria-current", "location");
|
|
284
|
-
} else {
|
|
285
|
-
section.link.removeAttribute("aria-current");
|
|
286
|
-
}
|
|
287
|
-
section.item.classList.toggle("wg_shellToc__item--trail", section.list === current.list && index < currentIndex);
|
|
288
|
-
section.item.classList.toggle("wg_shellToc__item--current", section === current);
|
|
289
|
-
|
|
290
|
-
for (const [subIndex, item] of section.subs.entries()) {
|
|
291
|
-
item.link.classList.toggle("wg_shellToc__subLink--active", item === sub);
|
|
292
|
-
if (item === sub) {
|
|
293
|
-
item.link.setAttribute("aria-current", "location");
|
|
294
|
-
} else {
|
|
295
|
-
item.link.removeAttribute("aria-current");
|
|
296
|
-
}
|
|
297
|
-
item.item.classList.toggle("wg_shellToc__subItem--trail", section === current && subIndex < currentSubIndex);
|
|
298
|
-
item.item.classList.toggle("wg_shellToc__subItem--current", item === sub);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
addEventListener("scroll", markActive, {passive: true});
|
|
304
|
-
addEventListener("resize", markActive);
|
|
305
|
-
addEventListener("toggle", markActive, true);
|
|
306
|
-
markActive();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const userScroll = new AbortController();
|
|
310
|
-
|
|
311
|
-
for (const type of ["wheel", "touchmove", "keydown", "mousedown"]) {
|
|
312
|
-
addEventListener(type, () => userScroll.abort(), {once: true, passive: true});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* 글꼴 로드 후 hash 위치 보정 · 사용자 입력 시 자동 이동 취소
|
|
317
|
-
* 초기 smooth scroll과 보정의 충돌 방지 · 보정 후 CSS 동작 복원
|
|
318
|
-
*/
|
|
319
|
-
const settleHashScroll = async () => {
|
|
320
|
-
await document.fonts.ready;
|
|
321
|
-
|
|
322
|
-
if (hashTarget && !userScroll.signal.aborted) {
|
|
323
|
-
hashTarget.scrollIntoView({behavior: "instant"});
|
|
324
|
-
highlightHeading(hashTarget);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
document.documentElement.style.removeProperty("scroll-behavior");
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
// 글꼴 요청 완료 시점 보장 · load 후 fonts.ready
|
|
331
|
-
if (document.readyState === "complete") {
|
|
332
|
-
settleHashScroll();
|
|
333
|
-
} else {
|
|
334
|
-
addEventListener("load", settleHashScroll, {once: true});
|
|
335
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import {navigation_mobile_query} from "@/component/widget/shell/_constant/navigation";
|
|
2
|
-
import {revealHashTarget} from "@/util/dom/reveal-hash-target";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 같은 문서 탐색을 데스크톱 사이드바와 모바일 모달 사이에서 이동
|
|
6
|
-
* 닫기·크기 변경에서 스크롤과 포커스 복원 · 초기화 실패 시 기본 목록 유지
|
|
7
|
-
*/
|
|
8
|
-
export const bindMobileNavigation = () => {
|
|
9
|
-
const navigation = document.querySelector<HTMLElement>("[data-navigation]");
|
|
10
|
-
const dialog = document.querySelector<HTMLDialogElement>("[data-navigation-drawer]");
|
|
11
|
-
const content = document.querySelector<HTMLElement>("[data-navigation-content]");
|
|
12
|
-
const openButton = document.querySelector<HTMLButtonElement>("[data-navigation-open]");
|
|
13
|
-
const closeButton = document.querySelector<HTMLButtonElement>("[data-navigation-close]");
|
|
14
|
-
const sidebar = navigation?.parentElement;
|
|
15
|
-
|
|
16
|
-
if (!navigation || !dialog || !content || !openButton || !closeButton || !sidebar) {
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const mobile = matchMedia(navigation_mobile_query);
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Escape 등 네이티브 닫기에서도 배경 스크롤과 버튼의 상태 복원
|
|
24
|
-
*/
|
|
25
|
-
const handleClose = () => {
|
|
26
|
-
// 닫기 직후 다시 연 모달에 이전 close 이벤트가 개입하지 않도록 함
|
|
27
|
-
if (dialog.open) {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
document.body.classList.remove("wg_shell__root--drawerOpen");
|
|
32
|
-
openButton.setAttribute("aria-expanded", "false");
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 기본 링크 이동보다 먼저 잠금 해제 · 네이티브 close가 열기 버튼으로 포커스 복원
|
|
37
|
-
*/
|
|
38
|
-
const closeNavigation = () => {
|
|
39
|
-
dialog.close();
|
|
40
|
-
handleClose();
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 네이티브 모달로 외부 포커스 차단 · 모바일에서만 열기
|
|
45
|
-
*/
|
|
46
|
-
const handleOpen = () => {
|
|
47
|
-
if (!mobile.matches || dialog.open) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
dialog.showModal();
|
|
52
|
-
document.body.classList.add("wg_shell__root--drawerOpen");
|
|
53
|
-
openButton.setAttribute("aria-expanded", "true");
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* 대화상자 바깥 배경 클릭만 닫기 · 내부의 빈 공간은 유지
|
|
58
|
-
*/
|
|
59
|
-
const handleBackdropClick = (event: MouseEvent) => {
|
|
60
|
-
const bounds = dialog.getBoundingClientRect();
|
|
61
|
-
|
|
62
|
-
if (event.target === dialog && (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom)) {
|
|
63
|
-
closeNavigation();
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 링크의 기본 이동은 유지 · 목차는 닫은 뒤 실제 제목에 키보드 위치 전달
|
|
69
|
-
*/
|
|
70
|
-
const handleNavigationClick = (event: MouseEvent) => {
|
|
71
|
-
if (
|
|
72
|
-
!dialog.open ||
|
|
73
|
-
event.defaultPrevented ||
|
|
74
|
-
event.button !== 0 ||
|
|
75
|
-
event.metaKey ||
|
|
76
|
-
event.ctrlKey ||
|
|
77
|
-
event.shiftKey ||
|
|
78
|
-
event.altKey ||
|
|
79
|
-
!(event.target instanceof Element)
|
|
80
|
-
) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const link = event.target.closest("a[href]");
|
|
85
|
-
|
|
86
|
-
if (!(link instanceof HTMLAnchorElement) || (link.target !== "" && link.target !== "_self") || link.hasAttribute("download")) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
closeNavigation();
|
|
91
|
-
|
|
92
|
-
if (link.origin === location.origin && link.pathname === location.pathname && link.search === location.search && link.hash) {
|
|
93
|
-
const target = revealHashTarget(link.hash);
|
|
94
|
-
|
|
95
|
-
if (target) {
|
|
96
|
-
target.tabIndex = -1;
|
|
97
|
-
target.focus({preventScroll: true});
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* 반응형 전환에도 목록 중복 없음 · 열린 모달을 데스크톱으로 가져가기 전에 닫기
|
|
104
|
-
*/
|
|
105
|
-
const handleLayoutChange = () => {
|
|
106
|
-
const focused = document.activeElement;
|
|
107
|
-
|
|
108
|
-
if (mobile.matches) {
|
|
109
|
-
openButton.classList.add("wg_shell__menuToggle--visible");
|
|
110
|
-
|
|
111
|
-
if (navigation.parentElement !== content) {
|
|
112
|
-
content.append(navigation);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (focused instanceof HTMLElement && navigation.contains(focused)) {
|
|
116
|
-
openButton.focus({preventScroll: true});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
closeNavigation();
|
|
123
|
-
|
|
124
|
-
// 첫 desktop 연결에서 같은 DOM을 재삽입하면 내부 스크롤이 0으로 초기화됨
|
|
125
|
-
if (navigation.parentElement !== sidebar) {
|
|
126
|
-
sidebar.append(navigation);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (focused instanceof HTMLElement && navigation.contains(focused)) {
|
|
130
|
-
focused.focus({preventScroll: true});
|
|
131
|
-
} else if (focused === openButton || (focused instanceof HTMLElement && dialog.contains(focused))) {
|
|
132
|
-
sidebar.querySelector<HTMLAnchorElement>(".wg_shell__brand")?.focus({preventScroll: true});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// CSS의 폭 조건이 먼저 버튼을 숨겨 포커스가 사라지지 않도록 복원 후 숨김
|
|
136
|
-
openButton.classList.remove("wg_shell__menuToggle--visible");
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
openButton.addEventListener("click", handleOpen);
|
|
140
|
-
closeButton.addEventListener("click", closeNavigation);
|
|
141
|
-
dialog.addEventListener("close", handleClose);
|
|
142
|
-
dialog.addEventListener("click", handleBackdropClick);
|
|
143
|
-
navigation.addEventListener("click", handleNavigationClick);
|
|
144
|
-
mobile.addEventListener("change", handleLayoutChange);
|
|
145
|
-
handleLayoutChange();
|
|
146
|
-
sidebar.classList.add("wg_shell__sidebar--navigationReady");
|
|
147
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 실제 남은 내용이 있는 위·아래 끝만 흐림 표시 · 스크롤·글꼴·화면 크기 추적
|
|
3
|
-
* HTML 직후 직렬화해 실행하므로 import나 바깥 변수 없이 브라우저 DOM만 사용
|
|
4
|
-
* 관찰자는 문서 수명 동안 유지 · BFCache 복귀에서도 같은 탐색 DOM 추적
|
|
5
|
-
*/
|
|
6
|
-
export const bindNavigationOverflow = () => {
|
|
7
|
-
const ports = [...document.querySelectorAll<HTMLElement>("[data-navigation-scroll]")];
|
|
8
|
-
const frame = {id: 0};
|
|
9
|
-
// 소수 픽셀 반올림으로 끝에서 흐림이 남는 것을 방지
|
|
10
|
-
const edgeTolerance = 1;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 콘텐츠가 잘리는 끝만 표시 · 네이티브 스크롤은 그대로 사용
|
|
14
|
-
*/
|
|
15
|
-
const updateOverflow = () => {
|
|
16
|
-
frame.id = 0;
|
|
17
|
-
|
|
18
|
-
for (const port of ports) {
|
|
19
|
-
port.classList.add("wg_shell__scroll--ready");
|
|
20
|
-
port.classList.toggle("wg_shell__scroll--above", port.scrollTop > edgeTolerance);
|
|
21
|
-
port.classList.toggle("wg_shell__scroll--below", port.scrollHeight - port.clientHeight - port.scrollTop > edgeTolerance);
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 연속 스크롤과 크기 변경은 프레임당 한 번만 반영
|
|
27
|
-
*/
|
|
28
|
-
const scheduleOverflow = () => {
|
|
29
|
-
if (frame.id === 0) {
|
|
30
|
-
frame.id = requestAnimationFrame(updateOverflow);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const observer = new ResizeObserver(scheduleOverflow);
|
|
35
|
-
|
|
36
|
-
for (const port of ports) {
|
|
37
|
-
port.addEventListener("scroll", scheduleOverflow, {passive: true});
|
|
38
|
-
observer.observe(port);
|
|
39
|
-
|
|
40
|
-
if (port.firstElementChild) {
|
|
41
|
-
observer.observe(port.firstElementChild);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
addEventListener("pageshow", scheduleOverflow);
|
|
46
|
-
updateOverflow();
|
|
47
|
-
};
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import {navigation_mobile_query, navigation_scroll_storage_key} from "@/component/widget/shell/_constant/navigation";
|
|
2
|
-
import {restoreNavigationScroll} from "@/component/widget/shell/_function/restore-navigation-scroll";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 문서 목록과 페이지 목차의 위치를 따로 저장 · TOC 길이가 목록 위치를 바꾸지 않음
|
|
6
|
-
* 첫 데스크톱 복원은 HTML에서 완료 · 늦은 module 실행에서 다시 이동하지 않음
|
|
7
|
-
*/
|
|
8
|
-
export const bindNavigationScroll = () => {
|
|
9
|
-
const navigation = document.querySelector<HTMLElement>("[data-navigation]");
|
|
10
|
-
const documents = document.querySelector<HTMLElement>('[data-navigation-scroll="documents"]');
|
|
11
|
-
const outline = document.querySelector<HTMLElement>('[data-navigation-scroll="outline"]');
|
|
12
|
-
const dialog = document.querySelector<HTMLDialogElement>("[data-navigation-drawer]");
|
|
13
|
-
const openButton = document.querySelector<HTMLButtonElement>("[data-navigation-open]");
|
|
14
|
-
|
|
15
|
-
if (!navigation || !documents || !outline || !dialog || !openButton) {
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const mobile = matchMedia(navigation_mobile_query);
|
|
20
|
-
const layout = {mobile: mobile.matches};
|
|
21
|
-
const ports = [documents, outline];
|
|
22
|
-
const positions = new Map<string, Map<HTMLElement, number>>();
|
|
23
|
-
const signature = [...navigation.querySelectorAll<HTMLAnchorElement>(".wg_shellNav__root a[href]")].map((link) => `${link.pathname}:${link.textContent}`).join("\n");
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 숨김·닫기보다 먼저 마지막 위치 보존 · 저장소 차단 시에도 드로어 재열기 유지
|
|
27
|
-
*/
|
|
28
|
-
const saveScroll = () => {
|
|
29
|
-
if (layout.mobile !== mobile.matches || (mobile.matches && !dialog.open)) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const key = `${navigation_scroll_storage_key}:${mobile.matches ? "drawer" : "desktop"}`;
|
|
34
|
-
positions.set(key, new Map(ports.map((port) => [port, port.scrollTop])));
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
sessionStorage.setItem(key, JSON.stringify({navigation: signature, pathname: location.pathname, documents: documents.scrollTop, outline: outline.scrollTop}));
|
|
38
|
-
} catch {
|
|
39
|
-
// 현재 페이지 안의 위치는 메모리에 유지 · 저장 실패가 링크 이동을 막지 않음
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 열기·반응형 DOM 이동 완료 후 화면별 위치 복원
|
|
45
|
-
*/
|
|
46
|
-
const restoreScroll = () => {
|
|
47
|
-
layout.mobile = mobile.matches;
|
|
48
|
-
|
|
49
|
-
if (mobile.matches && !dialog.open) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const key = `${navigation_scroll_storage_key}:${mobile.matches ? "drawer" : "desktop"}`;
|
|
54
|
-
const saved = positions.get(key);
|
|
55
|
-
|
|
56
|
-
if (saved === undefined) {
|
|
57
|
-
restoreNavigationScroll(key);
|
|
58
|
-
} else {
|
|
59
|
-
for (const port of ports) {
|
|
60
|
-
const top = saved.get(port);
|
|
61
|
-
|
|
62
|
-
if (top !== undefined) {
|
|
63
|
-
port.scrollTop = top;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
saveScroll();
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
for (const port of ports) {
|
|
72
|
-
port.addEventListener("scroll", saveScroll, {passive: true});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// 캡처 단계에서 모바일의 기본 닫기보다 먼저 저장
|
|
76
|
-
navigation.addEventListener("click", saveScroll, {capture: true});
|
|
77
|
-
dialog.addEventListener("cancel", saveScroll);
|
|
78
|
-
dialog.querySelector("[data-navigation-close]")?.addEventListener("click", saveScroll, {capture: true});
|
|
79
|
-
dialog.addEventListener("click", saveScroll, {capture: true});
|
|
80
|
-
openButton.addEventListener("click", restoreScroll);
|
|
81
|
-
mobile.addEventListener("change", restoreScroll);
|
|
82
|
-
addEventListener("pagehide", saveScroll);
|
|
83
|
-
saveScroll();
|
|
84
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 탐색 HTML 직후와 드로어 열기에서 저장 위치 복원 · 첫 paint 전 실행
|
|
3
|
-
* HTML에 함수 본문을 직렬화하므로 import나 바깥 변수 없이 브라우저 DOM만 사용
|
|
4
|
-
* @param storageKey 탭·화면 모드별 저장 키
|
|
5
|
-
*/
|
|
6
|
-
export const restoreNavigationScroll = (storageKey: string) => {
|
|
7
|
-
const navigation = document.querySelector<HTMLElement>("[data-navigation]");
|
|
8
|
-
const documents = document.querySelector<HTMLElement>('[data-navigation-scroll="documents"]');
|
|
9
|
-
const outline = document.querySelector<HTMLElement>('[data-navigation-scroll="outline"]');
|
|
10
|
-
|
|
11
|
-
if (!navigation || !documents || !outline) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const value = sessionStorage.getItem(storageKey);
|
|
17
|
-
|
|
18
|
-
if (value === null) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const stored: unknown = JSON.parse(value);
|
|
23
|
-
const signature = [...navigation.querySelectorAll<HTMLAnchorElement>(".wg_shellNav__root a[href]")].map((link) => `${link.pathname}:${link.textContent}`).join("\n");
|
|
24
|
-
|
|
25
|
-
if (typeof stored !== "object" || stored === null || !("navigation" in stored) || stored.navigation !== signature) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if ("documents" in stored && typeof stored.documents === "number" && Number.isFinite(stored.documents) && stored.documents >= 0) {
|
|
30
|
-
documents.scrollTop = stored.documents;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 문서 목록은 공통 위치 · 목차는 같은 페이지에 다시 들어왔을 때만 복원
|
|
34
|
-
if (
|
|
35
|
-
"pathname" in stored &&
|
|
36
|
-
stored.pathname === location.pathname &&
|
|
37
|
-
"outline" in stored &&
|
|
38
|
-
typeof stored.outline === "number" &&
|
|
39
|
-
Number.isFinite(stored.outline) &&
|
|
40
|
-
stored.outline >= 0
|
|
41
|
-
) {
|
|
42
|
-
outline.scrollTop = stored.outline;
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// 차단·손상된 저장소는 복원만 생략 · 기본 링크와 스크롤 유지
|
|
46
|
-
}
|
|
47
|
-
};
|