mirror of
https://github.com/wintercms/winter.git
synced 2024-06-28 05:33:29 +02:00
Establish base Snowboard framework in Backend (#548)
This PR establishes a base Snowboard framework in the Backend. While we won't likely have any specific Snowboard widgets or functionality in the 1.1 branch, it will allow people to use Snowboard in the Backend should they wish. Fixes #541. Co-authored-by: Luke Towers <github@luketowers.ca>
This commit is contained in:
parent
ac130c462c
commit
f79e672a13
8
.github/workflows/code-quality.yaml
vendored
8
.github/workflows/code-quality.yaml
vendored
@ -58,3 +58,11 @@ jobs:
|
||||
- name: Run code quality checks on System Module
|
||||
working-directory: ./modules/system
|
||||
run: npx eslint .
|
||||
|
||||
- name: Install Node dependencies for Backend Module
|
||||
working-directory: ./modules/backend
|
||||
run: npm install
|
||||
|
||||
- name: Run code quality checks on Backend Module
|
||||
working-directory: ./modules/backend
|
||||
run: npx eslint .
|
||||
|
14
modules/backend/.eslintignore
Normal file
14
modules/backend/.eslintignore
Normal file
@ -0,0 +1,14 @@
|
||||
# Ignore build files
|
||||
**/node_modules/**
|
||||
build/*.js
|
||||
**/build/*.js
|
||||
**/mix.webpack.js
|
||||
|
||||
# Ignore all JS except for Mix-based assets
|
||||
assets/js
|
||||
assets/vendor
|
||||
behaviors/**/*.js
|
||||
controllers/**/*.js
|
||||
formwidgets/**/*.js
|
||||
reportwidgets/**/*.js
|
||||
widgets/**/*.js
|
36
modules/backend/.eslintrc.json
Normal file
36
modules/backend/.eslintrc.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true
|
||||
},
|
||||
"globals": {
|
||||
"Snowboard": "writable"
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"plugin:vue/vue3-recommended"
|
||||
],
|
||||
"rules": {
|
||||
"class-methods-use-this": ["off"],
|
||||
"indent": ["error", 4, {
|
||||
"SwitchCase": 1
|
||||
}],
|
||||
"max-len": ["off"],
|
||||
"new-cap": ["error", { "properties": false }],
|
||||
"no-alert": ["off"],
|
||||
"no-param-reassign": ["error", {
|
||||
"props": false
|
||||
}],
|
||||
"vue/html-indent": ["error", 4],
|
||||
"vue/html-self-closing": ["error", {
|
||||
"html": {
|
||||
"void": "never",
|
||||
"normal": "any",
|
||||
"component": "always"
|
||||
},
|
||||
"svg": "always",
|
||||
"math": "always"
|
||||
}],
|
||||
"vue/multi-word-component-names": ["off"]
|
||||
}
|
||||
}
|
6
modules/backend/.gitignore
vendored
Normal file
6
modules/backend/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# Backend module ignores
|
||||
|
||||
# Ignore Mix files
|
||||
node_modules
|
||||
package-lock.json
|
||||
mix.webpack.js
|
@ -2,19 +2,6 @@
|
||||
* Winter General Utilities
|
||||
*/
|
||||
|
||||
/*
|
||||
* Ensure the CSRF token is added to all AJAX requests.
|
||||
*/
|
||||
|
||||
$.ajaxPrefilter(function(options) {
|
||||
var token = $('meta[name="csrf-token"]').attr('content')
|
||||
|
||||
if (token) {
|
||||
if (!options.headers) options.headers = {}
|
||||
options.headers['X-CSRF-TOKEN'] = token
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* Path helpers
|
||||
*/
|
||||
@ -36,133 +23,6 @@ $.wn.backendUrl = function(url) {
|
||||
return backendBasePath + '/' + url
|
||||
}
|
||||
|
||||
/*
|
||||
* Asset Manager
|
||||
*
|
||||
* Usage: assetManager.load({ css:[], js:[], img:[] }, onLoadedCallback)
|
||||
*/
|
||||
|
||||
AssetManager = function() {
|
||||
|
||||
var o = {
|
||||
|
||||
load: function(collection, callback) {
|
||||
var jsList = (collection.js) ? collection.js : [],
|
||||
cssList = (collection.css) ? collection.css : [],
|
||||
imgList = (collection.img) ? collection.img : []
|
||||
|
||||
jsList = $.grep(jsList, function(item){
|
||||
return $('head script[src="'+item+'"]').length == 0
|
||||
})
|
||||
|
||||
cssList = $.grep(cssList, function(item){
|
||||
return $('head link[href="'+item+'"]').length == 0
|
||||
})
|
||||
|
||||
var cssCounter = 0,
|
||||
jsLoaded = false,
|
||||
imgLoaded = false
|
||||
|
||||
if (jsList.length === 0 && cssList.length === 0 && imgList.length === 0) {
|
||||
callback && callback()
|
||||
return
|
||||
}
|
||||
|
||||
o.loadJavaScript(jsList, function(){
|
||||
jsLoaded = true
|
||||
checkLoaded()
|
||||
})
|
||||
|
||||
$.each(cssList, function(index, source){
|
||||
o.loadStyleSheet(source, function(){
|
||||
cssCounter++
|
||||
checkLoaded()
|
||||
})
|
||||
})
|
||||
|
||||
o.loadImage(imgList, function(){
|
||||
imgLoaded = true
|
||||
checkLoaded()
|
||||
})
|
||||
|
||||
function checkLoaded() {
|
||||
if (!imgLoaded)
|
||||
return false
|
||||
|
||||
if (!jsLoaded)
|
||||
return false
|
||||
|
||||
if (cssCounter < cssList.length)
|
||||
return false
|
||||
|
||||
callback && callback()
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Loads StyleSheet files
|
||||
*/
|
||||
loadStyleSheet: function(source, callback) {
|
||||
var cssElement = document.createElement('link')
|
||||
|
||||
cssElement.setAttribute('rel', 'stylesheet')
|
||||
cssElement.setAttribute('type', 'text/css')
|
||||
cssElement.setAttribute('href', source)
|
||||
cssElement.addEventListener('load', callback, false)
|
||||
|
||||
if (typeof cssElement != 'undefined') {
|
||||
document.getElementsByTagName('head')[0].appendChild(cssElement)
|
||||
}
|
||||
|
||||
return cssElement
|
||||
},
|
||||
|
||||
/*
|
||||
* Loads JavaScript files in sequence
|
||||
*/
|
||||
loadJavaScript: function(sources, callback) {
|
||||
if (sources.length <= 0)
|
||||
return callback()
|
||||
|
||||
var source = sources.shift(),
|
||||
jsElement = document.createElement('script');
|
||||
|
||||
jsElement.setAttribute('type', 'text/javascript')
|
||||
jsElement.setAttribute('src', source)
|
||||
jsElement.addEventListener('load', function() {
|
||||
o.loadJavaScript(sources, callback)
|
||||
}, false)
|
||||
|
||||
if (typeof jsElement != 'undefined') {
|
||||
document.getElementsByTagName('head')[0].appendChild(jsElement)
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Loads Image files
|
||||
*/
|
||||
loadImage: function(sources, callback) {
|
||||
if (sources.length <= 0)
|
||||
return callback()
|
||||
|
||||
var loaded = 0
|
||||
$.each(sources, function(index, source){
|
||||
var img = new Image()
|
||||
img.onload = function() {
|
||||
if (++loaded == sources.length && callback)
|
||||
callback()
|
||||
}
|
||||
img.src = source
|
||||
})
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return o;
|
||||
};
|
||||
|
||||
assetManager = new AssetManager();
|
||||
|
||||
/*
|
||||
* String escape
|
||||
*/
|
||||
|
42
modules/backend/assets/js/winter-min.js
vendored
42
modules/backend/assets/js/winter-min.js
vendored
@ -1271,10 +1271,7 @@ return result?result:items}
|
||||
$.fn.dateTimeConverter.Constructor=DateTimeConverter
|
||||
$.fn.dateTimeConverter.noConflict=function(){$.fn.dateTimeConverter=old
|
||||
return this}
|
||||
$(document).render(function(){$('time[data-datetime-control]').dateTimeConverter()})}(window.jQuery);$.ajaxPrefilter(function(options){var token=$('meta[name="csrf-token"]').attr('content')
|
||||
if(token){if(!options.headers)options.headers={}
|
||||
options.headers['X-CSRF-TOKEN']=token}})
|
||||
if($.wn===undefined)
|
||||
$(document).render(function(){$('time[data-datetime-control]').dateTimeConverter()})}(window.jQuery);if($.wn===undefined)
|
||||
$.wn={}
|
||||
if($.oc===undefined)
|
||||
$.oc=$.wn
|
||||
@ -1284,42 +1281,7 @@ return url
|
||||
if(url.substr(0,1)=='/')
|
||||
url=url.substr(1)
|
||||
return backendBasePath+'/'+url}
|
||||
AssetManager=function(){var o={load:function(collection,callback){var jsList=(collection.js)?collection.js:[],cssList=(collection.css)?collection.css:[],imgList=(collection.img)?collection.img:[]
|
||||
jsList=$.grep(jsList,function(item){return $('head script[src="'+item+'"]').length==0})
|
||||
cssList=$.grep(cssList,function(item){return $('head link[href="'+item+'"]').length==0})
|
||||
var cssCounter=0,jsLoaded=false,imgLoaded=false
|
||||
if(jsList.length===0&&cssList.length===0&&imgList.length===0){callback&&callback()
|
||||
return}
|
||||
o.loadJavaScript(jsList,function(){jsLoaded=true
|
||||
checkLoaded()})
|
||||
$.each(cssList,function(index,source){o.loadStyleSheet(source,function(){cssCounter++
|
||||
checkLoaded()})})
|
||||
o.loadImage(imgList,function(){imgLoaded=true
|
||||
checkLoaded()})
|
||||
function checkLoaded(){if(!imgLoaded)
|
||||
return false
|
||||
if(!jsLoaded)
|
||||
return false
|
||||
if(cssCounter<cssList.length)
|
||||
return false
|
||||
callback&&callback()}},loadStyleSheet:function(source,callback){var cssElement=document.createElement('link')
|
||||
cssElement.setAttribute('rel','stylesheet')
|
||||
cssElement.setAttribute('type','text/css')
|
||||
cssElement.setAttribute('href',source)
|
||||
cssElement.addEventListener('load',callback,false)
|
||||
if(typeof cssElement!='undefined'){document.getElementsByTagName('head')[0].appendChild(cssElement)}
|
||||
return cssElement},loadJavaScript:function(sources,callback){if(sources.length<=0)
|
||||
return callback()
|
||||
var source=sources.shift(),jsElement=document.createElement('script');jsElement.setAttribute('type','text/javascript')
|
||||
jsElement.setAttribute('src',source)
|
||||
jsElement.addEventListener('load',function(){o.loadJavaScript(sources,callback)},false)
|
||||
if(typeof jsElement!='undefined'){document.getElementsByTagName('head')[0].appendChild(jsElement)}},loadImage:function(sources,callback){if(sources.length<=0)
|
||||
return callback()
|
||||
var loaded=0
|
||||
$.each(sources,function(index,source){var img=new Image()
|
||||
img.onload=function(){if(++loaded==sources.length&&callback)
|
||||
callback()}
|
||||
img.src=source})}};return o;};assetManager=new AssetManager();if($.wn===undefined)
|
||||
if($.wn===undefined)
|
||||
$.wn={}
|
||||
if($.oc===undefined)
|
||||
$.oc=$.wn
|
||||
|
107
modules/backend/assets/ui/js/ajax/Handler.js
Normal file
107
modules/backend/assets/ui/js/ajax/Handler.js
Normal file
@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Backend AJAX handler.
|
||||
*
|
||||
* This is a utility script that resolves some backwards-compatibility issues with the functionality
|
||||
* that relies on the old framework, and ensures that Snowboard works well within the Backend
|
||||
* environment.
|
||||
*
|
||||
* Functions:
|
||||
* - Adds the "render" jQuery event to Snowboard requests that widgets use to initialise.
|
||||
* - Ensures the CSRF token is included in requests.
|
||||
*
|
||||
* @copyright 2021 Winter.
|
||||
* @author Ben Thomson <git@alfreido.com>
|
||||
*/
|
||||
export default class Handler extends Snowboard.Singleton {
|
||||
/**
|
||||
* Event listeners.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
listens() {
|
||||
return {
|
||||
ready: 'ready',
|
||||
ajaxFetchOptions: 'ajaxFetchOptions',
|
||||
ajaxUpdateComplete: 'ajaxUpdateComplete',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ready handler.
|
||||
*
|
||||
* Adds the jQuery AJAX prefilter that the old framework uses to inject the CSRF token in AJAX
|
||||
* calls, and fires off a "render" event.
|
||||
*/
|
||||
ready() {
|
||||
if (!window.jQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.jQuery.ajaxPrefilter((options) => {
|
||||
if (this.hasToken()) {
|
||||
if (!options.headers) {
|
||||
options.headers = {};
|
||||
}
|
||||
options.headers['X-CSRF-TOKEN'] = this.getToken();
|
||||
}
|
||||
});
|
||||
|
||||
// Add "render" event for backwards compatibility
|
||||
window.jQuery(document).trigger('render');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch options handler.
|
||||
*
|
||||
* Ensures that the CSRF token is included in Snowboard requests.
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
ajaxFetchOptions(options) {
|
||||
if (this.hasToken()) {
|
||||
options.headers['X-CSRF-TOKEN'] = this.getToken();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update complete handler.
|
||||
*
|
||||
* Fires off a "render" event when partials are updated so that any widgets included in
|
||||
* responses are correctly initialised.
|
||||
*/
|
||||
ajaxUpdateComplete() {
|
||||
if (!window.jQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add "render" event for backwards compatibility
|
||||
window.jQuery(document).trigger('render');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a CSRF token is available.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasToken() {
|
||||
const tokenElement = document.querySelector('meta[name="csrf-token"]');
|
||||
|
||||
if (!tokenElement) {
|
||||
return false;
|
||||
}
|
||||
if (!tokenElement.hasAttribute('content')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the CSRF token.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
getToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
}
|
||||
}
|
1
modules/backend/assets/ui/js/build/backend.js
Normal file
1
modules/backend/assets/ui/js/build/backend.js
Normal file
File diff suppressed because one or more lines are too long
23
modules/backend/assets/ui/js/index.js
Normal file
23
modules/backend/assets/ui/js/index.js
Normal file
@ -0,0 +1,23 @@
|
||||
import BackendAjaxHandler from './ajax/Handler';
|
||||
|
||||
if (window.Snowboard === undefined) {
|
||||
throw new Error('Snowboard must be loaded in order to use the Backend UI.');
|
||||
}
|
||||
|
||||
((Snowboard) => {
|
||||
Snowboard.addPlugin('backend.ajax.handler', BackendAjaxHandler);
|
||||
|
||||
// Add polyfill for AssetManager
|
||||
window.AssetManager = {
|
||||
load: (assets, callback) => {
|
||||
Snowboard.assetLoader().load(assets).finally(
|
||||
() => {
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
window.assetManager = window.AssetManager;
|
||||
})(window.Snowboard);
|
@ -1,6 +1,5 @@
|
||||
<?= Form::open(['class'=>'layout-relative dashboard-container']) ?>
|
||||
<div id="dashReportContainer" class="report-container loading">
|
||||
|
||||
<!-- Loading -->
|
||||
<div class="loading-indicator-container">
|
||||
<div class="loading-indicator indicator-center">
|
||||
@ -8,14 +7,17 @@
|
||||
<div><?= e(trans('backend::lang.list.loading')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?= Form::close() ?>
|
||||
|
||||
<?php Block::put('head'); ?>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$.request('onInitReportContainer').done(function() {
|
||||
$('#dashReportContainer').removeClass('loading')
|
||||
})
|
||||
})
|
||||
Snowboard.ready(() => {
|
||||
Snowboard.request(null, 'onInitReportContainer', {
|
||||
success: () => {
|
||||
$('#dashReportContainer').removeClass('loading');
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php Block::endPut(); ?>
|
||||
|
@ -26,9 +26,9 @@
|
||||
Backend::skinAsset('assets/js/vendor/jquery-migrate.min.js'),
|
||||
Url::asset('modules/system/assets/js/framework.js'),
|
||||
Url::asset('modules/system/assets/js/build/manifest.js'),
|
||||
Url::asset('modules/system/assets/js/build/vendor.js'),
|
||||
Url::asset('modules/system/assets/js/snowboard/build/snowboard.vendor.js'),
|
||||
Url::asset('modules/system/assets/js/build/system.js'),
|
||||
Url::asset('modules/backend/assets/ui/js/build/backend.js'),
|
||||
];
|
||||
if (Config::get('develop.decompileBackendAssets', false)) {
|
||||
$scripts = array_merge($scripts, Backend::decompileAsset('modules/system/assets/ui/storm.js'));
|
||||
|
35
modules/backend/package.json
Normal file
35
modules/backend/package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@wintercms/wn-backend-module",
|
||||
"version": "1.0.0",
|
||||
"description": "The Backend module for Winter CMS.",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wintercms/winter.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Ben Thomson",
|
||||
"email": "git@alfreido.com",
|
||||
"url": "https://wintercms.com"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Winter CMS Maintainers",
|
||||
"url": "https://wintercms.com"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wintercms/winter/issues"
|
||||
},
|
||||
"homepage": "https://wintercms.com/",
|
||||
"devDependencies": {
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"eslint": "^8.6.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-vue": "^8.5.0",
|
||||
"laravel-mix": "^6.0.34",
|
||||
"laravel-mix-polyfill": "^3.0.1"
|
||||
}
|
||||
}
|
27
modules/backend/winter.mix.js
Normal file
27
modules/backend/winter.mix.js
Normal file
@ -0,0 +1,27 @@
|
||||
/* eslint-disable */
|
||||
const mix = require('laravel-mix');
|
||||
require('laravel-mix-polyfill');
|
||||
/* eslint-enable */
|
||||
|
||||
mix.setPublicPath(__dirname);
|
||||
|
||||
mix
|
||||
.options({
|
||||
terser: {
|
||||
extractComments: false,
|
||||
},
|
||||
runtimeChunkPath: './assets/js/build',
|
||||
})
|
||||
|
||||
// Compile Snowboard assets for the Backend
|
||||
.js(
|
||||
'./assets/ui/js/index.js',
|
||||
'./assets/ui/js/build/backend.js',
|
||||
)
|
||||
|
||||
// Polyfill for all targeted browsers
|
||||
.polyfill({
|
||||
enabled: mix.inProduction(),
|
||||
useBuiltIns: 'usage',
|
||||
targets: '> 0.5%, last 2 versions, not dead, Firefox ESR, not ie > 0',
|
||||
});
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -66,6 +66,7 @@ class Request extends Snowboard.PluginBase {
|
||||
(response) => {
|
||||
if (response.cancelled) {
|
||||
this.cancelled = true;
|
||||
this.complete();
|
||||
return;
|
||||
}
|
||||
this.responseData = response;
|
||||
@ -83,24 +84,7 @@ class Request extends Snowboard.PluginBase {
|
||||
this.responseError = error;
|
||||
this.processError(error);
|
||||
},
|
||||
).finally(() => {
|
||||
if (this.cancelled === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.complete && typeof this.options.complete === 'function') {
|
||||
this.options.complete(this.responseData, this);
|
||||
}
|
||||
this.snowboard.globalEvent('ajaxDone', this.responseData, this);
|
||||
|
||||
if (this.element) {
|
||||
const event = new Event('ajaxAlways');
|
||||
event.request = this;
|
||||
event.responseData = this.responseData;
|
||||
event.responseError = this.responseError;
|
||||
this.element.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -108,6 +92,7 @@ class Request extends Snowboard.PluginBase {
|
||||
(response) => {
|
||||
if (response.cancelled) {
|
||||
this.cancelled = true;
|
||||
this.complete();
|
||||
return;
|
||||
}
|
||||
this.responseData = response;
|
||||
@ -125,24 +110,7 @@ class Request extends Snowboard.PluginBase {
|
||||
this.responseError = error;
|
||||
this.processError(error);
|
||||
},
|
||||
).finally(() => {
|
||||
if (this.cancelled === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.complete && typeof this.options.complete === 'function') {
|
||||
this.options.complete(this.responseData, this);
|
||||
}
|
||||
this.snowboard.globalEvent('ajaxDone', this.responseData, this);
|
||||
|
||||
if (this.element) {
|
||||
const event = new Event('ajaxAlways');
|
||||
event.request = this;
|
||||
event.responseData = this.responseData;
|
||||
event.responseError = this.responseError;
|
||||
this.element.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -329,13 +297,28 @@ class Request extends Snowboard.PluginBase {
|
||||
});
|
||||
|
||||
if (Object.keys(partials).length === 0) {
|
||||
resolve();
|
||||
if (response.X_WINTER_ASSETS) {
|
||||
this.processAssets(response.X_WINTER_ASSETS).then(
|
||||
() => {
|
||||
resolve();
|
||||
},
|
||||
() => {
|
||||
reject();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = this.snowboard.globalPromiseEvent('ajaxBeforeUpdate', response, this);
|
||||
promises.then(
|
||||
() => {
|
||||
async () => {
|
||||
if (response.X_WINTER_ASSETS) {
|
||||
await this.processAssets(response.X_WINTER_ASSETS);
|
||||
}
|
||||
|
||||
this.doUpdate(partials).then(
|
||||
() => {
|
||||
// Allow for HTML redraw
|
||||
@ -456,9 +439,7 @@ class Request extends Snowboard.PluginBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.X_WINTER_ASSETS) {
|
||||
this.processAssets(response.X_WINTER_ASSETS);
|
||||
}
|
||||
this.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -505,6 +486,8 @@ class Request extends Snowboard.PluginBase {
|
||||
this.processErrorMessage(error.X_WINTER_ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
this.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -625,6 +608,21 @@ class Request extends Snowboard.PluginBase {
|
||||
this.snowboard.globalEvent('ajaxValidationErrors', this.form, fields, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes assets returned by an AJAX request.
|
||||
*
|
||||
* By default, no asset processing will occur and this will return a resolved Promise.
|
||||
*
|
||||
* Plugins can augment this functionality from the `ajaxLoadAssets` event. This event is considered blocking, and
|
||||
* allows assets to be loaded or processed before continuing with any additional functionality.
|
||||
*
|
||||
* @param {Object} assets
|
||||
* @returns {Promise}
|
||||
*/
|
||||
processAssets(assets) {
|
||||
return this.snowboard.globalPromiseEvent('ajaxLoadAssets', assets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the request with the user before proceeding.
|
||||
*
|
||||
@ -668,6 +666,24 @@ class Request extends Snowboard.PluginBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires off completion events for the Request.
|
||||
*/
|
||||
complete() {
|
||||
if (this.options.complete && typeof this.options.complete === 'function') {
|
||||
this.options.complete(this.responseData, this);
|
||||
}
|
||||
this.snowboard.globalEvent('ajaxDone', this.responseData, this);
|
||||
|
||||
if (this.element) {
|
||||
const event = new Event('ajaxAlways');
|
||||
event.request = this;
|
||||
event.responseData = this.responseData;
|
||||
event.responseError = this.responseError;
|
||||
this.element.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
get form() {
|
||||
if (this.options.form) {
|
||||
return this.options.form;
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
(self.webpackChunk_wintercms_wn_system_module=self.webpackChunk_wintercms_wn_system_module||[]).push([[806],{3738:function(){function e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,a)}return r}function t(t){for(var a=1;a<arguments.length;a++){var n=null!=arguments[a]?arguments[a]:{};a%2?e(Object(n),!0).forEach((function(e){r(t,e,n[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):e(Object(n)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(n,e))}))}return t}function r(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}if(void 0===window.Snowboard)throw new Error("Snowboard must be loaded in order to use the Data Attributes plugin.");class a extends Snowboard.Singleton{listens(){return{ready:"ready",ajaxSetup:"onAjaxSetup"}}ready(){this.attachHandlers(),this.disableDefaultFormValidation()}dependencies(){return["request","jsonParser"]}destructor(){this.detachHandlers(),super.destructor()}attachHandlers(){window.addEventListener("change",(e=>this.changeHandler(e))),window.addEventListener("click",(e=>this.clickHandler(e))),window.addEventListener("keydown",(e=>this.keyDownHandler(e))),window.addEventListener("submit",(e=>this.submitHandler(e)))}disableDefaultFormValidation(){document.querySelectorAll("form[data-request]:not([data-browser-validate])").forEach((e=>{e.setAttribute("novalidate",!0)}))}detachHandlers(){window.removeEventListener("change",(e=>this.changeHandler(e))),window.removeEventListener("click",(e=>this.clickHandler(e))),window.removeEventListener("keydown",(e=>this.keyDownHandler(e))),window.removeEventListener("submit",(e=>this.submitHandler(e)))}changeHandler(e){e.target.matches("select[data-request], input[type=radio][data-request], input[type=checkbox][data-request], input[type=file][data-request]")&&this.processRequestOnElement(e.target)}clickHandler(e){let t=e.target;for(;"HTML"!==t.tagName;){if(t.matches("a[data-request], button[data-request], input[type=button][data-request], input[type=submit][data-request]")){e.preventDefault(),this.processRequestOnElement(t);break}t=t.parentElement}}keyDownHandler(e){if(!e.target.matches("input"))return;-1!==["checkbox","color","date","datetime","datetime-local","email","image","month","number","password","radio","range","search","tel","text","time","url","week"].indexOf(e.target.getAttribute("type"))&&("Enter"===e.key&&e.target.matches("*[data-request]")?(this.processRequestOnElement(e.target),e.preventDefault(),e.stopImmediatePropagation()):e.target.matches("*[data-track-input]")&&this.trackInput(e.target))}submitHandler(e){e.target.matches("form[data-request]")&&(e.preventDefault(),this.processRequestOnElement(e.target))}processRequestOnElement(e){const t=e.dataset,r=String(t.request),a={confirm:"requestConfirm"in t?String(t.requestConfirm):null,redirect:"requestRedirect"in t?String(t.requestRedirect):null,loading:"requestLoading"in t?String(t.requestLoading):null,flash:"requestFlash"in t,files:"requestFiles"in t,browserValidate:"requestBrowserValidate"in t,form:"requestForm"in t?String(t.requestForm):null,url:"requestUrl"in t?String(t.requestUrl):null,update:"requestUpdate"in t?this.parseData(String(t.requestUpdate)):[],data:"requestData"in t?this.parseData(String(t.requestData)):[]};this.snowboard.request(e,r,a)}onAjaxSetup(e){if(!e.element)return;const r=e.element.getAttribute("name"),a=t(t({},this.getParentRequestData(e.element)),e.options.data);e.element&&e.element.matches("input, textarea, select, button")&&!e.form&&r&&!e.options.data[r]&&(a[r]=e.element.value),e.options.data=a}getParentRequestData(e){const r=[];let a={},n=e;for(;n.parentElement&&"HTML"!==n.parentElement.tagName;)r.push(n.parentElement),n=n.parentElement;return r.reverse(),r.forEach((e=>{const r=e.dataset;"requestData"in r&&(a=t(t({},a),this.parseData(r.requestData)))})),a}parseData(e){let t;if(void 0===e&&(t=""),"object"==typeof t)return t;try{return this.snowboard.jsonparser().parse("{".concat(e,"}"))}catch(e){throw new Error("Error parsing the data attribute on element: ".concat(e.message))}}trackInput(e){const{lastValue:t}=e.dataset,r=e.dataset.trackInput||300;void 0!==t&&t===e.value||(this.resetTrackInputTimer(e),e.dataset.trackInput=window.setTimeout((()=>{if(e.dataset.request)return void this.processRequestOnElement(e);let t=e;for(;t.parentElement&&"HTML"!==t.parentElement.tagName;)if(t=t.parentElement,"FORM"===t.tagName&&t.dataset.request){this.processRequestOnElement(t);break}}),r))}resetTrackInputTimer(e){e.dataset.trackInput&&(window.clearTimeout(e.dataset.trackInput),e.dataset.trackInput=null)}}Snowboard.addPlugin("attributeRequest",a)}},function(e){var t;t=3738,e(e.s=t)}]);
|
||||
(self.webpackChunk_wintercms_wn_system_module=self.webpackChunk_wintercms_wn_system_module||[]).push([[806],{9250:function(){function e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,a)}return r}function t(t){for(var a=1;a<arguments.length;a++){var n=null!=arguments[a]?arguments[a]:{};a%2?e(Object(n),!0).forEach((function(e){r(t,e,n[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):e(Object(n)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(n,e))}))}return t}function r(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}if(void 0===window.Snowboard)throw new Error("Snowboard must be loaded in order to use the Data Attributes plugin.");class a extends Snowboard.Singleton{listens(){return{ready:"ready",ajaxSetup:"onAjaxSetup"}}ready(){this.attachHandlers(),this.disableDefaultFormValidation()}dependencies(){return["request","jsonParser"]}destructor(){this.detachHandlers(),super.destructor()}attachHandlers(){window.addEventListener("change",(e=>this.changeHandler(e))),window.addEventListener("click",(e=>this.clickHandler(e))),window.addEventListener("keydown",(e=>this.keyDownHandler(e))),window.addEventListener("submit",(e=>this.submitHandler(e)))}disableDefaultFormValidation(){document.querySelectorAll("form[data-request]:not([data-browser-validate])").forEach((e=>{e.setAttribute("novalidate",!0)}))}detachHandlers(){window.removeEventListener("change",(e=>this.changeHandler(e))),window.removeEventListener("click",(e=>this.clickHandler(e))),window.removeEventListener("keydown",(e=>this.keyDownHandler(e))),window.removeEventListener("submit",(e=>this.submitHandler(e)))}changeHandler(e){e.target.matches("select[data-request], input[type=radio][data-request], input[type=checkbox][data-request], input[type=file][data-request]")&&this.processRequestOnElement(e.target)}clickHandler(e){let t=e.target;for(;"HTML"!==t.tagName;){if(t.matches("a[data-request], button[data-request], input[type=button][data-request], input[type=submit][data-request]")){e.preventDefault(),this.processRequestOnElement(t);break}t=t.parentElement}}keyDownHandler(e){if(!e.target.matches("input"))return;-1!==["checkbox","color","date","datetime","datetime-local","email","image","month","number","password","radio","range","search","tel","text","time","url","week"].indexOf(e.target.getAttribute("type"))&&("Enter"===e.key&&e.target.matches("*[data-request]")?(this.processRequestOnElement(e.target),e.preventDefault(),e.stopImmediatePropagation()):e.target.matches("*[data-track-input]")&&this.trackInput(e.target))}submitHandler(e){e.target.matches("form[data-request]")&&(e.preventDefault(),this.processRequestOnElement(e.target))}processRequestOnElement(e){const t=e.dataset,r=String(t.request),a={confirm:"requestConfirm"in t?String(t.requestConfirm):null,redirect:"requestRedirect"in t?String(t.requestRedirect):null,loading:"requestLoading"in t?String(t.requestLoading):null,flash:"requestFlash"in t,files:"requestFiles"in t,browserValidate:"requestBrowserValidate"in t,form:"requestForm"in t?String(t.requestForm):null,url:"requestUrl"in t?String(t.requestUrl):null,update:"requestUpdate"in t?this.parseData(String(t.requestUpdate)):[],data:"requestData"in t?this.parseData(String(t.requestData)):[]};this.snowboard.request(e,r,a)}onAjaxSetup(e){if(!e.element)return;const r=e.element.getAttribute("name"),a=t(t({},this.getParentRequestData(e.element)),e.options.data);e.element&&e.element.matches("input, textarea, select, button")&&!e.form&&r&&!e.options.data[r]&&(a[r]=e.element.value),e.options.data=a}getParentRequestData(e){const r=[];let a={},n=e;for(;n.parentElement&&"HTML"!==n.parentElement.tagName;)r.push(n.parentElement),n=n.parentElement;return r.reverse(),r.forEach((e=>{const r=e.dataset;"requestData"in r&&(a=t(t({},a),this.parseData(r.requestData)))})),a}parseData(e){let t;if(void 0===e&&(t=""),"object"==typeof t)return t;try{return this.snowboard.jsonparser().parse("{".concat(e,"}"))}catch(e){throw new Error("Error parsing the data attribute on element: ".concat(e.message))}}trackInput(e){const{lastValue:t}=e.dataset,r=e.dataset.trackInput||300;void 0!==t&&t===e.value||(this.resetTrackInputTimer(e),e.dataset.trackInput=window.setTimeout((()=>{if(e.dataset.request)return void this.processRequestOnElement(e);let t=e;for(;t.parentElement&&"HTML"!==t.parentElement.tagName;)if(t=t.parentElement,"FORM"===t.tagName&&t.dataset.request){this.processRequestOnElement(t);break}}),r))}resetTrackInputTimer(e){e.dataset.trackInput&&(window.clearTimeout(e.dataset.trackInput),e.dataset.trackInput=null)}}Snowboard.addPlugin("attributeRequest",a)}},function(e){var t;t=9250,e(e.s=t)}]);
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
173
modules/system/assets/js/snowboard/extras/AssetLoader.js
Normal file
173
modules/system/assets/js/snowboard/extras/AssetLoader.js
Normal file
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Asset Loader.
|
||||
*
|
||||
* Provides simple asset loading functionality for Snowboard, making it easy to pre-load images or
|
||||
* include JavaScript or CSS assets on the fly.
|
||||
*
|
||||
* By default, this loader will listen to any assets that have been requested to load in an AJAX
|
||||
* response, such as responses from a component.
|
||||
*
|
||||
* You can also load assets manually by calling the following:
|
||||
*
|
||||
* ```js
|
||||
* Snowboard.addPlugin('assetLoader', AssetLoader);
|
||||
* Snowboard.assetLoader().processAssets(assets);
|
||||
* ```
|
||||
*
|
||||
* @copyright 2021 Winter.
|
||||
* @author Ben Thomson <git@alfreido.com>
|
||||
*/
|
||||
export default class AssetLoader extends window.Snowboard.Singleton {
|
||||
/**
|
||||
* Event listeners.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
listens() {
|
||||
return {
|
||||
ajaxLoadAssets: 'load',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Process and load assets.
|
||||
*
|
||||
* The `assets` property of this method requires an object with any of the following keys and an
|
||||
* array of paths:
|
||||
*
|
||||
* - `js`: An array of JavaScript URLs to load
|
||||
* - `css`: An array of CSS stylesheet URLs to load
|
||||
* - `img`: An array of image URLs to pre-load
|
||||
*
|
||||
* Both `js` and `css` files will be automatically injected, however `img` files will not.
|
||||
*
|
||||
* This method will return a Promise that resolves when all required assets are loaded. If an
|
||||
* asset fails to load, this Promise will be rejected.
|
||||
*
|
||||
* @param {Object} assets
|
||||
* @returns {Promise}
|
||||
*/
|
||||
load(assets) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const promises = [];
|
||||
|
||||
if (assets.js && assets.js.length > 0) {
|
||||
assets.js.forEach((script) => {
|
||||
promises.push(this.loadScript(script));
|
||||
});
|
||||
}
|
||||
|
||||
if (assets.css && assets.css.length > 0) {
|
||||
assets.css.forEach((style) => {
|
||||
promises.push(this.loadStyle(style));
|
||||
});
|
||||
}
|
||||
|
||||
if (assets.img && assets.img.length > 0) {
|
||||
assets.img.forEach((image) => {
|
||||
promises.push(this.loadImage(image));
|
||||
});
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
Promise.all(promises).then(
|
||||
() => {
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects and loads a JavaScript URL into the DOM.
|
||||
*
|
||||
* The script will be appended before the closing `</body>` tag.
|
||||
*
|
||||
* @param {String} script
|
||||
* @returns {Promise}
|
||||
*/
|
||||
loadScript(script) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Check that script is not already loaded
|
||||
const loaded = document.querySelector(`script[src="${script}"]`);
|
||||
if (loaded) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
// Create script
|
||||
const domScript = document.createElement('script');
|
||||
domScript.setAttribute('type', 'text/javascript');
|
||||
domScript.setAttribute('src', script);
|
||||
domScript.addEventListener('load', () => {
|
||||
this.snowboard.globalEvent('assetLoader.loaded', 'script', script, domScript);
|
||||
resolve();
|
||||
});
|
||||
domScript.addEventListener('error', () => {
|
||||
this.snowboard.globalEvent('assetLoader.error', 'script', script, domScript);
|
||||
reject(new Error(`Unable to load script file: "${script}"`));
|
||||
});
|
||||
document.body.append(domScript);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects and loads a CSS stylesheet into the DOM.
|
||||
*
|
||||
* The stylesheet will be appended before the closing `</head>` tag.
|
||||
*
|
||||
* @param {String} script
|
||||
* @returns {Promise}
|
||||
*/
|
||||
loadStyle(style) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Check that stylesheet is not already loaded
|
||||
const loaded = document.querySelector(`link[rel="stylesheet"][href="${style}"]`);
|
||||
if (loaded) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
// Create stylesheet
|
||||
const domCss = document.createElement('link');
|
||||
domCss.setAttribute('rel', 'stylesheet');
|
||||
domCss.setAttribute('href', style);
|
||||
domCss.addEventListener('load', () => {
|
||||
this.snowboard.globalEvent('assetLoader.loaded', 'style', style, domCss);
|
||||
resolve();
|
||||
});
|
||||
domCss.addEventListener('error', () => {
|
||||
this.snowboard.globalEvent('assetLoader.error', 'style', style, domCss);
|
||||
reject(new Error(`Unable to load stylesheet file: "${style}"`));
|
||||
});
|
||||
document.head.append(domCss);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-loads an image.
|
||||
*
|
||||
* The image will not be injected into the DOM.
|
||||
*
|
||||
* @param {String} image
|
||||
* @returns {Promise}
|
||||
*/
|
||||
loadImage(image) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.addEventListener('load', () => {
|
||||
this.snowboard.globalEvent('assetLoader.loaded', 'image', image, img);
|
||||
resolve();
|
||||
});
|
||||
img.addEventListener('error', () => {
|
||||
this.snowboard.globalEvent('assetLoader.error', 'image', image, img);
|
||||
reject(new Error(`Unable to load image file: "${image}"`));
|
||||
});
|
||||
img.src = image;
|
||||
});
|
||||
}
|
||||
}
|
212
modules/system/assets/js/snowboard/extras/DataConfig.js
Normal file
212
modules/system/assets/js/snowboard/extras/DataConfig.js
Normal file
@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Data configuration provider.
|
||||
*
|
||||
* Provides a mechanism for passing configuration data through an element's data attributes. This
|
||||
* is generally used for widgets or UI interactions to configure them.
|
||||
*
|
||||
* @copyright 2022 Winter.
|
||||
* @author Ben Thomson <git@alfreido.com>
|
||||
*/
|
||||
export default class DataConfig extends Snowboard.PluginBase {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param {Snowboard} snowboard
|
||||
* @param {Snowboard.PluginBase} instance
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
constructor(snowboard, instance, element) {
|
||||
super(snowboard);
|
||||
|
||||
if (instance instanceof Snowboard.PluginBase === false) {
|
||||
throw new Error('You must provide a Snowboard plugin to enable data configuration');
|
||||
}
|
||||
if (element instanceof HTMLElement === false) {
|
||||
throw new Error('Data configuration can only be extracted from HTML elements');
|
||||
}
|
||||
|
||||
this.instance = instance;
|
||||
this.element = element;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the config for this instance.
|
||||
*
|
||||
* If the `config` parameter is unspecified, returns the entire configuration.
|
||||
*
|
||||
* @param {string} config
|
||||
*/
|
||||
get(config) {
|
||||
if (config === undefined) {
|
||||
return this.instanceConfig;
|
||||
}
|
||||
|
||||
if (this.instanceConfig[config] !== undefined) {
|
||||
return this.instanceConfig[config];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the config for this instance.
|
||||
*
|
||||
* This allows you to override, at runtime, any configuration value as necessary.
|
||||
*
|
||||
* @param {string} config
|
||||
* @param {any} value
|
||||
* @param {boolean} persist
|
||||
*/
|
||||
set(config, value, persist) {
|
||||
if (config === undefined) {
|
||||
throw new Error('You must provide a configuration key to set');
|
||||
}
|
||||
|
||||
this.instanceConfig[config] = value;
|
||||
|
||||
if (persist === true) {
|
||||
this.element.dataset[config] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the configuration from the element.
|
||||
*
|
||||
* This will allow you to make changes to the data config on a DOM level and re-apply them
|
||||
* to the config on the JavaScript side.
|
||||
*/
|
||||
refresh() {
|
||||
this.acceptedConfigs = this.getAcceptedConfigs();
|
||||
this.instanceConfig = this.processConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the available configurations that can be set through the data config.
|
||||
*
|
||||
* If an instance has an `acceptAllDataConfigs` property, set to `true`, then all data
|
||||
* attributes will be available as configuration values. This can be a security concern, so
|
||||
* tread carefully.
|
||||
*
|
||||
* Otherwise, available configurations will be determined by the keys available in an object
|
||||
* returned by a `defaults()` method in the instance.
|
||||
*
|
||||
* @returns {string[]|boolean}
|
||||
*/
|
||||
getAcceptedConfigs() {
|
||||
if (
|
||||
this.instance.acceptAllDataConfigs !== undefined
|
||||
&& this.instance.acceptAllDataConfigs === true
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.instance.defaults !== undefined
|
||||
&& typeof this.instance.defaults === 'function'
|
||||
&& typeof this.instance.defaults() === 'object'
|
||||
) {
|
||||
return Object.keys(this.instance.defaults());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default values for the instance.
|
||||
*
|
||||
* This will be an empty object if the instance either does not have a `defaults()` method, or
|
||||
* the method itself does not return an object.
|
||||
*
|
||||
* @returns {object}
|
||||
*/
|
||||
getDefaults() {
|
||||
if (
|
||||
this.instance.defaults !== undefined
|
||||
&& typeof this.instance.defaults === 'function'
|
||||
&& typeof this.instance.defaults() === 'object'
|
||||
) {
|
||||
return this.instance.defaults();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the configuration.
|
||||
*
|
||||
* Loads up the defaults, then populates it with any configuration values provided by the data
|
||||
* attributes, based on the rules of the accepted configurations.
|
||||
*
|
||||
* This configuration object is then cached and available through `config.get()` calls.
|
||||
*
|
||||
* @returns {object}
|
||||
*/
|
||||
processConfig() {
|
||||
const config = this.getDefaults();
|
||||
|
||||
if (this.acceptedConfigs === false) {
|
||||
return config;
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
for (const key in this.element.dataset) {
|
||||
if (this.acceptedConfigs === true || this.acceptedConfigs.includes(key)) {
|
||||
config[key] = this.coerceValue(this.element.dataset[key]);
|
||||
}
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces configuration values for JavaScript.
|
||||
*
|
||||
* Takes the string value returned from the data attribute and coerces it into a more suitable
|
||||
* type for JavaScript processing.
|
||||
*
|
||||
* @param {*} value
|
||||
* @returns {*}
|
||||
*/
|
||||
coerceValue(value) {
|
||||
const stringValue = String(value);
|
||||
|
||||
// Null value
|
||||
if (stringValue === 'null') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Undefined value
|
||||
if (stringValue === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Base64 value
|
||||
if (stringValue.startsWith('base64:')) {
|
||||
const base64str = stringValue.replace(/^base64:/, '');
|
||||
const decoded = atob(base64str);
|
||||
return this.coerceValue(decoded);
|
||||
}
|
||||
|
||||
// Boolean value
|
||||
if (['true', 'yes'].includes(stringValue.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
if (['false', 'no'].includes(stringValue.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Numeric value
|
||||
if (/^[-+]?[0-9]+(\.[0-9]+)?$/.test(stringValue)) {
|
||||
return Number(stringValue);
|
||||
}
|
||||
|
||||
// JSON value
|
||||
try {
|
||||
return this.snowboard.jsonParser().parse(stringValue);
|
||||
} catch (e) {
|
||||
return (stringValue === '') ? true : stringValue;
|
||||
}
|
||||
}
|
||||
}
|
@ -29,7 +29,9 @@ export default class Snowboard {
|
||||
this.debugEnabled = (typeof debug === 'boolean' && debug === true);
|
||||
this.autoInitSingletons = (typeof autoSingletons === 'boolean' && autoSingletons === false);
|
||||
this.plugins = {};
|
||||
this.listeners = {};
|
||||
this.foundBaseUrl = null;
|
||||
this.domReady = false;
|
||||
|
||||
this.attachAbstracts();
|
||||
this.loadUtilities();
|
||||
@ -77,6 +79,7 @@ export default class Snowboard {
|
||||
this.initialiseSingletons();
|
||||
}
|
||||
this.globalEvent('ready');
|
||||
this.domReady = true;
|
||||
});
|
||||
}
|
||||
|
||||
@ -127,7 +130,8 @@ export default class Snowboard {
|
||||
|
||||
this.debug(`Plugin "${name}" registered`);
|
||||
|
||||
// Check if any singletons now have their dependencies fulfilled, and fire their "ready" handler.
|
||||
// Check if any singletons now have their dependencies fulfilled, and fire their "ready" handler if we're
|
||||
// in a ready state.
|
||||
Object.values(this.getPlugins()).forEach((plugin) => {
|
||||
if (
|
||||
plugin.isSingleton()
|
||||
@ -135,6 +139,7 @@ export default class Snowboard {
|
||||
&& plugin.dependenciesFulfilled()
|
||||
&& plugin.hasMethod('listens')
|
||||
&& Object.keys(plugin.callMethod('listens')).includes('ready')
|
||||
&& this.domReady
|
||||
) {
|
||||
const readyMethod = plugin.callMethod('listens').ready;
|
||||
plugin.callMethod(readyMethod);
|
||||
@ -249,6 +254,61 @@ export default class Snowboard {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a simple ready listener.
|
||||
*
|
||||
* Synonymous with jQuery's "$(document).ready()" functionality, this allows inline scripts to
|
||||
* attach themselves to Snowboard immediately but only fire when the DOM is ready.
|
||||
*
|
||||
* @param {Function} callback
|
||||
*/
|
||||
ready(callback) {
|
||||
if (this.domReady) {
|
||||
callback();
|
||||
}
|
||||
|
||||
this.on('ready', callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a simple listener for an event.
|
||||
*
|
||||
* This can be used for ad-hoc scripts that don't need a full plugin. The given callback will be
|
||||
* called when the event name provided fires. This works for both normal and Promise events. For
|
||||
* a Promise event, your callback must return a Promise.
|
||||
*
|
||||
* @param {String} eventName
|
||||
* @param {Function} callback
|
||||
*/
|
||||
on(eventName, callback) {
|
||||
if (!this.listeners[eventName]) {
|
||||
this.listeners[eventName] = [];
|
||||
}
|
||||
|
||||
if (!this.listeners[eventName].includes(callback)) {
|
||||
this.listeners[eventName].push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a simple listener for an event.
|
||||
*
|
||||
* @param {String} eventName
|
||||
* @param {Function} callback
|
||||
*/
|
||||
off(eventName, callback) {
|
||||
if (!this.listeners[eventName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.listeners[eventName].indexOf(callback);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listeners[eventName].splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a global event to all registered plugins.
|
||||
*
|
||||
@ -260,7 +320,7 @@ export default class Snowboard {
|
||||
globalEvent(eventName, ...parameters) {
|
||||
this.debug(`Calling global event "${eventName}"`);
|
||||
|
||||
// Find out which plugins listen to this event - if none listen to it, return true.
|
||||
// Find plugins listening to the event.
|
||||
const listeners = this.listensToEvent(eventName);
|
||||
if (listeners.length === 0) {
|
||||
this.debug(`No listeners found for global event "${eventName}"`);
|
||||
@ -300,6 +360,23 @@ export default class Snowboard {
|
||||
});
|
||||
});
|
||||
|
||||
// Find ad-hoc listeners for this event.
|
||||
if (!cancelled && this.listeners[eventName] && this.listeners[eventName].length > 0) {
|
||||
this.debug(`Found ${this.listeners[eventName].length} ad-hoc listener(s) for global event "${eventName}"`);
|
||||
|
||||
this.listeners[eventName].forEach((listener) => {
|
||||
// If a listener has cancelled the event, no further listeners are considered.
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listener(...parameters) === false) {
|
||||
cancelled = true;
|
||||
this.debug(`Global event "${eventName} cancelled by an ad-hoc listener.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return !cancelled;
|
||||
}
|
||||
|
||||
@ -314,7 +391,7 @@ export default class Snowboard {
|
||||
globalPromiseEvent(eventName, ...parameters) {
|
||||
this.debug(`Calling global promise event "${eventName}"`);
|
||||
|
||||
// Find out which plugins listen to this event - if none listen to it, return a resolved promise.
|
||||
// Find plugins listening to this event.
|
||||
const listeners = this.listensToEvent(eventName);
|
||||
if (listeners.length === 0) {
|
||||
this.debug(`No listeners found for global promise event "${eventName}"`);
|
||||
@ -347,6 +424,20 @@ export default class Snowboard {
|
||||
});
|
||||
});
|
||||
|
||||
// Find ad-hoc listeners listening to this event.
|
||||
if (this.listeners[eventName] && this.listeners[eventName].length > 0) {
|
||||
this.debug(`Found ${this.listeners[eventName].length} ad-hoc listener(s) for global promise event "${eventName}"`);
|
||||
|
||||
this.listeners[eventName].forEach((listener) => {
|
||||
const listenerPromise = listener(...parameters);
|
||||
if (listenerPromise instanceof Promise === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
promises.push(listenerPromise);
|
||||
});
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -3,12 +3,16 @@ import Transition from './extras/Transition';
|
||||
import AttachLoading from './extras/AttachLoading';
|
||||
import StripeLoader from './extras/StripeLoader';
|
||||
import StylesheetLoader from './extras/StylesheetLoader';
|
||||
import AssetLoader from './extras/AssetLoader';
|
||||
import DataConfig from './extras/DataConfig';
|
||||
|
||||
if (window.Snowboard === undefined) {
|
||||
throw new Error('Snowboard must be loaded in order to use the extra plugins.');
|
||||
}
|
||||
|
||||
((Snowboard) => {
|
||||
Snowboard.addPlugin('assetLoader', AssetLoader);
|
||||
Snowboard.addPlugin('dataConfig', DataConfig);
|
||||
Snowboard.addPlugin('extrasStyles', StylesheetLoader);
|
||||
Snowboard.addPlugin('transition', Transition);
|
||||
Snowboard.addPlugin('flash', Flash);
|
||||
|
@ -4,12 +4,16 @@ import Transition from './extras/Transition';
|
||||
import AttachLoading from './extras/AttachLoading';
|
||||
import StripeLoader from './extras/StripeLoader';
|
||||
import StylesheetLoader from './extras/StylesheetLoader';
|
||||
import AssetLoader from './extras/AssetLoader';
|
||||
import DataConfig from './extras/DataConfig';
|
||||
|
||||
if (window.Snowboard === undefined) {
|
||||
throw new Error('Snowboard must be loaded in order to use the extra plugins.');
|
||||
}
|
||||
|
||||
((Snowboard) => {
|
||||
Snowboard.addPlugin('assetLoader', AssetLoader);
|
||||
Snowboard.addPlugin('dataConfig', DataConfig);
|
||||
Snowboard.addPlugin('extrasStyles', StylesheetLoader);
|
||||
Snowboard.addPlugin('transition', Transition);
|
||||
Snowboard.addPlugin('flash', Flash);
|
||||
|
@ -24,9 +24,7 @@
|
||||
},
|
||||
"homepage": "https://wintercms.com/",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.4",
|
||||
"js-cookie": "^3.0.1",
|
||||
"vue": "^3.2.31"
|
||||
"js-cookie": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
@ -35,8 +33,6 @@
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-vue": "^8.5.0",
|
||||
"laravel-mix": "^6.0.34",
|
||||
"laravel-mix-polyfill": "^3.0.1",
|
||||
"less-loader": "^10.2.0",
|
||||
"vue-loader": "^16.8.3"
|
||||
"laravel-mix-polyfill": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -12,20 +12,12 @@ mix
|
||||
},
|
||||
runtimeChunkPath: './assets/js/build',
|
||||
})
|
||||
.vue({ version: 3 })
|
||||
|
||||
// Extract imported libraries
|
||||
.extract({
|
||||
libraries: ['js-cookie'],
|
||||
to: './assets/js/snowboard/build/snowboard.vendor.js',
|
||||
})
|
||||
.extract({
|
||||
libraries: [
|
||||
'@popperjs/core',
|
||||
'vue',
|
||||
],
|
||||
to: './assets/js/build/vendor.js',
|
||||
})
|
||||
|
||||
// Compile Snowboard for the Backend / System
|
||||
.js(
|
||||
|
@ -4,6 +4,7 @@
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"modules/backend",
|
||||
"modules/system"
|
||||
]
|
||||
}
|
||||
|
@ -8,7 +8,8 @@
|
||||
"module-resolver", {
|
||||
"root": ["."],
|
||||
"alias": {
|
||||
"helpers": "./helpers"
|
||||
"helpers": "./helpers",
|
||||
"snowboard": "../../modules/system/assets/js/snowboard",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
316
tests/js/cases/snowboard/extras/DataConfig.test.js
Normal file
316
tests/js/cases/snowboard/extras/DataConfig.test.js
Normal file
@ -0,0 +1,316 @@
|
||||
import FakeDom from '../../../helpers/FakeDom';
|
||||
|
||||
jest.setTimeout(2000);
|
||||
|
||||
describe('The Data Config extra functionality', function () {
|
||||
it('can read the config from an element\'s data attributes', function (done) {
|
||||
FakeDom
|
||||
.new()
|
||||
.addCss([
|
||||
'modules/system/assets/css/snowboard.extras.css',
|
||||
])
|
||||
.addScript([
|
||||
'modules/system/assets/js/build/manifest.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.base.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.extras.js',
|
||||
'tests/js/fixtures/dataConfig/DataConfigFixture.js',
|
||||
])
|
||||
.render(
|
||||
`<div
|
||||
id="testElement"
|
||||
data-id="389"
|
||||
data-string-value="Hi there"
|
||||
data-boolean="true"
|
||||
></div>
|
||||
|
||||
<div
|
||||
id="testElementTwo"
|
||||
data-string-value="Hi there again"
|
||||
data-name="Ben"
|
||||
data-boolean="false"
|
||||
data-extra-attr="This should not be available"
|
||||
data-base64="base64:SSdtIGEgQmFzZTY0LWRlY29kZWQgc3RyaW5n"
|
||||
></div>`
|
||||
)
|
||||
.then(
|
||||
(dom) => {
|
||||
const instance = dom.window.Snowboard.dataConfigFixture(
|
||||
dom.window.document.querySelector('#testElement')
|
||||
);
|
||||
|
||||
try {
|
||||
expect(instance.config.get('id')).toEqual(389);
|
||||
// Name should be null as it's the default value and not specified above
|
||||
expect(instance.config.get('name')).toBeNull();
|
||||
expect(instance.config.get('stringValue')).toBe('Hi there');
|
||||
// Missing should be undefined as it's neither defined nor part of the default data
|
||||
expect(instance.config.get('missing')).toBeUndefined();
|
||||
expect(instance.config.get('boolean')).toBe(true);
|
||||
|
||||
expect(instance.config.get()).toMatchObject({
|
||||
id: 389,
|
||||
name: null,
|
||||
stringValue: 'Hi there',
|
||||
boolean: true,
|
||||
base64: null,
|
||||
});
|
||||
} catch (error) {
|
||||
done(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const instanceTwo = dom.window.Snowboard.dataConfigFixture(
|
||||
dom.window.document.querySelector('#testElementTwo')
|
||||
);
|
||||
|
||||
try {
|
||||
// ID is null as it's the default value and not specified above
|
||||
expect(instanceTwo.config.get('id')).toBeNull();
|
||||
expect(instanceTwo.config.get('name')).toBe('Ben');
|
||||
expect(instanceTwo.config.get('stringValue')).toBe('Hi there again');
|
||||
expect(instanceTwo.config.get('missing')).toBeUndefined();
|
||||
expect(instanceTwo.config.get('boolean')).toBe(false);
|
||||
// Extra attr is specified above, but it should not be available as a config value
|
||||
// because it's not part of the `defaults()` in the fixture
|
||||
expect(instanceTwo.config.get('extraAttr')).toBeUndefined();
|
||||
// Base-64 decoded string
|
||||
expect(instanceTwo.config.get('base64')).toBe('I\'m a Base64-decoded string');
|
||||
|
||||
done();
|
||||
} catch (error) {
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('can read the config from every data attribute of an element with "acceptAllDataConfigs" enabled', function (done) {
|
||||
FakeDom
|
||||
.new()
|
||||
.addCss([
|
||||
'modules/system/assets/css/snowboard.extras.css',
|
||||
])
|
||||
.addScript([
|
||||
'modules/system/assets/js/build/manifest.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.base.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.extras.js',
|
||||
'tests/js/fixtures/dataConfig/DataConfigFixture.js',
|
||||
])
|
||||
.render(
|
||||
`<div
|
||||
id="testElementTwo"
|
||||
data-string-value="Hi there again"
|
||||
data-name="Ben"
|
||||
data-boolean="false"
|
||||
data-extra-attr="This should now be available"
|
||||
data-json="{ "name": "Ben" }"
|
||||
data-another-base64="base64:dHJ1ZQ=="
|
||||
data-json-base64="base64:eyAiaWQiOiAxLCAidGl0bGUiOiAiU29tZSB0aXRsZSIgfQ=="
|
||||
></div>`
|
||||
)
|
||||
.then(
|
||||
(dom) => {
|
||||
const instance = dom.window.Snowboard.dataConfigFixture(
|
||||
dom.window.document.querySelector('#testElementTwo')
|
||||
);
|
||||
instance.acceptAllDataConfigs = true;
|
||||
instance.config.refresh();
|
||||
|
||||
try {
|
||||
// ID is null as it's the default value and not specified above
|
||||
expect(instance.config.get('id')).toBeNull();
|
||||
expect(instance.config.get('name')).toBe('Ben');
|
||||
expect(instance.config.get('stringValue')).toBe('Hi there again');
|
||||
expect(instance.config.get('missing')).toBeUndefined();
|
||||
expect(instance.config.get('boolean')).toBe(false);
|
||||
// These attributes below are specified above, and although they're not part of the
|
||||
// defaults, they should be available because "acceptAllDataConfigs" is true
|
||||
expect(instance.config.get('extraAttr')).toBe('This should now be available');
|
||||
expect(instance.config.get('json')).toMatchObject({
|
||||
name: 'Ben'
|
||||
});
|
||||
expect(instance.config.get('anotherBase64')).toBe(true);
|
||||
expect(instance.config.get('jsonBase64')).toMatchObject({
|
||||
id: 1,
|
||||
title: 'Some title',
|
||||
});
|
||||
|
||||
expect(instance.config.get()).toMatchObject({
|
||||
id: null,
|
||||
name: 'Ben',
|
||||
stringValue: 'Hi there again',
|
||||
boolean: false,
|
||||
extraAttr: 'This should now be available',
|
||||
json: {
|
||||
name: 'Ben',
|
||||
},
|
||||
anotherBase64: true,
|
||||
jsonBase64: {
|
||||
id: 1,
|
||||
title: 'Some title',
|
||||
},
|
||||
});
|
||||
|
||||
done();
|
||||
} catch (error) {
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('can refresh the config from the data attributes on the fly', function (done) {
|
||||
FakeDom
|
||||
.new()
|
||||
.addCss([
|
||||
'modules/system/assets/css/snowboard.extras.css',
|
||||
])
|
||||
.addScript([
|
||||
'modules/system/assets/js/build/manifest.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.base.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.extras.js',
|
||||
'tests/js/fixtures/dataConfig/DataConfigFixture.js',
|
||||
])
|
||||
.render(
|
||||
`<div
|
||||
id="testElement"
|
||||
data-string-value="Hi there again"
|
||||
data-name="Ben"
|
||||
data-boolean="no"
|
||||
></div>`
|
||||
)
|
||||
.then(
|
||||
(dom) => {
|
||||
const instance = dom.window.Snowboard.dataConfigFixture(
|
||||
dom.window.document.querySelector('#testElement')
|
||||
);
|
||||
|
||||
try {
|
||||
expect(instance.config.get('id')).toBeNull();
|
||||
expect(instance.config.get('name')).toBe('Ben');
|
||||
expect(instance.config.get('stringValue')).toBe('Hi there again');
|
||||
expect(instance.config.get('boolean')).toBe(false);
|
||||
|
||||
expect(instance.config.get()).toMatchObject({
|
||||
id: null,
|
||||
name: 'Ben',
|
||||
stringValue: 'Hi there again',
|
||||
boolean: false,
|
||||
});
|
||||
|
||||
dom.window.document.querySelector('#testElement').setAttribute('data-id', '456');
|
||||
dom.window.document.querySelector('#testElement').setAttribute('data-string-value', 'Changed');
|
||||
dom.window.document.querySelector('#testElement').removeAttribute('data-boolean');
|
||||
|
||||
// Refresh config
|
||||
instance.config.refresh();
|
||||
|
||||
expect(instance.config.get('id')).toBe(456);
|
||||
expect(instance.config.get('name')).toBe('Ben');
|
||||
expect(instance.config.get('stringValue')).toBe('Changed');
|
||||
expect(instance.config.get('boolean')).toBeNull();
|
||||
|
||||
expect(instance.config.get()).toMatchObject({
|
||||
id: 456,
|
||||
name: 'Ben',
|
||||
stringValue: 'Changed',
|
||||
boolean: null,
|
||||
});
|
||||
|
||||
done();
|
||||
} catch (error) {
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('can set config values at runtime', function (done) {
|
||||
FakeDom
|
||||
.new()
|
||||
.addCss([
|
||||
'modules/system/assets/css/snowboard.extras.css',
|
||||
])
|
||||
.addScript([
|
||||
'modules/system/assets/js/build/manifest.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.base.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.extras.js',
|
||||
'tests/js/fixtures/dataConfig/DataConfigFixture.js',
|
||||
])
|
||||
.render(
|
||||
`<div
|
||||
id="testElement"
|
||||
data-string-value="Hi there again"
|
||||
data-name="Ben"
|
||||
data-boolean="false"
|
||||
></div>`
|
||||
)
|
||||
.then(
|
||||
(dom) => {
|
||||
const instance = dom.window.Snowboard.dataConfigFixture(
|
||||
dom.window.document.querySelector('#testElement')
|
||||
);
|
||||
|
||||
try {
|
||||
expect(instance.config.get('name')).toBe('Ben');
|
||||
// Set config
|
||||
instance.config.set('name', 'Luke');
|
||||
expect(instance.config.get('name')).toBe('Luke');
|
||||
// Refresh config
|
||||
instance.config.refresh();
|
||||
expect(instance.config.get('name')).toBe('Ben');
|
||||
done();
|
||||
} catch (error) {
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('can set config values at runtime that persist through a reset', function (done) {
|
||||
FakeDom
|
||||
.new()
|
||||
.addCss([
|
||||
'modules/system/assets/css/snowboard.extras.css',
|
||||
])
|
||||
.addScript([
|
||||
'modules/system/assets/js/build/manifest.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.vendor.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.base.js',
|
||||
'modules/system/assets/js/snowboard/build/snowboard.extras.js',
|
||||
'tests/js/fixtures/dataConfig/DataConfigFixture.js',
|
||||
])
|
||||
.render(
|
||||
`<div
|
||||
id="testElement"
|
||||
data-string-value="Hi there again"
|
||||
data-name="Ben"
|
||||
data-boolean="no"
|
||||
></div>`
|
||||
)
|
||||
.then(
|
||||
(dom) => {
|
||||
const instance = dom.window.Snowboard.dataConfigFixture(
|
||||
dom.window.document.querySelector('#testElement')
|
||||
);
|
||||
|
||||
try {
|
||||
expect(instance.config.get('name')).toBe('Ben');
|
||||
// Set config
|
||||
instance.config.set('name', 'Luke', true);
|
||||
expect(instance.config.get('name')).toBe('Luke');
|
||||
// Refresh config
|
||||
instance.config.refresh();
|
||||
expect(instance.config.get('name')).toBe('Luke');
|
||||
done();
|
||||
} catch (error) {
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
28
tests/js/fixtures/dataConfig/DataConfigFixture.js
Normal file
28
tests/js/fixtures/dataConfig/DataConfigFixture.js
Normal file
@ -0,0 +1,28 @@
|
||||
/* globals window */
|
||||
|
||||
((Snowboard) => {
|
||||
class DataConfigFixture extends Snowboard.PluginBase {
|
||||
constructor(snowboard, element) {
|
||||
super(snowboard);
|
||||
|
||||
this.element = element;
|
||||
this.config = this.snowboard.dataConfig(this, element);
|
||||
}
|
||||
|
||||
dependencies() {
|
||||
return ['dataConfig'];
|
||||
}
|
||||
|
||||
defaults() {
|
||||
return {
|
||||
id: null,
|
||||
name: null,
|
||||
stringValue: null,
|
||||
boolean: null,
|
||||
base64: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Snowboard.addPlugin('dataConfigFixture', DataConfigFixture);
|
||||
})(window.Snowboard);
|
@ -8,68 +8,98 @@ export default class FakeDom
|
||||
constructor(content, options)
|
||||
{
|
||||
if (options === undefined) {
|
||||
options = {}
|
||||
options = {};
|
||||
}
|
||||
|
||||
// Header settings
|
||||
this.url = options.url || `file://${path.resolve(__dirname, '../../')}`
|
||||
this.referer = options.referer
|
||||
this.contentType = options.contentType || 'text/html'
|
||||
this.url = options.url || `file://${path.resolve(__dirname, '../../')}`;
|
||||
this.referer = options.referer;
|
||||
this.contentType = options.contentType || 'text/html';
|
||||
|
||||
// Content settings
|
||||
this.head = options.head || '<!DOCTYPE html><html><head><title>Fake document</title></head>'
|
||||
this.bodyStart = options.bodyStart || '<body>'
|
||||
this.content = content || ''
|
||||
this.bodyEnd = options.bodyEnd || '</body>'
|
||||
this.foot = options.foot || '</html>'
|
||||
this.headStart = options.headStart || '<!DOCTYPE html><html><head><title>Fake document</title>';
|
||||
this.headEnd = options.headEnd || '</head>';
|
||||
this.bodyStart = options.bodyStart || '<body>';
|
||||
this.content = content || '';
|
||||
this.bodyEnd = options.bodyEnd || '</body>';
|
||||
this.foot = options.foot || '</html>';
|
||||
|
||||
// Callback settings
|
||||
this.beforeParse = (typeof options.beforeParse === 'function')
|
||||
? options.beforeParse
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
// Scripts
|
||||
this.scripts = []
|
||||
this.inline = []
|
||||
// Assets
|
||||
this.css = [];
|
||||
this.scripts = [];
|
||||
this.inline = [];
|
||||
}
|
||||
|
||||
static new(content, options)
|
||||
{
|
||||
return new FakeDom(content, options)
|
||||
return new FakeDom(content, options);
|
||||
}
|
||||
|
||||
setContent(content)
|
||||
{
|
||||
this.content = content
|
||||
return this
|
||||
this.content = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
addScript(script, id)
|
||||
{
|
||||
if (Array.isArray(script)) {
|
||||
script.forEach((item) => {
|
||||
this.addScript(item)
|
||||
})
|
||||
this.addScript(item);
|
||||
});
|
||||
|
||||
return this
|
||||
return this;
|
||||
}
|
||||
|
||||
let url = new URL(script, this.url)
|
||||
let base = new URL(this.url)
|
||||
let url = new URL(script, this.url);
|
||||
let base = new URL(this.url);
|
||||
|
||||
if (url.host === base.host) {
|
||||
this.scripts.push({
|
||||
url: `${url.pathname}`,
|
||||
id: id || this.generateId(),
|
||||
})
|
||||
});
|
||||
} else {
|
||||
this.scripts.push({
|
||||
url,
|
||||
id: id || this.generateId(),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return this
|
||||
return this;
|
||||
}
|
||||
|
||||
addCss(css, id)
|
||||
{
|
||||
if (Array.isArray(css)) {
|
||||
css.forEach((item) => {
|
||||
this.addCss(item)
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
let url = new URL(css, this.url);
|
||||
let base = new URL(this.url);
|
||||
|
||||
if (url.host === base.host) {
|
||||
this.css.push({
|
||||
url: `${url.pathname}`,
|
||||
id: id || this.generateId(),
|
||||
});
|
||||
} else {
|
||||
this.css.push({
|
||||
url,
|
||||
id: id || this.generateId(),
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addInlineScript(script, id)
|
||||
@ -78,23 +108,23 @@ export default class FakeDom
|
||||
script,
|
||||
id: id || this.generateId(),
|
||||
element: null,
|
||||
})
|
||||
});
|
||||
|
||||
return this
|
||||
return this;
|
||||
}
|
||||
|
||||
generateId()
|
||||
{
|
||||
let id = 'script-'
|
||||
let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-'
|
||||
let charLength = chars.length
|
||||
let id = 'script-';
|
||||
let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
|
||||
let charLength = chars.length;
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
let currentChar = chars.substr(Math.floor(Math.random() * charLength), 1)
|
||||
id = `${id}${currentChar}`
|
||||
let currentChar = chars.substr(Math.floor(Math.random() * charLength), 1);
|
||||
id = `${id}${currentChar}`;
|
||||
}
|
||||
|
||||
return id
|
||||
return id;
|
||||
}
|
||||
|
||||
render(content)
|
||||
@ -116,41 +146,44 @@ export default class FakeDom
|
||||
pretendToBeVisual: true,
|
||||
beforeParse: this.beforeParse,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
dom.window.resolver = () => {
|
||||
resolve(dom)
|
||||
}
|
||||
resolve(dom);
|
||||
};
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
_renderContent()
|
||||
{
|
||||
// Create content list
|
||||
const content = [
|
||||
this.head,
|
||||
this.bodyStart,
|
||||
this.content,
|
||||
]
|
||||
const content = [this.headStart];
|
||||
|
||||
// Embed CSS
|
||||
this.css.forEach((css) => {
|
||||
content.push(`<link rel="stylesheet" href="${css.url}" id="${css.id}">`);
|
||||
});
|
||||
|
||||
content.push(this.headEnd, this.bodyStart, this.content);
|
||||
|
||||
// Embed scripts
|
||||
this.scripts.forEach((script) => {
|
||||
content.push(`<script src="${script.url}" id="${script.id}"></script>`)
|
||||
})
|
||||
content.push(`<script src="${script.url}" id="${script.id}"></script>`);
|
||||
});
|
||||
this.inline.forEach((script) => {
|
||||
content.push(`<script id="${script.id}">${script.script}</script>`)
|
||||
})
|
||||
content.push(`<script id="${script.id}">${script.script}</script>`);
|
||||
});
|
||||
|
||||
// Add resolver
|
||||
content.push(`<script>window.resolver()</script>`)
|
||||
content.push(`<script>window.resolver()</script>`);
|
||||
|
||||
// Add final content
|
||||
content.push(this.bodyEnd)
|
||||
content.push(this.foot)
|
||||
content.push(this.bodyEnd);
|
||||
content.push(this.foot);
|
||||
|
||||
return content.join('\n')
|
||||
return content.join('\n');
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user