Add buttons to go through posts

Closes #8
This commit is contained in:
Cadence Fish 2020-02-26 20:12:48 +13:00
parent becd2c2ec7
commit d32a12a134
No known key found for this signature in database
GPG Key ID: 81015DF9AA8607E1
8 changed files with 219 additions and 61 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 6.35 6.35"><g transform="translate(0 -290.65)" paint-order="fill markers stroke"><circle cx="3.175" cy="293.825" r="3.175" fill="#505050"/><path d="M2.706 292.277l1.51 1.548-1.51 1.548" fill="none" stroke="#ddd" stroke-width=".529" stroke-linecap="round"/></g></svg>

After

Width:  |  Height:  |  Size: 342 B

View File

@ -18,6 +18,7 @@ const intersectionThreshold = 0
class NextPageController { class NextPageController {
constructor() { constructor() {
this.instance = null this.instance = null
this.activatedCallbacks = []
} }
add() { add() {
@ -27,10 +28,15 @@ class NextPageController {
} else { } else {
this.instance = null this.instance = null
} }
this.activatedCallbacks.forEach(c => c())
} }
activate() { addActivatedCallback(callback) {
if (this.instance) this.instance.activate() 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.fetching = true
this.freeze("Loading...") 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() q("#next-page-container").remove()
this.observer.disconnect() this.observer.disconnect()
q("#timeline").insertAdjacentHTML("beforeend", text) q("#timeline").insertAdjacentHTML("beforeend", text)
@ -81,7 +87,7 @@ class NextPage extends FreezeWidth {
activate() { activate() {
if (this.fetching) return if (this.fetching) return
this.class("disabled") this.class("disabled")
this.fetch() return this.fetch()
} }
} }

View File

@ -1,4 +1,5 @@
import {q, ElemJS} from "./elemjs/elemjs.js" import {q, ElemJS} from "./elemjs/elemjs.js"
import {timeline} from "./post_series.js"
/** @type {PostOverlay[]} */ /** @type {PostOverlay[]} */
const postOverlays = [] const postOverlays = []
@ -10,23 +11,28 @@ window.addEventListener("popstate", event => {
// console.log(event.state, postOverlays.length) // console.log(event.state, postOverlays.length)
if (event.state) { if (event.state) {
if (event.state.view === "post_overlay") { 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) { function pushOverlay(overlay) {
@ -43,14 +49,17 @@ function popOverlay() {
} }
class PostOverlay extends ElemJS { class PostOverlay extends ElemJS {
constructor() { constructor(identifier) {
super("div") super("div")
this.identifier = identifier
this.loaded = false
this.available = true
this.keyboardListeners = []
this.class("post-overlay") this.class("post-overlay")
this.event("click", event => { this.event("click", event => {
if (event.target === event.currentTarget) history.back() if (event.target === event.currentTarget) history.back()
}) })
this.loaded = false
this.available = true
setTimeout(() => { setTimeout(() => {
if (!this.loaded) { if (!this.loaded) {
this.class("loading") this.class("loading")
@ -61,6 +70,13 @@ class PostOverlay extends ElemJS {
}, 0) }, 0)
} }
addKeyboardCallback(callback) {
if (this.available) {
this.keyboardListeners.push(callback)
document.addEventListener("keydown", callback)
}
}
setContent(html) { setContent(html) {
this.html(html) this.html(html)
this.loaded = true this.loaded = true
@ -79,12 +95,15 @@ class PostOverlay extends ElemJS {
pop() { pop() {
this.element.remove() this.element.remove()
this.available = false this.available = false
while (this.keyboardListeners.length) {
document.removeEventListener("keydown", this.keyboardListeners.shift())
}
} }
} }
const timeline = q("#timeline") const timelineElement = q("#timeline")
if (timeline) { if (timelineElement) {
timeline.addEventListener("click", event => { timelineElement.addEventListener("click", event => {
/** @type {HTMLElement[]} */ /** @type {HTMLElement[]} */
//@ts-ignore //@ts-ignore
const path = event.composedPath() const path = event.composedPath()
@ -92,7 +111,7 @@ if (timeline) {
if (postLink) { if (postLink) {
event.preventDefault() event.preventDefault()
const shortcode = postLink.getAttribute("data-shortcode") 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()) else return fetch(`/fragment/post/${shortcode}`).then(res => res.json())
} }
function loadPostOverlay(shortcode, shouldPushState) { function loadPostOverlay(shortcode, stateChangeType) {
const overlay = new PostOverlay() const overlay = new PostOverlay(shortcode)
document.body.appendChild(overlay.element) document.body.appendChild(overlay.element)
pushOverlay(overlay) pushOverlay(overlay)
if (shouldPushState) history.pushState({view: "post_overlay", shortcode: shortcode}, "", `/p/${shortcode}`) if (stateChangeType === "push") {
const fetcher = fetchShortcodeFragment(shortcode) history.pushState({view: "post_overlay", shortcode: shortcode}, "", `/p/${shortcode}`)
fetcher.then(root => { } else if (stateChangeType === "replace") {
shortcodeDataMap.set(shortcode, root) history.replaceState({view: "post_overlay", shortcode: shortcode}, "", `/p/${shortcode}`)
if (overlay.available) { } else if (stateChangeType !== "none") {
const {title, html} = root throw new Error("Unknown stateChangeType: "+stateChangeType)
overlay.setContent(html) }
return new Promise((resolve, reject) => {
const fetcher = fetchShortcodeFragment(shortcode)
fetcher.then(root => {
shortcodeDataMap.set(shortcode, root)
if (overlay.available) { 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
}
}
}
} }
} resolve()
}) })
fetcher.catch(error => { fetcher.catch(error => {
console.error(error) console.error(error)
overlay.showError() overlay.showError()
reject(error)
})
}) })
} }

View File

@ -0,0 +1,56 @@
import {controller} from "./pagination.js"
class Timeline {
constructor() {
this.shortcodes = []
/** @type {Map<string, TimelineEntry>} */
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}

View File

@ -2,4 +2,4 @@
include ../includes/post.pug include ../includes/post.pug
+post(post) +post(post, true)

View File

@ -1,11 +1,17 @@
include ./display_structured include ./display_structured
mixin post(post) mixin post(post, headerWithNavigation)
.post-page-divider .post-page-divider
section.description-section section.description-section
header.user-header .user-header
img(src=post.ownerPfpCacheP width=150 height=150 alt="").pfp header.user-header-inner
a.name(href=`/u/${post.getBasicOwner().username}`)= `${post.data.owner.full_name} (@${post.getBasicOwner().username})` 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() if post.getCaption()
p.structured-text.description p.structured-text.description
+display_structured(post.getStructuredCaption()) +display_structured(post.getStructuredCaption())

View File

@ -13,4 +13,4 @@ html
script(type="module" src="/static/js/post_overlay.js") script(type="module" src="/static/js/post_overlay.js")
body.post-page body.post-page
main main
+post(post) +post(post, false)

View File

@ -248,27 +248,47 @@ body
height: inherit height: inherit
.user-header .user-header
display: flex display: grid
align-items: center align-items: center
grid-template-columns: auto 1fr auto
justify-content: center justify-content: center
background-color: #b3b3b3 background-color: #b3b3b3
padding: 10px padding: 10px
.pfp .navigate-posts
$size: 40px -webkit-appearance: none
-moz-appearance: none
border: none
margin: 0
padding: 0
cursor: pointer
background: none
width: $size .icon
height: $size display: block
margin-right: 10px
background-color: rgba(40, 40, 40, 0.25)
.name .user-header-inner
font-size: 20px display: flex
color: black align-items: center
text-decoration: none justify-content: center
grid-row: 1
grid-column: 2
&:hover .pfp
text-decoration: underline $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 .description
margin: 12px margin: 12px
@ -402,7 +422,7 @@ body
display: flex display: flex
.text, .button .text, .button
appearance: none -webkit-appearance: none
-moz-appearance: none -moz-appearance: none
display: flex display: flex
padding: 9px 8px 7px padding: 9px 8px 7px