|
@ -8039,6 +8039,10 @@ input.half-field:not(:active):not(:hover) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.blink {
|
.blink {
|
||||||
|
animation: 1s ease-in-out 2s infinite alternate blink;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
animation: 1s ease-in-out 3s infinite alternate blink;
|
animation: 1s ease-in-out 3s infinite alternate blink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -3,6 +3,7 @@ import components from './components'
|
||||||
|
|
||||||
import { Carousel, Pagination, Navigation, Slide } from 'vue3-carousel'
|
import { Carousel, Pagination, Navigation, Slide } from 'vue3-carousel'
|
||||||
|
|
||||||
|
|
||||||
const App = {
|
const App = {
|
||||||
el: '#app',
|
el: '#app',
|
||||||
delimiters: ['[[', ']]'],
|
delimiters: ['[[', ']]'],
|
||||||
|
@ -21,31 +22,6 @@ const App = {
|
||||||
computed: {
|
computed: {
|
||||||
player() { return window.aircox.player; },
|
player() { return window.aircox.player; },
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
carouselBreakpoints: {
|
|
||||||
400: {
|
|
||||||
itemsToShow: 1
|
|
||||||
},
|
|
||||||
600: {
|
|
||||||
itemsToShow: 1.75
|
|
||||||
},
|
|
||||||
800: {
|
|
||||||
itemsToShow: 3
|
|
||||||
},
|
|
||||||
1024: {
|
|
||||||
itemsToShow: 4
|
|
||||||
},
|
|
||||||
1280: {
|
|
||||||
itemsToShow: 4
|
|
||||||
},
|
|
||||||
1380: {
|
|
||||||
itemsToShow: 5
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlayerApp = {
|
export const PlayerApp = {
|
||||||
|
|
|
@ -1,144 +0,0 @@
|
||||||
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', historySave=true, ...options}={}) {
|
|
||||||
const fut = 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 })
|
|
||||||
})
|
|
||||||
if(historySave)
|
|
||||||
fut.then(() => this.historySave(url))
|
|
||||||
return fut
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
const app = createApp(config, props)
|
|
||||||
app.config.globalProperties.window = window
|
|
||||||
return app
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -109,6 +109,10 @@ input.half-field:not(:active):not(:hover) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.blink {
|
.blink {
|
||||||
|
animation: 1s ease-in-out 2s infinite alternate blink;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
animation: 1s ease-in-out 3s infinite alternate blink;
|
animation: 1s ease-in-out 3s infinite alternate blink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
|
|
||||||
//-- aircox
|
//-- aircox
|
||||||
import App, {PlayerApp} from './app'
|
import App, {PlayerApp} from './app'
|
||||||
import Builder from './appBuilder'
|
import VueLoader from './vueLoader'
|
||||||
import Sound from './sound'
|
import Sound from './sound'
|
||||||
import {Set} from './model'
|
import {Set} from './model'
|
||||||
|
|
||||||
|
@ -17,13 +17,13 @@ import './assets/styles.scss'
|
||||||
|
|
||||||
window.aircox = {
|
window.aircox = {
|
||||||
// main application
|
// main application
|
||||||
builder: new Builder(App),
|
loader: null,
|
||||||
get app() { return this.builder.app },
|
get app() { return this.loader.app },
|
||||||
|
|
||||||
// player application
|
// player application
|
||||||
playerBuilder: new Builder(PlayerApp),
|
playerLoader: null,
|
||||||
get playerApp() { return this.playerBuilder && this.playerBuilder.app },
|
get playerApp() { return this.playerLoader && this.playerLoader.app },
|
||||||
get player() { return this.playerBuilder.vm && this.playerBuilder.vm.$refs.player },
|
get player() { return this.playerLoader.vm && this.playerLoader.vm.$refs.player },
|
||||||
|
|
||||||
Set, Sound,
|
Set, Sound,
|
||||||
|
|
||||||
|
@ -31,27 +31,23 @@ window.aircox = {
|
||||||
/**
|
/**
|
||||||
* Initialize main application and player.
|
* Initialize main application and player.
|
||||||
*/
|
*/
|
||||||
init(props=null, {config=null, builder=null, initBuilder=true,
|
init(props=null, {hotReload=false, el=null,
|
||||||
initPlayer=true, hotReload=false, el=null}={})
|
config=null, playerConfig=null,
|
||||||
|
initApp=true, initPlayer=true,
|
||||||
|
loader=null, playerLoader=null}={})
|
||||||
{
|
{
|
||||||
if(initPlayer) {
|
if(initPlayer) {
|
||||||
let playerBuilder = this.playerBuilder
|
playerConfig = playerConfig || PlayerApp
|
||||||
playerBuilder.mount()
|
playerLoader = playerLoader || new VueLoader(playerConfig)
|
||||||
|
playerLoader.enable(false)
|
||||||
|
this.playerLoader = playerLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
if(initBuilder) {
|
if(initApp) {
|
||||||
builder = builder || this.builder
|
config = config || window.App || App
|
||||||
this.builder = builder
|
loader = loader || new VueLoader({el, props, ...config})
|
||||||
if(config || window.App)
|
loader.enable(hotReload)
|
||||||
builder.config = config || window.App
|
this.loader = loader
|
||||||
if(el)
|
|
||||||
builder.config.el = el
|
|
||||||
|
|
||||||
builder.title = document.title
|
|
||||||
builder.mount({props})
|
|
||||||
|
|
||||||
if(hotReload)
|
|
||||||
builder.enableHotReload(hotReload)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
174
assets/src/pageLoad.js
Normal file
174
assets/src/pageLoad.js
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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'))
|
||||||
|
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.historySave(url))
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
statePopped(event) {
|
||||||
|
const state = event.state
|
||||||
|
if(state && state.content)
|
||||||
|
this.mount({ content: state.content, title: state.title });
|
||||||
|
}
|
||||||
|
}
|
46
assets/src/vueLoader.js
Normal file
46
assets/src/vueLoader.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import {createApp} from 'vue'
|
||||||
|
|
||||||
|
import PageLoad from './pageLoad'
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles loading Vue js app on page load.
|
||||||
|
*/
|
||||||
|
export default class VueLoader {
|
||||||
|
constructor({el=null, props={}, ...appConfig}={}, loaderOptions={}) {
|
||||||
|
this.appConfig = appConfig;
|
||||||
|
this.props = props
|
||||||
|
this.pageLoad = new PageLoad(el, loaderOptions)
|
||||||
|
|
||||||
|
this.pageLoad.onPreMount = event => this.onPreMount(event)
|
||||||
|
this.pageLoad.onMount = event => this.onMount(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
enable(hotReload=true) {
|
||||||
|
hotReload && this.pageLoad.enable()
|
||||||
|
this.mount()
|
||||||
|
}
|
||||||
|
|
||||||
|
mount() {
|
||||||
|
if(this.app)
|
||||||
|
this.unmount()
|
||||||
|
|
||||||
|
const app = createApp(this.appConfig, this.props)
|
||||||
|
app.config.globalProperties.window = window
|
||||||
|
this.vm = app.mount(this.pageLoad.el)
|
||||||
|
this.app = app
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount() {
|
||||||
|
if(!this.app)
|
||||||
|
return
|
||||||
|
try { this.app.unmount() }
|
||||||
|
catch(_) { null }
|
||||||
|
this.app = null
|
||||||
|
this.vm = null
|
||||||
|
this.pageLoad.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
onPreMount() { this.unmount() }
|
||||||
|
onMount() { this.mount() }
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user