diff --git a/concert/concert.html b/concert/concert.html
index c5d582b..391f529 100644
--- a/concert/concert.html
+++ b/concert/concert.html
@@ -103,6 +103,17 @@
margin: 0;
}
+ .concert-search-hint {
+ width: 100%;
+ max-width: 720px;
+ margin: 0 0 12px;
+ padding: 0 6px;
+ text-align: left;
+ font-size: 0.82rem;
+ line-height: 1.6;
+ color: rgba(255, 255, 255, 0.78);
+ }
+
.concert-search-summary {
margin: 0 0 12px;
text-align: center;
@@ -130,6 +141,18 @@
line-height: 1.5;
}
+ .concert-search-hits li.concert-search-hit-jump {
+ cursor: pointer;
+ transition: background 0.2s ease, border-color 0.2s ease;
+ }
+
+ .concert-search-hits li.concert-search-hit-jump:hover,
+ .concert-search-hits li.concert-search-hit-jump:focus-visible {
+ background: rgba(255, 204, 0, 0.15);
+ border-color: #ffcc00;
+ outline: none;
+ }
+
.concert-search-hits .hit-track {
display: block;
font-weight: 700;
@@ -171,12 +194,17 @@
diff --git a/concert/search.js b/concert/search.js
index 42ab4cd..347a564 100644
--- a/concert/search.js
+++ b/concert/search.js
@@ -43,6 +43,36 @@ function clearSearchHighlights() {
});
}
+function expandVenueForTrackItem(trackItem) {
+ const venueBlock = trackItem?.closest(".track-list")?.parentElement;
+ const expand = venueBlock?.querySelector(".expand-trigger");
+ if (expand) {
+ expand.checked = true;
+ }
+}
+
+function scrollToSearchResults() {
+ if (!myResult) return;
+ myResult.scrollIntoView({ behavior: "smooth", block: "start" });
+}
+
+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" });
+}
+
+function setActiveSearchListItem(list, activeLi) {
+ if (!list) return;
+ list.querySelectorAll(".concert-search-hit-jump").forEach(function (item) {
+ item.classList.remove("is-active");
+ });
+ if (activeLi) activeLi.classList.add("is-active");
+}
+
function renderSearchResults(getQuery, getMember, hits) {
if (!myResult) return;
@@ -52,15 +82,15 @@ function renderSearchResults(getQuery, getMember, hits) {
summary.className = "concert-search-summary";
if (!hits.length) {
- summary.textContent = `關鍵字「${getQuery || "(未填)"}」${getMember.trim() ? `、成員「${getMember}」` : ""}:沒有比對到曲目,請換字再試。`;
+ summary.textContent = `曲目或場次「${getQuery || "(未填)"}」${getMember.trim() ? `、成員「${getMember}」` : ""}:沒有比對到曲目,請換字再試。`;
myResult.replaceChildren(summary);
return;
}
if (getMember.trim()) {
- summary.textContent = `關鍵字「${getQuery || "(未填)"}」、成員「${getMember}」:共 ${hits.length} 筆,以下列出前 ${Math.min(hits.length, SEARCH_LIST_MAX)} 筆。`;
+ summary.textContent = `曲目或場次「${getQuery || "(未填)"}」、成員「${getMember}」:共 ${hits.length} 筆。點任一列可跳到歌單。`;
} else {
- summary.textContent = `關鍵字「${getQuery}」:共 ${hits.length} 筆,以下列出前 ${Math.min(hits.length, SEARCH_LIST_MAX)} 筆。`;
+ summary.textContent = `曲目或場次「${getQuery}」:共 ${hits.length} 筆。點任一列可跳到歌單。`;
}
const list = document.createElement("ul");
@@ -70,6 +100,11 @@ function renderSearchResults(getQuery, getMember, hits) {
if (hit.el) hit.el.classList.add("is-search-hit");
const li = document.createElement("li");
+ li.className = "concert-search-hit-jump";
+ li.setAttribute("role", "button");
+ li.setAttribute("tabindex", "0");
+ li.setAttribute("aria-label", `跳到曲目:${hit.trackName}`);
+
const trackLine = document.createElement("span");
trackLine.className = "hit-track";
trackLine.textContent = hit.trackName;
@@ -80,41 +115,64 @@ function renderSearchResults(getQuery, getMember, hits) {
li.appendChild(trackLine);
if (metaLine.textContent) li.appendChild(metaLine);
+
+ li.addEventListener("click", function () {
+ jumpToTrackHit(hit);
+ });
+ li.addEventListener("keydown", function (event) {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ jumpToTrackHit(hit);
+ }
+ });
+
list.appendChild(li);
});
if (hits.length > SEARCH_LIST_MAX) {
const more = document.createElement("li");
more.className = "hit-more";
- more.textContent = `另有 ${hits.length - SEARCH_LIST_MAX} 筆未顯示,請縮小關鍵字。`;
+ more.textContent = `另有 ${hits.length - SEARCH_LIST_MAX} 筆未顯示,請縮小曲目或場次。`;
list.appendChild(more);
}
myResult.replaceChildren(summary, list);
- const firstHit = hits[0]?.el;
- if (firstHit) {
- firstHit.scrollIntoView({ behavior: "smooth", block: "nearest" });
+ jumpToTrackHit(hits[0]);
+}
+
+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;
}
+
+ const hits = findMatchingTracks(getQuery, getMember);
+ renderSearchResults(getQuery, getMember, hits);
+}
+
+function handleSearchEnter(event) {
+ if (event.key !== "Enter") return;
+ event.preventDefault();
+ runSearch();
}
// 2.action
if (myBtn && myResult) {
- myBtn.addEventListener("click", function () {
- let getQuery = myQuery ? myQuery.value : "";
- let getMember = myMember ? myMember.value : "";
+ myBtn.addEventListener("click", runSearch);
- if (!getQuery.trim() && !getMember.trim()) {
- myResult.replaceChildren();
- const summary = document.createElement("p");
- summary.className = "concert-search-summary";
- summary.textContent = "請至少輸入曲目/場次,或成員其中一項。";
- myResult.appendChild(summary);
- clearSearchHighlights();
- return;
- }
-
- const hits = findMatchingTracks(getQuery, getMember);
- renderSearchResults(getQuery, getMember, hits);
- });
+ if (myQuery) {
+ myQuery.addEventListener("keydown", handleSearchEnter);
+ }
+ if (myMember) {
+ myMember.addEventListener("keydown", handleSearchEnter);
+ }
}