feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
This commit is contained in:
43
modules/system/assets/js/snowboard/main/InnerProxyHandler.js
Normal file
43
modules/system/assets/js/snowboard/main/InnerProxyHandler.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Internal proxy for Snowboard.
|
||||
*
|
||||
* This handler wraps the Snowboard instance that is passed to the constructor of plugin instances.
|
||||
* It prevents access to the following methods:
|
||||
* - `attachAbstracts`: No need to attach abstracts again.
|
||||
* - `loadUtilties`: No need to load utilities again.
|
||||
* - `initialise`: Snowboard is already initialised.
|
||||
* - `initialiseSingletons`: Singletons are already initialised.
|
||||
*/
|
||||
export default {
|
||||
get(target, prop, receiver) {
|
||||
if (typeof prop === 'string') {
|
||||
const propLower = prop.toLowerCase();
|
||||
|
||||
if (['attachAbstracts', 'loadUtilities', 'initialise', 'initialiseSingletons'].includes(prop)) {
|
||||
throw new Error(`You cannot use the "${prop}" Snowboard method within a plugin.`);
|
||||
}
|
||||
|
||||
if (target.hasPlugin(propLower)) {
|
||||
return (...params) => Reflect.get(target, 'plugins')[propLower].getInstance(...params);
|
||||
}
|
||||
}
|
||||
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
|
||||
has(target, prop) {
|
||||
if (typeof prop === 'string') {
|
||||
const propLower = prop.toLowerCase();
|
||||
|
||||
if (['attachAbstracts', 'loadUtilities', 'initialise', 'initialiseSingletons'].includes(prop)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.hasPlugin(propLower)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return Reflect.has(target, prop);
|
||||
},
|
||||
};
|
||||
293
modules/system/assets/js/snowboard/main/PluginLoader.js
Normal file
293
modules/system/assets/js/snowboard/main/PluginLoader.js
Normal file
@@ -0,0 +1,293 @@
|
||||
import PluginBase from '../abstracts/PluginBase';
|
||||
import Singleton from '../abstracts/Singleton';
|
||||
import InnerProxyHandler from './InnerProxyHandler';
|
||||
|
||||
/**
|
||||
* Plugin loader class.
|
||||
*
|
||||
* This is a provider (factory) class for a single plugin and provides the link between Snowboard framework functionality
|
||||
* and the underlying plugin instances. It also provides some basic mocking of plugin methods for testing.
|
||||
*
|
||||
* @copyright 2021 Winter.
|
||||
* @author Ben Thomson <git@alfreido.com>
|
||||
*/
|
||||
export default class PluginLoader {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Binds the Winter framework to the instance.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Snowboard} snowboard
|
||||
* @param {PluginBase} instance
|
||||
*/
|
||||
constructor(name, snowboard, instance) {
|
||||
this.name = name;
|
||||
this.snowboard = new Proxy(
|
||||
snowboard,
|
||||
InnerProxyHandler,
|
||||
);
|
||||
this.instance = instance;
|
||||
|
||||
// Freeze instance that has been inserted into this loader
|
||||
Object.freeze(this.instance);
|
||||
|
||||
this.instances = [];
|
||||
this.singleton = {
|
||||
initialised: false,
|
||||
};
|
||||
// Prevent further extension of the singleton status object
|
||||
Object.seal(this.singleton);
|
||||
|
||||
this.mocks = {};
|
||||
this.originalFunctions = {};
|
||||
|
||||
// Freeze loader itself
|
||||
Object.freeze(PluginLoader.prototype);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current plugin has a specific method available.
|
||||
*
|
||||
* Returns false if the current plugin is a callback function.
|
||||
*
|
||||
* @param {string} methodName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasMethod(methodName) {
|
||||
if (this.isFunction()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (typeof this.instance.prototype[methodName] === 'function');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a prototype method for a plugin. This should generally be used for "static" calls.
|
||||
*
|
||||
* @param {string} methodName
|
||||
* @param {...} args
|
||||
* @returns {any}
|
||||
*/
|
||||
callMethod(...parameters) {
|
||||
if (this.isFunction()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const args = parameters;
|
||||
const methodName = args.shift();
|
||||
|
||||
return this.instance.prototype[methodName](args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of the current plugin.
|
||||
*
|
||||
* - If this is a callback function plugin, the function will be returned.
|
||||
* - If this is a singleton, the single instance of the plugin will be returned.
|
||||
*
|
||||
* @returns {PluginBase|Function}
|
||||
*/
|
||||
getInstance(...parameters) {
|
||||
if (this.isFunction()) {
|
||||
return this.instance(...parameters);
|
||||
}
|
||||
if (!this.dependenciesFulfilled()) {
|
||||
const unmet = this.getDependencies().filter((item) => !this.snowboard.getPluginNames().includes(item));
|
||||
throw new Error(`The "${this.name}" plugin requires the following plugins: ${unmet.join(', ')}`);
|
||||
}
|
||||
if (this.isSingleton()) {
|
||||
if (this.instances.length === 0) {
|
||||
this.initialiseSingleton(...parameters);
|
||||
}
|
||||
|
||||
// Apply mocked methods
|
||||
if (Object.keys(this.mocks).length > 0) {
|
||||
Object.entries(this.originalFunctions).forEach((entry) => {
|
||||
const [methodName, callback] = entry;
|
||||
this.instances[0][methodName] = callback;
|
||||
});
|
||||
Object.entries(this.mocks).forEach((entry) => {
|
||||
const [methodName, callback] = entry;
|
||||
this.instances[0][methodName] = (...params) => callback(this, ...params);
|
||||
});
|
||||
}
|
||||
|
||||
return this.instances[0];
|
||||
}
|
||||
|
||||
// Apply mocked methods to prototype
|
||||
if (Object.keys(this.mocks).length > 0) {
|
||||
Object.entries(this.originalFunctions).forEach((entry) => {
|
||||
const [methodName, callback] = entry;
|
||||
this.instance.prototype[methodName] = callback;
|
||||
});
|
||||
Object.entries(this.mocks).forEach((entry) => {
|
||||
const [methodName, callback] = entry;
|
||||
this.instance.prototype[methodName] = (...params) => callback(this, ...params);
|
||||
});
|
||||
}
|
||||
|
||||
const newInstance = new this.instance(this.snowboard, ...parameters);
|
||||
newInstance.detach = () => this.instances.splice(this.instances.indexOf(newInstance), 1);
|
||||
newInstance.construct(...parameters);
|
||||
this.instances.push(newInstance);
|
||||
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all instances of the current plugin.
|
||||
*
|
||||
* If this plugin is a callback function plugin, an empty array will be returned.
|
||||
*
|
||||
* @returns {PluginBase[]}
|
||||
*/
|
||||
getInstances() {
|
||||
if (this.isFunction()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current plugin is a simple callback function.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isFunction() {
|
||||
return (typeof this.instance === 'function' && this.instance.prototype instanceof PluginBase === false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current plugin is a singleton.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSingleton() {
|
||||
return this.instance.prototype instanceof Singleton === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a singleton has been initialised.
|
||||
*
|
||||
* Normal plugins will always return true.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isInitialised() {
|
||||
if (!this.isSingleton()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.singleton.initialised;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the singleton instance.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
initialiseSingleton(...parameters) {
|
||||
if (!this.isSingleton()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newInstance = new this.instance(this.snowboard, ...parameters);
|
||||
newInstance.detach = () => this.instances.splice(this.instances.indexOf(newInstance), 1);
|
||||
newInstance.construct(...parameters);
|
||||
this.instances.push(newInstance);
|
||||
this.singleton.initialised = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dependencies of the current plugin.
|
||||
*
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getDependencies() {
|
||||
// Callback functions cannot have dependencies.
|
||||
if (this.isFunction()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// No dependency method specified.
|
||||
if (typeof this.instance.prototype.dependencies !== 'function') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.instance.prototype.dependencies().map((item) => item.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current plugin has all its dependencies fulfilled.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
dependenciesFulfilled() {
|
||||
const dependencies = this.getDependencies();
|
||||
|
||||
let fulfilled = true;
|
||||
dependencies.forEach((plugin) => {
|
||||
if (!this.snowboard.hasPlugin(plugin)) {
|
||||
fulfilled = false;
|
||||
}
|
||||
});
|
||||
|
||||
return fulfilled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows a method of an instance to be mocked for testing.
|
||||
*
|
||||
* This mock will be applied for the life of an instance. For singletons, the mock will be applied for the life
|
||||
* of the page.
|
||||
*
|
||||
* Mocks cannot be applied to callback function plugins.
|
||||
*
|
||||
* @param {string} methodName
|
||||
* @param {Function} callback
|
||||
*/
|
||||
mock(methodName, callback) {
|
||||
if (this.isFunction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.instance.prototype[methodName]) {
|
||||
throw new Error(`Function "${methodName}" does not exist and cannot be mocked`);
|
||||
}
|
||||
|
||||
this.mocks[methodName] = callback;
|
||||
this.originalFunctions[methodName] = this.instance.prototype[methodName];
|
||||
|
||||
if (this.isSingleton() && this.instances.length === 0) {
|
||||
this.initialiseSingleton();
|
||||
|
||||
// Apply mocked method
|
||||
this.instances[0][methodName] = (...parameters) => callback(this, ...parameters);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a mock callback from future instances.
|
||||
*
|
||||
* @param {string} methodName
|
||||
*/
|
||||
unmock(methodName) {
|
||||
if (this.isFunction()) {
|
||||
return;
|
||||
}
|
||||
if (!this.mocks[methodName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isSingleton()) {
|
||||
this.instances[0][methodName] = this.originalFunctions[methodName];
|
||||
}
|
||||
|
||||
delete this.mocks[methodName];
|
||||
delete this.originalFunctions[methodName];
|
||||
}
|
||||
}
|
||||
25
modules/system/assets/js/snowboard/main/ProxyHandler.js
Normal file
25
modules/system/assets/js/snowboard/main/ProxyHandler.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default {
|
||||
get(target, prop, receiver) {
|
||||
if (typeof prop === 'string') {
|
||||
const propLower = prop.toLowerCase();
|
||||
|
||||
if (target.hasPlugin(propLower)) {
|
||||
return (...params) => Reflect.get(target, 'plugins')[propLower].getInstance(...params);
|
||||
}
|
||||
}
|
||||
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
|
||||
has(target, prop) {
|
||||
if (typeof prop === 'string') {
|
||||
const propLower = prop.toLowerCase();
|
||||
|
||||
if (target.hasPlugin(propLower)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return Reflect.has(target, prop);
|
||||
},
|
||||
};
|
||||
595
modules/system/assets/js/snowboard/main/Snowboard.js
Normal file
595
modules/system/assets/js/snowboard/main/Snowboard.js
Normal file
@@ -0,0 +1,595 @@
|
||||
import PluginBase from '../abstracts/PluginBase';
|
||||
import Singleton from '../abstracts/Singleton';
|
||||
import PluginLoader from './PluginLoader';
|
||||
|
||||
import Cookie from '../utilities/Cookie';
|
||||
import JsonParser from '../utilities/JsonParser';
|
||||
import Sanitizer from '../utilities/Sanitizer';
|
||||
import Url from '../utilities/Url';
|
||||
|
||||
/**
|
||||
* Snowboard - the Winter JavaScript framework.
|
||||
*
|
||||
* This class represents the base of a modern take on the Winter JS framework, being fully extensible and taking advantage
|
||||
* of modern JavaScript features by leveraging the Laravel Mix compilation framework. It also is coded up to remove the
|
||||
* dependency of jQuery.
|
||||
*
|
||||
* @copyright 2021 Winter.
|
||||
* @author Ben Thomson <git@alfreido.com>
|
||||
* @link https://wintercms.com/docs/snowboard/introduction
|
||||
*/
|
||||
export default class Snowboard {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param {boolean} autoSingletons Automatically load singletons when DOM is ready. Default: `true`.
|
||||
* @param {boolean} debug Whether debugging logs should be shown. Default: `false`.
|
||||
*/
|
||||
constructor(autoSingletons, debug) {
|
||||
this.debugEnabled = (typeof debug === 'boolean' && debug === true);
|
||||
this.autoInitSingletons = (typeof autoSingletons === 'boolean' && autoSingletons === false);
|
||||
this.plugins = {};
|
||||
this.listeners = {};
|
||||
this.foundBaseUrl = null;
|
||||
this.readiness = {
|
||||
dom: false,
|
||||
};
|
||||
// Seal readiness from being added to further, but allow the properties to be modified.
|
||||
Object.seal(this.readiness);
|
||||
this.attachAbstracts();
|
||||
|
||||
// Freeze the Snowboard class to prevent further modifications.
|
||||
Object.freeze(Snowboard.prototype);
|
||||
Object.freeze(this);
|
||||
|
||||
this.loadUtilities();
|
||||
this.initialise();
|
||||
|
||||
this.debug('Snowboard framework initialised');
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches abstract classes as properties of the Snowboard class.
|
||||
*
|
||||
* This will allow Javascript functionality with no build process to still extend these abstracts by prefixing
|
||||
* them with "Snowboard".
|
||||
*
|
||||
* ```
|
||||
* class MyClass extends Snowboard.PluginBase {
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
attachAbstracts() {
|
||||
this.PluginBase = PluginBase;
|
||||
this.Singleton = Singleton;
|
||||
|
||||
Object.freeze(this.PluginBase.prototype);
|
||||
Object.freeze(this.PluginBase);
|
||||
Object.freeze(this.Singleton.prototype);
|
||||
Object.freeze(this.Singleton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the default utilities.
|
||||
*/
|
||||
loadUtilities() {
|
||||
this.addPlugin('cookie', Cookie);
|
||||
this.addPlugin('jsonParser', JsonParser);
|
||||
this.addPlugin('sanitizer', Sanitizer);
|
||||
this.addPlugin('url', Url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the framework.
|
||||
*
|
||||
* Attaches a listener for the DOM being ready and triggers a global "ready" event for plugins to begin attaching
|
||||
* themselves to the DOM.
|
||||
*/
|
||||
initialise() {
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
if (this.autoInitSingletons) {
|
||||
this.initialiseSingletons();
|
||||
}
|
||||
this.globalEvent('ready');
|
||||
this.readiness.dom = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises an instance of every singleton.
|
||||
*/
|
||||
initialiseSingletons() {
|
||||
Object.values(this.plugins).forEach((plugin) => {
|
||||
if (plugin.isSingleton() && plugin.dependenciesFulfilled()) {
|
||||
plugin.initialiseSingleton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a plugin to the framework.
|
||||
*
|
||||
* Plugins are the cornerstone for additional functionality for Snowboard. A plugin must either be an ES2015 class
|
||||
* that extends the PluginBase or Singleton abstract classes, or a simple callback function.
|
||||
*
|
||||
* When a plugin is added, it is automatically assigned as a new magic method in the Snowboard class using the name
|
||||
* parameter, and can be called via this method. This method will always be the "lowercase" version of this name.
|
||||
*
|
||||
* For example, if a plugin is assigned to the name "myPlugin", it can be called via `Snowboard.myplugin()`.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {PluginBase|Function} instance
|
||||
*/
|
||||
addPlugin(name, instance) {
|
||||
const lowerName = name.toLowerCase();
|
||||
|
||||
if (this.hasPlugin(lowerName)) {
|
||||
throw new Error(`A plugin called "${name}" is already registered.`);
|
||||
}
|
||||
|
||||
if (typeof instance !== 'function' && instance instanceof PluginBase === false) {
|
||||
throw new Error('The provided plugin must extend the PluginBase class, or must be a callback function.');
|
||||
}
|
||||
|
||||
if (this[name] !== undefined || this[lowerName] !== undefined) {
|
||||
throw new Error('The given name is already in use for a property or method of the Snowboard class.');
|
||||
}
|
||||
|
||||
this.plugins[lowerName] = new PluginLoader(lowerName, this, instance);
|
||||
|
||||
this.debug(`Plugin "${name}" registered`);
|
||||
|
||||
// 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()
|
||||
&& !plugin.isInitialised()
|
||||
&& plugin.dependenciesFulfilled()
|
||||
&& plugin.hasMethod('listens')
|
||||
&& Object.keys(plugin.callMethod('listens')).includes('ready')
|
||||
&& this.readiness.dom
|
||||
) {
|
||||
const readyMethod = plugin.callMethod('listens').ready;
|
||||
plugin.callMethod(readyMethod);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a plugin.
|
||||
*
|
||||
* Removes a plugin from Snowboard, calling the destructor method for all active instances of the plugin.
|
||||
*
|
||||
* @param {string} name
|
||||
* @returns {void}
|
||||
*/
|
||||
removePlugin(name) {
|
||||
const lowerName = name.toLowerCase();
|
||||
|
||||
if (!this.hasPlugin(lowerName)) {
|
||||
this.debug(`Plugin "${name}" already removed`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Call destructors for all instances
|
||||
this.plugins[lowerName].getInstances().forEach((instance) => {
|
||||
instance.destruct();
|
||||
});
|
||||
|
||||
delete this.plugins[lowerName];
|
||||
delete this[lowerName];
|
||||
delete this[name];
|
||||
|
||||
this.debug(`Plugin "${name}" removed`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a plugin has been registered and is active.
|
||||
*
|
||||
* A plugin that is still waiting for dependencies to be registered will not be active.
|
||||
*
|
||||
* @param {string} name
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasPlugin(name) {
|
||||
const lowerName = name.toLowerCase();
|
||||
|
||||
return (this.plugins[lowerName] !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of registered plugins as PluginLoader objects.
|
||||
*
|
||||
* @returns {PluginLoader[]}
|
||||
*/
|
||||
getPlugins() {
|
||||
return this.plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of registered plugins, by name.
|
||||
*
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getPluginNames() {
|
||||
return Object.keys(this.plugins);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a PluginLoader object of a given plugin.
|
||||
*
|
||||
* @returns {PluginLoader}
|
||||
*/
|
||||
getPlugin(name) {
|
||||
const lowerName = name.toLowerCase();
|
||||
|
||||
if (!this.hasPlugin(lowerName)) {
|
||||
throw new Error(`No plugin called "${lowerName}" has been registered.`);
|
||||
}
|
||||
|
||||
return this.plugins[lowerName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all plugins that listen to the given event.
|
||||
*
|
||||
* This works for both normal and promise events. It does NOT check that the plugin's listener actually exists.
|
||||
*
|
||||
* @param {string} eventName
|
||||
* @returns {string[]} The name of the plugins that are listening to this event.
|
||||
*/
|
||||
listensToEvent(eventName) {
|
||||
const plugins = [];
|
||||
|
||||
Object.entries(this.plugins).forEach((entry) => {
|
||||
const [name, plugin] = entry;
|
||||
|
||||
if (plugin.isFunction()) {
|
||||
return;
|
||||
}
|
||||
if (!plugin.dependenciesFulfilled()) {
|
||||
return;
|
||||
}
|
||||
if (!plugin.hasMethod('listens')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listeners = plugin.callMethod('listens');
|
||||
|
||||
if (typeof listeners[eventName] === 'string' || typeof listeners[eventName] === 'function') {
|
||||
plugins.push(name);
|
||||
}
|
||||
});
|
||||
|
||||
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.readiness.dom) {
|
||||
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.
|
||||
*
|
||||
* If any plugin returns a `false`, the event is considered cancelled.
|
||||
*
|
||||
* @param {string} eventName
|
||||
* @returns {boolean} If event was not cancelled
|
||||
*/
|
||||
globalEvent(eventName, ...parameters) {
|
||||
this.debug(`Calling global event "${eventName}"`, ...parameters);
|
||||
|
||||
// Find plugins listening to the event.
|
||||
const listeners = this.listensToEvent(eventName);
|
||||
if (listeners.length === 0) {
|
||||
this.debug(`No listeners found for global event "${eventName}"`);
|
||||
}
|
||||
this.debug(`Listeners found for global event "${eventName}": ${listeners.join(', ')}`);
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
listeners.forEach((name) => {
|
||||
const plugin = this.getPlugin(name);
|
||||
|
||||
if (plugin.isFunction()) {
|
||||
return;
|
||||
}
|
||||
if (plugin.isSingleton() && plugin.getInstances().length === 0) {
|
||||
plugin.initialiseSingleton();
|
||||
}
|
||||
|
||||
const listenMethod = plugin.callMethod('listens')[eventName];
|
||||
|
||||
// Call event handler methods for all plugins, if they have a method specified for the event.
|
||||
plugin.getInstances().forEach((instance) => {
|
||||
// If a plugin has cancelled the event, no further plugins are considered.
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof listenMethod === 'function') {
|
||||
try {
|
||||
const result = listenMethod.apply(instance, parameters);
|
||||
if (result === false) {
|
||||
cancelled = true;
|
||||
}
|
||||
} catch (error) {
|
||||
this.error(
|
||||
`Error thrown in "${eventName}" event by "${name}" plugin.`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else if (typeof listenMethod === 'string') {
|
||||
if (!instance[listenMethod]) {
|
||||
throw new Error(`Missing "${listenMethod}" method in "${name}" plugin`);
|
||||
}
|
||||
|
||||
try {
|
||||
if (instance[listenMethod](...parameters) === false) {
|
||||
cancelled = true;
|
||||
this.debug(`Global event "${eventName}" cancelled by "${name}" plugin`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.error(
|
||||
`Error thrown in "${eventName}" event by "${name}" plugin.`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.error(`Listen method for "${eventName}" event in "${name}" plugin is not a function or string.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
try {
|
||||
if (listener(...parameters) === false) {
|
||||
cancelled = true;
|
||||
this.debug(`Global event "${eventName} cancelled by an ad-hoc listener.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.error(
|
||||
`Error thrown in "${eventName}" event by an ad-hoc listener.`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return !cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a global event to all registered plugins, expecting a Promise to be returned by all.
|
||||
*
|
||||
* This collates all plugins responses into one large Promise that either expects all to be resolved, or one to reject.
|
||||
* If no listeners are found, a resolved Promise is returned.
|
||||
*
|
||||
* @param {string} eventName
|
||||
*/
|
||||
globalPromiseEvent(eventName, ...parameters) {
|
||||
this.debug(`Calling global promise event "${eventName}"`);
|
||||
|
||||
// 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}"`);
|
||||
}
|
||||
this.debug(`Listeners found for global promise event "${eventName}": ${listeners.join(', ')}`);
|
||||
|
||||
const promises = [];
|
||||
|
||||
listeners.forEach((name) => {
|
||||
const plugin = this.getPlugin(name);
|
||||
|
||||
if (plugin.isFunction()) {
|
||||
return;
|
||||
}
|
||||
if (plugin.isSingleton() && plugin.getInstances().length === 0) {
|
||||
plugin.initialiseSingleton();
|
||||
}
|
||||
|
||||
const listenMethod = plugin.callMethod('listens')[eventName];
|
||||
|
||||
// Call event handler methods for all plugins, if they have a method specified for the event.
|
||||
plugin.getInstances().forEach((instance) => {
|
||||
if (typeof listenMethod === 'function') {
|
||||
try {
|
||||
const instancePromise = listenMethod.apply(instance, parameters);
|
||||
|
||||
if (instancePromise instanceof Promise === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
promises.push(instancePromise);
|
||||
} catch (error) {
|
||||
this.error(
|
||||
`Error thrown in "${eventName}" event by "${name}" plugin.`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else if (typeof listenMethod === 'string') {
|
||||
if (!instance[listenMethod]) {
|
||||
throw new Error(`Missing "${listenMethod}" method in "${name}" plugin`);
|
||||
}
|
||||
|
||||
try {
|
||||
const instancePromise = instance[listenMethod](...parameters);
|
||||
|
||||
if (instancePromise instanceof Promise === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
promises.push(instancePromise);
|
||||
} catch (error) {
|
||||
this.error(
|
||||
`Error thrown in "${eventName}" promise event by "${name}" plugin.`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.error(`Listen method for "${eventName}" event in "${name}" plugin is not a function or string.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
try {
|
||||
const listenerPromise = listener(...parameters);
|
||||
if (listenerPromise instanceof Promise === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
promises.push(listenerPromise);
|
||||
} catch (error) {
|
||||
this.error(
|
||||
`Error thrown in "${eventName}" promise event by an ad-hoc listener.`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a styled message in the console.
|
||||
*
|
||||
* Includes parameters and a stack trace.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
logMessage(color, bold, message, ...parameters) {
|
||||
/* eslint-disable */
|
||||
console.groupCollapsed(
|
||||
'%c[Snowboard]',
|
||||
`color: ${color}; font-weight: ${(bold) ? 'bold' : 'normal'};`,
|
||||
message
|
||||
);
|
||||
if (parameters.length) {
|
||||
console.groupCollapsed(
|
||||
`%cParameters %c(${parameters.length})`,
|
||||
'color: rgb(45, 167, 199); font-weight: bold;',
|
||||
'color: rgb(88, 88, 88); font-weight: normal;'
|
||||
);
|
||||
let index = 0;
|
||||
parameters.forEach((param) => {
|
||||
index += 1;
|
||||
console.log(`%c${index}:`, 'color: rgb(88, 88, 88); font-weight: normal;', param);
|
||||
});
|
||||
console.groupEnd();
|
||||
|
||||
console.groupCollapsed('%cTrace', 'color: rgb(45, 167, 199); font-weight: bold;');
|
||||
console.trace();
|
||||
console.groupEnd();
|
||||
} else {
|
||||
console.trace();
|
||||
}
|
||||
console.groupEnd();
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
log(message, ...parameters) {
|
||||
this.logMessage('rgb(45, 167, 199)', false, message, ...parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a debug message.
|
||||
*
|
||||
* These messages are only shown when debugging is enabled.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
debug(message, ...parameters) {
|
||||
if (!this.debugEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logMessage('rgb(45, 167, 199)', false, message, ...parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error message.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
error(message, ...parameters) {
|
||||
this.logMessage('rgb(229, 35, 35)', true, message, ...parameters);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user