From d32a12a134ff594c1ff84d7008e1b947ab88f87f Mon Sep 17 00:00:00 2001 From: Cadence Fish Date: Wed, 26 Feb 2020 20:12:48 +1300 Subject: [PATCH] Add buttons to go through posts Closes #8 --- src/site/html/static/img/arrow-circled.svg | 1 + src/site/html/static/js/pagination.js | 14 +- src/site/html/static/js/post_overlay.js | 143 +++++++++++++++------ src/site/html/static/js/post_series.js | 56 ++++++++ src/site/pug/fragments/post.pug | 2 +- src/site/pug/includes/post.pug | 14 +- src/site/pug/post.pug | 2 +- src/site/sass/main.sass | 48 +++++-- 8 files changed, 219 insertions(+), 61 deletions(-) create mode 100644 src/site/html/static/img/arrow-circled.svg create mode 100644 src/site/html/static/js/post_series.js diff --git a/src/site/html/static/img/arrow-circled.svg b/src/site/html/static/img/arrow-circled.svg new file mode 100644 index 0000000..a54833b --- /dev/null +++ b/src/site/html/static/img/arrow-circled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/site/html/static/js/pagination.js b/src/site/html/static/js/pagination.js index d942170..ce3caf8 100644 --- a/src/site/html/static/js/pagination.js +++ b/src/site/html/static/js/pagination.js @@ -18,6 +18,7 @@ const intersectionThreshold = 0 class NextPageController { constructor() { this.instance = null + this.activatedCallbacks = [] } add() { @@ -27,10 +28,15 @@ class NextPageController { } else { this.instance = null } + this.activatedCallbacks.forEach(c => c()) } - activate() { - if (this.instance) this.instance.activate() + addActivatedCallback(callback) { + this.activatedCallbacks.push(callback) + } + + async activate() { + if (this.instance) await this.instance.activate() } } @@ -70,7 +76,7 @@ class NextPage extends FreezeWidth { this.fetching = true this.freeze("Loading...") - fetch(`/fragment/user/${this.element.getAttribute("data-username")}/${this.nextPageNumber}`).then(res => res.text()).then(text => { + return fetch(`/fragment/user/${this.element.getAttribute("data-username")}/${this.nextPageNumber}`).then(res => res.text()).then(text => { q("#next-page-container").remove() this.observer.disconnect() q("#timeline").insertAdjacentHTML("beforeend", text) @@ -81,7 +87,7 @@ class NextPage extends FreezeWidth { activate() { if (this.fetching) return this.class("disabled") - this.fetch() + return this.fetch() } } diff --git a/src/site/html/static/js/post_overlay.js b/src/site/html/static/js/post_overlay.js index 99299e8..a8f994f 100644 --- a/src/site/html/static/js/post_overlay.js +++ b/src/site/html/static/js/post_overlay.js @@ -1,4 +1,5 @@ import {q, ElemJS} from "./elemjs/elemjs.js" +import {timeline} from "./post_series.js" /** @type {PostOverlay[]} */ const postOverlays = [] @@ -10,23 +11,28 @@ window.addEventListener("popstate", event => { // console.log(event.state, postOverlays.length) if (event.state) { if (event.state.view === "post_overlay") { - loadPostOverlay(event.state.shortcode, false) + console.log(event.state.shortcode, postOverlays.map(o => o.identifier)) + /*if (postOverlays.length >= 2 && postOverlays.slice(-2)[0].identifier === event.state.shortcode) { + // continue down to actually pop please + } else {*/ + return loadPostOverlay(event.state.shortcode, "none") + /*}*/ } - } else { // event.state === null which means back to originally loaded page, so pop overlay - setTimeout(() => { // make sure document is entirely loaded - if (titleHistory.length === 1) { - document.title = titleHistory[0] - } else if (titleHistory.length >= 2) { - titleHistory.pop() - document.title = titleHistory.slice(-1)[0] - } - if (postOverlays.length) { - popOverlay() - } else { - window.location.reload() - } - }) } + // event.state === null which means back to originally loaded page, so pop overlay + setTimeout(() => { // make sure document is entirely loaded + if (titleHistory.length === 1) { + document.title = titleHistory[0] + } else if (titleHistory.length >= 2) { + titleHistory.pop() + document.title = titleHistory.slice(-1)[0] + } + if (postOverlays.length) { + popOverlay() + } else { + window.location.reload() + } + }) }) function pushOverlay(overlay) { @@ -43,14 +49,17 @@ function popOverlay() { } class PostOverlay extends ElemJS { - constructor() { + constructor(identifier) { super("div") + this.identifier = identifier + this.loaded = false + this.available = true + this.keyboardListeners = [] + this.class("post-overlay") this.event("click", event => { if (event.target === event.currentTarget) history.back() }) - this.loaded = false - this.available = true setTimeout(() => { if (!this.loaded) { this.class("loading") @@ -61,6 +70,13 @@ class PostOverlay extends ElemJS { }, 0) } + addKeyboardCallback(callback) { + if (this.available) { + this.keyboardListeners.push(callback) + document.addEventListener("keydown", callback) + } + } + setContent(html) { this.html(html) this.loaded = true @@ -79,12 +95,15 @@ class PostOverlay extends ElemJS { pop() { this.element.remove() this.available = false + while (this.keyboardListeners.length) { + document.removeEventListener("keydown", this.keyboardListeners.shift()) + } } } -const timeline = q("#timeline") -if (timeline) { - timeline.addEventListener("click", event => { +const timelineElement = q("#timeline") +if (timelineElement) { + timelineElement.addEventListener("click", event => { /** @type {HTMLElement[]} */ //@ts-ignore const path = event.composedPath() @@ -92,7 +111,7 @@ if (timeline) { if (postLink) { event.preventDefault() const shortcode = postLink.getAttribute("data-shortcode") - loadPostOverlay(shortcode, true) + loadPostOverlay(shortcode, "push") } }) } @@ -102,24 +121,74 @@ function fetchShortcodeFragment(shortcode) { else return fetch(`/fragment/post/${shortcode}`).then(res => res.json()) } -function loadPostOverlay(shortcode, shouldPushState) { - const overlay = new PostOverlay() +function loadPostOverlay(shortcode, stateChangeType) { + const overlay = new PostOverlay(shortcode) document.body.appendChild(overlay.element) pushOverlay(overlay) - if (shouldPushState) history.pushState({view: "post_overlay", shortcode: shortcode}, "", `/p/${shortcode}`) - const fetcher = fetchShortcodeFragment(shortcode) - fetcher.then(root => { - shortcodeDataMap.set(shortcode, root) - if (overlay.available) { - const {title, html} = root - overlay.setContent(html) + if (stateChangeType === "push") { + history.pushState({view: "post_overlay", shortcode: shortcode}, "", `/p/${shortcode}`) + } else if (stateChangeType === "replace") { + history.replaceState({view: "post_overlay", shortcode: shortcode}, "", `/p/${shortcode}`) + } else if (stateChangeType !== "none") { + throw new Error("Unknown stateChangeType: "+stateChangeType) + } + return new Promise((resolve, reject) => { + const fetcher = fetchShortcodeFragment(shortcode) + fetcher.then(root => { + shortcodeDataMap.set(shortcode, root) if (overlay.available) { - document.title = title + const {title, html} = root + overlay.setContent(html) + if (overlay.available) { + document.title = title + } + while (postOverlays.length >= 2) postOverlays.shift().pop() + const entry = timeline.entries.get(shortcode) + let canInteractWithNavigation = true + overlay.element.querySelectorAll(".navigate-posts").forEach(button => { + button.addEventListener("click", async event => { + /** @type {HTMLButtonElement} */ + //@ts-ignore + const button = event.currentTarget + if (button.classList.contains("next")) { + navigate("next") + } else { + navigate("previous") + } + }) + }) + overlay.addKeyboardCallback(event => { + if (event.key === "ArrowRight") navigate("next") + else if (event.key === "ArrowLeft") navigate("previous") + }) + async function navigate(direction) { + if (canInteractWithNavigation) { + /** @type {HTMLButtonElement} */ + //@ts-ignore + if (direction === "next") { + canInteractWithNavigation = false + if (entry.isLastEntry()) await timeline.fetch() + if (!overlay.available) return + var futureShortcode = entry.getNextShortcode() + } else { // "previous" + if (entry.isFirstEntry()) return + canInteractWithNavigation = false + var futureShortcode = entry.getPreviousShortcode() + } + await loadPostOverlay(futureShortcode, "replace") + const newOverlay = postOverlays.slice(-1)[0] + if (newOverlay === overlay) { // was cancelled + canInteractWithNavigation = true + } + } + } } - } - }) - fetcher.catch(error => { - console.error(error) - overlay.showError() + resolve() + }) + fetcher.catch(error => { + console.error(error) + overlay.showError() + reject(error) + }) }) } diff --git a/src/site/html/static/js/post_series.js b/src/site/html/static/js/post_series.js new file mode 100644 index 0000000..a7048c5 --- /dev/null +++ b/src/site/html/static/js/post_series.js @@ -0,0 +1,56 @@ +import {controller} from "./pagination.js" + +class Timeline { + constructor() { + this.shortcodes = [] + /** @type {Map} */ + this.entries = new Map() + controller.addActivatedCallback(() => this.update()) + this.update() + } + + update() { + this.shortcodes = [] + document.querySelectorAll("#timeline .sized-link").forEach(element => { + const shortcode = element.getAttribute("data-shortcode") + this.shortcodes.push(shortcode) + this.entries.set(shortcode, new TimelineEntry(this, shortcode)) + }) + console.log(this.shortcodes) + } + + fetch() { + return controller.activate() + } +} + +/** + * @param {Timeline} timeline + * @param {string} shortcode + */ +class TimelineEntry { + constructor(timeline, shortcode) { + this.timeline = timeline + this.shortcode = shortcode + } + + getNextShortcode() { + return this.timeline.shortcodes[this.timeline.shortcodes.indexOf(this.shortcode)+1] + } + + getPreviousShortcode() { + return this.timeline.shortcodes[this.timeline.shortcodes.indexOf(this.shortcode)-1] + } + + isFirstEntry() { + return this.timeline.shortcodes.indexOf(this.shortcode) === 0 + } + + isLastEntry() { + return this.timeline.shortcodes.indexOf(this.shortcode) === this.timeline.shortcodes.length-1 + } +} + +const timeline = new Timeline() + +export {timeline} diff --git a/src/site/pug/fragments/post.pug b/src/site/pug/fragments/post.pug index 2c2799c..2fbb353 100644 --- a/src/site/pug/fragments/post.pug +++ b/src/site/pug/fragments/post.pug @@ -2,4 +2,4 @@ include ../includes/post.pug -+post(post) ++post(post, true) diff --git a/src/site/pug/includes/post.pug b/src/site/pug/includes/post.pug index 371ef97..d564c67 100644 --- a/src/site/pug/includes/post.pug +++ b/src/site/pug/includes/post.pug @@ -1,11 +1,17 @@ include ./display_structured -mixin post(post) +mixin post(post, headerWithNavigation) .post-page-divider section.description-section - header.user-header - img(src=post.ownerPfpCacheP width=150 height=150 alt="").pfp - a.name(href=`/u/${post.getBasicOwner().username}`)= `${post.data.owner.full_name} (@${post.getBasicOwner().username})` + .user-header + header.user-header-inner + img(src=post.ownerPfpCacheP width=150 height=150 alt="").pfp + a.name(href=`/u/${post.getBasicOwner().username}`)= `${post.data.owner.full_name} (@${post.getBasicOwner().username})` + if headerWithNavigation + button.navigate-posts.previous + img(src="/static/img/arrow-circled.svg" alt="Previous post." style="transform: rotate(180deg)").icon + button.navigate-posts.next + img(src="/static/img/arrow-circled.svg" alt="Next post.").icon if post.getCaption() p.structured-text.description +display_structured(post.getStructuredCaption()) diff --git a/src/site/pug/post.pug b/src/site/pug/post.pug index b106e93..3169d6d 100644 --- a/src/site/pug/post.pug +++ b/src/site/pug/post.pug @@ -13,4 +13,4 @@ html script(type="module" src="/static/js/post_overlay.js") body.post-page main - +post(post) + +post(post, false) diff --git a/src/site/sass/main.sass b/src/site/sass/main.sass index 29c0b43..c0a7b7c 100644 --- a/src/site/sass/main.sass +++ b/src/site/sass/main.sass @@ -248,27 +248,47 @@ body height: inherit .user-header - display: flex + display: grid align-items: center + grid-template-columns: auto 1fr auto justify-content: center background-color: #b3b3b3 padding: 10px - .pfp - $size: 40px + .navigate-posts + -webkit-appearance: none + -moz-appearance: none + border: none + margin: 0 + padding: 0 + cursor: pointer + background: none - width: $size - height: $size - margin-right: 10px - background-color: rgba(40, 40, 40, 0.25) + .icon + display: block - .name - font-size: 20px - color: black - text-decoration: none + .user-header-inner + display: flex + align-items: center + justify-content: center + grid-row: 1 + grid-column: 2 - &:hover - text-decoration: underline + .pfp + $size: 40px + + width: $size + height: $size + margin-right: 10px + background-color: rgba(40, 40, 40, 0.25) + + .name + font-size: 20px + color: black + text-decoration: none + + &:hover + text-decoration: underline .description margin: 12px @@ -402,7 +422,7 @@ body display: flex .text, .button - appearance: none + -webkit-appearance: none -moz-appearance: none display: flex padding: 9px 8px 7px