2017-07-14 21:50:34 +00:00
|
|
|
import * as actions from "./Actions"
|
2017-07-07 15:16:40 +00:00
|
|
|
import { displayAiringDate, displayDate } from "./DateView"
|
2017-07-08 13:40:13 +00:00
|
|
|
import { findAll, delay, canUseWebP } from "./Utils"
|
2017-07-14 21:50:34 +00:00
|
|
|
import { Application } from "./Application"
|
|
|
|
import { Diff } from "./Diff"
|
2017-07-05 19:06:38 +00:00
|
|
|
import { MutationQueue } from "./MutationQueue"
|
2017-07-12 18:37:34 +00:00
|
|
|
import { StatusMessage } from "./StatusMessage"
|
2017-07-14 21:50:34 +00:00
|
|
|
import { PushManager } from "./PushManager"
|
2017-07-19 16:47:17 +00:00
|
|
|
import { TouchController } from "./TouchController"
|
2017-06-29 06:32:46 +00:00
|
|
|
|
2017-06-19 14:49:24 +00:00
|
|
|
export class AnimeNotifier {
|
|
|
|
app: Application
|
2017-06-30 15:51:17 +00:00
|
|
|
user: HTMLElement
|
2017-07-06 20:08:49 +00:00
|
|
|
title: string
|
2017-07-08 13:40:13 +00:00
|
|
|
webpEnabled: boolean
|
2017-07-14 02:58:21 +00:00
|
|
|
contentLoadedActions: Promise<any>
|
2017-07-12 18:37:34 +00:00
|
|
|
statusMessage: StatusMessage
|
2017-07-05 19:06:38 +00:00
|
|
|
visibilityObserver: IntersectionObserver
|
2017-07-14 21:50:34 +00:00
|
|
|
pushManager: PushManager
|
2017-07-19 16:47:17 +00:00
|
|
|
touchController: TouchController
|
|
|
|
sideBar: HTMLElement
|
2017-07-19 04:55:21 +00:00
|
|
|
mainPageLoaded: boolean
|
2017-07-19 05:39:09 +00:00
|
|
|
lastReloadContentPath: string
|
2017-07-05 19:06:38 +00:00
|
|
|
|
|
|
|
imageFound: MutationQueue
|
|
|
|
imageNotFound: MutationQueue
|
|
|
|
unmount: MutationQueue
|
2017-06-19 14:49:24 +00:00
|
|
|
|
|
|
|
constructor(app: Application) {
|
|
|
|
this.app = app
|
2017-06-30 15:51:17 +00:00
|
|
|
this.user = null
|
2017-07-06 20:08:49 +00:00
|
|
|
this.title = "Anime Notifier"
|
2017-06-24 14:28:16 +00:00
|
|
|
|
2017-07-05 19:06:38 +00:00
|
|
|
this.imageFound = new MutationQueue(elem => elem.classList.add("image-found"))
|
|
|
|
this.imageNotFound = new MutationQueue(elem => elem.classList.add("image-not-found"))
|
|
|
|
this.unmount = new MutationQueue(elem => elem.classList.remove("mounted"))
|
|
|
|
|
2017-07-19 02:18:56 +00:00
|
|
|
// These classes will never be removed on DOM diffs
|
|
|
|
Diff.persistentClasses.add("mounted")
|
|
|
|
Diff.persistentClasses.add("image-found")
|
|
|
|
|
2017-07-19 07:09:55 +00:00
|
|
|
// Never remove src property on diffs
|
|
|
|
Diff.persistentAttributes.add("src")
|
|
|
|
|
2017-06-24 14:28:16 +00:00
|
|
|
if("IntersectionObserver" in window) {
|
|
|
|
// Enable lazy load
|
|
|
|
this.visibilityObserver = new IntersectionObserver(
|
|
|
|
entries => {
|
|
|
|
for(let entry of entries) {
|
|
|
|
if(entry.intersectionRatio > 0) {
|
|
|
|
entry.target["became visible"]()
|
|
|
|
this.visibilityObserver.unobserve(entry.target)
|
|
|
|
}
|
2017-06-24 14:17:38 +00:00
|
|
|
}
|
2017-06-24 14:28:16 +00:00
|
|
|
},
|
|
|
|
{}
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
// Disable lazy load feature
|
|
|
|
this.visibilityObserver = {
|
|
|
|
disconnect: () => {},
|
|
|
|
observe: (elem: HTMLElement) => {
|
|
|
|
elem["became visible"]()
|
|
|
|
},
|
|
|
|
unobserve: (elem: HTMLElement) => {}
|
|
|
|
} as IntersectionObserver
|
|
|
|
}
|
2017-06-19 14:49:24 +00:00
|
|
|
}
|
|
|
|
|
2017-06-30 15:51:17 +00:00
|
|
|
init() {
|
|
|
|
document.addEventListener("readystatechange", this.onReadyStateChange.bind(this))
|
|
|
|
document.addEventListener("DOMContentLoaded", this.onContentLoaded.bind(this))
|
|
|
|
document.addEventListener("keydown", this.onKeyDown.bind(this), false)
|
|
|
|
window.addEventListener("popstate", this.onPopState.bind(this))
|
|
|
|
|
2017-07-14 02:58:21 +00:00
|
|
|
// Idle
|
2017-07-03 15:21:00 +00:00
|
|
|
this.requestIdleCallback(this.onIdle.bind(this))
|
|
|
|
}
|
|
|
|
|
|
|
|
requestIdleCallback(func: Function) {
|
2017-06-30 15:51:17 +00:00
|
|
|
if("requestIdleCallback" in window) {
|
2017-07-03 15:21:00 +00:00
|
|
|
window["requestIdleCallback"](func)
|
2017-06-30 15:51:17 +00:00
|
|
|
} else {
|
2017-07-03 15:21:00 +00:00
|
|
|
func()
|
2017-06-30 15:51:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-19 15:45:27 +00:00
|
|
|
onReadyStateChange() {
|
|
|
|
if(document.readyState !== "interactive") {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.run()
|
|
|
|
}
|
|
|
|
|
2017-06-19 14:49:24 +00:00
|
|
|
run() {
|
2017-07-03 23:20:27 +00:00
|
|
|
// Add "osx" class on macs so we can set a proper font-size
|
|
|
|
if(navigator.platform.includes("Mac")) {
|
2017-07-03 23:25:20 +00:00
|
|
|
document.documentElement.classList.add("osx")
|
2017-07-03 23:20:27 +00:00
|
|
|
}
|
|
|
|
|
2017-07-08 13:40:13 +00:00
|
|
|
// Check for WebP support
|
|
|
|
this.webpEnabled = canUseWebP()
|
|
|
|
|
2017-07-03 23:20:27 +00:00
|
|
|
// Initiate the elements we need
|
2017-06-30 15:51:17 +00:00
|
|
|
this.user = this.app.find("user")
|
2017-06-19 14:49:24 +00:00
|
|
|
this.app.content = this.app.find("content")
|
|
|
|
this.app.loading = this.app.find("loading")
|
2017-07-03 23:20:27 +00:00
|
|
|
|
2017-07-12 18:37:34 +00:00
|
|
|
// Status message
|
|
|
|
this.statusMessage = new StatusMessage(
|
|
|
|
this.app.find("status-message"),
|
|
|
|
this.app.find("status-message-text")
|
|
|
|
)
|
|
|
|
|
2017-07-13 15:56:14 +00:00
|
|
|
// Let"s start
|
2017-06-19 14:49:24 +00:00
|
|
|
this.app.run()
|
2017-07-13 22:11:25 +00:00
|
|
|
|
2017-07-14 21:50:34 +00:00
|
|
|
// Push manager
|
|
|
|
this.pushManager = new PushManager()
|
2017-07-19 14:56:02 +00:00
|
|
|
|
|
|
|
// Sidebar control
|
2017-07-19 16:47:17 +00:00
|
|
|
this.sideBar = this.app.find("sidebar")
|
|
|
|
|
2017-07-19 14:56:02 +00:00
|
|
|
document.body.addEventListener("click", e => {
|
2017-07-19 16:47:17 +00:00
|
|
|
this.sideBar.classList.remove("sidebar-visible")
|
2017-07-19 14:56:02 +00:00
|
|
|
})
|
2017-07-19 16:47:17 +00:00
|
|
|
|
|
|
|
this.touchController = new TouchController()
|
|
|
|
this.touchController.leftSwipe = () => this.sideBar.classList.remove("sidebar-visible")
|
|
|
|
this.touchController.rightSwipe = () => this.sideBar.classList.add("sidebar-visible")
|
2017-06-19 14:49:24 +00:00
|
|
|
}
|
2017-06-19 15:45:27 +00:00
|
|
|
|
2017-07-14 23:32:06 +00:00
|
|
|
async onContentLoaded() {
|
2017-06-30 15:51:17 +00:00
|
|
|
// Stop watching all the objects from the previous page.
|
2017-06-24 14:17:38 +00:00
|
|
|
this.visibilityObserver.disconnect()
|
|
|
|
|
2017-07-14 02:58:21 +00:00
|
|
|
this.contentLoadedActions = Promise.all([
|
|
|
|
Promise.resolve().then(() => this.mountMountables()),
|
|
|
|
Promise.resolve().then(() => this.lazyLoadImages()),
|
|
|
|
Promise.resolve().then(() => this.displayLocalDates()),
|
|
|
|
Promise.resolve().then(() => this.setSelectBoxValue()),
|
2017-07-14 23:32:06 +00:00
|
|
|
Promise.resolve().then(() => this.assignActions()),
|
2017-07-17 01:14:05 +00:00
|
|
|
Promise.resolve().then(() => this.updatePushUI()),
|
|
|
|
Promise.resolve().then(() => this.countUp())
|
2017-07-14 02:58:21 +00:00
|
|
|
])
|
2017-07-06 20:08:49 +00:00
|
|
|
|
2017-07-14 23:32:06 +00:00
|
|
|
// Apply page title
|
2017-07-06 20:08:49 +00:00
|
|
|
let headers = document.getElementsByTagName("h1")
|
|
|
|
|
|
|
|
if(this.app.currentPath === "/" || headers.length === 0) {
|
|
|
|
if(document.title !== this.title) {
|
|
|
|
document.title = this.title
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
document.title = headers[0].innerText
|
|
|
|
}
|
2017-06-29 13:55:04 +00:00
|
|
|
}
|
|
|
|
|
2017-07-14 23:32:06 +00:00
|
|
|
async updatePushUI() {
|
2017-07-14 23:53:08 +00:00
|
|
|
if(!this.pushManager.pushSupported || !this.app.currentPath.includes("/settings")) {
|
2017-07-14 23:32:06 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let subscription = await this.pushManager.subscription()
|
|
|
|
|
|
|
|
if(subscription) {
|
|
|
|
this.app.find("enable-notifications").style.display = "none"
|
|
|
|
this.app.find("disable-notifications").style.display = "flex"
|
|
|
|
} else {
|
|
|
|
this.app.find("enable-notifications").style.display = "flex"
|
|
|
|
this.app.find("disable-notifications").style.display = "none"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-30 15:51:17 +00:00
|
|
|
onIdle() {
|
2017-07-19 03:23:06 +00:00
|
|
|
// Service worker
|
|
|
|
this.registerServiceWorker()
|
|
|
|
|
|
|
|
// Analytics
|
2017-07-13 15:56:14 +00:00
|
|
|
this.pushAnalytics()
|
2017-07-15 00:01:14 +00:00
|
|
|
|
2017-07-19 03:23:06 +00:00
|
|
|
// Offline message
|
2017-07-15 00:01:14 +00:00
|
|
|
if(navigator.onLine === false) {
|
|
|
|
this.statusMessage.showError("You are viewing an offline version of the site now.")
|
|
|
|
}
|
2017-07-13 15:56:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
registerServiceWorker() {
|
2017-07-13 22:11:25 +00:00
|
|
|
if(!("serviceWorker" in navigator)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-07-19 03:23:06 +00:00
|
|
|
console.log("register service worker")
|
|
|
|
|
2017-07-13 23:50:10 +00:00
|
|
|
navigator.serviceWorker.register("/service-worker").then(registration => {
|
2017-07-19 05:39:09 +00:00
|
|
|
// registration.update()
|
2017-07-13 15:56:14 +00:00
|
|
|
})
|
|
|
|
|
2017-07-14 02:58:21 +00:00
|
|
|
navigator.serviceWorker.addEventListener("message", evt => {
|
2017-07-13 22:11:25 +00:00
|
|
|
this.onServiceWorkerMessage(evt)
|
2017-07-14 02:58:21 +00:00
|
|
|
})
|
|
|
|
|
2017-07-19 04:32:31 +00:00
|
|
|
// This will send a message to the service worker that the DOM has been loaded
|
2017-07-19 03:23:06 +00:00
|
|
|
let sendContentLoadedEvent = () => {
|
2017-07-14 02:58:21 +00:00
|
|
|
if(!navigator.serviceWorker.controller) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-07-19 05:39:09 +00:00
|
|
|
// A reloadContent call should never trigger another reload
|
|
|
|
if(this.app.currentPath === this.lastReloadContentPath) {
|
|
|
|
console.log("reload finished.")
|
|
|
|
this.lastReloadContentPath = ""
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-07-14 02:58:21 +00:00
|
|
|
let message = {
|
|
|
|
type: "loaded",
|
|
|
|
url: ""
|
|
|
|
}
|
|
|
|
|
2017-07-19 04:55:21 +00:00
|
|
|
// If mainPageLoaded is set, it means every single request is now an AJAX request for the /_/ prefixed page
|
|
|
|
if(this.mainPageLoaded) {
|
|
|
|
message.url = window.location.origin + "/_" + window.location.pathname
|
2017-07-14 02:58:21 +00:00
|
|
|
} else {
|
2017-07-19 04:55:21 +00:00
|
|
|
this.mainPageLoaded = true
|
2017-07-14 02:58:21 +00:00
|
|
|
message.url = window.location.href
|
|
|
|
}
|
|
|
|
|
2017-07-19 05:39:09 +00:00
|
|
|
console.log("checking for updates:", message.url)
|
2017-07-19 04:55:21 +00:00
|
|
|
|
2017-07-14 02:58:21 +00:00
|
|
|
navigator.serviceWorker.controller.postMessage(JSON.stringify(message))
|
2017-07-19 03:23:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// For future loaded events
|
|
|
|
document.addEventListener("DOMContentLoaded", sendContentLoadedEvent)
|
|
|
|
|
|
|
|
// If the page is loaded already, send the loaded event right now.
|
|
|
|
if(document.readyState !== "loading") {
|
|
|
|
sendContentLoadedEvent()
|
|
|
|
}
|
2017-07-13 22:11:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
onServiceWorkerMessage(evt: ServiceWorkerMessageEvent) {
|
|
|
|
let message = JSON.parse(evt.data)
|
|
|
|
|
|
|
|
switch(message.type) {
|
2017-07-14 02:58:21 +00:00
|
|
|
case "new content":
|
|
|
|
if(message.url.includes("/_/")) {
|
|
|
|
// Content reload
|
|
|
|
this.contentLoadedActions.then(() => {
|
2017-07-19 06:45:41 +00:00
|
|
|
this.reloadContent(true)
|
2017-07-14 02:58:21 +00:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
// Full page reload
|
|
|
|
this.contentLoadedActions.then(() => {
|
2017-07-13 22:11:25 +00:00
|
|
|
this.reloadPage()
|
2017-07-14 02:58:21 +00:00
|
|
|
})
|
2017-07-13 22:11:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
break
|
|
|
|
}
|
2017-07-13 15:56:14 +00:00
|
|
|
}
|
|
|
|
|
2017-07-17 01:14:05 +00:00
|
|
|
countUp() {
|
|
|
|
for(let element of findAll("count-up")) {
|
|
|
|
let final = parseInt(element.innerText)
|
|
|
|
let duration = 2000.0
|
|
|
|
let start = Date.now()
|
|
|
|
|
|
|
|
element.innerText = "0"
|
|
|
|
|
|
|
|
let callback = () => {
|
|
|
|
let progress = (Date.now() - start) / duration
|
|
|
|
|
|
|
|
if(progress > 1) {
|
|
|
|
progress = 1
|
|
|
|
}
|
|
|
|
|
|
|
|
element.innerText = String(Math.round(progress * final))
|
|
|
|
|
|
|
|
if(progress < 1) {
|
|
|
|
window.requestAnimationFrame(callback)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
window.requestAnimationFrame(callback)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-13 15:56:14 +00:00
|
|
|
pushAnalytics() {
|
2017-06-30 15:51:17 +00:00
|
|
|
if(!this.user) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let analytics = {
|
|
|
|
general: {
|
|
|
|
timezoneOffset: new Date().getTimezoneOffset()
|
|
|
|
},
|
|
|
|
screen: {
|
|
|
|
width: screen.width,
|
|
|
|
height: screen.height,
|
|
|
|
availableWidth: screen.availWidth,
|
|
|
|
availableHeight: screen.availHeight,
|
|
|
|
pixelRatio: window.devicePixelRatio
|
|
|
|
},
|
|
|
|
system: {
|
|
|
|
cpuCount: navigator.hardwareConcurrency,
|
|
|
|
platform: navigator.platform
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-05 21:48:29 +00:00
|
|
|
fetch("/dark-flame-master", {
|
2017-06-30 15:51:17 +00:00
|
|
|
method: "POST",
|
|
|
|
credentials: "same-origin",
|
|
|
|
body: JSON.stringify(analytics)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-06-29 13:55:04 +00:00
|
|
|
setSelectBoxValue() {
|
|
|
|
for(let element of document.getElementsByTagName("select")) {
|
|
|
|
element.value = element.getAttribute("value")
|
|
|
|
}
|
2017-06-29 06:32:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
displayLocalDates() {
|
|
|
|
const now = new Date()
|
|
|
|
|
2017-07-07 15:16:40 +00:00
|
|
|
for(let element of findAll("utc-airing-date")) {
|
|
|
|
displayAiringDate(element, now)
|
|
|
|
}
|
|
|
|
|
2017-06-29 06:32:46 +00:00
|
|
|
for(let element of findAll("utc-date")) {
|
2017-07-07 15:16:40 +00:00
|
|
|
displayDate(element, now)
|
2017-06-29 06:32:46 +00:00
|
|
|
}
|
2017-06-24 14:17:38 +00:00
|
|
|
}
|
|
|
|
|
2017-07-19 06:45:41 +00:00
|
|
|
reloadContent(cached?: boolean) {
|
2017-07-19 06:49:38 +00:00
|
|
|
// console.log("reload content", "/_" + this.app.currentPath)
|
2017-07-19 05:39:09 +00:00
|
|
|
|
2017-07-13 22:11:25 +00:00
|
|
|
let headers = new Headers()
|
2017-07-19 06:45:41 +00:00
|
|
|
|
|
|
|
if(!cached) {
|
|
|
|
headers.append("X-Reload", "true")
|
|
|
|
} else {
|
|
|
|
headers.append("X-CacheOnly", "true")
|
|
|
|
}
|
2017-07-13 22:11:25 +00:00
|
|
|
|
2017-07-13 23:07:49 +00:00
|
|
|
let path = this.app.currentPath
|
2017-07-19 05:39:09 +00:00
|
|
|
this.lastReloadContentPath = path
|
2017-07-13 23:07:49 +00:00
|
|
|
|
2017-07-19 04:55:21 +00:00
|
|
|
return fetch("/_" + path, {
|
2017-07-13 22:11:25 +00:00
|
|
|
credentials: "same-origin",
|
|
|
|
headers
|
|
|
|
})
|
|
|
|
.then(response => {
|
2017-07-13 23:07:49 +00:00
|
|
|
if(this.app.currentPath !== path) {
|
|
|
|
return Promise.reject("old request")
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.resolve(response)
|
2017-06-21 16:44:20 +00:00
|
|
|
})
|
|
|
|
.then(response => response.text())
|
|
|
|
.then(html => Diff.innerHTML(this.app.content, html))
|
2017-06-24 00:10:04 +00:00
|
|
|
.then(() => this.app.emit("DOMContentLoaded"))
|
2017-06-21 16:44:20 +00:00
|
|
|
}
|
|
|
|
|
2017-07-13 22:11:25 +00:00
|
|
|
reloadPage() {
|
2017-07-19 05:39:09 +00:00
|
|
|
console.log("reload page", this.app.currentPath)
|
2017-07-19 03:23:06 +00:00
|
|
|
|
|
|
|
let path = this.app.currentPath
|
2017-07-19 05:39:09 +00:00
|
|
|
this.lastReloadContentPath = path
|
2017-07-19 03:23:06 +00:00
|
|
|
|
|
|
|
return fetch(path, {
|
2017-07-19 04:32:31 +00:00
|
|
|
credentials: "same-origin"
|
2017-07-19 03:23:06 +00:00
|
|
|
})
|
|
|
|
.then(response => {
|
|
|
|
if(this.app.currentPath !== path) {
|
|
|
|
return Promise.reject("old request")
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.resolve(response)
|
|
|
|
})
|
|
|
|
.then(response => response.text())
|
|
|
|
.then(html => {
|
|
|
|
Diff.root(document.documentElement, html)
|
|
|
|
})
|
|
|
|
.then(() => this.app.emit("DOMContentLoaded"))
|
2017-07-19 03:27:37 +00:00
|
|
|
.then(() => this.loading(false)) // Because our loading element gets reset due to full page diff
|
2017-07-13 22:11:25 +00:00
|
|
|
}
|
|
|
|
|
2017-06-20 10:41:26 +00:00
|
|
|
loading(isLoading: boolean) {
|
|
|
|
if(isLoading) {
|
2017-07-19 07:09:55 +00:00
|
|
|
document.documentElement.style.cursor = "progress"
|
2017-06-20 10:41:26 +00:00
|
|
|
this.app.loading.classList.remove(this.app.fadeOutClass)
|
|
|
|
} else {
|
2017-07-19 07:09:55 +00:00
|
|
|
document.documentElement.style.cursor = "auto"
|
2017-06-20 10:41:26 +00:00
|
|
|
this.app.loading.classList.add(this.app.fadeOutClass)
|
|
|
|
}
|
|
|
|
}
|
2017-06-21 12:00:52 +00:00
|
|
|
|
2017-06-26 01:57:29 +00:00
|
|
|
assignActions() {
|
2017-06-21 13:29:06 +00:00
|
|
|
for(let element of findAll("action")) {
|
2017-07-08 21:27:24 +00:00
|
|
|
let actionTrigger = element.dataset.trigger
|
2017-06-20 10:41:26 +00:00
|
|
|
let actionName = element.dataset.action
|
|
|
|
|
2017-07-08 21:27:24 +00:00
|
|
|
let oldAction = element["action assigned"]
|
|
|
|
|
|
|
|
if(oldAction) {
|
|
|
|
if(oldAction.trigger === actionTrigger && oldAction.action === actionName) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
element.removeEventListener(oldAction.trigger, oldAction.handler)
|
|
|
|
}
|
|
|
|
|
|
|
|
let actionHandler = e => {
|
2017-06-20 20:54:45 +00:00
|
|
|
actions[actionName](this, element, e)
|
2017-06-26 01:57:29 +00:00
|
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
e.preventDefault()
|
2017-07-08 21:27:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
element.addEventListener(actionTrigger, actionHandler)
|
2017-06-20 20:54:45 +00:00
|
|
|
|
2017-06-24 15:38:10 +00:00
|
|
|
// Use "action assigned" flag instead of removing the class.
|
|
|
|
// This will make sure that DOM diffs which restore the class name
|
|
|
|
// will not assign the action multiple times to the same element.
|
2017-07-08 21:27:24 +00:00
|
|
|
element["action assigned"] = {
|
|
|
|
trigger: actionTrigger,
|
|
|
|
action: actionName,
|
|
|
|
handler: actionHandler
|
|
|
|
}
|
2017-06-20 10:41:26 +00:00
|
|
|
}
|
2017-06-19 15:45:27 +00:00
|
|
|
}
|
|
|
|
|
2017-06-24 14:17:38 +00:00
|
|
|
lazyLoadImages() {
|
2017-06-24 15:38:10 +00:00
|
|
|
for(let element of findAll("lazy")) {
|
2017-06-24 14:17:38 +00:00
|
|
|
this.lazyLoadImage(element as HTMLImageElement)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
lazyLoadImage(img: HTMLImageElement) {
|
|
|
|
// Once the image becomes visible, load it
|
|
|
|
img["became visible"] = () => {
|
2017-07-08 13:40:13 +00:00
|
|
|
// Replace URL with WebP if supported
|
|
|
|
if(this.webpEnabled && img.dataset.webp) {
|
|
|
|
let dot = img.dataset.src.lastIndexOf(".")
|
|
|
|
img.src = img.dataset.src.substring(0, dot) + ".webp"
|
|
|
|
} else {
|
|
|
|
img.src = img.dataset.src
|
|
|
|
}
|
2017-06-19 15:45:27 +00:00
|
|
|
|
|
|
|
if(img.naturalWidth === 0) {
|
2017-07-05 19:06:38 +00:00
|
|
|
img.onload = () => {
|
|
|
|
this.imageFound.queue(img)
|
2017-06-19 15:45:27 +00:00
|
|
|
}
|
|
|
|
|
2017-07-05 19:06:38 +00:00
|
|
|
img.onerror = () => {
|
|
|
|
this.imageNotFound.queue(img)
|
2017-06-19 15:45:27 +00:00
|
|
|
}
|
|
|
|
} else {
|
2017-07-05 19:06:38 +00:00
|
|
|
this.imageFound.queue(img)
|
2017-06-19 15:45:27 +00:00
|
|
|
}
|
|
|
|
}
|
2017-06-24 14:17:38 +00:00
|
|
|
|
|
|
|
this.visibilityObserver.observe(img)
|
2017-06-19 15:45:27 +00:00
|
|
|
}
|
2017-06-20 12:16:23 +00:00
|
|
|
|
2017-06-26 01:57:29 +00:00
|
|
|
mountMountables() {
|
|
|
|
this.modifyDelayed("mountable", element => element.classList.add("mounted"))
|
|
|
|
}
|
|
|
|
|
|
|
|
unmountMountables() {
|
2017-06-26 10:32:07 +00:00
|
|
|
for(let element of findAll("mountable")) {
|
2017-07-03 15:21:00 +00:00
|
|
|
if(element.classList.contains("never-unmount")) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-07-05 19:06:38 +00:00
|
|
|
this.unmount.queue(element)
|
2017-06-26 01:57:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
modifyDelayed(className: string, func: (element: HTMLElement) => void) {
|
2017-07-06 01:24:56 +00:00
|
|
|
const maxDelay = 1000
|
2017-07-19 11:21:01 +00:00
|
|
|
const delay = 20
|
2017-06-20 18:13:04 +00:00
|
|
|
|
|
|
|
let time = 0
|
2017-07-05 13:29:18 +00:00
|
|
|
let start = Date.now()
|
2017-07-06 01:24:56 +00:00
|
|
|
let maxTime = start + maxDelay
|
2017-07-05 13:29:18 +00:00
|
|
|
|
2017-07-20 12:26:43 +00:00
|
|
|
let mountableTypes = new Map<string, number>()
|
|
|
|
let mountableTypeMutations = new Map<string, Array<any>>()
|
2017-06-20 18:13:04 +00:00
|
|
|
|
2017-07-19 11:21:01 +00:00
|
|
|
let collection = document.getElementsByClassName(className)
|
|
|
|
|
|
|
|
if(collection.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// let delay = Math.min(maxDelay / collection.length, 20)
|
|
|
|
|
2017-07-05 13:00:58 +00:00
|
|
|
for(let i = 0; i < collection.length; i++) {
|
|
|
|
let element = collection.item(i) as HTMLElement
|
2017-07-02 15:51:17 +00:00
|
|
|
let type = element.dataset.mountableType || "general"
|
|
|
|
|
2017-07-20 12:26:43 +00:00
|
|
|
if(mountableTypes.has(type)) {
|
|
|
|
time = mountableTypes.get(type) + delay
|
|
|
|
mountableTypes.set(type, time)
|
2017-07-02 15:51:17 +00:00
|
|
|
} else {
|
2017-07-20 12:26:43 +00:00
|
|
|
time = start
|
|
|
|
mountableTypes.set(type, time)
|
|
|
|
mountableTypeMutations.set(type, [])
|
2017-07-02 15:51:17 +00:00
|
|
|
}
|
2017-06-20 18:13:04 +00:00
|
|
|
|
2017-07-06 01:24:56 +00:00
|
|
|
if(time > maxTime) {
|
|
|
|
time = maxTime
|
|
|
|
}
|
|
|
|
|
2017-07-20 12:26:43 +00:00
|
|
|
mountableTypeMutations.get(type).push({
|
2017-07-05 13:29:18 +00:00
|
|
|
element,
|
|
|
|
time
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-07-20 12:26:43 +00:00
|
|
|
for(let mountableType of mountableTypeMutations.keys()) {
|
|
|
|
let mutations = mountableTypeMutations.get(mountableType)
|
|
|
|
let mutationIndex = 0
|
|
|
|
|
|
|
|
let updateBatch = () => {
|
|
|
|
let now = Date.now()
|
2017-07-05 13:29:18 +00:00
|
|
|
|
2017-07-20 12:26:43 +00:00
|
|
|
for(; mutationIndex < mutations.length; mutationIndex++) {
|
|
|
|
let mutation = mutations[mutationIndex]
|
2017-07-05 13:29:18 +00:00
|
|
|
|
2017-07-20 12:26:43 +00:00
|
|
|
if(mutation.time > now) {
|
|
|
|
break
|
|
|
|
}
|
2017-07-05 13:29:18 +00:00
|
|
|
|
2017-07-20 12:26:43 +00:00
|
|
|
func(mutation.element)
|
2017-07-05 13:29:18 +00:00
|
|
|
}
|
|
|
|
|
2017-07-20 12:26:43 +00:00
|
|
|
if(mutationIndex < mutations.length) {
|
|
|
|
window.requestAnimationFrame(updateBatch)
|
|
|
|
}
|
2017-07-05 13:29:18 +00:00
|
|
|
}
|
|
|
|
|
2017-07-20 12:26:43 +00:00
|
|
|
window.requestAnimationFrame(updateBatch)
|
2017-06-20 18:13:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-27 02:15:52 +00:00
|
|
|
diff(url: string) {
|
2017-07-13 23:50:10 +00:00
|
|
|
if(url === this.app.currentPath) {
|
2017-07-06 01:24:56 +00:00
|
|
|
return Promise.reject(null)
|
2017-07-03 15:21:00 +00:00
|
|
|
}
|
|
|
|
|
2017-07-19 04:32:31 +00:00
|
|
|
let path = "/_" + url
|
|
|
|
|
|
|
|
let request = fetch(path, {
|
2017-06-26 18:37:04 +00:00
|
|
|
credentials: "same-origin"
|
|
|
|
})
|
2017-07-14 02:58:21 +00:00
|
|
|
.then(response => {
|
|
|
|
return response.text()
|
|
|
|
})
|
2017-06-26 18:37:04 +00:00
|
|
|
|
2017-06-26 17:03:48 +00:00
|
|
|
history.pushState(url, null, url)
|
|
|
|
this.app.currentPath = url
|
|
|
|
this.app.markActiveLinks()
|
|
|
|
this.unmountMountables()
|
2017-06-26 18:37:04 +00:00
|
|
|
this.loading(true)
|
2017-06-26 17:03:48 +00:00
|
|
|
|
2017-06-29 14:49:26 +00:00
|
|
|
// Delay by transition-speed
|
|
|
|
return delay(300).then(() => {
|
2017-07-06 01:24:56 +00:00
|
|
|
return request
|
2017-06-26 17:03:48 +00:00
|
|
|
.then(html => this.app.setContent(html, true))
|
|
|
|
.then(() => this.app.markActiveLinks())
|
|
|
|
.then(() => this.app.emit("DOMContentLoaded"))
|
|
|
|
.then(() => this.loading(false))
|
|
|
|
.catch(console.error)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-07-21 08:10:48 +00:00
|
|
|
post(url, body) {
|
|
|
|
if(typeof body !== "string") {
|
|
|
|
body = JSON.stringify(body)
|
|
|
|
}
|
|
|
|
|
|
|
|
this.loading(true)
|
|
|
|
|
2017-06-27 02:15:52 +00:00
|
|
|
return fetch(url, {
|
|
|
|
method: "POST",
|
2017-07-21 08:10:48 +00:00
|
|
|
body,
|
2017-06-27 02:15:52 +00:00
|
|
|
credentials: "same-origin"
|
|
|
|
})
|
|
|
|
.then(response => response.text())
|
|
|
|
.then(body => {
|
2017-07-21 08:10:48 +00:00
|
|
|
this.loading(false)
|
|
|
|
|
2017-06-27 02:15:52 +00:00
|
|
|
if(body !== "ok") {
|
|
|
|
throw body
|
|
|
|
}
|
|
|
|
})
|
2017-07-21 08:10:48 +00:00
|
|
|
.catch(err => {
|
|
|
|
this.loading(false)
|
|
|
|
throw err
|
|
|
|
})
|
2017-06-27 02:15:52 +00:00
|
|
|
}
|
|
|
|
|
2017-07-06 14:54:10 +00:00
|
|
|
scrollTo(target: HTMLElement) {
|
|
|
|
const duration = 250.0
|
|
|
|
const steps = 60
|
|
|
|
const interval = duration / steps
|
|
|
|
const fullSin = Math.PI / 2
|
|
|
|
const contentPadding = 24
|
|
|
|
|
|
|
|
let scrollHandle: number
|
|
|
|
let oldScroll = this.app.content.parentElement.scrollTop
|
|
|
|
let newScroll = 0
|
|
|
|
let finalScroll = Math.max(target.offsetTop - contentPadding, 0)
|
|
|
|
let scrollDistance = finalScroll - oldScroll
|
|
|
|
let timeStart = Date.now()
|
|
|
|
let timeEnd = timeStart + duration
|
|
|
|
|
|
|
|
let scroll = () => {
|
|
|
|
let time = Date.now()
|
|
|
|
let progress = (time - timeStart) / duration
|
|
|
|
|
|
|
|
if(progress > 1.0) {
|
|
|
|
progress = 1.0
|
|
|
|
}
|
|
|
|
|
|
|
|
newScroll = oldScroll + scrollDistance * Math.sin(progress * fullSin)
|
|
|
|
this.app.content.parentElement.scrollTop = newScroll
|
|
|
|
|
|
|
|
if(time < timeEnd && newScroll != finalScroll) {
|
|
|
|
window.requestAnimationFrame(scroll)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
window.requestAnimationFrame(scroll)
|
|
|
|
}
|
|
|
|
|
|
|
|
findAPIEndpoint(element: HTMLElement) {
|
|
|
|
let apiObject: HTMLElement
|
|
|
|
let parent = element
|
|
|
|
|
|
|
|
while(parent = parent.parentElement) {
|
|
|
|
if(parent.dataset.api !== undefined) {
|
|
|
|
apiObject = parent
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!apiObject) {
|
|
|
|
throw "API object not found"
|
|
|
|
}
|
|
|
|
|
|
|
|
return apiObject.dataset.api
|
|
|
|
}
|
|
|
|
|
2017-06-21 12:00:52 +00:00
|
|
|
onPopState(e: PopStateEvent) {
|
|
|
|
if(e.state) {
|
|
|
|
this.app.load(e.state, {
|
|
|
|
addToHistory: false
|
|
|
|
})
|
|
|
|
} else if(this.app.currentPath !== this.app.originalPath) {
|
|
|
|
this.app.load(this.app.originalPath, {
|
|
|
|
addToHistory: false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onKeyDown(e: KeyboardEvent) {
|
2017-06-30 21:52:42 +00:00
|
|
|
let activeElement = document.activeElement
|
|
|
|
|
2017-06-24 14:41:22 +00:00
|
|
|
// Ignore hotkeys on input elements
|
2017-06-30 21:52:42 +00:00
|
|
|
switch(activeElement.tagName) {
|
2017-06-24 14:41:22 +00:00
|
|
|
case "INPUT":
|
|
|
|
case "TEXTAREA":
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-06-30 21:52:42 +00:00
|
|
|
// Disallow Enter key in contenteditables
|
|
|
|
if(activeElement.getAttribute("contenteditable") === "true" && e.keyCode == 13) {
|
|
|
|
if("blur" in activeElement) {
|
|
|
|
activeElement["blur"]()
|
|
|
|
}
|
|
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
e.stopPropagation()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-06-24 14:31:54 +00:00
|
|
|
// F = Search
|
2017-07-04 14:13:20 +00:00
|
|
|
if(e.keyCode == 70 && !e.ctrlKey) {
|
2017-06-21 12:00:52 +00:00
|
|
|
let search = this.app.find("search") as HTMLInputElement
|
|
|
|
|
|
|
|
search.focus()
|
|
|
|
search.select()
|
|
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
e.stopPropagation()
|
2017-06-30 21:52:42 +00:00
|
|
|
return
|
2017-06-21 12:00:52 +00:00
|
|
|
}
|
|
|
|
}
|
2017-06-19 14:49:24 +00:00
|
|
|
}
|