Improved forum navigation
This commit is contained in:
parent
44a26c62a2
commit
a4efc2b313
@ -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
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
@ -64,11 +70,13 @@ export class Application {
|
|||||||
if(options.addToHistory === undefined) {
|
if(options.addToHistory === undefined) {
|
||||||
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) {
|
||||||
this.content.innerHTML = html
|
Diff.innerHTML(this.content, html)
|
||||||
|
} else {
|
||||||
|
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) {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -6,4 +6,8 @@ export function* findAll(className: string) {
|
|||||||
for(let i = 0; i < elements.length; ++i) {
|
for(let i = 0; i < elements.length; ++i) {
|
||||||
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))
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user