Skip to content

Commit e611d1e

Browse files
committed
Merge branch 'develop'
2 parents f94a9cf + 6281d7a commit e611d1e

5 files changed

Lines changed: 700 additions & 115 deletions

File tree

WebSite/assets/js/portfolio.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,6 @@ document.addEventListener("DOMContentLoaded", () => {
1010
"portfolioCategoryFilter",
1111
);
1212

13-
// モーダル要素
14-
let modalOverlay,
15-
modalContainer,
16-
modalContent,
17-
modalCloseBtn;
18-
1913
if (!listEl) return;
2014

2115
/** @type {Array<any>} */
@@ -414,8 +408,25 @@ document.addEventListener("DOMContentLoaded", () => {
414408

415409
// カード全体クリックでモーダル表示
416410
card.addEventListener("click", () => {
417-
if (work.contentPath) {
418-
openModal(work.contentPath);
411+
if (work.contentPath && window.openPortfolioModal) {
412+
const lang = document.documentElement.lang;
413+
const isEn = lang === "en";
414+
415+
// モーダル内のタグクリック時のハンドラ
416+
const linkHandler = (tag) => {
417+
if (tag && searchInput) {
418+
searchInput.value = tag;
419+
if (categorySelect) categorySelect.value = "";
420+
render();
421+
updateUrlParams(tag, "");
422+
}
423+
};
424+
425+
window.openPortfolioModal(
426+
work.contentPath,
427+
isEn,
428+
linkHandler,
429+
);
419430
}
420431
});
421432

@@ -424,7 +435,7 @@ document.addEventListener("DOMContentLoaded", () => {
424435
}
425436

426437
// 初期化実行
427-
initModal();
438+
428439
if (searchInput) {
429440
searchInput.addEventListener("input", render);
430441
}

WebSite/assets/js/top.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,36 @@ document.addEventListener("DOMContentLoaded", async () => {
161161

162162
const a = document.createElement("a");
163163
a.className = "home-latest-link";
164-
a.href = relativePrefix + work.contentPath;
164+
a.href = "#";
165+
a.addEventListener("click", (e) => {
166+
e.preventDefault();
167+
if (
168+
work.contentPath &&
169+
window.openPortfolioModal
170+
) {
171+
const contentPath =
172+
relativePrefix + work.contentPath;
173+
174+
// On the top page, if a tag link in the modal is clicked,
175+
// navigate to the portfolio list page with that tag filtered.
176+
const linkHandler = (tag) => {
177+
const portfolioUrl = "portfolio.html";
178+
if (tag) {
179+
window.location.href = `${portfolioUrl}?tag=${encodeURIComponent(
180+
tag,
181+
)}`;
182+
} else {
183+
window.location.href = portfolioUrl;
184+
}
185+
};
186+
187+
window.openPortfolioModal(
188+
contentPath,
189+
isEn,
190+
linkHandler,
191+
);
192+
}
193+
});
165194

166195
const titleSpan = document.createElement("span");
167196
titleSpan.className = "home-latest-link-title";

WebSite/assets/js/ui.js

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = "&times;";
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

Comments
 (0)