update assets dependencies; still work to be done to solve it all
This commit is contained in:
		@ -1,95 +1,25 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import AAutocomplete from './autocomplete'
 | 
			
		||||
import AEpisode from './episode'
 | 
			
		||||
import APlayer from './player'
 | 
			
		||||
import APlaylist from './playlist'
 | 
			
		||||
import ASoundItem from './soundItem'
 | 
			
		||||
 | 
			
		||||
export const defaultConfig = {
 | 
			
		||||
const App = {
 | 
			
		||||
    el: '#app',
 | 
			
		||||
    delimiters: ['[[', ']]'],
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
        player() { return window.aircox.player; },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    components: {AAutocomplete, AEpisode, APlayer, APlaylist, ASoundItem},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default class AppBuilder {
 | 
			
		||||
    constructor(config={}) {
 | 
			
		||||
        this._config = config;
 | 
			
		||||
        this.title = null;
 | 
			
		||||
        this.app = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get config() {
 | 
			
		||||
        let config = this._config instanceof Function ? this._config() : this._config;
 | 
			
		||||
        for(var k of new Set([...Object.keys(config || {}), ...Object.keys(defaultConfig)])) {
 | 
			
		||||
            if(!config[k] && defaultConfig[k])
 | 
			
		||||
                config[k] = defaultConfig[k]
 | 
			
		||||
            else if(config[k] instanceof Object)
 | 
			
		||||
                config[k] = {...defaultConfig[k], ...config[k]}
 | 
			
		||||
        }
 | 
			
		||||
        return config;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    set config(value) {
 | 
			
		||||
        this._config = value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
        self.app && self.app.$destroy();
 | 
			
		||||
        self.app = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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.getElementById('app');
 | 
			
		||||
                content = app ? app.innerHTML : content;
 | 
			
		||||
                return this.load({sync: true, content, title: doc.title, url })
 | 
			
		||||
            })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    load({async=false,content=null,title=null,el='app'}={}) {
 | 
			
		||||
        var self = this;
 | 
			
		||||
        return new Promise((resolve, reject) => {
 | 
			
		||||
            let func = () => {
 | 
			
		||||
                try {
 | 
			
		||||
                    let config = self.config;
 | 
			
		||||
                    const el = document.querySelector(config.el);
 | 
			
		||||
                    if(!el)
 | 
			
		||||
                        return reject(`Error: can't get element ${config.el}`)
 | 
			
		||||
 | 
			
		||||
                    if(content)
 | 
			
		||||
                        el.innerHTML = content
 | 
			
		||||
                    if(title)
 | 
			
		||||
                        document.title = title;
 | 
			
		||||
                    this.app = new Vue(config);
 | 
			
		||||
                    window.scroll(0, 0);
 | 
			
		||||
                    resolve(self.app)
 | 
			
		||||
                } catch(error) {
 | 
			
		||||
                    self.destroy();
 | 
			
		||||
                    reject(error)
 | 
			
		||||
                }};
 | 
			
		||||
            async ? window.addEventListener('load', func) : func();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// 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.load({ content: state.content, title: state.title });
 | 
			
		||||
    }
 | 
			
		||||
export const PlayerApp = {
 | 
			
		||||
    el: '#player',
 | 
			
		||||
    components: {APlayer},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default App
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										137
									
								
								assets/public/appBuilder.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								assets/public/appBuilder.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,137 @@
 | 
			
		||||
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.getElementById('app')
 | 
			
		||||
                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}={}) {
 | 
			
		||||
        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})
 | 
			
		||||
            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}) {
 | 
			
		||||
        const container = document.querySelector(el)
 | 
			
		||||
        if(!container)
 | 
			
		||||
            throw `Error: can't get element ${el}`
 | 
			
		||||
        if(content)
 | 
			
		||||
            container.innerHTML = content
 | 
			
		||||
        if(title)
 | 
			
		||||
            document.title = title
 | 
			
		||||
        return createApp(config)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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._onPageChange(event), true)
 | 
			
		||||
        node.addEventListener('submit', event => this._onPageChange(event), true)
 | 
			
		||||
        node.addEventListener('popstate', event => this._onPopState(event), true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _onPageChange(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');
 | 
			
		||||
        if(url===null || !(url === '' || url.startsWith('/') || url.startsWith('?')))
 | 
			
		||||
            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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _onPopState(event) {
 | 
			
		||||
        if(event.state && event.state.content)
 | 
			
		||||
            // document.title = this.title;
 | 
			
		||||
            this.historyLoad(event.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 })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,8 +12,7 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import debounce from 'lodash/debounce'
 | 
			
		||||
import {Autocomplete} from 'buefy/dist/components/autocomplete';
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import {Autocomplete} from 'buefy/dist/components/autocomplete'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    props: {
 | 
			
		||||
@ -31,25 +30,25 @@ export default {
 | 
			
		||||
            data: [],
 | 
			
		||||
            selected: null,
 | 
			
		||||
            isFetching: false,
 | 
			
		||||
        };
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        onSelect(option) {
 | 
			
		||||
            console.log('selected', option)
 | 
			
		||||
            Vue.set(this, 'selected', option);
 | 
			
		||||
            this.$emit('select', option);
 | 
			
		||||
            this.selected = option
 | 
			
		||||
            this.$emit('select', option)
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        fetch: debounce(function(query) {
 | 
			
		||||
            if(!query)
 | 
			
		||||
                return;
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            this.isFetching = true;
 | 
			
		||||
            this.isFetching = true
 | 
			
		||||
            this.model.fetchAll(this.url.replace('${query}', query))
 | 
			
		||||
                .then(data => {
 | 
			
		||||
                    this.data = data;
 | 
			
		||||
                    this.isFetching = false;
 | 
			
		||||
                    this.data = data
 | 
			
		||||
                    this.isFetching = false
 | 
			
		||||
                }, data => { this.isFetching = false; Promise.reject(data) })
 | 
			
		||||
        }),
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -3,93 +3,37 @@
 | 
			
		||||
 * administration interface)
 | 
			
		||||
 */
 | 
			
		||||
//-- vendor
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
import '@fortawesome/fontawesome-free/css/all.min.css';
 | 
			
		||||
import '@fortawesome/fontawesome-free/css/fontawesome.min.css';
 | 
			
		||||
import '@fortawesome/fontawesome-free/css/all.min.css'
 | 
			
		||||
import '@fortawesome/fontawesome-free/css/fontawesome.min.css'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
//-- aircox
 | 
			
		||||
import AppBuilder from './app';
 | 
			
		||||
import Sound from './sound';
 | 
			
		||||
import {Set} from './model';
 | 
			
		||||
 | 
			
		||||
import './styles.scss';
 | 
			
		||||
 | 
			
		||||
import Autocomplete from './autocomplete';
 | 
			
		||||
import Episode from './episode';
 | 
			
		||||
import Player from './player';
 | 
			
		||||
import Playlist from './playlist';
 | 
			
		||||
import SoundItem from './soundItem';
 | 
			
		||||
 | 
			
		||||
Vue.component('a-autocomplete', Autocomplete)
 | 
			
		||||
Vue.component('a-episode', Episode)
 | 
			
		||||
Vue.component('a-player', Player)
 | 
			
		||||
Vue.component('a-playlist', Playlist)
 | 
			
		||||
Vue.component('a-sound-item', SoundItem)
 | 
			
		||||
import App, {PlayerApp} from './app'
 | 
			
		||||
import Builder from './appBuilder'
 | 
			
		||||
import Sound from './sound'
 | 
			
		||||
import {Set} from './model'
 | 
			
		||||
 | 
			
		||||
import './styles.scss'
 | 
			
		||||
 | 
			
		||||
window.aircox = {
 | 
			
		||||
    // main application
 | 
			
		||||
    appBuilder: null,
 | 
			
		||||
    appConfig: {},
 | 
			
		||||
    get app() { return this.appBuilder.app  },
 | 
			
		||||
    builder: new Builder(App),
 | 
			
		||||
    get app() { return this.builder.app  },
 | 
			
		||||
 | 
			
		||||
    // player application
 | 
			
		||||
    playerBuilder: null,
 | 
			
		||||
    playerBuilder: new Builder(PlayerApp),
 | 
			
		||||
    get playerApp() { return this.playerBuilder && this.playerBuilder.app },
 | 
			
		||||
    get player() { return this.playerApp && this.playerApp.$refs.player },
 | 
			
		||||
 | 
			
		||||
    // Handle hot-reload (link click and form submits).
 | 
			
		||||
    onPageFetch(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');
 | 
			
		||||
        if(url===null || !(url === '' || url.startsWith('/') || url.startsWith('?')))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        let options = {};
 | 
			
		||||
        if(submit) {
 | 
			
		||||
            let formData = new FormData(event.target);
 | 
			
		||||
            if(target.method == 'get')
 | 
			
		||||
                url += '?' + (new URLSearchParams(formData)).toString();
 | 
			
		||||
            else {
 | 
			
		||||
                options['method'] = target.method;
 | 
			
		||||
                options['body'] = formData;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        this.appBuilder.fetch(url, options).then(app => {
 | 
			
		||||
            this.appBuilder.historySave(url);
 | 
			
		||||
        });
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
    },
 | 
			
		||||
    get player() { return this.playerBuilder.vm && this.playerBuilder.vm.$refs.player },
 | 
			
		||||
 | 
			
		||||
    Set: Set, Sound: Sound,
 | 
			
		||||
};
 | 
			
		||||
window.Vue = Vue;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.addEventListener('load', e => {
 | 
			
		||||
    const [app, player] = [aircox.builder, aircox.playerBuilder]
 | 
			
		||||
    app.title = document.title
 | 
			
		||||
    app.mount()
 | 
			
		||||
    app.enableHotReload(window)
 | 
			
		||||
 | 
			
		||||
aircox.playerBuilder = new AppBuilder({el: '#player'});
 | 
			
		||||
aircox.playerBuilder.load({async:true});
 | 
			
		||||
aircox.appBuilder = new AppBuilder(x => window.aircox.appConfig);
 | 
			
		||||
aircox.appBuilder.load({async:true}).then(app => {
 | 
			
		||||
    aircox.appBuilder.historySave(document.location, true);
 | 
			
		||||
 | 
			
		||||
    //-- load page hooks
 | 
			
		||||
    window.addEventListener('click', event => aircox.onPageFetch(event), true);
 | 
			
		||||
    window.addEventListener('submit', event => aircox.onPageFetch(event), true);
 | 
			
		||||
    window.addEventListener('popstate', event => {
 | 
			
		||||
        if(event.state && event.state.content) {
 | 
			
		||||
            document.title = aircox.appBuilder.title;
 | 
			
		||||
            aircox.appBuilder.historyLoad(event.state);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    player.mount()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
function getCookie(name) {
 | 
			
		||||
    if(document.cookie && document.cookie !== '') {
 | 
			
		||||
@ -82,7 +81,7 @@ export default class Model {
 | 
			
		||||
     */
 | 
			
		||||
    commit(data) {
 | 
			
		||||
        this.id = this.constructor.getId(data);
 | 
			
		||||
        Vue.set(this, 'data', data);
 | 
			
		||||
        this.data = data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -123,12 +122,12 @@ export class Set {
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch multiple items from server
 | 
			
		||||
     */
 | 
			
		||||
    static fetch(url, options=null, args=null) {
 | 
			
		||||
    static fetch(model, url, options=null, args=null) {
 | 
			
		||||
        options = this.getOptions(options)
 | 
			
		||||
        return fetch(url, options)
 | 
			
		||||
            .then(response => response.json())
 | 
			
		||||
            .then(data => (data instanceof Array ? data : data.results)
 | 
			
		||||
                              .map(d => new this.model(d, {url: url, ...args})))
 | 
			
		||||
                              .map(d => new model(d, {url: url, ...args})))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user