From a4efc2b313fc55b32cba49e2e38ca44d5a39a252 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 26 Jun 2017 03:57:29 +0200 Subject: [PATCH] Improved forum navigation --- mixins/ForumTags.pixy | 2 +- scripts/AnimeNotifier.ts | 25 +++++++++++++++++++------ scripts/Application.ts | 32 ++++++++++++++++++++------------ scripts/Diff.ts | 34 +++++++++++++++++++++------------- scripts/actions.ts | 31 +++++++++++++++++++++++++++++++ scripts/utils.ts | 4 ++++ 6 files changed, 96 insertions(+), 32 deletions(-) diff --git a/mixins/ForumTags.pixy b/mixins/ForumTags.pixy index b1754cbe..ff367246 100644 --- a/mixins/ForumTags.pixy +++ b/mixins/ForumTags.pixy @@ -9,6 +9,6 @@ component ForumTags ForumTag("Bugs", "bug", "list") 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)) span.forum-tag-text= title \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index d705e594..f9999ad3 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -53,8 +53,8 @@ export class AnimeNotifier { this.visibilityObserver.disconnect() // Update each of these asynchronously - Promise.resolve().then(() => this.updateMountables()) - Promise.resolve().then(() => this.updateActions()) + Promise.resolve().then(() => this.mountMountables()) + Promise.resolve().then(() => this.assignActions()) Promise.resolve().then(() => this.lazyLoadImages()) } @@ -75,7 +75,7 @@ export class AnimeNotifier { } } - updateActions() { + assignActions() { for(let element of findAll("action")) { if(element["action assigned"]) { continue @@ -85,6 +85,9 @@ export class AnimeNotifier { element.addEventListener(element.dataset.trigger, e => { actions[actionName](this, element, e) + + e.stopPropagation() + e.preventDefault() }) // Use "action assigned" flag instead of removing the class. @@ -121,15 +124,25 @@ export class AnimeNotifier { 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 maxDelay = 1000 let time = 0 - for(let element of findAll("mountable")) { + for(let element of findAll(className)) { setTimeout(() => { - window.requestAnimationFrame(() => element.classList.add("mounted")) + window.requestAnimationFrame(() => func(element)) }, time) time += delay diff --git a/scripts/Application.ts b/scripts/Application.ts index 54f60e7f..a11f3f06 100644 --- a/scripts/Application.ts +++ b/scripts/Application.ts @@ -1,3 +1,5 @@ +import { Diff } from "./Diff" + class LoadOptions { addToHistory?: boolean forceReload?: boolean @@ -57,6 +59,10 @@ export class Application { } load(url: string, options?: LoadOptions) { + // Start sending a network request + let request = this.get("/_" + url).catch(error => error) + + // Parse options if(!options) { options = new LoadOptions() } @@ -64,11 +70,13 @@ export class Application { if(options.addToHistory === undefined) { options.addToHistory = true } - + + // Set current path this.currentPath = url - // Start sending a network request - let request = this.get("/_" + url).catch(error => error) + // Add to browser history + if(options.addToHistory) + history.pushState(url, null, url) let onTransitionEnd = e => { // Ignore transitions of child elements. @@ -82,13 +90,8 @@ export class Application { // Wait for the network request to end. request.then(html => { - // Add to browser history - if(options.addToHistory) - history.pushState(url, null, url) - // Set content - this.setContent(html) - this.scrollToTop() + this.setContent(html, false) // Fade animations this.content.classList.remove(this.fadeOutClass) @@ -108,11 +111,16 @@ export class Application { return request } - setContent(html: string) { - // Diff.innerHTML(this.content, html) - this.content.innerHTML = html + setContent(html: string, diff: boolean) { + if(diff) { + Diff.innerHTML(this.content, html) + } else { + this.content.innerHTML = html + } + this.ajaxify(this.content) this.markActiveLinks(this.content) + this.scrollToTop() } markActiveLinks(element?: HTMLElement) { diff --git a/scripts/Diff.ts b/scripts/Diff.ts index aa4ab348..ea3c2620 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -1,18 +1,18 @@ export class Diff { - static childNodes(aRoot: HTMLElement, bRoot: HTMLElement) { + static childNodes(aRoot: Node, bRoot: Node) { let aChild = [...aRoot.childNodes] let bChild = [...bRoot.childNodes] let numNodes = Math.max(aChild.length, bChild.length) for(let i = 0; i < numNodes; i++) { - let a = aChild[i] as HTMLElement + let a = aChild[i] if(i >= bChild.length) { aRoot.removeChild(a) continue } - let b = bChild[i] as HTMLElement + let b = bChild[i] if(i >= aChild.length) { aRoot.appendChild(b) @@ -24,38 +24,46 @@ export class Diff { continue } + if(a.nodeType === Node.TEXT_NODE) { + a.textContent = b.textContent + continue + } + 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 } let removeAttributes: Attr[] = [] - for(let x = 0; x < a.attributes.length; x++) { - let attrib = a.attributes[x] + for(let x = 0; x < elemA.attributes.length; x++) { + let attrib = elemA.attributes[x] if(attrib.specified) { - if(!b.hasAttribute(attrib.name)) { + if(!elemB.hasAttribute(attrib.name)) { removeAttributes.push(attrib) } } } for(let attr of removeAttributes) { - a.removeAttributeNode(attr) + elemA.removeAttributeNode(attr) } - for(let x = 0; x < b.attributes.length; x++) { - let attrib = b.attributes[x] + for(let x = 0; x < elemB.attributes.length; x++) { + let attrib = elemB.attributes[x] 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 - if(a !== document.activeElement && a instanceof HTMLInputElement && b instanceof HTMLInputElement) { - a.value = b.value + if(elemA !== document.activeElement && elemA instanceof HTMLInputElement && elemB instanceof HTMLInputElement) { + elemA.value = elemB.value } } diff --git a/scripts/actions.ts b/scripts/actions.ts index 8d939899..d1dcc4e5 100644 --- a/scripts/actions.ts +++ b/scripts/actions.ts @@ -1,6 +1,7 @@ import { Application } from "./Application" import { AnimeNotifier } from "./AnimeNotifier" import { Diff } from "./Diff" +import { delay, findAll } from "./utils" // Save new data from an input field 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 export function search(arn: AnimeNotifier, search: HTMLInputElement, e: KeyboardEvent) { if(e.ctrlKey || e.altKey) { diff --git a/scripts/utils.ts b/scripts/utils.ts index 41ebc19e..7d1ab232 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -6,4 +6,8 @@ export function* findAll(className: string) { for(let i = 0; i < elements.length; ++i) { yield elements[i] as HTMLElement } +} + +export function delay(millis: number, value?: T): Promise { + return new Promise(resolve => setTimeout(() => resolve(value), millis)) } \ No newline at end of file