forked from rc/aircox
180 lines
5.7 KiB
JavaScript
180 lines
5.7 KiB
JavaScript
|
|
/**
|
|
* Load page without leaving current one (hot-reload).
|
|
*/
|
|
export default class PageLoad {
|
|
constructor(el, {loadingClass="loading", append=false}={}) {
|
|
this.el = el
|
|
this.append = append
|
|
this.loadingClass = loadingClass
|
|
}
|
|
|
|
get target() {
|
|
if(!this._target)
|
|
this._target = document.querySelector(this.el)
|
|
return this._target
|
|
}
|
|
|
|
reset() {
|
|
this._target = null
|
|
}
|
|
|
|
/**
|
|
* Enable hot reload: catch page change in order to fetch them and
|
|
* load page without actually leaving current one.
|
|
*/
|
|
enable(target=null) {
|
|
if(this._pageChanged)
|
|
throw "Already enabled, please disable me"
|
|
|
|
if(!target)
|
|
target = this.target || document.body
|
|
this.historySave(document.location, true)
|
|
|
|
this._pageChanged = event => this.pageChanged(event)
|
|
this._statePopped = event => this.statePopped(event)
|
|
|
|
target.addEventListener('click', this._pageChanged, true)
|
|
target.addEventListener('submit', this._pageChanged, true)
|
|
window.addEventListener('popstate', this._statePopped, true)
|
|
}
|
|
|
|
/**
|
|
* Disable hot reload, remove listeners.
|
|
*/
|
|
disable() {
|
|
this.target.removeEventListener('click', this._pageChanged, true)
|
|
this.target.removeEventListener('submit', this._pageChanged, true)
|
|
window.removeEventListener('popstate', this._statePopped, true)
|
|
|
|
this._pageChanged = null
|
|
this._statePopped = null
|
|
}
|
|
|
|
/**
|
|
* Fetch url, return promise, similar to standard Fetch API.
|
|
* Default implementation just forward argument to it.
|
|
*/
|
|
fetch(url, options) {
|
|
return fetch(url, options)
|
|
}
|
|
|
|
/**
|
|
* Fetch app from remote and mount application.
|
|
*/
|
|
load(url, {mount=true, scroll=[0,0], ...options}={}) {
|
|
if(this.loadingClass)
|
|
this.target.classList.add(this.loadingClass)
|
|
|
|
if(this.onLoad)
|
|
this.onLoad({url, el: this.el, options})
|
|
if(scroll)
|
|
window.scroll(...scroll)
|
|
return this.fetch(url, options).then(response => response.text())
|
|
.then(content => {
|
|
if(this.loadingClass)
|
|
this.target.classList.remove(this.loadingClass)
|
|
|
|
var doc = new DOMParser().parseFromString(content, 'text/html')
|
|
var dom = doc.querySelectorAll(this.el)
|
|
var result = {url,
|
|
content: dom || [document.createTextNode(content)],
|
|
title: doc.title,
|
|
append: this.append}
|
|
mount && this.mount(result)
|
|
return result
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Mount the page on provided target element
|
|
*/
|
|
mount({content, title=null, ...options}={}) {
|
|
if(this.onPreMount)
|
|
this.onPreMount({target: this.target, content, items, title})
|
|
var items = null;
|
|
if(content)
|
|
items = this.mountContent(content, options)
|
|
if(title)
|
|
document.title = title
|
|
if(this.onMount)
|
|
this.onMount({target: this.target, content, items, title})
|
|
}
|
|
|
|
/**
|
|
* Mount page content
|
|
*/
|
|
mountContent(content, {append=false}={}) {
|
|
if(typeof content == "string") {
|
|
this.target.innerHTML = append ? this.target.innerHTML + content
|
|
: content;
|
|
// TODO
|
|
return []
|
|
}
|
|
|
|
if(!append)
|
|
this.target.innerHTML = ""
|
|
|
|
var fragment = document.createDocumentFragment()
|
|
var items = []
|
|
for(var node of content)
|
|
while(node.firstChild) {
|
|
items.push(node.firstChild)
|
|
fragment.appendChild(node.firstChild)
|
|
}
|
|
this.target.append(fragment)
|
|
return items
|
|
}
|
|
|
|
/// Save application state into browser history
|
|
historySave(url,replace=false) {
|
|
const state = { content: this.target.innerHTML,
|
|
title: document.title, }
|
|
if(replace)
|
|
history.replaceState(state, '', url)
|
|
else
|
|
history.pushState(state, '', url)
|
|
}
|
|
|
|
dispatchPageLoaded(url) {
|
|
var evt = new CustomEvent("pageLoaded", {detail: url})
|
|
document.dispatchEvent(evt)
|
|
}
|
|
|
|
// --- events
|
|
pageChanged(event) {
|
|
let submit = event.type == 'submit';
|
|
let target = submit || event.target.tagName == 'A'
|
|
? event.target : event.target.closest('a');
|
|
if(!target || target.hasAttribute('target') || (target.dataset && target.dataset.forceReload))
|
|
return;
|
|
|
|
let url = submit ? target.getAttribute('action') || ''
|
|
: target.getAttribute('href');
|
|
let domain = window.location.protocol + '//' + window.location.hostname
|
|
let stay = (url === '' || url.startsWith('/') || url.startsWith('?') ||
|
|
url.startsWith(domain)) && url.indexOf('wp-admin') == -1
|
|
if(url===null || !stay) {
|
|
return;
|
|
}
|
|
|
|
let options = {};
|
|
if(submit) {
|
|
let formData = new FormData(event.target);
|
|
if(target.method == 'get')
|
|
url += '?' + (new URLSearchParams(formData)).toString();
|
|
else
|
|
options = {...options, method: target.method, body: formData}
|
|
}
|
|
this.load(url, options).then(() => this.dispatchPageLoaded(url)).then(() => this.historySave(url))
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
statePopped(event) {
|
|
const state = event.state
|
|
if(state && state.content)
|
|
this.mount({ content: state.content, title: state.title });
|
|
}
|
|
}
|