@@ -24,3 +24,213 @@ document.addEventListener("DOMContentLoaded", () => {
2424
2525 targets . forEach ( ( el ) => observer . observe ( el ) ) ;
2626} ) ;
27+
28+ // Portfolio Modal Logic
29+ ( function ( ) {
30+ let modalOverlay ,
31+ modalContainer ,
32+ modalContent ,
33+ modalCloseBtn ,
34+ scrollPosition = 0 ;
35+
36+ function closeModal ( ) {
37+ if ( ! modalOverlay ) return ;
38+ modalOverlay . classList . remove ( "is-open" ) ;
39+
40+ // Restore body position
41+ document . body . style . paddingRight = "" ;
42+ document . body . style . position = "" ;
43+ document . body . style . top = "" ;
44+ document . body . style . width = "" ;
45+ window . scrollTo ( 0 , scrollPosition ) ;
46+
47+ setTimeout ( ( ) => {
48+ if (
49+ modalOverlay &&
50+ ! modalOverlay . classList . contains ( "is-open" )
51+ ) {
52+ modalContent . innerHTML = "" ;
53+ }
54+ } , 300 ) ;
55+ }
56+
57+ /**
58+ * Adjusts asset paths inside loaded HTML content.
59+ * On non-english pages, paths like ../assets/ need to become assets/
60+ * On english pages (/en/), ../assets/ is correct relative to the page, so no change.
61+ * @param {HTMLElement } element The root element of the loaded content.
62+ * @param {boolean } isEn True if the current page is English.
63+ */
64+ function fixPaths ( element , isEn ) {
65+ if ( isEn ) return ;
66+
67+ const attrs = [ "src" , "href" ] ;
68+ attrs . forEach ( ( attr ) => {
69+ const nodes = element . querySelectorAll ( `[${ attr } ]` ) ;
70+ nodes . forEach ( ( node ) => {
71+ const val = node . getAttribute ( attr ) ;
72+ if ( val && val . startsWith ( "../" ) ) {
73+ // Do not adjust absolute URLs or anchor links
74+ if (
75+ ! val . startsWith ( "../#" ) &&
76+ val . indexOf ( ":" ) === - 1
77+ ) {
78+ node . setAttribute ( attr , val . substring ( 3 ) ) ;
79+ }
80+ }
81+ } ) ;
82+ } ) ;
83+ }
84+
85+ /**
86+ * Hooks into links within the modal content.
87+ * Specifically targets portfolio tag links to trigger a handler instead of navigating.
88+ * @param {HTMLElement } element The root element of the loaded content.
89+ * @param {Function } linkHandler A function to call when a tag link is clicked.
90+ */
91+ function hookLinks ( element , linkHandler ) {
92+ if ( typeof linkHandler !== "function" ) return ;
93+
94+ const links = element . querySelectorAll ( "a" ) ;
95+ links . forEach ( ( a ) => {
96+ const href = a . getAttribute ( "href" ) ;
97+ if (
98+ href &&
99+ ( href . includes ( "portfolio.html" ) ||
100+ href . startsWith ( "?" ) )
101+ ) {
102+ // Exclude external links that might contain "portfolio.html"
103+ if (
104+ a . hostname !== location . hostname &&
105+ a . hostname !== ""
106+ ) {
107+ return ;
108+ }
109+
110+ a . addEventListener ( "click" , ( e ) => {
111+ e . preventDefault ( ) ;
112+ const qStart = href . indexOf ( "?" ) ;
113+ let tag = null ;
114+ if ( qStart !== - 1 ) {
115+ const search = href . substring ( qStart ) ;
116+ const params = new URLSearchParams ( search ) ;
117+ tag = params . get ( "tag" ) ;
118+ }
119+
120+ linkHandler ( tag ) ;
121+ closeModal ( ) ;
122+ } ) ;
123+ }
124+ } ) ;
125+ }
126+
127+ function initModal ( ) {
128+ if ( document . querySelector ( ".modal-overlay" ) ) {
129+ // Ensure elements are selected if they already exist
130+ modalOverlay = document . querySelector (
131+ ".modal-overlay" ,
132+ ) ;
133+ modalContainer = document . querySelector (
134+ ".modal-container" ,
135+ ) ;
136+ modalContent = document . querySelector (
137+ ".modal-content" ,
138+ ) ;
139+ modalCloseBtn =
140+ document . querySelector ( ".modal-close" ) ;
141+ return ;
142+ }
143+
144+ modalOverlay = document . createElement ( "div" ) ;
145+ modalOverlay . className = "modal-overlay" ;
146+ modalContainer = document . createElement ( "div" ) ;
147+ modalContainer . className = "modal-container" ;
148+ modalCloseBtn = document . createElement ( "button" ) ;
149+ modalCloseBtn . className = "modal-close" ;
150+ modalCloseBtn . innerHTML = "×" ;
151+ modalCloseBtn . ariaLabel = "Close" ;
152+ modalContent = document . createElement ( "div" ) ;
153+ modalContent . className = "modal-content" ;
154+
155+ modalContainer . appendChild ( modalCloseBtn ) ;
156+ modalContainer . appendChild ( modalContent ) ;
157+ modalOverlay . appendChild ( modalContainer ) ;
158+ document . body . appendChild ( modalOverlay ) ;
159+
160+ modalOverlay . addEventListener ( "click" , ( e ) => {
161+ if ( e . target === modalOverlay ) closeModal ( ) ;
162+ } ) ;
163+ modalCloseBtn . addEventListener ( "click" , closeModal ) ;
164+ document . addEventListener ( "keydown" , ( e ) => {
165+ if (
166+ e . key === "Escape" &&
167+ modalOverlay . classList . contains ( "is-open" )
168+ ) {
169+ closeModal ( ) ;
170+ }
171+ } ) ;
172+ }
173+
174+ /**
175+ * Opens the portfolio detail modal.
176+ * @param {string } contentPath Path to the HTML content to load.
177+ * @param {boolean } isEn True if the current page is English (to handle pathing).
178+ * @param {Function } linkHandler Callback for when a tag link is clicked.
179+ */
180+ async function openPortfolioModal (
181+ contentPath ,
182+ isEn ,
183+ linkHandler ,
184+ ) {
185+ if ( ! contentPath ) return ;
186+ initModal ( ) ;
187+
188+ // Prevent layout shift (horizontal) by accounting for scrollbar width
189+ const scrollbarWidth =
190+ window . innerWidth -
191+ document . documentElement . clientWidth ;
192+ if ( scrollbarWidth > 0 ) {
193+ document . body . style . paddingRight = `${ scrollbarWidth } px` ;
194+ }
195+
196+ // Store scroll position and fix body to prevent layout shift (vertical)
197+ scrollPosition = window . scrollY ;
198+ document . body . style . position = "fixed" ;
199+ document . body . style . top = `-${ scrollPosition } px` ;
200+ document . body . style . width = "100%" ;
201+
202+ modalContent . innerHTML =
203+ '<div style="padding:4rem;text-align:center;color:var(--color-text-muted);">Loading...</div>' ;
204+ modalOverlay . classList . add ( "is-open" ) ;
205+
206+ try {
207+ const res = await fetch ( contentPath ) ;
208+ if ( ! res . ok )
209+ throw new Error (
210+ `HTTP ${ res . status } ${ res . statusText } ` ,
211+ ) ;
212+ const text = await res . text ( ) ;
213+ const parser = new DOMParser ( ) ;
214+ const doc = parser . parseFromString ( text , "text/html" ) ;
215+ const detail = doc . querySelector ( ".work-detail" ) ;
216+
217+ if ( detail ) {
218+ detail . classList . remove ( "reveal-on-scroll" ) ;
219+ fixPaths ( detail , isEn ) ;
220+ hookLinks ( detail , linkHandler ) ;
221+ modalContent . innerHTML = "" ;
222+ modalContent . appendChild ( detail ) ;
223+ } else {
224+ throw new Error (
225+ "Content not found in fetched HTML." ,
226+ ) ;
227+ }
228+ } catch ( err ) {
229+ console . error ( "Modal load error:" , err ) ;
230+ modalContent . innerHTML =
231+ '<div style="padding:4rem;text-align:center;color:var(--color-text-muted);">コンテンツの読み込みに失敗しました。</div>' ;
232+ }
233+ }
234+
235+ window . openPortfolioModal = openPortfolioModal ;
236+ } ) ( ) ;
0 commit comments