2026-06-03 15:58:01 +08:00
|
|
|
// 1.bind
|
|
|
|
|
let myQuery = document.querySelector("#concert-query");
|
|
|
|
|
let myMember = document.querySelector("#concert-member");
|
|
|
|
|
let myBtn = document.querySelector("#concert-search-btn");
|
2026-06-03 15:59:52 +08:00
|
|
|
let myResult = document.querySelector("#concert-search-result");
|
2026-06-03 15:58:01 +08:00
|
|
|
|
2026-06-03 15:59:52 +08:00
|
|
|
const SEARCH_LIST_MAX = 12;
|
2026-06-03 15:58:01 +08:00
|
|
|
|
2026-06-03 15:59:52 +08:00
|
|
|
function getTrackContext(trackEl) {
|
|
|
|
|
const venueBlock = trackEl.closest(".track-list")?.parentElement;
|
|
|
|
|
const tourCard = trackEl.closest(".con-1, .con-2, .con-3, .con-4");
|
|
|
|
|
return {
|
|
|
|
|
trackName: trackEl.textContent.trim(),
|
|
|
|
|
tourTitle: tourCard?.querySelector(".title")?.textContent.trim().replace(/\s+/g, " ") || "",
|
|
|
|
|
location: venueBlock?.querySelector(".location")?.textContent.trim() || ""
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findMatchingTracks(keyword, member) {
|
2026-06-03 15:58:01 +08:00
|
|
|
const tracks = document.querySelectorAll(".track-item .track");
|
|
|
|
|
const k = keyword.trim().toLowerCase();
|
|
|
|
|
const m = member.trim().toLowerCase();
|
2026-06-03 15:59:52 +08:00
|
|
|
const hits = [];
|
2026-06-03 15:58:01 +08:00
|
|
|
|
|
|
|
|
tracks.forEach(function (trackEl) {
|
|
|
|
|
const text = trackEl.textContent.toLowerCase();
|
|
|
|
|
const matchKeyword = !k || text.includes(k);
|
|
|
|
|
const matchMember = !m || text.includes(m);
|
|
|
|
|
if (matchKeyword && matchMember) {
|
2026-06-03 15:59:52 +08:00
|
|
|
hits.push({
|
|
|
|
|
el: trackEl.closest(".track-item"),
|
|
|
|
|
...getTrackContext(trackEl)
|
|
|
|
|
});
|
2026-06-03 15:58:01 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-03 15:59:52 +08:00
|
|
|
return hits;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearSearchHighlights() {
|
|
|
|
|
document.querySelectorAll(".track-item.is-search-hit").forEach(function (item) {
|
|
|
|
|
item.classList.remove("is-search-hit");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 16:04:13 +08:00
|
|
|
function expandVenueForTrackItem(trackItem) {
|
|
|
|
|
const venueBlock = trackItem?.closest(".track-list")?.parentElement;
|
|
|
|
|
const expand = venueBlock?.querySelector(".expand-trigger");
|
|
|
|
|
if (expand) {
|
|
|
|
|
expand.checked = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function jumpToTrackHit(hit) {
|
|
|
|
|
if (!hit?.el) return;
|
|
|
|
|
|
|
|
|
|
clearSearchHighlights();
|
|
|
|
|
expandVenueForTrackItem(hit.el);
|
|
|
|
|
hit.el.classList.add("is-search-hit");
|
|
|
|
|
hit.el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 16:33:58 +08:00
|
|
|
function setActiveSearchHit(container, activeEl) {
|
|
|
|
|
if (!container) return;
|
|
|
|
|
container.querySelectorAll(".concert-search-hit-jump").forEach(function (item) {
|
2026-06-03 16:04:13 +08:00
|
|
|
item.classList.remove("is-active");
|
|
|
|
|
});
|
2026-06-03 16:33:58 +08:00
|
|
|
if (activeEl) activeEl.classList.add("is-active");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createSearchHitVenue(hit) {
|
|
|
|
|
const venue = document.createElement("div");
|
|
|
|
|
venue.className = "con-yk-1";
|
|
|
|
|
|
|
|
|
|
const locText = [hit.tourTitle, hit.location].filter(Boolean).join(" · ");
|
|
|
|
|
if (locText) {
|
|
|
|
|
const loc = document.createElement("span");
|
|
|
|
|
loc.className = "location";
|
|
|
|
|
loc.textContent = locText;
|
|
|
|
|
venue.appendChild(loc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const trackList = document.createElement("div");
|
|
|
|
|
trackList.className = "track-list";
|
|
|
|
|
const trackItem = document.createElement("div");
|
|
|
|
|
trackItem.className = "track-item";
|
|
|
|
|
const trackSpan = document.createElement("span");
|
|
|
|
|
trackSpan.className = "track";
|
|
|
|
|
trackSpan.textContent = hit.trackName;
|
|
|
|
|
trackItem.appendChild(trackSpan);
|
|
|
|
|
trackList.appendChild(trackItem);
|
|
|
|
|
venue.appendChild(trackList);
|
|
|
|
|
|
|
|
|
|
return venue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bindSearchHitJump(venueEl, hit, container) {
|
|
|
|
|
venueEl.classList.add("concert-search-hit-jump");
|
|
|
|
|
venueEl.setAttribute("role", "button");
|
|
|
|
|
venueEl.setAttribute("tabindex", "0");
|
|
|
|
|
venueEl.setAttribute("aria-label", `跳到曲目:${hit.trackName}`);
|
|
|
|
|
|
|
|
|
|
function go() {
|
|
|
|
|
setActiveSearchHit(container, venueEl);
|
|
|
|
|
jumpToTrackHit(hit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
venueEl.addEventListener("click", go);
|
|
|
|
|
venueEl.addEventListener("keydown", function (event) {
|
|
|
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
go();
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-06-03 16:04:13 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 15:59:52 +08:00
|
|
|
function renderSearchResults(getQuery, getMember, hits) {
|
|
|
|
|
if (!myResult) return;
|
|
|
|
|
|
|
|
|
|
clearSearchHighlights();
|
|
|
|
|
|
|
|
|
|
const summary = document.createElement("p");
|
|
|
|
|
summary.className = "concert-search-summary";
|
|
|
|
|
|
|
|
|
|
if (!hits.length) {
|
2026-06-03 16:04:13 +08:00
|
|
|
summary.textContent = `曲目或場次「${getQuery || "(未填)"}」${getMember.trim() ? `、成員「${getMember}」` : ""}:沒有比對到曲目,請換字再試。`;
|
2026-06-03 15:59:52 +08:00
|
|
|
myResult.replaceChildren(summary);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (getMember.trim()) {
|
2026-06-03 16:06:14 +08:00
|
|
|
summary.textContent = `曲目或場次「${getQuery || "(未填)"}」、成員「${getMember}」:共 ${hits.length} 筆。請點列表中的場次,再跳到下方歌單。`;
|
2026-06-03 15:59:52 +08:00
|
|
|
} else {
|
2026-06-03 16:06:14 +08:00
|
|
|
summary.textContent = `曲目或場次「${getQuery}」:共 ${hits.length} 筆。請點列表中的場次,再跳到下方歌單。`;
|
2026-06-03 15:59:52 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 16:33:58 +08:00
|
|
|
const hitsWrap = document.createElement("div");
|
|
|
|
|
hitsWrap.className = "concert-search-hits";
|
|
|
|
|
|
|
|
|
|
const mirrorCard = document.createElement("div");
|
|
|
|
|
mirrorCard.className = "con-1 concert-search-mirror-card";
|
2026-06-03 15:59:52 +08:00
|
|
|
|
|
|
|
|
hits.slice(0, SEARCH_LIST_MAX).forEach(function (hit) {
|
2026-06-03 16:33:58 +08:00
|
|
|
const venue = createSearchHitVenue(hit);
|
|
|
|
|
bindSearchHitJump(venue, hit, mirrorCard);
|
|
|
|
|
mirrorCard.appendChild(venue);
|
2026-06-03 15:59:52 +08:00
|
|
|
});
|
|
|
|
|
|
2026-06-03 16:33:58 +08:00
|
|
|
hitsWrap.appendChild(mirrorCard);
|
|
|
|
|
|
2026-06-03 15:59:52 +08:00
|
|
|
if (hits.length > SEARCH_LIST_MAX) {
|
2026-06-03 16:33:58 +08:00
|
|
|
const more = document.createElement("p");
|
|
|
|
|
more.className = "concert-search-more";
|
2026-06-03 16:04:13 +08:00
|
|
|
more.textContent = `另有 ${hits.length - SEARCH_LIST_MAX} 筆未顯示,請縮小曲目或場次。`;
|
2026-06-03 16:33:58 +08:00
|
|
|
hitsWrap.appendChild(more);
|
2026-06-03 15:59:52 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 16:33:58 +08:00
|
|
|
myResult.replaceChildren(summary, hitsWrap);
|
2026-06-03 16:04:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function runSearch() {
|
|
|
|
|
let getQuery = myQuery ? myQuery.value : "";
|
|
|
|
|
let getMember = myMember ? myMember.value : "";
|
|
|
|
|
|
|
|
|
|
if (!getQuery.trim() && !getMember.trim()) {
|
|
|
|
|
myResult.replaceChildren();
|
|
|
|
|
const summary = document.createElement("p");
|
|
|
|
|
summary.className = "concert-search-summary";
|
|
|
|
|
summary.textContent = "請至少輸入曲目/場次,或成員其中一項。";
|
|
|
|
|
myResult.appendChild(summary);
|
|
|
|
|
clearSearchHighlights();
|
|
|
|
|
return;
|
2026-06-03 15:59:52 +08:00
|
|
|
}
|
2026-06-03 16:04:13 +08:00
|
|
|
|
|
|
|
|
const hits = findMatchingTracks(getQuery, getMember);
|
|
|
|
|
renderSearchResults(getQuery, getMember, hits);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSearchEnter(event) {
|
|
|
|
|
if (event.key !== "Enter") return;
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
runSearch();
|
2026-06-03 15:58:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2.action
|
2026-06-03 15:59:52 +08:00
|
|
|
if (myBtn && myResult) {
|
2026-06-03 16:04:13 +08:00
|
|
|
myBtn.addEventListener("click", runSearch);
|
2026-06-03 15:58:01 +08:00
|
|
|
|
2026-06-03 16:04:13 +08:00
|
|
|
if (myQuery) {
|
|
|
|
|
myQuery.addEventListener("keydown", handleSearchEnter);
|
|
|
|
|
}
|
|
|
|
|
if (myMember) {
|
|
|
|
|
myMember.addEventListener("keydown", handleSearchEnter);
|
|
|
|
|
}
|
2026-06-03 15:58:01 +08:00
|
|
|
}
|