294 lines
6.6 KiB
TypeScript
Raw Normal View History

2017-06-19 14:49:24 +00:00
import { Application } from "./Application"
2017-06-21 16:44:20 +00:00
import { Diff } from "./Diff"
2017-06-26 17:03:48 +00:00
import { findAll, delay } from "./utils"
2017-06-20 10:41:26 +00:00
import * as actions from "./actions"
2017-06-19 14:49:24 +00:00
2017-06-29 06:32:46 +00:00
var monthNames = [
"January", "February", "March",
"April", "May", "June", "July",
"August", "September", "October",
"November", "December"
]
var dayNames = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
]
2017-06-19 14:49:24 +00:00
export class AnimeNotifier {
app: Application
2017-06-24 14:17:38 +00:00
visibilityObserver: IntersectionObserver
2017-06-19 14:49:24 +00:00
constructor(app: Application) {
this.app = app
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
}
},
{}
)
} 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-19 15:45:27 +00:00
onReadyStateChange() {
if(document.readyState !== "interactive") {
return
}
this.run()
}
2017-06-19 14:49:24 +00:00
run() {
this.app.content = this.app.find("content")
this.app.loading = this.app.find("loading")
this.app.run()
}
2017-06-19 15:45:27 +00:00
2017-06-24 14:17:38 +00:00
onContentLoaded() {
this.visibilityObserver.disconnect()
// Update each of these asynchronously
2017-06-26 01:57:29 +00:00
Promise.resolve().then(() => this.mountMountables())
Promise.resolve().then(() => this.assignActions())
2017-06-24 14:17:38 +00:00
Promise.resolve().then(() => this.lazyLoadImages())
2017-06-29 06:32:46 +00:00
Promise.resolve().then(() => this.displayLocalDates())
}
displayLocalDates() {
const oneDay = 24 * 60 * 60 * 1000
const now = new Date()
for(let element of findAll("utc-date")) {
let startDate = new Date(element.dataset.startDate)
let endDate = new Date(element.dataset.endDate)
let h = startDate.getHours()
let m = startDate.getMinutes()
let startTime = (h <= 9 ? "0" + h : h) + ":" + (m <= 9 ? "0" + m : m)
h = endDate.getHours()
m = endDate.getMinutes()
let endTime = (h <= 9 ? "0" + h : h) + ":" + (m <= 9 ? "0" + m : m)
2017-06-29 07:02:06 +00:00
let dayDifference = Math.round((startDate.getTime() - now.getTime()) / oneDay)
2017-06-29 06:32:46 +00:00
let dayInfo = dayNames[startDate.getDay()] + ", " + monthNames[startDate.getMonth()] + " " + startDate.getDate()
2017-06-29 07:02:06 +00:00
let airingVerb = "will be airing"
switch(dayDifference) {
case 0:
element.innerText = "Today"
case 1:
element.innerText = "Tomorrow"
case -1:
element.innerText = "Yesterday"
default:
let text = Math.abs(dayDifference) + " days"
if(dayDifference < 0) {
text += " ago"
airingVerb = "aired"
} else {
element.innerText = text
}
2017-06-29 06:32:46 +00:00
}
2017-06-29 07:02:06 +00:00
element.title = "Episode " + element.dataset.episodeNumber + " " + airingVerb + " " + startTime + " - " + endTime + " your time"
2017-06-29 06:32:46 +00:00
}
2017-06-24 14:17:38 +00:00
}
2017-06-21 16:44:20 +00:00
reloadContent() {
return fetch("/_" + this.app.currentPath, {
credentials: "same-origin"
})
.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-06-20 10:41:26 +00:00
loading(isLoading: boolean) {
if(isLoading) {
this.app.loading.classList.remove(this.app.fadeOutClass)
} else {
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-06-24 00:10:04 +00:00
if(element["action assigned"]) {
continue
}
2017-06-20 10:41:26 +00:00
let actionName = element.dataset.action
2017-06-20 20:54:45 +00:00
element.addEventListener(element.dataset.trigger, e => {
actions[actionName](this, element, e)
2017-06-26 01:57:29 +00:00
e.stopPropagation()
e.preventDefault()
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-06-24 00:10:04 +00:00
element["action assigned"] = true
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-06-24 15:38:10 +00:00
img.src = img.dataset.src
2017-06-19 15:45:27 +00:00
if(img.naturalWidth === 0) {
img.onload = function() {
2017-06-24 15:38:10 +00:00
this.classList.add("image-found")
2017-06-19 15:45:27 +00:00
}
img.onerror = function() {
2017-06-24 15:38:10 +00:00
this.classList.add("image-not-found")
2017-06-19 15:45:27 +00:00
}
} else {
2017-06-24 15:38:10 +00:00
img.classList.add("image-found")
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-06-26 01:57:29 +00:00
element.classList.remove("mounted")
}
}
modifyDelayed(className: string, func: (element: HTMLElement) => void) {
2017-06-20 18:13:04 +00:00
const delay = 20
2017-06-26 13:17:53 +00:00
const maxDelay = 500
2017-06-20 18:13:04 +00:00
let time = 0
2017-06-26 01:57:29 +00:00
for(let element of findAll(className)) {
2017-06-20 18:13:04 +00:00
time += delay
if(time > maxDelay) {
2017-06-26 13:17:53 +00:00
func(element)
} else {
setTimeout(() => {
window.requestAnimationFrame(() => func(element))
}, time)
2017-06-20 18:13:04 +00:00
}
}
}
2017-06-27 02:15:52 +00:00
diff(url: string) {
let request = fetch("/_" + url, {
credentials: "same-origin"
})
.then(response => response.text())
2017-06-26 17:03:48 +00:00
history.pushState(url, null, url)
this.app.currentPath = url
this.app.markActiveLinks()
this.unmountMountables()
this.loading(true)
2017-06-26 17:03:48 +00:00
2017-06-26 21:04:15 +00:00
return delay(300).then(() => {
2017-06-26 17:03:48 +00:00
request
.then(html => this.app.setContent(html, true))
.then(() => this.app.markActiveLinks())
.then(() => this.app.emit("DOMContentLoaded"))
.then(() => this.loading(false))
.catch(console.error)
})
}
2017-06-27 02:15:52 +00:00
post(url, obj) {
return fetch(url, {
method: "POST",
body: JSON.stringify(obj),
credentials: "same-origin"
})
.then(response => response.text())
.then(body => {
if(body !== "ok") {
throw body
}
})
}
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-24 14:41:22 +00:00
// Ignore hotkeys on input elements
switch(document.activeElement.tagName) {
case "INPUT":
case "TEXTAREA":
return
}
2017-06-24 14:31:54 +00:00
// F = Search
if(e.keyCode == 70) {
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-20 12:16:23 +00:00
// onResize(e: UIEvent) {
// let hasScrollbar = this.app.content.clientHeight === this.app.content.scrollHeight
// if(hasScrollbar) {
// this.app.content.classList.add("has-scrollbar")
// } else {
// this.app.content.classList.remove("has-scrollbar")
// }
// }
2017-06-19 14:49:24 +00:00
}