271 lines
5.9 KiB
TypeScript
Raw Normal View History

2018-04-02 05:34:16 +00:00
import Diff from "./Diff"
2018-04-02 15:20:25 +00:00
import { delay } from "./Utils"
2017-06-26 01:57:29 +00:00
2017-06-20 13:46:49 +00:00
class LoadOptions {
addToHistory?: boolean
forceReload?: boolean
}
2018-04-02 05:34:16 +00:00
export default class Application {
2017-06-19 14:20:46 +00:00
fadeOutClass: string
2017-06-19 14:43:20 +00:00
activeLinkClass: string
2017-06-19 14:20:46 +00:00
content: HTMLElement
loading: HTMLElement
2017-06-20 10:41:26 +00:00
currentPath: string
originalPath: string
2017-12-01 18:37:19 +00:00
lastRequest: XMLHttpRequest | null
2018-04-02 20:38:00 +00:00
contentInvisible: boolean
2018-04-02 15:20:25 +00:00
onError: (err: Error) => void
2017-06-19 14:20:46 +00:00
constructor() {
2017-06-20 10:41:26 +00:00
this.currentPath = window.location.pathname
this.originalPath = window.location.pathname
2017-06-19 14:43:20 +00:00
this.activeLinkClass = "active"
2017-06-19 14:20:46 +00:00
this.fadeOutClass = "fade-out"
2018-04-02 15:20:25 +00:00
this.onError = console.error
2017-06-19 14:20:46 +00:00
}
2017-07-21 10:55:36 +00:00
init() {
2018-04-24 11:29:46 +00:00
document.addEventListener("DOMContentLoaded", this.onContentLoaded.bind(this))
}
onContentLoaded() {
let links = document.getElementsByTagName("a")
2018-04-24 11:29:46 +00:00
this.markActiveLinks(links)
this.ajaxify(links)
2017-07-21 10:55:36 +00:00
}
2017-06-19 14:20:46 +00:00
get(url: string): Promise<string> {
2017-12-01 18:37:19 +00:00
// return fetch(url, {
// credentials: "same-origin"
// }).then(response => response.text())
if(this.lastRequest) {
this.lastRequest.abort()
this.lastRequest = null
}
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest()
request.onerror = () => reject(new Error("You are either offline or the requested page doesn't exist."))
request.ontimeout = () => reject(new Error("The page took too much time to respond."))
request.onload = () => {
if(request.status < 200 || request.status >= 400)
reject(request.responseText)
else
resolve(request.responseText)
}
request.open("GET", url, true)
request.send()
this.lastRequest = request
})
2017-06-19 14:20:46 +00:00
}
2017-06-20 13:46:49 +00:00
load(url: string, options?: LoadOptions) {
2018-03-25 13:35:31 +00:00
// Remove protocol and hostname if it was specified
if(url.startsWith(location.origin)) {
url = url.substr(location.origin.length)
}
2017-06-26 01:57:29 +00:00
// Start sending a network request
2018-04-02 15:20:25 +00:00
let request: Promise<string>
let retry = () => {
return this.get("/_" + url).catch(async error => {
2018-04-07 13:39:55 +00:00
// Are we still on that page?
if(this.currentPath !== url) {
return
}
2018-04-02 15:20:25 +00:00
// Display connection error
this.onError(error)
// Retry after 3 seconds
await delay(3000)
2018-04-07 13:39:55 +00:00
// Are we still on that page?
if(this.currentPath !== url) {
return
}
2018-04-02 15:20:25 +00:00
return retry()
})
}
request = retry()
2017-06-26 01:57:29 +00:00
// Parse options
2017-06-20 13:46:49 +00:00
if(!options) {
options = new LoadOptions()
}
if(options.addToHistory === undefined) {
options.addToHistory = true
}
2017-11-10 07:41:45 +00:00
2017-06-26 01:57:29 +00:00
// Set current path
2017-06-20 10:41:26 +00:00
this.currentPath = url
2017-06-19 14:20:46 +00:00
2017-06-26 01:57:29 +00:00
// Add to browser history
if(options.addToHistory) {
history.pushState(url, "", url)
}
2017-06-19 14:20:46 +00:00
// Mark active links
this.markActiveLinks()
2018-04-02 20:38:00 +00:00
let consume = async () => {
let html = await request
2017-11-10 07:41:45 +00:00
2017-12-01 14:38:44 +00:00
if(this.currentPath !== url) {
return
}
2018-04-02 20:38:00 +00:00
// Set content
this.setContent(html)
this.scrollToTop()
2017-06-19 14:43:20 +00:00
2018-04-02 20:38:00 +00:00
// Fade in listener
let onFadedIn: EventListener = (e: Event) => {
// Ignore transitions of child elements.
// We only care about the transition event on the content element.
if(e.target !== this.content) {
return
}
2017-06-19 14:20:46 +00:00
2018-04-02 20:38:00 +00:00
// Reset the transition ended flag
this.contentInvisible = false
2017-06-19 15:45:27 +00:00
2018-04-02 20:38:00 +00:00
// Remove listener after we finally got the correct event.
this.content.removeEventListener("transitionend", onFadedIn)
}
this.content.addEventListener("transitionend", onFadedIn)
// Fade animations
this.content.classList.remove(this.fadeOutClass)
this.loading.classList.add(this.fadeOutClass)
// Send DOMContentLoaded Event
this.emit("DOMContentLoaded")
2017-06-19 14:43:20 +00:00
}
2018-04-02 20:38:00 +00:00
if(this.contentInvisible) {
consume()
} else {
// Fade out listener
let onFadedOut: EventListener = (e: Event) => {
// Ignore transitions of child elements.
// We only care about the transition event on the content element.
if(e.target !== this.content) {
return
}
2017-06-19 14:20:46 +00:00
2018-04-02 20:38:00 +00:00
this.contentInvisible = true
// Remove listener after we finally got the correct event.
this.content.removeEventListener("transitionend", onFadedOut)
// Outdated response.
if(this.currentPath !== url) {
return
}
// Wait for the network request to end.
consume()
}
this.content.addEventListener("transitionend", onFadedOut)
// Add fade out class
this.content.classList.add(this.fadeOutClass)
this.loading.classList.remove(this.fadeOutClass)
}
2017-06-20 10:41:26 +00:00
return request
2017-06-19 14:20:46 +00:00
}
2017-11-10 07:41:45 +00:00
setContent(html: string) {
this.content.innerHTML = html
2017-06-19 14:43:20 +00:00
}
markActiveLinks(links?: HTMLCollectionOf<HTMLAnchorElement>) {
if(!links) {
links = document.getElementsByTagName("a")
}
2017-06-19 14:43:20 +00:00
for(let i = 0; i < links.length; i++) {
let link = links[i]
Diff.mutations.queue(() => {
if(link.getAttribute("href") === this.currentPath) {
link.classList.add(this.activeLinkClass)
} else {
link.classList.remove(this.activeLinkClass)
}
})
2017-06-19 14:43:20 +00:00
}
2017-06-19 14:20:46 +00:00
}
ajaxify(links?: HTMLCollectionOf<HTMLAnchorElement>) {
if(!links) {
links = document.getElementsByTagName("a")
}
2017-06-19 14:20:46 +00:00
for(let i = 0; i < links.length; i++) {
2018-03-23 20:29:28 +00:00
let link = links[i] as HTMLAnchorElement
// Don't ajaxify links to a different host
if(link.hostname !== window.location.hostname) {
if(!link.target) {
link.target = "_blank"
}
continue
}
2018-03-23 21:02:43 +00:00
// Don't ajaxify invalid links, links with a target or links that disable ajax specifically
if(link.href === "" || link.href.includes("#") || link.target.length > 0 || link.dataset.ajax === "false") {
2018-03-23 20:29:28 +00:00
continue
}
2017-06-19 14:49:24 +00:00
let self = this
2017-06-19 14:20:46 +00:00
link.onclick = function(e) {
2018-04-02 05:50:30 +00:00
// Middle mouse button and Ctrl clicks should have standard behaviour
if(e.which === 2 || e.ctrlKey) {
2017-06-19 14:20:46 +00:00
return
}
2017-06-19 14:20:46 +00:00
e.preventDefault()
2017-07-19 14:56:02 +00:00
let url = (this as HTMLAnchorElement).getAttribute("href")
if(!url || url === self.currentPath) {
2017-06-19 14:20:46 +00:00
return
}
2017-11-10 07:41:45 +00:00
2017-06-19 14:20:46 +00:00
// Load requested page
2017-06-20 13:46:49 +00:00
self.load(url)
2017-06-19 14:20:46 +00:00
}
}
}
scrollToTop() {
let parent : HTMLElement | null = this.content
2017-06-19 14:20:46 +00:00
2018-03-23 20:29:28 +00:00
Diff.mutations.queue(() => {
while(parent = parent.parentElement) {
parent.scrollTop = 0
}
})
2017-06-19 14:20:46 +00:00
}
2017-06-19 15:45:27 +00:00
emit(eventName: string) {
2018-03-29 09:36:54 +00:00
document.dispatchEvent(new Event(eventName))
2017-06-19 15:45:27 +00:00
}
2017-06-19 14:49:24 +00:00
}