for-humanity 0.4.3 → 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 +23 -0
- package/README.md +5 -5
- package/dist/browser.js +39 -0
- package/dist/cli.css +372 -299
- package/dist/cli.js +1125 -301
- 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} +43 -26
- 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 -309
- package/src/component/widget/shell/wg-shell.tsx +16 -75
- 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 +4 -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 -6
- 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 -140
- package/src/component/widget/shell/_function/bind-navigation-scroll.ts +0 -123
- 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,42 @@
|
|
|
1
|
+
import type {NavigationPosition, NavigationScrollState} from "@/store/navigation/navigation-state";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 알 수 없는 저장 형태·음수·무한 좌표 제외 · 초기 HTML과 Zustand에 같은 검증 적용
|
|
5
|
+
*/
|
|
6
|
+
export const toNavigationScrollState = (value: unknown): NavigationScrollState => {
|
|
7
|
+
/**
|
|
8
|
+
* 한 배치의 완전한 좌표만 복원 대상으로 수용
|
|
9
|
+
*/
|
|
10
|
+
const toPosition = (position: unknown): NavigationPosition | undefined => {
|
|
11
|
+
if (
|
|
12
|
+
typeof position !== "object" ||
|
|
13
|
+
position === null ||
|
|
14
|
+
!("navigation" in position) ||
|
|
15
|
+
typeof position.navigation !== "string" ||
|
|
16
|
+
!("pathname" in position) ||
|
|
17
|
+
typeof position.pathname !== "string" ||
|
|
18
|
+
!("documents" in position) ||
|
|
19
|
+
typeof position.documents !== "number" ||
|
|
20
|
+
!Number.isFinite(position.documents) ||
|
|
21
|
+
position.documents < 0 ||
|
|
22
|
+
!("outline" in position) ||
|
|
23
|
+
typeof position.outline !== "number" ||
|
|
24
|
+
!Number.isFinite(position.outline) ||
|
|
25
|
+
position.outline < 0
|
|
26
|
+
) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
return {navigation: position.navigation, pathname: position.pathname, documents: position.documents, outline: position.outline};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (typeof value !== "object" || value === null || !("positions" in value) || typeof value.positions !== "object" || value.positions === null) {
|
|
33
|
+
return {positions: {}};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const positions = {
|
|
37
|
+
wide: toPosition("wide" in value.positions ? value.positions.wide : undefined),
|
|
38
|
+
desktop: toPosition("desktop" in value.positions ? value.positions.desktop : undefined),
|
|
39
|
+
drawer: toPosition("drawer" in value.positions ? value.positions.drawer : undefined),
|
|
40
|
+
};
|
|
41
|
+
return {positions: Object.fromEntries(Object.entries(positions).filter((entry) => entry[1] !== undefined))};
|
|
42
|
+
};
|
|
@@ -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,13 +102,15 @@
|
|
|
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));
|
|
109
109
|
--app-size-mobile-header: 56px;
|
|
110
110
|
--app-size-menu-button: 44px;
|
|
111
111
|
--app-size-drawer: 320px;
|
|
112
|
+
--app-size-navigation-fade: 28px;
|
|
113
|
+
--app-size-tree-toggle: 24px;
|
|
112
114
|
|
|
113
115
|
/* 화면 위쪽 제목 여백 · hash 도착점과 목차 읽는 선의 공통 기준 */
|
|
114
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,6 +0,0 @@
|
|
|
1
|
-
var T="fh-cursor-face";var w="(width < 1024px)",x="for-humanity:navigation-scroll";var b=e=>{if(!e)return null;try{return document.getElementById(decodeURIComponent(e.slice(1)))}catch(t){if(t instanceof URIError)return null;throw t}};var y=e=>{let t=b(e);for(let n=t?.closest("details");n;n=n.parentElement?.closest("details"))n.open=!0;return t};var I=()=>{let e=document.querySelector("[data-navigation]"),t=document.querySelector("[data-navigation-drawer]"),n=document.querySelector("[data-navigation-content]"),a=document.querySelector("[data-navigation-open]"),s=document.querySelector("[data-navigation-close]"),u=e?.parentElement;if(!e||!t||!n||!a||!s||!u)return;let d=matchMedia(w),l=()=>{t.open||(document.body.classList.remove("wg_shell__root--drawerOpen"),a.setAttribute("aria-expanded","false"))},m=()=>{t.close(),l()},g=()=>{!d.matches||t.open||(t.showModal(),document.body.classList.add("wg_shell__root--drawerOpen"),a.setAttribute("aria-expanded","true"))},i=r=>{let o=t.getBoundingClientRect();r.target===t&&(r.clientX<o.left||r.clientX>o.right||r.clientY<o.top||r.clientY>o.bottom)&&m()},L=r=>{if(!t.open||r.defaultPrevented||r.button!==0||r.metaKey||r.ctrlKey||r.shiftKey||r.altKey||!(r.target instanceof Element))return;let o=r.target.closest("a[href]");if(!(!(o instanceof HTMLAnchorElement)||o.target!==""&&o.target!=="_self"||o.hasAttribute("download"))&&(m(),o.origin===location.origin&&o.pathname===location.pathname&&o.search===location.search&&o.hash)){let c=y(o.hash);c&&(c.tabIndex=-1,c.focus({preventScroll:!0}))}},p=()=>{let r=document.activeElement;if(d.matches){a.classList.add("wg_shell__menuToggle--visible"),n.append(e),r instanceof HTMLElement&&e.contains(r)&&a.focus({preventScroll:!0});return}m(),u.append(e),r instanceof HTMLElement&&e.contains(r)?r.focus({preventScroll:!0}):(r===a||r instanceof HTMLElement&&t.contains(r))&&u.querySelector(".wg_shell__brand")?.focus({preventScroll:!0}),a.classList.remove("wg_shell__menuToggle--visible")};a.addEventListener("click",g),s.addEventListener("click",m),t.addEventListener("close",l),t.addEventListener("click",i),e.addEventListener("click",L),d.addEventListener("change",p),p(),u.classList.add("wg_shell__sidebar--navigationReady")};var N=()=>{let e=document.querySelector("[data-navigation]"),t=document.querySelector(".wg_shell__sidebar"),n=document.querySelector("[data-navigation-drawer]"),a=document.querySelector("[data-navigation-open]");if(!e||!t||!n||!a)return;let s=matchMedia(w),u={mobile:s.matches},d=[...e.querySelectorAll(".wg_shellNav__root a[href]")].map(o=>`${o.pathname}:${o.textContent}`).join(`
|
|
2
|
-
`),l=new Map,m=new Map([[t,`${x}:desktop`],[n,`${x}:drawer`]]),g=new AbortController;for(let[o,c]of m){try{let _=sessionStorage.getItem(c);if(_!==null){let h=JSON.parse(_);typeof h=="object"&&h!==null&&"navigation"in h&&h.navigation===d&&"top"in h&&typeof h.top=="number"&&Number.isFinite(h.top)&&h.top>=0&&l.set(o,h.top)}}catch{}for(let _ of["wheel","touchstart","pointerdown","keydown"])o.addEventListener(_,()=>g.abort(),{once:!0,passive:!0})}let i=new Map(l),L=()=>{u.mobile=s.matches;let o=s.matches?n:t,c=l.get(o);c===void 0||s.matches&&!n.open||(o.scrollTop=c)},p=o=>{let c=s.matches?n:t,_=m.get(c);if(!(u.mobile!==s.matches||o.type==="scroll"&&o.currentTarget!==c||_===void 0||s.matches&&!n.open)){l.set(c,c.scrollTop);try{sessionStorage.setItem(_,JSON.stringify({navigation:d,top:c.scrollTop}))}catch{}}},r=async()=>{if(await document.fonts.ready,!g.signal.aborted){let o=s.matches?n:t,c=i.get(o);c!==void 0&&(!s.matches||n.open)&&(o.scrollTop=c)}};t.addEventListener("scroll",p,{passive:!0}),n.addEventListener("scroll",p,{passive:!0}),e.addEventListener("click",p,{capture:!0}),a.addEventListener("click",L),s.addEventListener("change",L),addEventListener("pagehide",p),L(),document.readyState==="complete"?r():addEventListener("load",r,{once:!0})};I();N();var f=document.querySelector("[data-cursor-face]");if(f){let e=matchMedia("(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)"),t={x:0,y:0,frame:0},n=()=>{cancelAnimationFrame(t.frame),t.frame=0,f.classList.remove("wg_shell__cursorFace--visible")},a=()=>{e.matches||n()},s=()=>{t.frame===0&&(t.frame=requestAnimationFrame(()=>{t.frame=0,f.style.transform=`translate3d(
|
|
3
|
-
${Math.max(0,Math.min(t.x+16,document.documentElement.clientWidth-f.width-16))}px,
|
|
4
|
-
${Math.max(0,Math.min(t.y+16,document.documentElement.clientHeight-f.height-16))}px,
|
|
5
|
-
0
|
|
6
|
-
)`,f.classList.add("wg_shell__cursorFace--visible")}))},u=g=>{if(!(e.matches&&g.pointerType==="mouse")){n();return}t.x=g.clientX,t.y=g.clientY,s()},d=()=>{f.classList.contains("wg_shell__cursorFace--visible")&&s()},l=()=>{if(f.classList.contains("wg_shell__cursorFace--visible"))try{sessionStorage.setItem(T,JSON.stringify({x:t.x,y:t.y}))}catch{}},m=()=>{try{let g=sessionStorage.getItem(T);if(sessionStorage.removeItem(T),g!==null&&e.matches){let i=JSON.parse(g);typeof i=="object"&&i!==null&&"x"in i&&"y"in i&&typeof i.x=="number"&&typeof i.y=="number"&&Number.isFinite(i.x)&&Number.isFinite(i.y)&&(t.x=i.x,t.y=i.y,s())}}catch{}};m(),document.addEventListener("pointermove",u,{passive:!0}),document.addEventListener("pointerdown",u,{passive:!0}),document.documentElement.addEventListener("pointerleave",n),addEventListener("blur",n),addEventListener("resize",d),addEventListener("pagehide",l),addEventListener("pageshow",m),e.addEventListener("change",a)}var k=y(location.hash),E=null,S=0,v=null,C=()=>{clearTimeout(S),v?.abort(),v=null,E?.classList.remove("wg_prose__headingText--highlighted"),E=null},q=e=>{C(),E=e===null?null:e.querySelector(".wg_prose__headingText"),E!==null&&(E.classList.add("wg_prose__headingText--highlighted"),S=window.setTimeout(C,3e3),v=new AbortController,document.addEventListener("scrollend",P,{signal:v.signal}))},P=e=>{e.target===document&&(clearTimeout(S),v?.abort(),v=null,S=window.setTimeout(C,3e3))},R=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=y(e.currentTarget.hash);e.currentTarget.hash===location.hash&&q(t)}},$=()=>{let e=y(location.hash);e?.scrollIntoView(),q(e)};for(let e of document.querySelectorAll("a[href]"))e.addEventListener("click",R);addEventListener("hashchange",$);var H=[...document.querySelectorAll("[data-toc-link]")].flatMap(e=>{let t=b(e.hash),n=e.parentElement;return!t||!n?[]:[{heading:t,link:e,item:n,list:n.parentElement,subs:[...n.querySelectorAll("[data-toc-sub-link]")].flatMap(a=>{let s=b(a.hash),u=a.parentElement;return s&&u?[{heading:s,link:a,item:u}]:[]})}]}),A=H.at(0);if(A){let e=()=>{let t=Number.parseFloat(getComputedStyle(A.heading).scrollMarginTop)+1,n=H.findLast(d=>d.heading.checkVisibility()&&d.heading.getBoundingClientRect().top<=t)??A,a=n.subs.findLast(d=>d.heading.checkVisibility()&&d.heading.getBoundingClientRect().top<=t),s=H.indexOf(n),u=a===void 0?-1:n.subs.indexOf(a);for(let[d,l]of H.entries()){let m=l===n&&a===void 0;l.link.classList.toggle("wg_shellToc__link--active",m),m?l.link.setAttribute("aria-current","location"):l.link.removeAttribute("aria-current"),l.item.classList.toggle("wg_shellToc__item--trail",l.list===n.list&&d<s),l.item.classList.toggle("wg_shellToc__item--current",l===n);for(let[g,i]of l.subs.entries())i.link.classList.toggle("wg_shellToc__subLink--active",i===a),i===a?i.link.setAttribute("aria-current","location"):i.link.removeAttribute("aria-current"),i.item.classList.toggle("wg_shellToc__subItem--trail",l===n&&g<u),i.item.classList.toggle("wg_shellToc__subItem--current",i===a)}};addEventListener("scroll",e,{passive:!0}),addEventListener("resize",e),addEventListener("toggle",e,!0),e()}var K=new AbortController;for(let e of["wheel","touchmove","keydown","mousedown"])addEventListener(e,()=>K.abort(),{once:!0,passive:!0});var F=async()=>{await document.fonts.ready,k&&!K.signal.aborted&&(k.scrollIntoView({behavior:"instant"}),q(k)),document.documentElement.style.removeProperty("scroll-behavior")};document.readyState==="complete"?F():addEventListener("load",F,{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,140 +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
|
-
content.append(navigation);
|
|
111
|
-
|
|
112
|
-
if (focused instanceof HTMLElement && navigation.contains(focused)) {
|
|
113
|
-
openButton.focus({preventScroll: true});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
closeNavigation();
|
|
120
|
-
sidebar.append(navigation);
|
|
121
|
-
|
|
122
|
-
if (focused instanceof HTMLElement && navigation.contains(focused)) {
|
|
123
|
-
focused.focus({preventScroll: true});
|
|
124
|
-
} else if (focused === openButton || (focused instanceof HTMLElement && dialog.contains(focused))) {
|
|
125
|
-
sidebar.querySelector<HTMLAnchorElement>(".wg_shell__brand")?.focus({preventScroll: true});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// CSS의 폭 조건이 먼저 버튼을 숨겨 포커스가 사라지지 않도록 복원 후 숨김
|
|
129
|
-
openButton.classList.remove("wg_shell__menuToggle--visible");
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
openButton.addEventListener("click", handleOpen);
|
|
133
|
-
closeButton.addEventListener("click", closeNavigation);
|
|
134
|
-
dialog.addEventListener("close", handleClose);
|
|
135
|
-
dialog.addEventListener("click", handleBackdropClick);
|
|
136
|
-
navigation.addEventListener("click", handleNavigationClick);
|
|
137
|
-
mobile.addEventListener("change", handleLayoutChange);
|
|
138
|
-
handleLayoutChange();
|
|
139
|
-
sidebar.classList.add("wg_shell__sidebar--navigationReady");
|
|
140
|
-
};
|