diff --git a/aircox/static/aircox/css/chunk-common.css b/aircox/static/aircox/css/chunk-common.css index 52d2c41..239ea85 100644 --- a/aircox/static/aircox/css/chunk-common.css +++ b/aircox/static/aircox/css/chunk-common.css @@ -8039,6 +8039,10 @@ input.half-field:not(:active):not(:hover) { } } .blink { + animation: 1s ease-in-out 2s infinite alternate blink; +} + +.loading { animation: 1s ease-in-out 3s infinite alternate blink; } diff --git a/aircox/static/aircox/js/chunk-common.js b/aircox/static/aircox/js/chunk-common.js index 76bf117..6a7a249 100644 --- a/aircox/static/aircox/js/chunk-common.js +++ b/aircox/static/aircox/js/chunk-common.js @@ -335,17 +335,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac \********************/ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ PlayerApp: function() { return /* binding */ PlayerApp; }\n/* harmony export */ });\n/* harmony import */ var v_calendar__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! v-calendar */ \"./node_modules/v-calendar/dist/es/index.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n/* harmony import */ var vue3_carousel__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue3-carousel */ \"./node_modules/vue3-carousel/dist/carousel.es.js\");\n\n\n\nconst App = {\n el: '#app',\n delimiters: ['[[', ']]'],\n components: {\n ..._components__WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n Slide: vue3_carousel__WEBPACK_IMPORTED_MODULE_2__.Slide,\n Carousel: vue3_carousel__WEBPACK_IMPORTED_MODULE_2__.Carousel,\n Pagination: vue3_carousel__WEBPACK_IMPORTED_MODULE_2__.Pagination,\n Navigation: vue3_carousel__WEBPACK_IMPORTED_MODULE_2__.Navigation,\n ...{\n VCalendar: v_calendar__WEBPACK_IMPORTED_MODULE_0__.Calendar,\n VDatepicker: v_calendar__WEBPACK_IMPORTED_MODULE_0__.DatePicker\n }\n },\n computed: {\n player() {\n return window.aircox.player;\n }\n },\n data() {\n return {\n carouselBreakpoints: {\n 400: {\n itemsToShow: 1\n },\n 600: {\n itemsToShow: 1.75\n },\n 800: {\n itemsToShow: 3\n },\n 1024: {\n itemsToShow: 4\n },\n 1280: {\n itemsToShow: 4\n },\n 1380: {\n itemsToShow: 5\n }\n }\n };\n }\n};\nconst PlayerApp = {\n el: '#player',\n delimiters: ['[[', ']]'],\n components: {\n ..._components__WEBPACK_IMPORTED_MODULE_1__[\"default\"]\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (App);\n\n//# sourceURL=webpack://aircox-assets/./src/app.js?"); - -/***/ }), - -/***/ "./src/appBuilder.js": -/*!***************************!*\ - !*** ./src/appBuilder.js ***! - \***************************/ -/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { - -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* binding */ Builder; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-bundler.js\");\n\n\n/**\n * Utility class used to handle Vue applications. It provides way to load\n * remote application and update history.\n */\nclass Builder {\n constructor(config = {}) {\n this.config = config;\n this.title = null;\n this.app = null;\n this.vm = null;\n }\n\n /**\n * Fetch app from remote and mount application.\n */\n fetch(url, {\n el = '#app',\n historySave = true,\n ...options\n } = {}) {\n const fut = fetch(url, options).then(response => response.text()).then(content => {\n let doc = new DOMParser().parseFromString(content, 'text/html');\n let app = doc.querySelector(el);\n content = app ? app.innerHTML : content;\n return this.mount({\n content,\n title: doc.title,\n reset: true,\n url\n });\n });\n if (historySave) fut.then(() => this.historySave(url));\n return fut;\n }\n\n /**\n * Mount application, using `create_app` if required.\n *\n * @param {String} options.content: replace app container content with it\n * @param {String} options.title: set DOM document title.\n * @param {String} [options.el=this.config.el]: mount application on this element (querySelector argument)\n * @param {Boolean} [reset=False]: if True, force application recreation.\n * @return `app.mount`'s result.\n */\n mount({\n content = null,\n title = null,\n el = null,\n reset = false,\n props = null\n } = {}) {\n try {\n this.unmount();\n let config = this.config;\n if (el === null) el = config.el;\n if (reset || !this.app) this.app = this.createApp({\n title,\n content,\n el,\n ...config\n }, props);\n this.vm = this.app.mount(el);\n window.scroll(0, 0);\n return this.vm;\n } catch (error) {\n this.unmount();\n throw error;\n }\n }\n createApp({\n el,\n title = null,\n content = null,\n ...config\n }, props) {\n const container = document.querySelector(el);\n if (!container) return;\n if (content) container.innerHTML = content;\n if (title) document.title = title;\n const app = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createApp)(config, props);\n app.config.globalProperties.window = window;\n return app;\n }\n unmount() {\n this.app && this.app.unmount();\n this.app = null;\n this.vm = null;\n }\n\n /**\n * Enable hot reload: catch page change in order to fetch them and\n * load page without actually leaving current one.\n */\n enableHotReload(node = null, historySave = true) {\n if (historySave) this.historySave(document.location, true);\n node.addEventListener('click', event => this.pageChanged(event), true);\n node.addEventListener('submit', event => this.pageChanged(event), true);\n node.addEventListener('popstate', event => this.statePopped(event), true);\n }\n pageChanged(event) {\n let submit = event.type == 'submit';\n let target = submit || event.target.tagName == 'A' ? event.target : event.target.closest('a');\n if (!target || target.hasAttribute('target')) return;\n let url = submit ? target.getAttribute('action') || '' : target.getAttribute('href');\n let domain = window.location.protocol + '//' + window.location.hostname;\n let stay = (url === '' || url.startsWith('/') || url.startsWith('?') || url.startsWith(domain)) && url.indexOf('wp-admin') == -1;\n if (url === null || !stay) {\n return;\n }\n let options = {};\n if (submit) {\n let formData = new FormData(event.target);\n if (target.method == 'get') url += '?' + new URLSearchParams(formData).toString();else options = {\n ...options,\n method: target.method,\n body: formData\n };\n }\n this.fetch(url, options);\n event.preventDefault();\n event.stopPropagation();\n }\n statePopped(event) {\n const state = event.state;\n if (state && state.content)\n // document.title = this.title;\n this.historyLoad(state);\n }\n\n /// Save application state into browser history\n historySave(url, replace = false) {\n const el = document.querySelector(this.config.el);\n const state = {\n content: el.innerHTML,\n title: document.title\n };\n if (replace) history.replaceState(state, '', url);else history.pushState(state, '', url);\n }\n\n /// Load application from browser history's state\n historyLoad(state) {\n return this.mount({\n content: state.content,\n title: state.title\n });\n }\n}\n\n//# sourceURL=webpack://aircox-assets/./src/appBuilder.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ PlayerApp: function() { return /* binding */ PlayerApp; }\n/* harmony export */ });\n/* harmony import */ var v_calendar__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! v-calendar */ \"./node_modules/v-calendar/dist/es/index.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n/* harmony import */ var vue3_carousel__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue3-carousel */ \"./node_modules/vue3-carousel/dist/carousel.es.js\");\n\n\n\nconst App = {\n el: '#app',\n delimiters: ['[[', ']]'],\n components: {\n ..._components__WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n Slide: vue3_carousel__WEBPACK_IMPORTED_MODULE_2__.Slide,\n Carousel: vue3_carousel__WEBPACK_IMPORTED_MODULE_2__.Carousel,\n Pagination: vue3_carousel__WEBPACK_IMPORTED_MODULE_2__.Pagination,\n Navigation: vue3_carousel__WEBPACK_IMPORTED_MODULE_2__.Navigation,\n ...{\n VCalendar: v_calendar__WEBPACK_IMPORTED_MODULE_0__.Calendar,\n VDatepicker: v_calendar__WEBPACK_IMPORTED_MODULE_0__.DatePicker\n }\n },\n computed: {\n player() {\n return window.aircox.player;\n }\n }\n};\nconst PlayerApp = {\n el: '#player',\n delimiters: ['[[', ']]'],\n components: {\n ..._components__WEBPACK_IMPORTED_MODULE_1__[\"default\"]\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (App);\n\n//# sourceURL=webpack://aircox-assets/./src/app.js?"); /***/ }), @@ -365,7 +355,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac \**********************/ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _appBuilder__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./appBuilder */ \"./src/appBuilder.js\");\n/* harmony import */ var _sound__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./sound */ \"./src/sound.js\");\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./model */ \"./src/model.js\");\n/* harmony import */ var _assets_styles_scss__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./assets/styles.scss */ \"./src/assets/styles.scss\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n\n//-- aircox\n\n\n\n\n\nwindow.aircox = {\n // main application\n builder: new _appBuilder__WEBPACK_IMPORTED_MODULE_2__[\"default\"](_app__WEBPACK_IMPORTED_MODULE_1__[\"default\"]),\n get app() {\n return this.builder.app;\n },\n // player application\n playerBuilder: new _appBuilder__WEBPACK_IMPORTED_MODULE_2__[\"default\"](_app__WEBPACK_IMPORTED_MODULE_1__.PlayerApp),\n get playerApp() {\n return this.playerBuilder && this.playerBuilder.app;\n },\n get player() {\n return this.playerBuilder.vm && this.playerBuilder.vm.$refs.player;\n },\n Set: _model__WEBPACK_IMPORTED_MODULE_4__.Set,\n Sound: _sound__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n /**\n * Initialize main application and player.\n */\n init(props = null, {\n config = null,\n builder = null,\n initBuilder = true,\n initPlayer = true,\n hotReload = false,\n el = null\n } = {}) {\n if (initPlayer) {\n let playerBuilder = this.playerBuilder;\n playerBuilder.mount();\n }\n if (initBuilder) {\n builder = builder || this.builder;\n this.builder = builder;\n if (config || window.App) builder.config = config || window.App;\n if (el) builder.config.el = el;\n builder.title = document.title;\n builder.mount({\n props\n });\n if (hotReload) builder.enableHotReload(hotReload);\n }\n },\n /**\n * Filter navbar dropdown menu items\n */\n filter_menu(event) {\n var filter = new RegExp(event.target.value, 'gi');\n var container = event.target.closest('.navbar-dropdown');\n if (event.target.value) for (let item of container.querySelectorAll('a.navbar-item')) item.style.display = item.innerHTML.search(filter) == -1 ? 'none' : null;else for (let item of container.querySelectorAll('a.navbar-item')) item.style.display = null;\n },\n pickDate(url, date) {\n url = `${url}?date=${date.id}`;\n this.builder.fetch(url);\n }\n};\n\n//# sourceURL=webpack://aircox-assets/./src/index.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _vueLoader__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./vueLoader */ \"./src/vueLoader.js\");\n/* harmony import */ var _sound__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./sound */ \"./src/sound.js\");\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./model */ \"./src/model.js\");\n/* harmony import */ var _assets_styles_scss__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./assets/styles.scss */ \"./src/assets/styles.scss\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n\n//-- aircox\n\n\n\n\n\nwindow.aircox = {\n // main application\n loader: null,\n get app() {\n return this.loader.app;\n },\n // player application\n playerLoader: null,\n get playerApp() {\n return this.playerLoader && this.playerLoader.app;\n },\n get player() {\n return this.playerLoader.vm && this.playerLoader.vm.$refs.player;\n },\n Set: _model__WEBPACK_IMPORTED_MODULE_4__.Set,\n Sound: _sound__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n /**\n * Initialize main application and player.\n */\n init(props = null, {\n hotReload = false,\n el = null,\n config = null,\n playerConfig = null,\n initApp = true,\n initPlayer = true,\n loader = null,\n playerLoader = null\n } = {}) {\n if (initPlayer) {\n playerConfig = playerConfig || _app__WEBPACK_IMPORTED_MODULE_1__.PlayerApp;\n playerLoader = playerLoader || new _vueLoader__WEBPACK_IMPORTED_MODULE_2__[\"default\"](playerConfig);\n playerLoader.enable(false);\n this.playerLoader = playerLoader;\n }\n if (initApp) {\n config = config || window.App || _app__WEBPACK_IMPORTED_MODULE_1__[\"default\"];\n loader = loader || new _vueLoader__WEBPACK_IMPORTED_MODULE_2__[\"default\"]({\n el,\n props,\n ...config\n });\n loader.enable(hotReload);\n this.loader = loader;\n }\n },\n /**\n * Filter navbar dropdown menu items\n */\n filter_menu(event) {\n var filter = new RegExp(event.target.value, 'gi');\n var container = event.target.closest('.navbar-dropdown');\n if (event.target.value) for (let item of container.querySelectorAll('a.navbar-item')) item.style.display = item.innerHTML.search(filter) == -1 ? 'none' : null;else for (let item of container.querySelectorAll('a.navbar-item')) item.style.display = null;\n },\n pickDate(url, date) {\n url = `${url}?date=${date.id}`;\n this.builder.fetch(url);\n }\n};\n\n//# sourceURL=webpack://aircox-assets/./src/index.js?"); /***/ }), @@ -389,6 +379,16 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ }), +/***/ "./src/pageLoad.js": +/*!*************************!*\ + !*** ./src/pageLoad.js ***! + \*************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* binding */ PageLoad; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.push.js */ \"./node_modules/core-js/modules/es.array.push.js\");\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0__);\n\n/**\n * Load page without leaving current one (hot-reload).\n */\nclass PageLoad {\n constructor(el, {\n loadingClass = \"loading\",\n append = false\n } = {}) {\n this.el = el;\n this.append = append;\n this.loadingClass = loadingClass;\n }\n get target() {\n if (!this._target) this._target = document.querySelector(this.el);\n return this._target;\n }\n reset() {\n this._target = null;\n }\n\n /**\n * Enable hot reload: catch page change in order to fetch them and\n * load page without actually leaving current one.\n */\n enable(target = null) {\n if (this._pageChanged) throw \"Already enabled, please disable me\";\n if (!target) target = this.target || document.body;\n this.historySave(document.location, true);\n this._pageChanged = event => this.pageChanged(event);\n this._statePopped = event => this.statePopped(event);\n target.addEventListener('click', this._pageChanged, true);\n target.addEventListener('submit', this._pageChanged, true);\n window.addEventListener('popstate', this._statePopped, true);\n }\n\n /**\n * Disable hot reload, remove listeners.\n */\n disable() {\n this.target.removeEventListener('click', this._pageChanged, true);\n this.target.removeEventListener('submit', this._pageChanged, true);\n window.removeEventListener('popstate', this._statePopped, true);\n this._pageChanged = null;\n this._statePopped = null;\n }\n\n /**\n * Fetch url, return promise, similar to standard Fetch API.\n * Default implementation just forward argument to it.\n */\n fetch(url, options) {\n return fetch(url, options);\n }\n\n /**\n * Fetch app from remote and mount application.\n */\n load(url, {\n mount = true,\n scroll = [0, 0],\n ...options\n } = {}) {\n if (this.loadingClass) this.target.classList.add(this.loadingClass);\n if (this.onLoad) this.onLoad({\n url,\n el: this.el,\n options\n });\n if (scroll) window.scroll(...scroll);\n return this.fetch(url, options).then(response => response.text()).then(content => {\n if (this.loadingClass) this.target.classList.remove(this.loadingClass);\n var doc = new DOMParser().parseFromString(content, 'text/html');\n var dom = doc.querySelectorAll(this.el);\n var result = {\n url,\n content: dom || [document.createTextNode(content)],\n title: doc.title,\n append: this.append\n };\n mount && this.mount(result);\n return result;\n });\n }\n\n /**\n * Mount the page on provided target element\n */\n mount({\n content,\n title = null,\n ...options\n } = {}) {\n if (this.onPreMount) this.onPreMount({\n target: this.target,\n content,\n items,\n title\n });\n var items = null;\n if (content) items = this.mountContent(content, options);\n if (title) document.title = title;\n if (this.onMount) this.onMount({\n target: this.target,\n content,\n items,\n title\n });\n }\n\n /**\n * Mount page content\n */\n mountContent(content, {\n append = false\n } = {}) {\n if (typeof content == \"string\") {\n this.target.innerHTML = append ? this.target.innerHTML + content : content;\n // TODO\n return [];\n }\n if (!append) this.target.innerHTML = \"\";\n var fragment = document.createDocumentFragment();\n var items = [];\n for (var node of content) while (node.firstChild) {\n items.push(node.firstChild);\n fragment.appendChild(node.firstChild);\n }\n this.target.append(fragment);\n return items;\n }\n\n /// Save application state into browser history\n historySave(url, replace = false) {\n const state = {\n content: this.target.innerHTML,\n title: document.title\n };\n if (replace) history.replaceState(state, '', url);else history.pushState(state, '', url);\n }\n\n // --- events\n pageChanged(event) {\n let submit = event.type == 'submit';\n let target = submit || event.target.tagName == 'A' ? event.target : event.target.closest('a');\n if (!target || target.hasAttribute('target')) return;\n let url = submit ? target.getAttribute('action') || '' : target.getAttribute('href');\n let domain = window.location.protocol + '//' + window.location.hostname;\n let stay = (url === '' || url.startsWith('/') || url.startsWith('?') || url.startsWith(domain)) && url.indexOf('wp-admin') == -1;\n if (url === null || !stay) {\n return;\n }\n let options = {};\n if (submit) {\n let formData = new FormData(event.target);\n if (target.method == 'get') url += '?' + new URLSearchParams(formData).toString();else options = {\n ...options,\n method: target.method,\n body: formData\n };\n }\n this.load(url, options).then(() => this.historySave(url));\n event.preventDefault();\n event.stopPropagation();\n }\n statePopped(event) {\n const state = event.state;\n if (state && state.content) this.mount({\n content: state.content,\n title: state.title\n });\n }\n}\n\n//# sourceURL=webpack://aircox-assets/./src/pageLoad.js?"); + +/***/ }), + /***/ "./src/sound.js": /*!**********************!*\ !*** ./src/sound.js ***! @@ -429,6 +429,16 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ }), +/***/ "./src/vueLoader.js": +/*!**************************!*\ + !*** ./src/vueLoader.js ***! + \**************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* binding */ VueLoader; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-bundler.js\");\n/* harmony import */ var _pageLoad__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./pageLoad */ \"./src/pageLoad.js\");\n\n\n\n/**\n * Handles loading Vue js app on page load.\n */\nclass VueLoader {\n constructor({\n el = null,\n props = {},\n ...appConfig\n } = {}, loaderOptions = {}) {\n this.appConfig = appConfig;\n this.props = props;\n this.pageLoad = new _pageLoad__WEBPACK_IMPORTED_MODULE_1__[\"default\"](el, loaderOptions);\n this.pageLoad.onPreMount = event => this.onPreMount(event);\n this.pageLoad.onMount = event => this.onMount(event);\n }\n enable(hotReload = true) {\n hotReload && this.pageLoad.enable();\n this.mount();\n }\n mount() {\n if (this.app) this.unmount();\n const app = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createApp)(this.appConfig, this.props);\n app.config.globalProperties.window = window;\n this.vm = app.mount(this.pageLoad.el);\n this.app = app;\n }\n unmount() {\n if (!this.app) return;\n try {\n this.app.unmount();\n } catch (_) {\n null;\n }\n this.app = null;\n this.vm = null;\n this.pageLoad.reset();\n }\n onPreMount() {\n this.unmount();\n }\n onMount() {\n this.mount();\n }\n}\n\n//# sourceURL=webpack://aircox-assets/./src/vueLoader.js?"); + +/***/ }), + /***/ "./node_modules/mini-css-extract-plugin/dist/loader.js??clonedRuleSet-12.use[0]!./node_modules/css-loader/dist/cjs.js??clonedRuleSet-12.use[1]!./node_modules/vue-loader/dist/stylePostLoader.js!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-12.use[2]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/ACarousel.vue?vue&type=style&index=0&id=b79f173e&scoped=true&lang=css": /*!****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\ !*** ./node_modules/mini-css-extract-plugin/dist/loader.js??clonedRuleSet-12.use[0]!./node_modules/css-loader/dist/cjs.js??clonedRuleSet-12.use[1]!./node_modules/vue-loader/dist/stylePostLoader.js!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-12.use[2]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/ACarousel.vue?vue&type=style&index=0&id=b79f173e&scoped=true&lang=css ***! diff --git a/assets/src/app.js b/assets/src/app.js index 1ee2e1d..06e7071 100644 --- a/assets/src/app.js +++ b/assets/src/app.js @@ -3,6 +3,7 @@ import components from './components' import { Carousel, Pagination, Navigation, Slide } from 'vue3-carousel' + const App = { el: '#app', delimiters: ['[[', ']]'], @@ -21,31 +22,6 @@ const App = { computed: { 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 = { diff --git a/assets/src/appBuilder.js b/assets/src/appBuilder.js deleted file mode 100644 index 892f7c1..0000000 --- a/assets/src/appBuilder.js +++ /dev/null @@ -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 }) - } -} diff --git a/assets/src/assets/styles.scss b/assets/src/assets/styles.scss index 4a383fe..38d35e3 100644 --- a/assets/src/assets/styles.scss +++ b/assets/src/assets/styles.scss @@ -109,6 +109,10 @@ input.half-field:not(:active):not(:hover) { } .blink { + animation: 1s ease-in-out 2s infinite alternate blink; +} + +.loading { animation: 1s ease-in-out 3s infinite alternate blink; } diff --git a/assets/src/index.js b/assets/src/index.js index c42caef..b9ac582 100644 --- a/assets/src/index.js +++ b/assets/src/index.js @@ -8,7 +8,7 @@ import '@fortawesome/fontawesome-free/css/all.min.css'; //-- aircox import App, {PlayerApp} from './app' -import Builder from './appBuilder' +import VueLoader from './vueLoader' import Sound from './sound' import {Set} from './model' @@ -17,13 +17,13 @@ import './assets/styles.scss' window.aircox = { // main application - builder: new Builder(App), - get app() { return this.builder.app }, + loader: null, + get app() { return this.loader.app }, // player application - playerBuilder: new Builder(PlayerApp), - get playerApp() { return this.playerBuilder && this.playerBuilder.app }, - get player() { return this.playerBuilder.vm && this.playerBuilder.vm.$refs.player }, + playerLoader: null, + get playerApp() { return this.playerLoader && this.playerLoader.app }, + get player() { return this.playerLoader.vm && this.playerLoader.vm.$refs.player }, Set, Sound, @@ -31,27 +31,23 @@ window.aircox = { /** * Initialize main application and player. */ - init(props=null, {config=null, builder=null, initBuilder=true, - initPlayer=true, hotReload=false, el=null}={}) + init(props=null, {hotReload=false, el=null, + config=null, playerConfig=null, + initApp=true, initPlayer=true, + loader=null, playerLoader=null}={}) { if(initPlayer) { - let playerBuilder = this.playerBuilder - playerBuilder.mount() + playerConfig = playerConfig || PlayerApp + playerLoader = playerLoader || new VueLoader(playerConfig) + playerLoader.enable(false) + this.playerLoader = playerLoader } - if(initBuilder) { - builder = builder || this.builder - this.builder = builder - if(config || window.App) - builder.config = config || window.App - if(el) - builder.config.el = el - - builder.title = document.title - builder.mount({props}) - - if(hotReload) - builder.enableHotReload(hotReload) + if(initApp) { + config = config || window.App || App + loader = loader || new VueLoader({el, props, ...config}) + loader.enable(hotReload) + this.loader = loader } }, diff --git a/assets/src/pageLoad.js b/assets/src/pageLoad.js new file mode 100644 index 0000000..e41c0a1 --- /dev/null +++ b/assets/src/pageLoad.js @@ -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 }); + } +} diff --git a/assets/src/vueLoader.js b/assets/src/vueLoader.js new file mode 100644 index 0000000..48e7a28 --- /dev/null +++ b/assets/src/vueLoader.js @@ -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() } +}