for-humanity 0.4.4 → 0.5.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 +16 -0
- package/README.md +5 -5
- package/dist/browser.js +39 -0
- package/dist/cli.css +350 -262
- package/dist/cli.js +1123 -363
- 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/entry/browser/browser.tsx +22 -0
- package/src/{app.tsx → entry/cli/_function/create-app.tsx} +6 -6
- package/src/{content → entry/cli/_function/create-processor}/create-processor.ts +3 -3
- package/src/{content → entry/cli/_function}/read-docs.ts +1 -1
- package/src/{content → entry/cli/_function}/read-home.ts +1 -1
- package/src/{content/read-docs.test.ts → entry/cli/cli.test.ts} +105 -30
- package/src/{cli.ts → entry/cli/cli.ts} +10 -11
- 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/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/{dev.ts → entry/cli/_function/create-dev-app.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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type {NavigationTreeState} from "@/store/navigation/navigation-state";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* persist 저장 자료에서 boolean 접힘 선택만 수용
|
|
5
|
+
*/
|
|
6
|
+
export const toNavigationTreeState = (value: unknown): NavigationTreeState => {
|
|
7
|
+
if (
|
|
8
|
+
typeof value !== "object" ||
|
|
9
|
+
value === null ||
|
|
10
|
+
!("collapsed" in value) ||
|
|
11
|
+
typeof value.collapsed !== "object" ||
|
|
12
|
+
value.collapsed === null ||
|
|
13
|
+
Array.isArray(value.collapsed)
|
|
14
|
+
) {
|
|
15
|
+
return {collapsed: {}};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {collapsed: Object.fromEntries(Object.entries(value.collapsed).filter((entry) => typeof entry[1] === "boolean"))};
|
|
19
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {useSyncExternalStore} from "hono/jsx";
|
|
2
|
+
import {tree_server_state} from "@/store/navigation/_constant/tree";
|
|
3
|
+
import type {createNavigationStores} from "@/store/navigation/create-navigation-stores";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hono 컴포넌트의 persist 구독 · 브라우저 최초 렌더도 저장된 값을 즉시 소비
|
|
7
|
+
*/
|
|
8
|
+
export const useNavigationTreeStore = (tree?: ReturnType<typeof createNavigationStores>["tree"]) => {
|
|
9
|
+
const subscribe = tree === undefined ? () => () => {} : tree.subscribe;
|
|
10
|
+
const getSnapshot = tree === undefined ? () => tree_server_state : tree.getState;
|
|
11
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
12
|
+
};
|
package/src/style/token.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* 공통 토큰의 단일 출처 · --app-종류-쓰임
|
|
3
3
|
* 사용 기준: templates/document/design.md · 테마 분기는 color-scheme에 한정
|
|
4
|
-
* 폭 기준 1024px·640px ·
|
|
4
|
+
* 폭 기준 1536px·1024px·640px · 양쪽 탐색과 본문 배치
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
:root {
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
--app-space-gutter: 28px;
|
|
103
103
|
--app-space-column: 32px;
|
|
104
104
|
--app-space-sidebar-gap: 32px;
|
|
105
|
-
--app-size-sidebar:
|
|
105
|
+
--app-size-sidebar: 240px;
|
|
106
106
|
--app-size-main: 900px;
|
|
107
107
|
--app-size-page: calc(var(--app-size-main) + (var(--app-size-sidebar) + var(--app-space-sidebar-gap) + var(--app-space-gutter)) * 2);
|
|
108
108
|
--app-size-brand: calc(var(--app-font-size-title) * var(--app-line-height-title) + var(--app-space-page-top) + var(--app-space-group));
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
--app-size-menu-button: 44px;
|
|
111
111
|
--app-size-drawer: 320px;
|
|
112
112
|
--app-size-navigation-fade: 28px;
|
|
113
|
+
--app-size-tree-toggle: 24px;
|
|
113
114
|
|
|
114
115
|
/* 화면 위쪽 제목 여백 · hash 도착점과 목차 읽는 선의 공통 기준 */
|
|
115
116
|
--app-space-anchor: 80px;
|
package/src/type/doc-content.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {DocSection} from "@/component/widget/prose/_type/doc-section";
|
|
2
1
|
import type {DocData} from "@/type/doc-data";
|
|
3
2
|
import type {DocHeading} from "@/type/doc-heading";
|
|
3
|
+
import type {DocSection} from "@/type/doc-section";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* 문서 처리 중간 계약 · read-docs의 머리말과 rehype의 제목·절 정보
|
package/src/type/site-assets.ts
CHANGED
package/dist/client.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
var E="fh-cursor-face";var b="(width < 1024px)",S="for-humanity:navigation-scroll";var v=e=>{if(!e)return null;try{return document.getElementById(decodeURIComponent(e.slice(1)))}catch(t){if(t instanceof URIError)return null;throw t}};var p=e=>{let t=v(e);for(let o=t?.closest("details");o;o=o.parentElement?.closest("details"))o.open=!0;return t};var q=()=>{let e=document.querySelector("[data-navigation]"),t=document.querySelector("[data-navigation-drawer]"),o=document.querySelector("[data-navigation-content]"),r=document.querySelector("[data-navigation-open]"),c=document.querySelector("[data-navigation-close]"),n=e?.parentElement;if(!e||!t||!o||!r||!c||!n)return;let l=matchMedia(b),s=()=>{t.open||(document.body.classList.remove("wg_shell__root--drawerOpen"),r.setAttribute("aria-expanded","false"))},u=()=>{t.close(),s()},g=()=>{!l.matches||t.open||(t.showModal(),document.body.classList.add("wg_shell__root--drawerOpen"),r.setAttribute("aria-expanded","true"))},a=i=>{let d=t.getBoundingClientRect();i.target===t&&(i.clientX<d.left||i.clientX>d.right||i.clientY<d.top||i.clientY>d.bottom)&&u()},y=i=>{if(!t.open||i.defaultPrevented||i.button!==0||i.metaKey||i.ctrlKey||i.shiftKey||i.altKey||!(i.target instanceof Element))return;let d=i.target.closest("a[href]");if(!(!(d instanceof HTMLAnchorElement)||d.target!==""&&d.target!=="_self"||d.hasAttribute("download"))&&(u(),d.origin===location.origin&&d.pathname===location.pathname&&d.search===location.search&&d.hash)){let f=p(d.hash);f&&(f.tabIndex=-1,f.focus({preventScroll:!0}))}},m=()=>{let i=document.activeElement;if(l.matches){r.classList.add("wg_shell__menuToggle--visible"),e.parentElement!==o&&o.append(e),i instanceof HTMLElement&&e.contains(i)&&r.focus({preventScroll:!0});return}u(),e.parentElement!==n&&n.append(e),i instanceof HTMLElement&&e.contains(i)?i.focus({preventScroll:!0}):(i===r||i instanceof HTMLElement&&t.contains(i))&&n.querySelector(".wg_shell__brand")?.focus({preventScroll:!0}),r.classList.remove("wg_shell__menuToggle--visible")};r.addEventListener("click",g),c.addEventListener("click",u),t.addEventListener("close",s),t.addEventListener("click",a),e.addEventListener("click",y),l.addEventListener("change",m),m(),n.classList.add("wg_shell__sidebar--navigationReady")};var C=e=>{let t=document.querySelector("[data-navigation]"),o=document.querySelector('[data-navigation-scroll="documents"]'),r=document.querySelector('[data-navigation-scroll="outline"]');if(!(!t||!o||!r))try{let c=sessionStorage.getItem(e);if(c===null)return;let n=JSON.parse(c),l=[...t.querySelectorAll(".wg_shellNav__root a[href]")].map(s=>`${s.pathname}:${s.textContent}`).join(`
|
|
2
|
-
`);if(typeof n!="object"||n===null||!("navigation"in n)||n.navigation!==l)return;"documents"in n&&typeof n.documents=="number"&&Number.isFinite(n.documents)&&n.documents>=0&&(o.scrollTop=n.documents),"pathname"in n&&n.pathname===location.pathname&&"outline"in n&&typeof n.outline=="number"&&Number.isFinite(n.outline)&&n.outline>=0&&(r.scrollTop=n.outline)}catch{}};var N=()=>{let e=document.querySelector("[data-navigation]"),t=document.querySelector('[data-navigation-scroll="documents"]'),o=document.querySelector('[data-navigation-scroll="outline"]'),r=document.querySelector("[data-navigation-drawer]"),c=document.querySelector("[data-navigation-open]");if(!e||!t||!o||!r||!c)return;let n=matchMedia(b),l={mobile:n.matches},s=[t,o],u=new Map,g=[...e.querySelectorAll(".wg_shellNav__root a[href]")].map(m=>`${m.pathname}:${m.textContent}`).join(`
|
|
3
|
-
`),a=()=>{if(l.mobile!==n.matches||n.matches&&!r.open)return;let m=`${S}:${n.matches?"drawer":"desktop"}`;u.set(m,new Map(s.map(i=>[i,i.scrollTop])));try{sessionStorage.setItem(m,JSON.stringify({navigation:g,pathname:location.pathname,documents:t.scrollTop,outline:o.scrollTop}))}catch{}},y=()=>{if(l.mobile=n.matches,n.matches&&!r.open)return;let m=`${S}:${n.matches?"drawer":"desktop"}`,i=u.get(m);if(i===void 0)C(m);else for(let d of s){let f=i.get(d);f!==void 0&&(d.scrollTop=f)}a()};for(let m of s)m.addEventListener("scroll",a,{passive:!0});e.addEventListener("click",a,{capture:!0}),r.addEventListener("cancel",a),r.querySelector("[data-navigation-close]")?.addEventListener("click",a,{capture:!0}),r.addEventListener("click",a,{capture:!0}),c.addEventListener("click",y),n.addEventListener("change",y),addEventListener("pagehide",a),a()};q();N();var h=document.querySelector("[data-cursor-face]");if(h){let e=matchMedia("(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)"),t={x:0,y:0,frame:0},o=()=>{cancelAnimationFrame(t.frame),t.frame=0,h.classList.remove("wg_shell__cursorFace--visible")},r=()=>{e.matches||o()},c=()=>{t.frame===0&&(t.frame=requestAnimationFrame(()=>{t.frame=0,h.style.transform=`translate3d(
|
|
4
|
-
${Math.max(0,Math.min(t.x+16,document.documentElement.clientWidth-h.width-16))}px,
|
|
5
|
-
${Math.max(0,Math.min(t.y+16,document.documentElement.clientHeight-h.height-16))}px,
|
|
6
|
-
0
|
|
7
|
-
)`,h.classList.add("wg_shell__cursorFace--visible")}))},n=g=>{if(!(e.matches&&g.pointerType==="mouse")){o();return}t.x=g.clientX,t.y=g.clientY,c()},l=()=>{h.classList.contains("wg_shell__cursorFace--visible")&&c()},s=()=>{if(h.classList.contains("wg_shell__cursorFace--visible"))try{sessionStorage.setItem(E,JSON.stringify({x:t.x,y:t.y}))}catch{}},u=()=>{try{let g=sessionStorage.getItem(E);if(sessionStorage.removeItem(E),g!==null&&e.matches){let a=JSON.parse(g);typeof a=="object"&&a!==null&&"x"in a&&"y"in a&&typeof a.x=="number"&&typeof a.y=="number"&&Number.isFinite(a.x)&&Number.isFinite(a.y)&&(t.x=a.x,t.y=a.y,c())}}catch{}};u(),document.addEventListener("pointermove",n,{passive:!0}),document.addEventListener("pointerdown",n,{passive:!0}),document.documentElement.addEventListener("pointerleave",o),addEventListener("blur",o),addEventListener("resize",l),addEventListener("pagehide",s),addEventListener("pageshow",u),e.addEventListener("change",r)}var H=p(location.hash),L=null,w=0,_=null,k=()=>{clearTimeout(w),_?.abort(),_=null,L?.classList.remove("wg_prose__headingText--highlighted"),L=null},A=e=>{k(),L=e===null?null:e.querySelector(".wg_prose__headingText"),L!==null&&(L.classList.add("wg_prose__headingText--highlighted"),w=window.setTimeout(k,3e3),_=new AbortController,document.addEventListener("scrollend",O,{signal:_.signal}))},O=e=>{e.target===document&&(clearTimeout(w),_?.abort(),_=null,w=window.setTimeout(k,3e3))},K=e=>{if(!(e.defaultPrevented||e.button!==0||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)&&e.currentTarget instanceof HTMLAnchorElement&&(e.currentTarget.target===""||e.currentTarget.target==="_self")&&!e.currentTarget.hasAttribute("download")&&e.currentTarget.origin===location.origin&&e.currentTarget.pathname===location.pathname&&e.currentTarget.search===location.search&&e.currentTarget.hash){let t=p(e.currentTarget.hash);e.currentTarget.hash===location.hash&&A(t)}},P=()=>{let e=p(location.hash);e?.scrollIntoView(),A(e)};for(let e of document.querySelectorAll("a[href]"))e.addEventListener("click",K);addEventListener("hashchange",P);var M=[...document.querySelectorAll("[data-toc-link]")].flatMap(e=>{let t=v(e.hash),o=e.parentElement;return!t||!o?[]:[{heading:t,link:e,item:o,list:o.parentElement,subs:[...o.querySelectorAll("[data-toc-sub-link]")].flatMap(r=>{let c=v(r.hash),n=r.parentElement;return c&&n?[{heading:c,link:r,item:n}]:[]})}]}),x=M.at(0);if(x){let e=()=>{let t=Number.parseFloat(getComputedStyle(x.heading).scrollMarginTop)+1,o=M.findLast(l=>l.heading.checkVisibility()&&l.heading.getBoundingClientRect().top<=t)??x,r=o.subs.findLast(l=>l.heading.checkVisibility()&&l.heading.getBoundingClientRect().top<=t),c=M.indexOf(o),n=r===void 0?-1:o.subs.indexOf(r);for(let[l,s]of M.entries()){let u=s===o&&r===void 0;s.link.classList.toggle("wg_shellToc__link--active",u),u?s.link.setAttribute("aria-current","location"):s.link.removeAttribute("aria-current"),s.item.classList.toggle("wg_shellToc__item--trail",s.list===o.list&&l<c),s.item.classList.toggle("wg_shellToc__item--current",s===o);for(let[g,a]of s.subs.entries())a.link.classList.toggle("wg_shellToc__subLink--active",a===r),a===r?a.link.setAttribute("aria-current","location"):a.link.removeAttribute("aria-current"),a.item.classList.toggle("wg_shellToc__subItem--trail",s===o&&g<n),a.item.classList.toggle("wg_shellToc__subItem--current",a===r)}};addEventListener("scroll",e,{passive:!0}),addEventListener("resize",e),addEventListener("toggle",e,!0),e()}var B=new AbortController;for(let e of["wheel","touchmove","keydown","mousedown"])addEventListener(e,()=>B.abort(),{once:!0,passive:!0});var I=async()=>{await document.fonts.ready,H&&!B.signal.aborted&&(H.scrollIntoView({behavior:"instant"}),A(H)),document.documentElement.style.removeProperty("scroll-behavior")};document.readyState==="complete"?I():addEventListener("load",I,{once:!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
|
-
};
|