@if (item.File is not null) {
- @item.Filename + @item.Filename
} @if (item.RawHtml.Trim().Length > 0) { diff --git a/quest_reader.csproj b/quest_reader.csproj index 6af972c..b446252 100644 --- a/quest_reader.csproj +++ b/quest_reader.csproj @@ -15,9 +15,10 @@ + - 1.0.2 + 1.0.3 QuestReader diff --git a/web/main.css b/web/main.css index 5329416..7f348fe 100644 --- a/web/main.css +++ b/web/main.css @@ -115,17 +115,19 @@ a:visited { } .post-image { - margin: 0; + margin: 0 0 1rem; align-self: center; + /* Note: make sure to check this does not break main.ts if changed! */ max-width: 95%; } .post-image img { max-width: 100%; + display: block; } .post-text { - padding: 16px 40px; + padding: 0 40px 16px; } diff --git a/web/main.ts b/web/main.ts index 3b96a91..7403e3d 100644 --- a/web/main.ts +++ b/web/main.ts @@ -1,5 +1,9 @@ class VisitAnalytics { landmarksObserver: IntersectionObserver; + lazyloadObserver: IntersectionObserver; + lastWidth: number = 0; + resizeTimeout: number | null = null; + images: HTMLImageElement[] = []; constructor() { window.plausible = window.plausible || function () { (window.plausible.q = window.plausible.q || []).push(arguments); }; @@ -20,6 +24,14 @@ class VisitAnalytics { threshold: 1.0 } ); + this.lazyloadObserver = new IntersectionObserver( + (entries, observer) => this.handleLazyload(entries, observer), + { + root: null, + rootMargin: "75% 0px 75% 0px", + threshold: 0.1 + } + ); { var all = document.querySelectorAll(".chapter-announce"); @@ -27,7 +39,18 @@ class VisitAnalytics { this.landmarksObserver.observe(document.querySelector("footer")); } - console.log("Intersection observer ready"); + { + var all = document.querySelectorAll(".image-post"); + all.forEach(elem => { + this.lazyloadObserver.observe(elem); + this.images.push(elem.querySelector(".post-image > img")); + }); + + this.resizeTimeout = setTimeout(() => this.resizeDebounced(), 200); + window.addEventListener("resize", (event) => this.handleResize(event)); + } + + console.log("Event handlers and observers ready"); } handleLandmarks(entries: IntersectionObserverEntry[], observer: IntersectionObserver) { @@ -42,5 +65,46 @@ class VisitAnalytics { } ); } + + resizeDebounced() { + clearTimeout(this.resizeTimeout); + this.resizeTimeout = null; + + const elem = document.querySelector(".post-content"); + // Note: depends on main.css + const elemWidth = Math.floor(elem.clientWidth * 0.95); + + if (elemWidth != this.lastWidth) { + this.lastWidth = elemWidth; + this.images.forEach(img => { + const naturalHeight = +img.getAttribute("data-height"); + const naturalWidth = +img.getAttribute("data-width"); + img.height = Math.floor(naturalHeight * Math.min(1, elemWidth / naturalWidth)); + }) + } + } + + handleResize(event) { + if (event && event.type == "resize") { + if (this.resizeTimeout != null) + clearTimeout(this.resizeTimeout); + this.resizeTimeout = setTimeout(() => this.resizeDebounced(), 200); + } + } + + handleLazyload(entries: IntersectionObserverEntry[], observer: IntersectionObserver) { + entries.filter(e => e.isIntersecting).forEach(e => { + const imgElem = e.target.querySelector(`img`); + + imgElem.addEventListener("load", () => { + imgElem.classList.add("loaded"); + }); + imgElem.src = imgElem.getAttribute("data-src"); + + observer.unobserve(e.target); + console.log(`Lazyloaded ${ e.target.id ? e.target.id : e.target.tagName }`); + } + ); + } } new VisitAnalytics(); \ No newline at end of file