strudel
Version:
A front-end framework for back-end powered web.
1,475 lines (1,282 loc) • 37.6 kB
JavaScript
/*!
* Strudel.js v1.0.5
* (c) 2016-2019 Mateusz Łuczak
* Released under the MIT License.
*/
let warn = () => {};
let error = () => {};
if (process.env.NODE_ENV !== 'production') {
const generateTrace = (vm) => {
const componentName = vm.prototype ? vm.prototype.name || vm.name : vm.constructor.name;
return ` (found in ${componentName})`;
};
warn = (msg, vm) => {
const trace = vm ? generateTrace(vm) : '';
console.warn(`[Strudel]: ${msg}${trace}`);
};
error = (msg, vm) => {
const trace = vm ? generateTrace(vm) : '';
console.error(`[Strudel]: ${msg}${trace}`);
};
}
const handleError = (err, vm, info) => {
if (process.env.NODE_ENV !== 'production') {
error(`Error in ${info}: "${err.toString()}"`, vm);
}
console.error(err);
};
/* eslint-disable */
const selectors = {};
selectors[/^\.[\w\-]+$/] = function (param) {
return document.getElementsByClassName(param.substring(1));
};
selectors[/^\w+$/] = function (param) {
return document.getElementsByTagName(param);
};
selectors[/^\#[\w\-]+$/] = function (param) {
return document.getElementById(param.substring(1));
};
selectors[/^</] = function (param) {
return new Element().generate(param);
};
/**
* Wrapper for query selector
* @param {String} selector - CSS selector
* @param {Node} context - Node to select from
* @returns {NodeList}
*/
const byCss = (selector, context) => {
return (context || document).querySelectorAll(selector);
};
/**
* Wrapper for byCss
* @param {String} selector
* @param {Node} context
* @returns {NodeList}
*/
const select = (selector, context) => {
selector = selector.replace(/^\s*/, '').replace(/\s*$/, '');
if (context) {
return byCss(selector, context);
}
for (var key in selectors) {
context = key.split('/');
if ((new RegExp(context[1], context[2])).test(selector)) {
return selectors[key](selector);
}
}
return byCss(selector);
};
// Store all of the operations to perform when cloning elements
const mirror = {
/**
* Copy all JavaScript events of source node to destination node.
*/
events: function (src, dest) {
if (!src._e) return;
for (var type in src._e) {
src._e[type].forEach(function (event) {
new Element(dest).on(type, event);
});
}
},
/**
* Copy select input value to its clone.
*/
select: function (src, dest) {
if (new Element(src).is('select')) {
dest.value = src.value;
}
},
/**
* Copy textarea input value to its clone
*/
textarea: function (src, dest) {
if (new Element(src).is('textarea')) {
dest.value = src.value;
}
}
};
/**
* @classdesc Element class used for DOM manipulation
* @class
*/
class Element {
/**
* @constructor
* @param {string} selector - CSS selector
* @param {Node} context - Node to wrap into Element
* @returns {Element}
*/
constructor(selector, context) {
if (selector instanceof Element) {
return selector;
}
if (typeof selector === 'string') {
selector = select(selector, context);
}
if (selector && selector.nodeName || selector && selector === window) {
selector = [selector];
}
this._nodes = this.slice(selector);
}
/**
* Returns size of nodes
*/
get length() {
return this._nodes.length;
}
/**
* Extracts structured data from DOM
* @param {Function} callback - A callback to be called on each node. Returned value is added to the set
* @returns {*}
*/
array(callback) {
let self = this;
return this._nodes.reduce(function (list, node, i) {
let val;
if (callback) {
val = callback.call(self, node, i);
if (!val) val = false;
if (typeof val === 'string') val = new Element(val);
if (val instanceof Element) val = val._nodes;
} else {
val = node.innerHTML;
}
return list.concat(val !== false ? val : []);
}, []);
}
/**
* Create a string from different things
* @private
*/
str(node, i) {
return function (arg) {
if (typeof arg === 'function') {
return arg.call(this, node, i);
}
return arg.toString();
};
}
/**
* Check the current matched set of elements against a selector and return true if at least one of these elements matches the given arguments.
* @param {selector} selector - A string containing a selector expression to match elements against.
* @returns {boolean}
*/
is(selector) {
return this.filter(selector).length > 0;
}
/**
* Reduce the set of matched elements to those that match the selector or pass the function's test.
* @param {selector} selector A string containing a selector expression to match elements against.
* @returns {Element}
*/
filter(selector) {
let callback = function (node) {
node.matches = node.matches || node.msMatchesSelector || node.webkitMatchesSelector;
return node.matches(selector || '*');
};
if (typeof selector === 'function') callback = selector;
if (selector instanceof Element) {
callback = function (node) {
return (selector._nodes).indexOf(node) !== -1;
};
}
return new Element(this._nodes.filter(callback));
}
/**
* Reduce the set of matched elements to the one at the specified index.
* @param {Number} index - An integer indicating the 0-based position of the element.
* @returns {Element|boolean}
*/
eq(index) {
return new Element(this._nodes[index]) || false;
}
/**
* Reduce the set of matched elements to the HTMLElement at the specified index.
* @param {Number} index - An integer indicating the 0-based position of the element.
* @returns {HTMLElement}
*/
get(index) {
return ((index || index === 0) && index <= this._nodes.length) ? this._nodes[index] : this._nodes;
}
/**
* Reduce the set of matched elements to the first in the set.
* @returns {HTMLElement}
*/
first() {
return this._nodes[0] || false;
}
/**
* Returns index of a given element
* @param {HTMLElement|Element} element
* @returns {Number}
*/
index(element) {
const siblings = this.children()._nodes;
const node = element instanceof HTMLElement ? element : element.first();
return Array.prototype.indexOf.call(siblings, node);
}
/**
* Converts Arraylike to array
* @private
*/
slice(pseudo) {
if (!pseudo ||
pseudo.length === 0 ||
typeof pseudo === 'string' ||
pseudo.toString() === '[object Function]') return [];
return pseudo.length ? [].slice.call(pseudo._nodes || pseudo) : [pseudo];
}
/**
* Removes duplicated nodes
* @private
*/
unique() {
return new Element(this._nodes.reduce(function (clean, node) {
let isTruthy = node !== null && node !== undefined && node !== false;
return (isTruthy && clean.indexOf(node) === -1) ? clean.concat(node) : clean;
}, []));
}
/**
* Get the direct children of all of the nodes with an optional filter
* @param [string] selector - Filter what children to get
* @returns {Element}
*/
children(selector) {
return this.map(function (node) {
return this.slice(node.children);
}).filter(selector);
}
/**
* Generates element from htmlString
* @private
*/
generate(html) {
if (/^\s*<t(h|r|d)/.test(html)) {
return new Element(document.createElement('table')).html(html).children()._nodes;
} else if (/^\s*</.test(html)) {
return new Element(document.createElement('div')).html(html).children()._nodes;
} else {
return document.createTextNode(html);
}
}
/**
* Normalize the arguments to an array of strings
* @private
*/
args(args, node, i) {
if (typeof args === 'function') {
args = args(node, i);
}
if (typeof args !== 'string') {
args = this.slice(args).map(this.str(node, i));
}
return args.toString().split(/[\s,]+/).filter(function (e) {
return e.length;
});
}
/**
* Loops through the nodes and executes callback for each
* @param {Function} callback - The function that will be called
* @returns {Element}
*/
each(callback) {
this._nodes.forEach(callback.bind(this));
return this;
}
/**
* Loop through the combination of every node and every argument passed
* @private
*/
eacharg(args, callback) {
return this.each(function (node, i) {
this.args(args, node, i).forEach(function (arg) {
callback.call(this, node, arg);
}, this);
});
}
/**
* Checks if node exists on a page
* @private
*/
isInPage(node) {
return (node === document.body) ? false : document.body.contains(node);
}
/**
* Changes the content of the current instance by running a callback for each Element
* @param {Function} callback - A callback that returns an element that are going to be kept
* @returns {Element}
*/
map(callback) {
return callback ? new Element(this.array(callback)).unique() : this;
}
/**
* Add texts in specific position
* @private
*/
adjacent(html, data, callback) {
if (typeof data === 'number') {
if (data === 0) {
data = [];
} else {
data = new Array(data).join().split(',').map(Number.call, Number);
}
}
return this.each(function (node, j) {
let fragment = document.createDocumentFragment();
new Element(data || {}).map(function (el, i) {
let part = (typeof html === 'function') ? html.call(this, el, i, node, j) : html;
if (typeof part === 'string') {
return this.generate(part);
}
return new Element(part);
}).each(function (n) {
this.isInPage(n)
? fragment.appendChild(new Element(n).clone().first())
: fragment.appendChild(n);
});
callback.call(this, node, fragment);
});
}
/**
* Return an array of DOM nodes of a source node and its children.
* @private
*/
getAll(context) {
return new Element([context].concat(new Element('*', context)._nodes));
}
/**
* Deep clone a DOM node and its descendants.
* @returns {Element}
*/
clone() {
return this.map(function (node) {
var clone = node.cloneNode(true);
var dest = this.getAll(clone);
this.getAll(node).each(function (src, i) {
for (var key in mirror) {
mirror[key](src, dest._nodes[i]);
}
});
return clone;
});
}
/**
* Gets the HTML contents of the first element in a set.
* When parameter is provided set the HTML contents of each element in the set.
* @param {htmlString} [text] - A string of HTML to set as the content of each matched element
* @returns {htmlString|Element}
*/
html(text) {
if (text === undefined) {
return this.first().innerHTML || '';
}
return this.each(function (node) {
node.innerHTML = text;
});
}
/**
* Gets the text contents of the first element in a set.
* When parameter is provided set the text contents of each element in the set.
* @param {string} [text] - A string to set as the text content of each matched element.
* @returns {string|Element}
*/
text(text) {
if (text === undefined) {
return this.first().textContent || '';
}
return this.each(function (node) {
node.textContent = text;
});
}
/**
* Remove the set of matched elements from the DOM.
* @returns {Element}
*/
remove() {
return this.each(function (node) {
node.parentNode.removeChild(node);
});
}
/**
* Travel the matched elements one node up
* @param {selector} CSS Selector
* @returns {Element}
*/
parent(selector) {
return this.map(function (node) {
return node.parentNode;
}).filter(selector);
}
/**
* Find the first ancestor that matches the selector for each node
* @param {selector} CSS Selector
* @returns {Element}
*/
closest(selector) {
return this.map(function (node) {
do {
if (new Element(node).is(selector)) {
return node;
}
} while ((node = node.parentNode) && node !== document);
});
}
/**
* Insert content, specified by the parameter, to the end of each element in the set of matched elements
* Additional data can be provided, which will be used for populating the html
* @param {string|Element} html - Html string or Element
* @param [data]
* @returns {Element}
*/
append(html, data) {
return this.adjacent(html, data, function (node, fragment) {
node.appendChild(fragment);
});
}
/**
* Insert content, specified by the parameter, to the begining of each element in the set of matched elements
* Additional data can be provided, which will be used for populating the html
* @param {string|Element} html - Html string or Element
* @param [data]
* @returns {Element}
*/
prepend(html, data) {
return this.adjacent(html, data, function (node, fragment) {
node.insertBefore(fragment, node.firstChild);
});
}
/**
* Get the descendants of each element in the current set of matched elements, filtered by a selector.
* @param {selector} selector - A string containing a selector expression to match elements against.
* @returns {Element}
*/
find(selector) {
return this.map(function (node) {
const startsWithImmediateChildrenSelector = selector[0] === '>';
let hadId;
if (startsWithImmediateChildrenSelector) {
hadId = true;
if (!node.id) {
hadId = false;
node.id = `strudel-${Math.random().toString(36).substr(2, 9)}`;
}
selector = `#${node.id}${selector}`;
}
const result = new Element(selector || '*', node);
if (startsWithImmediateChildrenSelector && !hadId) {
node.removeAttribute('id');
}
return result;
});
}
/**
* Adds the specified class(es) to each element in the set of matched elements.
* @param {...string} className - Class(es) to be added
* @returns {Element}
*/
addClass(className) {
return this.eacharg(arguments, function (el, name) {
el.classList.add(name);
});
}
/**
* Toggles the specified class(es) to each element in the set of matched elements.
* @param {...string} className - Class(es) to be toggled
* @returns {Element}
*/
toggleClass(className) {
return this.eacharg(arguments, function (el, name) {
el.classList.toggle(name);
});
}
/**
* Removes the specified class(es) from each element in the set of matched elements.
* @param {...string} className - Class(es) to be removed
* @returns {Element}
*/
removeClass(className) {
return this.eacharg(arguments, function (el, name) {
el.classList.remove(name);
});
}
/**
* Attach event handlers
* @param {string} events - Events to attach handlers for - can be space separated or comma separated list, or array of strings
* @param {string|Function} cb - Callback or CSS selector
* @param [Function] cb2 - Callback when second parameter is a selector
* @returns {Element}
*/
on(events, cb, cb2) {
let providedHandler = cb;
if (typeof cb === 'string') {
let sel = cb;
cb = function (e) {
let args = arguments;
let el = new Element(e.currentTarget);
let set = el.is(sel) ? el : el.find(sel);
set.each(function (target) {
if (target === e.target || target.contains(e.target)) {
try {
Object.defineProperty(e, 'currentTarget', {
get: function () {
return target;
}
});
} catch (err) { }
cb2.apply(target, args);
}
});
};
providedHandler = cb2;
}
let eventHandler = function (e) {
return cb.apply(this, [e].concat(e.detail || []));
};
return this.eacharg(events, function (node, event) {
node.addEventListener(event, eventHandler);
node._e = node._e || {};
node._e[event] = node._e[event] || [];
node._e[event].push({
providedHandler,
eventHandler,
});
});
}
/**
* Remove an event handler
* @param {string} events
* @param {function} handler to be removed
*/
off(events, handler) {
if (events === undefined && handler === undefined) {
this.each(function (node) {
for (let event in node._e) {
node._e[event].forEach(function ({eventHandler}) {
node.removeEventListener(event, eventHandler);
});
}
node._e = {};
});
}
return this.eacharg(events, function (node, event) {
new Element(node._e ? node._e[event] : []).each(function ({providedHandler, eventHandler}, index) {
if(handler) {
if (handler === providedHandler) {
node.removeEventListener(event, eventHandler);
node._e[event] = [
...node._e[event].slice(0, index),
...node._e[event].slice(index + 1),
];
}
} else {
node.removeEventListener(event, eventHandler);
node._e[event] = [];
}
});
});
}
/**
* Execute all handlers attached to the event type
* @param {string} events - Event types to be executed
* @returns {*}
*/
trigger(events) {
let data = this.slice(arguments).slice(1);
return this.eacharg(events, function (node, event) {
let ev;
let opts = { bubbles: true, cancelable: true, detail: data };
try {
ev = new window.CustomEvent(event, opts);
} catch (e) {
ev = document.createEvent('CustomEvent');
ev.initCustomEvent(event, true, true, data);
}
node.dispatchEvent(ev);
});
}
/**
* Get the value of an attribute for the first element in the set.
* When parameter is provided set the text contents of each element in the set.
* @param [string|object] name - Name of the attribute to be retrieved/set. Can be object of attributes/values.
* @param [string] value - Value of the attribute to be set.
* @returns {string|Element}
*/
attr(name, value, data) {
data = data ? 'data-' : '';
if (value !== undefined) {
let nm = name;
name = {};
name[nm] = value;
}
if (typeof name === 'object') {
return this.each(function (node) {
for (let key in name) {
if (name[key] !== null) {
node.setAttribute(data + key, name[key]);
} else {
node.removeAttribute(data + key);
}
}
});
}
return this.length ? this.first().getAttribute(data + name) : '';
}
/**
* Get the prop for the each element in the set of matched elements or set one or more attributes for every matched element.
* @param [string|object] name - Name of the property to be retrieved/set. Can be object of attributes/values.
* @param [string] value - Value of the property to be set.
* @returns {string|Element}
*/
prop(name, value) {
if (value !== undefined) {
let nm = name;
name = {};
name[nm] = value;
}
if (typeof name === 'object') {
return this.each(function (node) {
for (let key in name) {
node[key] = name[key];
}
});
}
return this.length ? this.first()[name] : '';
}
/**
* Get the value of an daata attribute for the each element in the set of matched elements or set one or more attributes for every matched element.
* @param [string|object] name - Name of the data attribute to be retrieved/set. Can be object of attributes/values.
* @param [string] value - Value of the data attribute to be set.
* @returns {object|Element}
*/
data(name, value) {
if (!name) {
return this.first().dataset;
}
return this.attr(name, value, true);
}
}
function $(selector, element) {
return new Element(selector, element);
}
/**
* List of instance methods that won't be overriden by a component
* when prototypes are mixed.
*/
const protectedMethods = [
'constructor',
'$teardown',
'$on',
'$off',
'$emit'
];
/**
* Check if passed parameter is a function
* @param obj
* @returns {boolean}
*/
const isFunction = (obj) => {
return typeof obj === 'function' || false;
};
/**
* Small util for mixing prototypes
* @param {Function} target
* @param {Function} source
*/
const mixPrototypes = (target, source) => {
const targetProto = target.prototype;
const sourceProto = source.prototype;
const inst = (typeof source === 'object') ? source : new source(); // eslint-disable-line new-cap
Object.getOwnPropertyNames(inst).forEach((name) => {
const desc = Object.getOwnPropertyDescriptor(inst, name);
desc.writable = true;
Object.defineProperty(targetProto, name, desc);
});
Object.getOwnPropertyNames(sourceProto).forEach((name) => {
if (protectedMethods.indexOf(name) !== -1) {
if (name !== 'constructor') {
warn(`Component tried to override instance method ${name}`, source);
}
} else {
Object.defineProperty(targetProto, name, Object.getOwnPropertyDescriptor(sourceProto, name));
}
});
};
/**
* Util used to create decorators
* @param {Function} factory - The function that the decorator will be created from
*/
const createDecorator = (factory) => {
return (...args) => {
return (Ctor, property) => {
if (!Ctor.__decorators__) {
Ctor.__decorators__ = [];
}
Ctor.__decorators__.push((component) => {
return factory(component, property, args);
});
};
};
};
/**
* Util used to merge two objects together
* @param obj
* @param obj
* @returns {{}|*}
*/
const mergeObjects = (obj1, obj2) => {
return [obj1, obj2].reduce((prev, curr) => {
Object.keys(curr).forEach((key) => {
prev[key] = curr[key];
});
return prev;
});
};
/**
* Simple registry for storing selector-constructor pairs
*/
class Registry {
/**
* @constructor
*/
constructor() {
this._registry = {};
this._registrationQueue = {};
this._isRegistrationScheduled = false;
}
/**
* Returns both permanent registry and the registration queue entires as one object
* @returns {{}|*}
*/
getData() {
return mergeObjects(this._registry, this._registrationQueue);
}
/**
* Returns an Array of registry entires
* @returns {Array} registry entries
*/
getRegisteredSelectors() {
return Object
.keys(this._registry);
}
/**
* Returns an Array of temporary registry entires
* @returns {Array} registry entries
*/
getSelectorsFromRegistrationQueue() {
return Object
.keys(this._registrationQueue);
}
/**
* Moves all entries from the registration queue to permanent registry and clears queue
* @param {string} selector
*/
setSelectorsAsRegistered() {
this._registry = mergeObjects(this._registry, this._registrationQueue);
this._registrationQueue = {};
}
/**
* Returns component constructor for selector from map
* @param {string} selector
* @returns {Function} constructor
*/
getComponent(selector) {
return this._registrationQueue[selector] || this._registry[selector];
}
/**
* Adds selector/constructor pair to map
* @param {string} selector
* @param {Function} constructor
*/
registerComponent(selector, klass) {
if (this._registry[selector] || this._registrationQueue[selector]) {
warn(`Component registered under selector: ${selector} already exists.`, klass);
} else {
this._registrationQueue[selector] = klass;
if (!this._isRegistrationScheduled) {
this._isRegistrationScheduled = true;
window.requestAnimationFrame(() => {
this._isRegistrationScheduled = false;
$(document).trigger('content:loaded');
});
}
}
}
}
var registry = new Registry();
const initializedClassName = 'strudel-init';
var config = {
/**
* Class added on components when initialised
*/
initializedClassName,
/**
* Selector for components that have been initialized
*/
initializedSelector: `.${initializedClassName}`,
/**
* Whether to enable devtools
*/
devtools: process.env.NODE_ENV !== 'production',
/**
* Whether to show production mode tip message on boot
*/
productionTip: process.env.NODE_ENV !== 'production'
};
/**
* Event listeners
* @type {{}}
*/
const events = {};
/**
* @classdesc Simple Event Emitter implementation - global
* @class
*/
class EventEmitter {
static getEvents() {
return events;
}
static removeAllListeners() {
Object.keys(events).forEach((prop) => {
delete events[prop];
});
}
/**
* Add event listener to the map
* @param {string} label
* @param {Function} callback
*/
$on(label, callback) {
if (!events[label]) {
events[label] = [];
}
events[label].push(callback);
}
/**
* Remove event listener from registry
* @param {string} label
* @param {Function} callback
* @returns {boolean}
*/
$off(label, callback) {
const listeners = events[label];
if (listeners && listeners.length) {
const index = listeners.reduce((i, listener, ind) => {
return (isFunction(listener) && listener === callback) ? i = ind : i;
}, -1);
if (index > -1) {
listeners.splice(index, 1);
events[label] = listeners;
return true;
}
}
return false;
}
/**
* Notifies listeners attached to event
* @param {string} label
* @param args
* @returns {boolean}
*/
$emit(label, ...args) {
const listeners = events[label];
if (listeners && listeners.length) {
try {
listeners.forEach((listener) => {
listener(...args);
});
} catch (e) {
handleError(e, this.constructor, 'event handler');
}
return true;
}
return false;
}
}
const mix = (target, source) => {
Object.keys(source).forEach((prop) => {
if (!target[prop]) {
target[prop] = source[prop];
}
});
};
/**
* @classdesc Base class for all components, implementing event emitter
* @class
* @hideconstructor
*/
class Component extends EventEmitter {
constructor({ element, data } = {}) {
super();
this.mixins = this.mixins || [];
try {
this.mixins.forEach((mixin) => {
if (isFunction(mixin.beforeInit)) {
mixin.beforeInit.call(this);
}
mix(this, mixin);
});
this.beforeInit();
this.$element = element;
this.$data = data;
if (this.__decorators__) {
this.__decorators__.forEach((fn) => {
fn(this);
});
delete this.__decorators__;
}
this.mixins.forEach((mixin) => {
if (isFunction(mixin.init)) {
mixin.init.call(this);
}
});
this.init();
} catch (e) {
handleError(e, this.constructor, 'component hook');
}
this.$element.addClass(config.initializedClassName);
}
/**
* Function called before component is initialized
* @interface
*/
beforeInit() {}
/**
* Function called when component is initialized
* @interface
*/
init() {}
/**
* Function called before component is destroyed
* @interface
*/
beforeDestroy() {}
/**
* Function called after component is destroyed
* @interface
*/
destroy() {}
/**
* Teardown the component and clear events
*/
$teardown() {
try {
this.mixins.forEach((mixin) => {
if (isFunction(mixin.beforeDestroy)) {
mixin.beforeDestroy.call(this);
}
});
this.beforeDestroy();
this.$element.off();
this.$element.removeClass(config.initializedClassName);
delete this.$element.first().__strudel__;
delete this.$element;
this.mixins.forEach((mixin) => {
if (isFunction(mixin.destroy)) {
mixin.destroy.call(this);
}
});
this.destroy();
} catch (e) {
handleError(e, this.constructor, 'component hook');
}
}
}
/**
* Component decorator - Registers decorated class in {@link Registry} as a component
* @param {string} CSS selector
*/
const register = (target, selector) => {
if (!selector) {
warn('Selector must be provided for Component decorator', target);
}
if (!target.prototype) {
warn('Decorator works only for classes', target);
return target;
}
const component = class extends Component {
constructor(...args) { /* eslint no-useless-constructor: 0 */
super(...args);
}
};
mixPrototypes(component, target);
Object.defineProperty(component.prototype, '_selector', { value: selector });
Object.defineProperty(component.prototype, 'isStrudelClass', { value: true });
Object.defineProperty(component.prototype, 'name', { value: target.name });
registry.registerComponent(selector, component);
return component;
};
function decorator(selector) {
return function _decorator(target) {
return register(target, selector);
};
}
const delegate = (element, eventName, selector, listener) => {
if (selector) {
element.on(eventName, selector, listener);
} else {
element.on(eventName, listener);
}
};
/**
* Event decorator - binds method to event based on the event string
* @param {string} event
* @returns (Function} decorator
*/
var event = createDecorator((component, property, params) => {
let event;
let selector;
if (!params || !params[0]) {
warn('Event descriptor must be provided for Evt decorator');
} else {
[event, selector] = params;
}
if (!component._events) {
component._events = [];
}
const callback = function handler(...argz) {
try {
component[property].apply(this, argz);
} catch (e) {
handleError(e, component.constructor, 'component handler');
}
};
if (event) {
const eventName = (selector) ? `${event} ${selector}` : event;
component._events[eventName] = callback;
delegate(component.$element, event, selector, callback.bind(component));
}
});
/**
* Element decorator - Creates {@link Element} for matching selector and assigns to decorated property.
* @param {string} CSS selector
* @returns (Function} decorator
*/
var el = createDecorator((component, property, params) => {
if (params && params[0]) {
component[property] = component.$element.find(params[0]);
} else {
warn('Selector must be provided for El decorator');
}
if (!component._els) {
component._els = [];
}
component._els[property] = property;
});
/**
* OnInit decorator - sets method to be run at init
* @returns {Function} decorator
*/
var onInit = createDecorator((component, property) => {
const emptyFnc = function () {};
const org = component.init || emptyFnc;
if (property === 'init') {
return;
}
component.init = function (...args) {
component[property].apply(this, ...args);
return org.apply(this, ...args);
};
})();
const VERSION = '1.0.5';
const INIT_CLASS = config.initializedClassName;
const INIT_SELECTOR = config.initializedSelector;
const options = {
components: registry.getData()
};
var Strudel = /*#__PURE__*/Object.freeze({
VERSION: VERSION,
INIT_CLASS: INIT_CLASS,
INIT_SELECTOR: INIT_SELECTOR,
options: options,
config: config,
Component: decorator,
Evt: event,
El: el,
OnInit: onInit,
EventEmitter: EventEmitter,
createDecorator: createDecorator,
element: $,
$: $
});
/**
* @classdesc Class linking components with DOM
* @class
*/
class Linker {
/**
* @constructor
* @param {Registry} component registry
*/
constructor(registry) {
this.registry = registry;
}
/**
* Finds all components within selector and destroy them
* @param {DOMElement} container
*/
unlink(container = document) {
this.registry.getRegisteredSelectors().forEach((selector) => {
const elements = Array.prototype.slice.call(container.querySelectorAll(selector));
if (container !== document && $(container).is(config.initializedSelector)) {
elements.push(container);
}
[].forEach.call(elements, (el) => {
if (el.__strudel__) {
el.__strudel__.$teardown();
}
});
});
}
/**
* Iterates over selectors in registry, find occurrences in container and initialize components
* @param {DOMElement} container
*/
link(container = document) {
const isRootNode = (container === document);
const selectors = (isRootNode)
? this.registry.getSelectorsFromRegistrationQueue()
: this.registry.getRegisteredSelectors();
if (selectors.length === 0) {
return;
}
selectors.forEach((selector) => {
const elements = Array.prototype.slice.call(container.querySelectorAll(selector));
if (container !== document && $(container).is(selector)) {
elements.push(container);
}
[].forEach.call(elements, (el) => {
if (!el.__strudel__) {
const element = $(el);
const data = element.data();
const Instance = this.registry.getComponent(selector);
el.__strudel__ = new Instance({ element, data });
} else {
warn(`Trying to attach component to already initialized node, component with selector ${selector} will not be attached`);
}
});
});
if (isRootNode) {
this.registry.setSelectorsAsRegistered();
}
}
}
const onChildrenAddition = (mutation, callback) => {
if (
mutation.type === 'childList'
&& mutation.addedNodes.length > 0
) {
callback(mutation);
}
};
const onChildrenRemoval = (mutation, callback) => {
if (
mutation.type === 'childList'
&& mutation.removedNodes.length > 0
) {
callback(mutation);
}
};
const defaultObserverConfig = {
childList: true,
subtree: true
};
const mutationCallback = (mutations, additionCallback, removalCallback) => {
mutations.forEach((mutation) => {
onChildrenRemoval(mutation, removalCallback);
onChildrenAddition(mutation, additionCallback);
});
};
const attachDOMObserver = (observerRoot, additionCallback, removalCallback) => {
const DOMObserver = new MutationObserver((mutations) => {
mutationCallback(mutations, additionCallback, removalCallback);
});
DOMObserver.observe(observerRoot, defaultObserverConfig);
};
const devtools = window.__STRUDEL_DEVTOOLS_GLOBAL_HOOK__;
const mount = () => {
setTimeout(() => {
if (config.devtools) {
if (devtools) {
devtools.emit('init', Strudel);
} else if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
console.info(
'Download the Strudel Devtools extension for a better development experience:\n' +
'https://github.com/strudeljs/strudel-devtools'
);
}
}
if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test' && config.productionTip !== false) {
console.info(
'You are running Strudel in development mode.\n' +
'Make sure to turn on production mode when deploying for production.'
);
}
}, 0);
};
const linker = new Linker(registry);
const channel = $(document);
const isValidNode = ({ nodeName, nodeType }) => {
return nodeName !== 'SCRIPT' && nodeName !== 'svg' && nodeType === 1;
};
const getElement = (detail) => {
let element;
if (detail && detail.length > 0) {
element = isValidNode(detail[0]) ? detail[0] : detail[0].first();
}
return element;
};
const bootstrap = (root) => {
linker.link(getElement(root));
channel.trigger('strudel:loaded');
};
const bindContentEvents = () => {
channel.on('content:loaded', (evt) => {
bootstrap(evt.detail);
});
};
const onAutoInitCallback = (mutation) => {
const registeredSelectors = registry.getRegisteredSelectors();
Array.prototype.slice.call(mutation.addedNodes)
.filter((node) => {
return isValidNode(node);
})
.forEach((node) => {
if (registeredSelectors.some((el) => {
const lookupSelector = `${el}:not(${config.initializedSelector})`;
return $(node).is(lookupSelector) || $(node).find(lookupSelector).length;
})) {
bootstrap([node]);
}
});
};
const onAutoTeardownCallback = (mutation) => {
Array.prototype.slice.call(mutation.removedNodes)
.filter((node) => {
return isValidNode(node) && ($(node).is(config.initializedSelector) || $(node).find(config.initializedSelector).length);
})
.forEach((node) => {
const initializedSubNodes = node.querySelector(config.initializedSelector);
if (initializedSubNodes) {
Array.prototype.slice.call(initializedSubNodes).forEach(
(subNode) => { linker.unlink(subNode); },
);
}
linker.unlink(node);
});
};
const init = () => {
if (/comp|inter|loaded/.test(document.readyState)) {
setTimeout(bootstrap, 0);
}
mount();
bindContentEvents();
attachDOMObserver(channel._nodes[0].body, onAutoInitCallback, onAutoTeardownCallback);
};
/**
* Expose Strudel in component prototype and start processing
*/
Component.prototype.getInstance = () => { return Strudel; };
init();
export { VERSION, INIT_CLASS, INIT_SELECTOR, options, config, decorator as Component, event as Evt, el as El, onInit as OnInit, EventEmitter, createDecorator, $ as element, $ };