import {createApp} from 'vue' /** * Utility class used to handle Vue applications. It provides way to load * remote application and update history. */ export default class Builder { constructor(config={}) { this.config = config this.title = null this.app = null this.vm = null } /** * Fetch app from remote and mount application. */ fetch(url, {el='#app', ...options}={}) { return fetch(url, options).then(response => response.text()) .then(content => { let doc = new DOMParser().parseFromString(content, 'text/html') let app = doc.querySelector(el) content = app ? app.innerHTML : content return this.mount({content, title: doc.title, reset:true, url }) }) } /** * Mount application, using `create_app` if required. * * @param {String} options.content: replace app container content with it * @param {String} options.title: set DOM document title. * @param {String} [options.el=this.config.el]: mount application on this element (querySelector argument) * @param {Boolean} [reset=False]: if True, force application recreation. * @return `app.mount`'s result. */ mount({content=null, title=null, el=null, reset=false, props=null}={}) { try { this.unmount() let config = this.config if(el === null) el = config.el if(reset || !this.app) this.app = this.createApp({title,content,el,...config}, props) this.vm = this.app.mount(el) window.scroll(0, 0) return this.vm } catch(error) { this.unmount() throw error } } createApp({el, title=null, content=null, ...config}, props) { const container = document.querySelector(el) if(!container) return if(content) container.innerHTML = content if(title) document.title = title return createApp(config, props) } unmount() { this.app && this.app.unmount() this.app = null this.vm = null } /** * Enable hot reload: catch page change in order to fetch them and * load page without actually leaving current one. */ enableHotReload(node=null, historySave=true) { if(historySave) this.historySave(document.location, true) node.addEventListener('click', event => this.pageChanged(event), true) node.addEventListener('submit', event => this.pageChanged(event), true) node.addEventListener('popstate', event => this.statePopped(event), true) } 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')) 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.fetch(url, options).then(() => this.historySave(url)) event.preventDefault(); event.stopPropagation(); } statePopped(event) { const state = event.state if(state && state.content) // document.title = this.title; this.historyLoad(state); } /// Save application state into browser history historySave(url,replace=false) { const el = document.querySelector(this.config.el) const state = { content: el.innerHTML, title: document.title, } if(replace) history.replaceState(state, '', url) else history.pushState(state, '', url) } /// Load application from browser history's state historyLoad(state) { return this.mount({ content: state.content, title: state.title }) } }