UNPKG

strudel

Version:

A front-end framework for back-end powered web.

1,475 lines (1,282 loc) 37.6 kB
/*! * 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, $ };