scroll highlighting

This commit is contained in:
Simon Martens
2025-09-23 17:38:48 +02:00
parent 10cfc5369e
commit 6ddded953b
6 changed files with 153 additions and 33 deletions

View File

@@ -134,8 +134,8 @@ class H extends HTMLElement {
document.documentElement.offsetHeight document.documentElement.offsetHeight
), o = window.innerHeight, s = n - o, a = s > 0 ? window.scrollY / s : 0, l = t.clientHeight, d = t.scrollHeight - l; ), o = window.innerHeight, s = n - o, a = s > 0 ? window.scrollY / s : 0, l = t.clientHeight, d = t.scrollHeight - l;
if (d > 0) { if (d > 0) {
const g = a * d, u = i.getBoundingClientRect(), p = t.getBoundingClientRect(), f = u.top - p.top + t.scrollTop, m = l / 2, L = f - m, y = 0.7, I = y * g + (1 - y) * L, x = Math.max(0, Math.min(d, I)), T = t.scrollTop; const g = a * d, u = i.getBoundingClientRect(), p = t.getBoundingClientRect(), b = u.top - p.top + t.scrollTop, m = l / 2, I = b - m, y = 0.7, T = y * g + (1 - y) * I, x = Math.max(0, Math.min(d, T)), L = t.scrollTop;
Math.abs(x - T) > 10 && t.scrollTo({ Math.abs(x - L) > 10 && t.scrollTo({
top: x, top: x,
behavior: "smooth" behavior: "smooth"
}); });
@@ -289,13 +289,13 @@ class A extends HTMLElement {
if (l) if (l)
u = l; u = l;
else { else {
const f = this.getIssueContext(i); const b = this.getIssueContext(i);
u = f ? `${f}, ${i}` : `${i}`; u = b ? `${b}, ${i}` : `${i}`;
} }
if (d.innerHTML = u, o && i === o) { if (d.innerHTML = u, o && i === o) {
d.style.position = "relative"; d.style.position = "relative";
const f = d.querySelector(".target-page-dot"); const b = d.querySelector(".target-page-dot");
f && f.remove(); b && b.remove();
const m = document.createElement("span"); const m = document.createElement("span");
m.className = "target-page-dot absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full z-10", m.title = "verlinkte Seite", d.appendChild(m); m.className = "target-page-dot absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full z-10", m.title = "verlinkte Seite", d.appendChild(m);
} }
@@ -580,7 +580,7 @@ class N extends HTMLElement {
const n = i.target.getAttribute("data-page-container"), o = this.pageContainers.get(n); const n = i.target.getAttribute("data-page-container"), o = this.pageContainers.get(n);
if (o) { if (o) {
const a = i.isIntersecting && i.intersectionRatio >= 0.5 || this.singlePageViewerActive ? "full" : "short"; const a = i.isIntersecting && i.intersectionRatio >= 0.5 || this.singlePageViewerActive ? "full" : "short";
o.state !== a && (o.state = a, this.updateEntriesState(o)); o.state !== a ? (o.state = a, this.updateEntriesState(o)) : a === "full" && i.isIntersecting && i.intersectionRatio >= 0.5 && this.scrollPageIntoInhaltsverzeichnis(o);
} }
}); });
}, { }, {
@@ -611,13 +611,45 @@ class N extends HTMLElement {
const i = e.container.getAttribute("data-page-container"), n = this.querySelector(`[data-page-number="${i}"]`); const i = e.container.getAttribute("data-page-container"), n = this.querySelector(`[data-page-number="${i}"]`);
(a = n == null ? void 0 : n.closest(".page-entry")) == null || a.querySelector(".icon-container"); (a = n == null ? void 0 : n.closest(".page-entry")) == null || a.querySelector(".icon-container");
const o = n == null ? void 0 : n.closest(".page-entry"); const o = n == null ? void 0 : n.closest(".page-entry");
o && (t ? (o.classList.add("!border-l-red-500"), o.classList.remove("border-slate-300")) : (o.classList.remove("!border-l-red-500"), o.classList.add("border-slate-300"))); o && (t ? (o.classList.add("!border-l-red-500"), o.classList.remove("border-slate-300")) : (o.classList.remove("!border-l-red-500"), o.classList.add("border-slate-300")), t && this.scrollEntryIntoView(o));
const s = document.querySelector(`[data-page="${i}"].page-indicator`); const s = document.querySelector(`[data-page="${i}"].page-indicator`);
if (s) { if (s) {
const l = s.querySelectorAll("i:not(.text-slate-400)"); const l = s.querySelectorAll("i:not(.text-slate-400)");
t ? (s.classList.add("!bg-red-50", "!text-red-600"), l.forEach((c) => c.classList.add("!text-red-600"))) : (s.classList.remove("!bg-red-50", "!text-red-600"), l.forEach((c) => c.classList.remove("!text-red-600"))); t ? (s.classList.add("!bg-red-50", "!text-red-600"), l.forEach((c) => c.classList.add("!text-red-600"))) : (s.classList.remove("!bg-red-50", "!text-red-600"), l.forEach((c) => c.classList.remove("!text-red-600")));
} }
} }
scrollEntryIntoView(e) {
const t = document.querySelector(".overflow-y-auto");
if (!t || !e)
return;
const i = t.querySelectorAll(".page-entry"), n = i.length > 0 && i[0] === e, o = i.length > 0 && i[i.length - 1] === e;
if (n) {
t.scrollTo({
top: 0,
behavior: "smooth"
});
return;
}
if (o) {
t.scrollTo({
top: t.scrollHeight,
behavior: "smooth"
});
return;
}
const s = t.getBoundingClientRect(), a = e.getBoundingClientRect();
if (!(a.top >= s.top && a.bottom <= s.bottom)) {
const c = t.scrollTop, d = a.top - s.top + c, g = s.height, u = a.height, p = d - (g - u) / 2;
t.scrollTo({
top: Math.max(0, p),
behavior: "smooth"
});
}
}
scrollPageIntoInhaltsverzeichnis(e) {
const t = e.container.getAttribute("data-page-container"), i = this.querySelector(`[data-page-number="${t}"]`), n = i == null ? void 0 : i.closest(".page-entry");
n && this.scrollEntryIntoView(n);
}
setupSinglePageViewerDetection() { setupSinglePageViewerDetection() {
document.addEventListener("singlepageviewer:opened", this.boundHandleSinglePageViewer), document.addEventListener("singlepageviewer:closed", this.boundHandleSinglePageViewer), document.addEventListener("singlepageviewer:pagechanged", this.boundHandleSinglePageViewer), this.checkSinglePageViewerState(); document.addEventListener("singlepageviewer:opened", this.boundHandleSinglePageViewer), document.addEventListener("singlepageviewer:closed", this.boundHandleSinglePageViewer), document.addEventListener("singlepageviewer:pagechanged", this.boundHandleSinglePageViewer), this.checkSinglePageViewerState();
} }
@@ -664,7 +696,7 @@ function k() {
document.getElementById("pageModal").classList.add("hidden"); document.getElementById("pageModal").classList.add("hidden");
} }
function V() { function V() {
if (window.pageObserver && (window.pageObserver.disconnect(), window.pageObserver = null), window.currentPageContainers = Array.from(document.querySelectorAll(".newspaper-page-container")), window.currentActiveIndex = 0, b(), document.querySelector(".newspaper-page-container")) { if (window.pageObserver && (window.pageObserver.disconnect(), window.pageObserver = null), window.currentPageContainers = Array.from(document.querySelectorAll(".newspaper-page-container")), window.currentActiveIndex = 0, f(), document.querySelector(".newspaper-page-container")) {
let e = /* @__PURE__ */ new Set(); let e = /* @__PURE__ */ new Set();
window.pageObserver = new IntersectionObserver( window.pageObserver = new IntersectionObserver(
(t) => { (t) => {
@@ -673,7 +705,7 @@ function V() {
n !== -1 && (i.isIntersecting ? e.add(n) : e.delete(n)); n !== -1 && (i.isIntersecting ? e.add(n) : e.delete(n));
}), e.size > 0) { }), e.size > 0) {
const n = Array.from(e).sort((o, s) => o - s)[0]; const n = Array.from(e).sort((o, s) => o - s)[0];
n !== window.currentActiveIndex && (window.currentActiveIndex = n, b()); n !== window.currentActiveIndex && (window.currentActiveIndex = n, f());
} }
}, },
{ {
@@ -701,7 +733,7 @@ function $() {
r === -1 && t > 0 && (r = t - 1), r >= 0 && (window.currentActiveIndex = r, window.currentPageContainers[window.currentActiveIndex].scrollIntoView({ r === -1 && t > 0 && (r = t - 1), r >= 0 && (window.currentActiveIndex = r, window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
block: "start" block: "start"
}), setTimeout(() => { }), setTimeout(() => {
b(); f();
}, 100)); }, 100));
} }
} }
@@ -722,7 +754,7 @@ function M() {
r === -1 && t < window.currentPageContainers.length - 1 && (r = t + 1), r >= 0 && r < window.currentPageContainers.length && (window.currentActiveIndex = r, window.currentPageContainers[window.currentActiveIndex].scrollIntoView({ r === -1 && t < window.currentPageContainers.length - 1 && (r = t + 1), r >= 0 && r < window.currentPageContainers.length && (window.currentActiveIndex = r, window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
block: "start" block: "start"
}), setTimeout(() => { }), setTimeout(() => {
b(); f();
}, 100)); }, 100));
} }
} }
@@ -733,10 +765,17 @@ function R() {
block: "start" block: "start"
}); });
} else { } else {
const e = document.querySelector('[class*="border-t-2 border-amber-200"]'); const e = document.querySelector('[data-beilage="true"]');
e && e.scrollIntoView({ if (e)
block: "start" e.scrollIntoView({
}); block: "start"
});
else {
const t = document.querySelector(".bg-amber-50");
t && t.scrollIntoView({
block: "start"
});
}
} }
} }
function E() { function E() {
@@ -752,7 +791,7 @@ function E() {
} }
return !1; return !1;
} }
function b() { function f() {
const r = document.getElementById("prevPageBtn"), e = document.getElementById("nextPageBtn"), t = document.getElementById("beilageBtn"); const r = document.getElementById("prevPageBtn"), e = document.getElementById("nextPageBtn"), t = document.getElementById("beilageBtn");
if (r && (r.style.display = "flex", window.currentActiveIndex <= 0 ? (r.disabled = !0, r.classList.add("opacity-50", "cursor-not-allowed"), r.classList.remove("hover:bg-gray-200")) : (r.disabled = !1, r.classList.remove("opacity-50", "cursor-not-allowed"), r.classList.add("hover:bg-gray-200"))), e && (e.style.display = "flex", window.currentActiveIndex >= window.currentPageContainers.length - 1 ? (e.disabled = !0, e.classList.add("opacity-50", "cursor-not-allowed"), e.classList.remove("hover:bg-gray-200")) : (e.disabled = !1, e.classList.remove("opacity-50", "cursor-not-allowed"), e.classList.add("hover:bg-gray-200"))), t) { if (r && (r.style.display = "flex", window.currentActiveIndex <= 0 ? (r.disabled = !0, r.classList.add("opacity-50", "cursor-not-allowed"), r.classList.remove("hover:bg-gray-200")) : (r.disabled = !1, r.classList.remove("opacity-50", "cursor-not-allowed"), r.classList.add("hover:bg-gray-200"))), e && (e.style.display = "flex", window.currentActiveIndex >= window.currentPageContainers.length - 1 ? (e.disabled = !0, e.classList.add("opacity-50", "cursor-not-allowed"), e.classList.remove("hover:bg-gray-200")) : (e.disabled = !1, e.classList.remove("opacity-50", "cursor-not-allowed"), e.classList.add("hover:bg-gray-200"))), t) {
const i = E(), n = t.querySelector("i"); const i = E(), n = t.querySelector("i");
@@ -910,7 +949,7 @@ function D(r, e) {
function P() { function P() {
V(), window.addEventListener("scroll", function() { V(), window.addEventListener("scroll", function() {
clearTimeout(window.scrollTimeout), window.scrollTimeout = setTimeout(() => { clearTimeout(window.scrollTimeout), window.scrollTimeout = setTimeout(() => {
b(); f();
}, 50); }, 50);
}), document.addEventListener("keydown", function(r) { }), document.addEventListener("keydown", function(r) {
r.key === "Escape" && k(); r.key === "Escape" && k();
@@ -968,7 +1007,7 @@ window.shareCurrentPage = z;
window.generateCitation = O; window.generateCitation = O;
window.copyPagePermalink = K; window.copyPagePermalink = K;
window.generatePageCitation = D; window.generatePageCitation = D;
function Z() { function W() {
C(), w(), document.querySelector(".newspaper-page-container") && P(); C(), w(), document.querySelector(".newspaper-page-container") && P();
let r = function(t) { let r = function(t) {
C(), w(), j(), setTimeout(() => { C(), w(), j(), setTimeout(() => {
@@ -979,5 +1018,5 @@ function Z() {
document.body.addEventListener("htmx:afterSettle", r), document.body.addEventListener("htmx:afterSettle", w), document.body.addEventListener("htmx:beforeRequest", e); document.body.addEventListener("htmx:afterSettle", r), document.body.addEventListener("htmx:afterSettle", w), document.body.addEventListener("htmx:beforeRequest", e);
} }
export { export {
Z as setup W as setup
}; };

File diff suppressed because one or more lines are too long

View File

@@ -44,13 +44,12 @@
</button> </button>
{{ if $model.HasBeilageButton }} {{ if $model.HasBeilageButton }}
<button <a
id="beilageBtn" href="#beilage"
onclick="scrollToBeilage()" class="w-14 h-10 lg:w-16 lg:h-12 px-2 py-1 bg-amber-100 hover:bg-amber-200 text-amber-700 hover:text-amber-800 border border-amber-300 transition-colors duration-200 flex items-center justify-center cursor-pointer no-underline"
class="w-14 h-10 lg:w-16 lg:h-12 px-2 py-1 bg-amber-100 hover:bg-amber-200 text-amber-700 hover:text-amber-800 border border-amber-300 transition-colors duration-200 flex items-center justify-center cursor-pointer"
title="Zu Beilage"> title="Zu Beilage">
<i class="ri-attachment-line text-lg lg:text-xl"></i> <i class="ri-attachment-line text-lg lg:text-xl"></i>
</button> </a>
{{ end }} {{ end }}

View File

@@ -8,7 +8,7 @@
{{ $pageCount := len $pages }} {{ $pageCount := len $pages }}
<!-- Historical printing layout grid --> <!-- Historical printing layout grid -->
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2">
{{ template "_historical_layout" (dict "pages" $pages "pageCount" $pageCount "isBeilage" false "targetPage" $.targetPage) }} {{ template "_historical_layout" (dict "pages" $pages "pageCount" $pageCount "isBeilage" false "targetPage" $.targetPage) }}
</div> </div>
{{ end }} {{ end }}
@@ -19,14 +19,14 @@
{{ if $beilagePages }} {{ if $beilagePages }}
<div class="mt-12 pt-8"> <div class="mt-12 pt-8">
<!-- Header for beilage --> <!-- Header for beilage -->
<div class="flex items-center gap-3 mb-6 bg-amber-50 px-4 py-3 rounded border border-amber-200"> <div id="beilage" class="flex items-center gap-3 mb-6 bg-amber-50 px-4 py-3 rounded border border-amber-200">
<i class="ri-attachment-line text-2xl text-amber-600"></i> <i class="ri-attachment-line text-2xl text-amber-600"></i>
<h2 class="text-xl font-semibold text-slate-800">Beilage</h2> <h2 class="text-xl font-semibold text-slate-800">Beilage</h2>
</div> </div>
{{ $pageCount := len $beilagePages }} {{ $pageCount := len $beilagePages }}
<!-- Historical printing layout grid for Beilage --> <!-- Historical printing layout grid for Beilage -->
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2">
{{ template "_historical_layout" (dict "pages" $beilagePages "pageCount" $pageCount "isBeilage" true "targetPage" $.targetPage) }} {{ template "_historical_layout" (dict "pages" $beilagePages "pageCount" $pageCount "isBeilage" true "targetPage" $.targetPage) }}
</div> </div>
</div> </div>
@@ -125,7 +125,7 @@
{{ $idPrefix = "beilage-1-page" }} {{ $idPrefix = "beilage-1-page" }}
{{ end }} {{ end }}
<div class="newspaper-page-container" id="{{ $idPrefix }}-{{ $page.PageNumber }}" data-page-container="{{ $page.PageNumber }}" data-page-icon-type="{{ $page.PageIcon }}"{{ if $isBeilage }} data-beilage="true"{{ end }}> <div class="newspaper-page-container pt-4" id="{{ $idPrefix }}-{{ $page.PageNumber }}" data-page-container="{{ $page.PageNumber }}" data-page-icon-type="{{ $page.PageIcon }}"{{ if $isBeilage }} data-beilage="true"{{ end }}>
<!-- Anchor for navigation --> <!-- Anchor for navigation -->
{{ if $isBeilage }} {{ if $isBeilage }}
<div id="beilage-{{ $page.PageNumber }}"></div> <div id="beilage-{{ $page.PageNumber }}"></div>

View File

@@ -67,9 +67,13 @@ export class InhaltsverzeichnisScrollspy extends HTMLElement {
const shouldBeFullMode = entry.isIntersecting && entry.intersectionRatio >= 0.5; const shouldBeFullMode = entry.isIntersecting && entry.intersectionRatio >= 0.5;
const newState = shouldBeFullMode || this.singlePageViewerActive ? 'full' : 'short'; const newState = shouldBeFullMode || this.singlePageViewerActive ? 'full' : 'short';
if (pageData.state !== newState) { const stateChanged = pageData.state !== newState;
if (stateChanged) {
pageData.state = newState; pageData.state = newState;
this.updateEntriesState(pageData); this.updateEntriesState(pageData);
} else if (newState === 'full' && entry.isIntersecting && entry.intersectionRatio >= 0.5) {
// Page is becoming visible and should be highlighted - trigger scroll even if state didn't change
this.scrollPageIntoInhaltsverzeichnis(pageData);
} }
} }
}); });
@@ -142,6 +146,11 @@ export class InhaltsverzeichnisScrollspy extends HTMLElement {
pageEntryContainer.classList.remove('!border-l-red-500'); pageEntryContainer.classList.remove('!border-l-red-500');
pageEntryContainer.classList.add('border-slate-300'); pageEntryContainer.classList.add('border-slate-300');
} }
// Always scroll highlighted entry into view when it becomes active
if (highlight) {
this.scrollEntryIntoView(pageEntryContainer);
}
} }
// 2. Highlight in layout view (page indicator above image) // 2. Highlight in layout view (page indicator above image)
@@ -160,6 +169,71 @@ export class InhaltsverzeichnisScrollspy extends HTMLElement {
} }
} }
scrollEntryIntoView(pageEntryContainer) {
// Find the scrollable Inhaltsverzeichnis container
const scrollableContainer = document.querySelector('.overflow-y-auto');
if (!scrollableContainer || !pageEntryContainer) {
return;
}
// Check if this is the first or last page entry to handle edge cases
const allPageEntries = scrollableContainer.querySelectorAll('.page-entry');
const isFirstPage = allPageEntries.length > 0 && allPageEntries[0] === pageEntryContainer;
const isLastPage = allPageEntries.length > 0 && allPageEntries[allPageEntries.length - 1] === pageEntryContainer;
if (isFirstPage) {
// Scroll to the very top for the first page
scrollableContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
return;
}
if (isLastPage) {
// Scroll to the very bottom for the last page
scrollableContainer.scrollTo({
top: scrollableContainer.scrollHeight,
behavior: 'smooth'
});
return;
}
// Get container and element positions for middle pages
const containerRect = scrollableContainer.getBoundingClientRect();
const elementRect = pageEntryContainer.getBoundingClientRect();
// Check if element is already fully visible
const isVisible = elementRect.top >= containerRect.top &&
elementRect.bottom <= containerRect.bottom;
if (!isVisible) {
// Calculate the scroll position to center the element in the container
const containerScrollTop = scrollableContainer.scrollTop;
const elementTop = elementRect.top - containerRect.top + containerScrollTop;
const containerHeight = containerRect.height;
const elementHeight = elementRect.height;
// Center the element in the container
const scrollTo = elementTop - (containerHeight - elementHeight) / 2;
scrollableContainer.scrollTo({
top: Math.max(0, scrollTo),
behavior: 'smooth'
});
}
}
scrollPageIntoInhaltsverzeichnis(pageData) {
const pageNumber = pageData.container.getAttribute('data-page-container');
const pageLink = this.querySelector(`[data-page-number="${pageNumber}"]`);
const pageEntryContainer = pageLink?.closest('.page-entry');
if (pageEntryContainer) {
this.scrollEntryIntoView(pageEntryContainer);
}
}
setupSinglePageViewerDetection() { setupSinglePageViewerDetection() {
// Listen for single page viewer events // Listen for single page viewer events

View File

@@ -229,12 +229,20 @@ export function scrollToBeilage() {
}); });
} }
} else { } else {
// Go to first beilage container // Go to first beilage container - look for the first beilage page container
const beilageContainer = document.querySelector('[class*="border-t-2 border-amber-200"]'); const beilageContainer = document.querySelector('[data-beilage="true"]');
if (beilageContainer) { if (beilageContainer) {
beilageContainer.scrollIntoView({ beilageContainer.scrollIntoView({
block: "start", block: "start",
}); });
} else {
// Fallback: try to find beilage header section
const beilageHeader = document.querySelector('.bg-amber-50');
if (beilageHeader) {
beilageHeader.scrollIntoView({
block: "start",
});
}
} }
} }
} }