Improved forum navigation

This commit is contained in:
Eduard Urbach 2017-06-26 03:57:29 +02:00
parent 44a26c62a2
commit a4efc2b313
6 changed files with 96 additions and 32 deletions

View File

@ -9,6 +9,6 @@ component ForumTags
ForumTag("Bugs", "bug", "list") ForumTag("Bugs", "bug", "list")
component ForumTag(title string, category string, icon string) component ForumTag(title string, category string, icon string)
a.button.forum-tag.ajax(href=strings.TrimSuffix("/forum/" + category, "/")) a.button.forum-tag.action(href=strings.TrimSuffix("/forum/" + category, "/"), data-action="diff", data-trigger="click")
Icon(arn.GetForumIcon(category)) Icon(arn.GetForumIcon(category))
span.forum-tag-text= title span.forum-tag-text= title

View File

@ -53,8 +53,8 @@ export class AnimeNotifier {
this.visibilityObserver.disconnect() this.visibilityObserver.disconnect()
// Update each of these asynchronously // Update each of these asynchronously
Promise.resolve().then(() => this.updateMountables()) Promise.resolve().then(() => this.mountMountables())
Promise.resolve().then(() => this.updateActions()) Promise.resolve().then(() => this.assignActions())
Promise.resolve().then(() => this.lazyLoadImages()) Promise.resolve().then(() => this.lazyLoadImages())
} }
@ -75,7 +75,7 @@ export class AnimeNotifier {
} }
} }
updateActions() { assignActions() {
for(let element of findAll("action")) { for(let element of findAll("action")) {
if(element["action assigned"]) { if(element["action assigned"]) {
continue continue
@ -85,6 +85,9 @@ export class AnimeNotifier {
element.addEventListener(element.dataset.trigger, e => { element.addEventListener(element.dataset.trigger, e => {
actions[actionName](this, element, e) actions[actionName](this, element, e)
e.stopPropagation()
e.preventDefault()
}) })
// Use "action assigned" flag instead of removing the class. // Use "action assigned" flag instead of removing the class.
@ -121,15 +124,25 @@ export class AnimeNotifier {
this.visibilityObserver.observe(img) this.visibilityObserver.observe(img)
} }
updateMountables() { mountMountables() {
this.modifyDelayed("mountable", element => element.classList.add("mounted"))
}
unmountMountables() {
for(let element of findAll("mounted")) {
element.classList.remove("mounted")
}
}
modifyDelayed(className: string, func: (element: HTMLElement) => void) {
const delay = 20 const delay = 20
const maxDelay = 1000 const maxDelay = 1000
let time = 0 let time = 0
for(let element of findAll("mountable")) { for(let element of findAll(className)) {
setTimeout(() => { setTimeout(() => {
window.requestAnimationFrame(() => element.classList.add("mounted")) window.requestAnimationFrame(() => func(element))
}, time) }, time)
time += delay time += delay

View File

@ -1,3 +1,5 @@
import { Diff } from "./Diff"
class LoadOptions { class LoadOptions {
addToHistory?: boolean addToHistory?: boolean
forceReload?: boolean forceReload?: boolean
@ -57,6 +59,10 @@ export class Application {
} }
load(url: string, options?: LoadOptions) { load(url: string, options?: LoadOptions) {
// Start sending a network request
let request = this.get("/_" + url).catch(error => error)
// Parse options
if(!options) { if(!options) {
options = new LoadOptions() options = new LoadOptions()
} }
@ -65,10 +71,12 @@ export class Application {
options.addToHistory = true options.addToHistory = true
} }
// Set current path
this.currentPath = url this.currentPath = url
// Start sending a network request // Add to browser history
let request = this.get("/_" + url).catch(error => error) if(options.addToHistory)
history.pushState(url, null, url)
let onTransitionEnd = e => { let onTransitionEnd = e => {
// Ignore transitions of child elements. // Ignore transitions of child elements.
@ -82,13 +90,8 @@ export class Application {
// Wait for the network request to end. // Wait for the network request to end.
request.then(html => { request.then(html => {
// Add to browser history
if(options.addToHistory)
history.pushState(url, null, url)
// Set content // Set content
this.setContent(html) this.setContent(html, false)
this.scrollToTop()
// Fade animations // Fade animations
this.content.classList.remove(this.fadeOutClass) this.content.classList.remove(this.fadeOutClass)
@ -108,11 +111,16 @@ export class Application {
return request return request
} }
setContent(html: string) { setContent(html: string, diff: boolean) {
// Diff.innerHTML(this.content, html) if(diff) {
Diff.innerHTML(this.content, html)
} else {
this.content.innerHTML = html this.content.innerHTML = html
}
this.ajaxify(this.content) this.ajaxify(this.content)
this.markActiveLinks(this.content) this.markActiveLinks(this.content)
this.scrollToTop()
} }
markActiveLinks(element?: HTMLElement) { markActiveLinks(element?: HTMLElement) {

View File

@ -1,18 +1,18 @@
export class Diff { export class Diff {
static childNodes(aRoot: HTMLElement, bRoot: HTMLElement) { static childNodes(aRoot: Node, bRoot: Node) {
let aChild = [...aRoot.childNodes] let aChild = [...aRoot.childNodes]
let bChild = [...bRoot.childNodes] let bChild = [...bRoot.childNodes]
let numNodes = Math.max(aChild.length, bChild.length) let numNodes = Math.max(aChild.length, bChild.length)
for(let i = 0; i < numNodes; i++) { for(let i = 0; i < numNodes; i++) {
let a = aChild[i] as HTMLElement let a = aChild[i]
if(i >= bChild.length) { if(i >= bChild.length) {
aRoot.removeChild(a) aRoot.removeChild(a)
continue continue
} }
let b = bChild[i] as HTMLElement let b = bChild[i]
if(i >= aChild.length) { if(i >= aChild.length) {
aRoot.appendChild(b) aRoot.appendChild(b)
@ -24,38 +24,46 @@ export class Diff {
continue continue
} }
if(a.nodeType === Node.TEXT_NODE) {
a.textContent = b.textContent
continue
}
if(a.nodeType === Node.ELEMENT_NODE) { if(a.nodeType === Node.ELEMENT_NODE) {
if(a.tagName === "IFRAME") { let elemA = a as HTMLElement
let elemB = b as HTMLElement
if(elemA.tagName === "IFRAME") {
continue continue
} }
let removeAttributes: Attr[] = [] let removeAttributes: Attr[] = []
for(let x = 0; x < a.attributes.length; x++) { for(let x = 0; x < elemA.attributes.length; x++) {
let attrib = a.attributes[x] let attrib = elemA.attributes[x]
if(attrib.specified) { if(attrib.specified) {
if(!b.hasAttribute(attrib.name)) { if(!elemB.hasAttribute(attrib.name)) {
removeAttributes.push(attrib) removeAttributes.push(attrib)
} }
} }
} }
for(let attr of removeAttributes) { for(let attr of removeAttributes) {
a.removeAttributeNode(attr) elemA.removeAttributeNode(attr)
} }
for(let x = 0; x < b.attributes.length; x++) { for(let x = 0; x < elemB.attributes.length; x++) {
let attrib = b.attributes[x] let attrib = elemB.attributes[x]
if(attrib.specified) { if(attrib.specified) {
a.setAttribute(attrib.name, b.getAttribute(attrib.name)) elemA.setAttribute(attrib.name, elemB.getAttribute(attrib.name))
} }
} }
// Special case: Apply state of input elements // Special case: Apply state of input elements
if(a !== document.activeElement && a instanceof HTMLInputElement && b instanceof HTMLInputElement) { if(elemA !== document.activeElement && elemA instanceof HTMLInputElement && elemB instanceof HTMLInputElement) {
a.value = b.value elemA.value = elemB.value
} }
} }

View File

@ -1,6 +1,7 @@
import { Application } from "./Application" import { Application } from "./Application"
import { AnimeNotifier } from "./AnimeNotifier" import { AnimeNotifier } from "./AnimeNotifier"
import { Diff } from "./Diff" import { Diff } from "./Diff"
import { delay, findAll } from "./utils"
// Save new data from an input field // Save new data from an input field
export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaElement) { export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaElement) {
@ -53,6 +54,36 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE
}) })
} }
// Diff
export function diff(arn: AnimeNotifier, element: HTMLElement) {
let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href")
let request = fetch("/_" + url).then(response => response.text())
history.pushState(url, null, url)
arn.app.currentPath = url
arn.app.markActiveLinks()
arn.loading(true)
arn.unmountMountables()
// for(let element of findAll("mountable")) {
// element.classList.remove("mountable")
// }
delay(300).then(() => {
request
.then(html => arn.app.setContent(html, true))
.then(() => arn.app.markActiveLinks())
// .then(() => {
// for(let element of findAll("mountable")) {
// element.classList.remove("mountable")
// }
// })
.then(() => arn.app.emit("DOMContentLoaded"))
.then(() => arn.loading(false))
.catch(console.error)
})
}
// Search // Search
export function search(arn: AnimeNotifier, search: HTMLInputElement, e: KeyboardEvent) { export function search(arn: AnimeNotifier, search: HTMLInputElement, e: KeyboardEvent) {
if(e.ctrlKey || e.altKey) { if(e.ctrlKey || e.altKey) {

View File

@ -7,3 +7,7 @@ export function* findAll(className: string) {
yield elements[i] as HTMLElement yield elements[i] as HTMLElement
} }
} }
export function delay<T>(millis: number, value?: T): Promise<T> {
return new Promise(resolve => setTimeout(() => resolve(value), millis))
}