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 {
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()
}
}

View File

@ -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)
})
})
}

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
+post(post)
+post(post, true)

View File

@ -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())

View File

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

View File

@ -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