/** * 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 }); } }