(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define('plugins', factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.plugins = factory()); })(this, (function () { 'use strict'; function identity(x) { return x; } function alwaysTrue(x) { return true; } var config = { preconfigure: identity, filter: alwaysTrue, observeDocument: true, processInterval: 10, idleTimeout: 10, cleanupInterval: 10, attributeNamePrefix: 'data-plugin', lazyLoadRootMargin: '0px 0px 200px 0px', lazyLoadAttribute: 'plugins-lazy', shouldLazyLoad: function shouldLazyLoad(node, pluginOptions, lazyLoadAttribute) { return pluginOptions.lazyLoad || node.getAttribute(lazyLoadAttribute) !== null; }, ignoreAttribute: 'plugins-ignore' }; var create = function create(system, extensions) { var app = system.app; var createInvoke = function createInvoke(instance) { return function (f) { return f(instance); }; }; var uid = 0, elementInstancesMap = null, _instances = []; var getOrCreateMap = function getOrCreateMap() { if (!elementInstancesMap) { elementInstancesMap = _instances.reduce(function (map, instance) { var elm = instance.definition.element, entry = map.get(elm); if (typeof entry === 'undefined') { entry = []; } entry.push(instance); map.set(elm, entry); return map; }, new Map()); } return elementInstancesMap; }; var registerPlugin = function registerPlugin(context) { var instance = createInstance(context); _instances.push(instance); var loaded = loadPlugin(instance); extensions.forEach(function (extend) { extend(instance, context, loaded); }); loaded.then(function () { elementInstancesMap = null; }); loaded["catch"](function (err) { console && console.warn("Plugin failed", err); }); return loaded; }; var createInstance = function createInstance(context) { var instance = { $$uid: ++uid, $$state: { tearDownHandlers: [], disconnectHandlers: [], connectHandlers: [] }, application: app, definition: { type: context.plugin, options: context.options, element: context.element, alias: context.alias }, onDestroy: function onDestroy(h) { this.$$state.tearDownHandlers.push(h); }, onDisconnect: function onDisconnect(h) { this.$$state.disconnectHandlers.push(h); }, onConnect: function onConnect(h) { this.$$state.connectHandlers.push(h); }, shouldKeepAlive: context.options && !!context.options.keepAlive, loaded: false, destroyed: false, connected: false, stateChangedAt: performance.now(), configuration: context.configuration }; return instance; }; function discardInstance(instance) { var i = _instances.length, h; while (i--) { h = _instances[i]; if (instance === h) { _instances.splice(i, 1); break; } } } function disconnect(element) { var map = getOrCreateMap(); var instances = map.get(element); if (instances && instances.length) { instances.forEach(function (instance) { setConnected(instance, false); }); } } function reconnect(element, pluginType) { var map = getOrCreateMap(); var instances = map.get(element); if (instances && instances.length) { var instance = instances.find(function (i) { return i.definition.type === pluginType; }); if (instance) { setConnected(instance, true); return; } } console && console.warn('Could not find a suitable stored plugin instance to reconnect', pluginType, element); } function setConnected(instance, connected) { var handlers = connected ? instance.$$state.connectHandlers : instance.$$state.disconnectHandlers; if (instance.connected !== connected) { instance.connected = connected; instance.stateChangedAt = performance.now(); handlers.forEach(createInvoke(instance)); } } function releaseInstance(instance) { try { instance.destroy && instance.destroy(); } catch (e) { console && console.warn("Destroy function of '" + instance.definition.type + "' failed. Exception: " + e + ". Function: " + instance.destroy.toString() + ""); } instance.$$state.tearDownHandlers.forEach(createInvoke(instance)); Object.keys(instance).forEach(function (key) { instance[key] = null; }); } function destroy(shouldSkip) { var result = _instances.reduce(function (state, instance) { var type = instance.definition.type, elm = instance.definition.element; if (!instance.loaded || instance.shouldKeepAlive || instance.connected || shouldSkip && shouldSkip(instance)) { state.instances.push(instance); return state; } releaseInstance(instance); var entry = state.released.get(elm); if (typeof entry === 'undefined') { entry = []; } entry.push(type); state.released.set(elm, entry); return state; }, { instances: [], released: new Map() }); if (result.released.size > 0) { elementInstancesMap = null; } _instances = result.instances; return result.released; } function loadPlugin(instance) { var pluginType = instance.definition.type, el = instance.definition.element; return system.moduleLoader.load(pluginType, el).then(onLoad); function onLoad(plugin) { if (typeof plugin === 'undefined') { discardInstance(instance); instance = null; return Promise.reject('plugin ' + pluginType + ' is undefined'); } try { return Promise.resolve(plugin.call(instance, el, instance.definition.options)).then(function (pluginApi) { instance.loaded = instance.connected = true; return { instance: instance, api: pluginApi }; }); } catch (err) { return Promise.reject(err); } } } var pluginManager = { register: registerPlugin, release: destroy, disconnect: disconnect, reconnect: reconnect, elements: getOrCreateMap, instances: function instances() { return _instances; }, inspect: function inspect() { console.table(this.instances()); } }; return pluginManager; }; var pluginManagerFactory = { create: create }; function emitter(context, options) { var _events = {}, o = options || {}, _constantEvents = {}, noop = function () {}; function invoke(handler, args) { window.setTimeout(function () { handler.apply(context, args); }); } function emit(event) { var handlers = _events[event] || (_events[event] = []); if (!handlers) { return; } var args = Array.prototype.slice.call(arguments, 1); if (o.emitting) { o.emitting({ event: event, data: args, context: context }); } for (var i = 0, l = handlers.length; i < l; i++) { invoke(handlers[i], args); } } function replay(event) { var args = Array.prototype.slice.call(arguments, 1); _constantEvents[event] = args; emit.apply(null, arguments); } function on(event, fn) { if (_constantEvents[event]) { invoke(fn, _constantEvents[event]); return noop; } var handlers = _events[event] || (_events[event] = []); handlers.push(fn); return function () { off(event, fn); }; } function off(event, fn) { var handlers = _events[event], i, h, l; if (!handlers) { return; } if (Array.isArray(event)) { for (i = 0, l = event.length; i < l; i++) { off(event[i], fn); } return; } i = handlers.length; while (i--) { h = handlers[i]; if (fn === h) { handlers.splice(i, 1); break; } } } function once(event, fn) { var unsub = on(event, function () { fn.apply(context, arguments); unsub(); }); return unsub; } return { on: on, off: off, emit: emit, once: once, replay: replay }; } function install$4(context) { var scopes = new WeakMap(); var app = context.app; app.acquire = acquire; app.resolveScope = get; app.scopes = getAll; app.configureScope = function (name, element, registerFn) { if (typeof element === "function") { registerFn = element; element = document.documentElement; } return configure(name, element, registerFn); }; app.resolve = function (name, element) { return this.resolveScope(name, element).then(function (scopeEntry) { return scopeEntry.scope; }); }; context.onCreate(function (instance, creationContext, definitionLoaded) { configure((creationContext.alias || creationContext.plugin).toLowerCase(), instance.definition.element, function () { return definitionLoaded; }); instance.resolve = function (name, element) { return this.resolveScope(name, element).then(function (scopeEntry) { return scopeEntry.scope; }); }; instance.resolveScope = function (name, element) { return get(name, element || instance.definition.element); }; instance.onDestroy(function () { var elScopes = scopes.get(instance.definition.element), elementExists = window.document.contains(instance.definition.element); if (!elementExists) { scopes["delete"](instance.definition.element); } else { delete elScopes[instance.definition.type]; } }); }); function getAll() { return scopes; } //scope creation/configuration function configure(name, element, provide) { name = name.toLowerCase(); var elScopes = scopes.get(element), scope = elScopes && elScopes[name]; if (!scope) { if (!elScopes) { elScopes = {}; scopes.set(element, elScopes); } var built = Promise.resolve(provide()).then(function (obj) { if (obj && obj.api) { Object.freeze(obj.api); return obj; } else { return "***MISSING SCOPE***"; } })["catch"](function (err) { console && console.error("failed to load scope for plugin", name, err); }); elScopes[name] = built.then(function (obj) { if (element === window.document.documentElement) { context.emitter.replay("scope_" + name + ":ready", obj.api, element, obj.instance); } return obj; }); } } //scope retrieval function get(name, element) { var scope; name = name.toLowerCase(); while (!scope && element) { var s = scopes.get(element); scope = s && s[name]; if (!scope) { element = element.parentElement; } } if (scope) { return scope.then(function (resolved) { return { host: element, instance: resolved.instance, scope: resolved.api }; }); } else { return Promise.reject("scope '" + name + "' not found"); } } //retrieving global scopes function acquire(scopes) { var loadScopes; if (typeof scopes === "string") { return waitForScope(scopes); } loadScopes = scopes.map(function (s, i) { return waitForScope(s); }); return Promise.all(loadScopes).then(function (values) { return scopes.reduce(function (resolved, prop, index) { resolved[prop] = values[index]; return resolved; }, {}); }); function waitForScope(s) { s = s.toLowerCase(); return new Promise(function (resolve, reject) { context.emitter.once("scope_" + s + ":ready", function (scope, element) { resolve(scope); }); }); } } } function subscribe(method, eventsEmitter) { return function (event, fn, eventSource) { var plugin = this, subs = plugin.$$state.subs, unsub, eventTarget = eventSource, handler; if (typeof fn !== "function" && typeof eventSource === "function") { eventTarget = fn; fn = eventSource; } if (eventTarget instanceof HTMLElement || eventTarget instanceof Window || eventTarget instanceof Document) { handler = function handler() { if (!plugin.connected) { return; } fn.apply(this, arguments); if (method === "once") { unsub(); } }; unsub = function unsub() { eventTarget.removeEventListener(event, handler); }; eventTarget.addEventListener(event, handler); } else { unsub = eventsEmitter[method](event, function () { if (!plugin.connected) { return; } fn.apply(plugin, arguments); }); } subs.push(unsub); return function () { unsub(); subs.splice(subs.indexOf(unsub), 1); }; }; } function install$3(context) { var eventsEmitter = context.emitter; context.app.on = eventsEmitter.on; context.app.off = eventsEmitter.off; context.app.once = eventsEmitter.once; context.app.emit = eventsEmitter.emit; context.app.replay = eventsEmitter.replay; context.onCreate(function (instance) { instance.$$state.subs = []; instance.emit = eventsEmitter.emit; instance.on = subscribe("on", eventsEmitter); instance.once = subscribe("once", eventsEmitter); instance.off = eventsEmitter.off; instance.onDestroy(function () { instance.$$state.subs.forEach(function (f) { return f(); }); instance.$$state.subs = null; }); }); } var find = function find(selector, element) { return element.querySelectorAll(selector); }; function install$2(context) { context.onCreate(function (instance) { instance.find = function (key) { var results = find("[data-ref='" + key + "']", this.definition.element); if (results && results.length) { return results[0]; } return null; }, instance.findAll = function (key) { return find("[data-ref='" + key + "']", this.definition.element); }; }); } //logging factory for creating a simple configurable logger function logger(name, options) { options = options || { trace: true, debug: true, error: true, info: true }; function logging(method) { method = method || "info"; return function () { if (options[method]) { var args = [name, "=>"].concat(Array.prototype.slice.call(arguments)); console && console[method].apply(null, args); } }; } return { info: logging("info"), debug: logging("debug"), trace: logging("trace"), error: logging("error") }; } var readConfig = function readConfig() { var loggingConfigString = localStorage.getItem("plugins.configuration.logging"); return loggingConfigString && JSON.parse(loggingConfigString) || null; }; function install$1(context) { context.onInitialize(function (app) { app.setLogging = function (options, reload) { localStorage.setItem("plugins.configuration.logging", JSON.stringify(options)); if (reload) { window.location.reload(); } }; }); context.onCreate(function (instance, registrationCtx) { registrationCtx.configuration.logging = readConfig() || {}; instance.logger = logger("Plugin-" + instance.definition.type + " (" + instance.$$uid + ")", registrationCtx.configuration.logging); }); } var _void = function _void() {}; //setTimeout - setInterval with managed timers function schedule(fn, ms, times) { var plugin = this, timer, fn = fn.bind(plugin), release; if (times === window.Infinity) { release = createReleaser(plugin.$$state.intervals, function () { window.clearInterval(timer); }); timer = window.setInterval(function () { if (plugin.connected && fn() === _void) { release(); } }, ms); } else { times = Math.max(times || 1, 1); release = createReleaser(plugin.$$state.timers, function () { window.clearTimeout(timer); }); timer = window.setTimeout(function () { release(); if (!plugin.connected || fn() !== _void && --times > 0) { plugin.schedule(fn, ms, times); } }, ms); } return release; } function createReleaser(store, release) { store.push(release); return function () { release(); store.splice(store.indexOf(release), 1); }; } function install(context) { context.onCreate(function (instance) { instance.$$state.timers = []; instance.$$state.intervals = []; instance.schedule = schedule.bind(instance); instance["void"] = _void; instance.onDestroy(function () { instance.$$state.intervals.concat(instance.$$state.timers).forEach(function (f) { return f(); }); }); }); } function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function (obj) { return typeof obj; }; } else { _typeof = function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function createScanner(options) { function parseNameToken(name) { var token = { name: name, alias: null }, index; if ((index = name.indexOf(":")) !== -1) { token.alias = name.substring(index + 1); token.name = name.substring(0, index); } return token; } function pluginsAccessor(node) { var attrs = node.attributes, plugins = [], attributeNamePrefix = options.attributeNamePrefix + "-"; for (var i = 0; i < node.attributes.length; i++) { var attr = attrs[i]; if (attr.name.indexOf(attributeNamePrefix) === 0) { var token = parseNameToken(attr.name.substring(attributeNamePrefix.length)); plugins.push({ node: node, name: token.name, alias: token.alias, options: attr.value }); } } return plugins; } function isElement(obj) { //instanceof fails for elements that have been created by another document (iframe case) //in that case we check other properties existing in all element nodes return obj instanceof HTMLElement || _typeof(obj) === "object" && obj.nodeType === 1 && _typeof(obj.style) === "object" && _typeof(obj.ownerDocument) === "object"; } function discoverPlugins(node, predicate) { var collection = []; if (!isElement(node)) { return collection; } // if (processed.has(node)) { // return collection; // } // processed.add(node); if (node.getAttribute(options.ignoreAttribute) !== null) { return collection; } if (predicate && !predicate(node)) { return collection; } var plugins = pluginsAccessor(node); if (plugins.length) { collection = plugins; } if (node.children) { Array.prototype.forEach.call(node.children, function (e) { collection = collection.concat(discoverPlugins(e, predicate)); }); } return collection; } var scan = function scan(container) { return discoverPlugins(container, options.filter); }; return { scan: scan //processed }; } function createProcessor(context, options) { var onRegister = options.onRegister || function (ctx) { return ctx; }; var intersectionMargins = options.lazyLoadRootMargin || '0px 0px 200px 0px'; var shouldLazyLoad = options.shouldLazyLoad; var deferred = new WeakMap(); var observer = new IntersectionObserver(function (entries, obs) { entries.forEach(function (entry) { var node = entry.target; if (entry.isIntersecting && deferred.has(node)) { var pluginsContexts = deferred.get(node); pluginsContexts.forEach(function (ctx) { registerContext(ctx); }); deferred["delete"](node); obs.unobserve(node); } }); }, { rootMargin: intersectionMargins, threshold: 0 }); var pendingForInitialization = []; function processInit() { var results = []; if (pendingForInitialization.length) { pendingForInitialization.reduce(function (acc, definition) { acc.push(initialize(definition)); return acc; }, results); pendingForInitialization = []; } return results; } function setDeferred(node, pluginContext) { var nodeDeferreds; if (!deferred.has(node)) { deferred.set(node, nodeDeferreds = []); } else { nodeDeferreds = deferred.get(node); } nodeDeferreds.push(pluginContext); } function initialize(definition) { var node = definition.node, name = definition.name, initialized = node.initializedPlugins = node.initializedPlugins || {}, result; if (typeof name === 'undefined') { return Promise.resolve(true); } if (!initialized[name]) { var pluginOptions = parseOptions(definition.options, node), ctx = { element: node, alias: definition.alias, plugin: name, options: pluginOptions, configuration: {} }; initialized[name] = 'INIT'; if (shouldLazyLoad(node, pluginOptions, options.lazyLoadAttribute)) { setDeferred(node, ctx); observer.observe(node); return Promise.resolve(true); } else { result = registerContext(ctx); } return result; } else if (initialized[name] === 'ON') { context.pluginManager.reconnect(node, name); } return Promise.resolve(true); } function registerContext(ctx) { return Promise.resolve(onRegister(ctx)).then(function () { return context.pluginManager.register(ctx); }).then(function () { ctx.element.initializedPlugins[ctx.plugin] = "ON"; }); } function register(el, alias, pluginName, pluginConfiguration) { var ctx = { element: el, alias: alias, plugin: pluginName, options: pluginConfiguration, configuration: {} }; return registerContext(ctx); } var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/; function parseOptions(data, element) { if (data === "true") { return true; } if (data === "false") { return false; } if (data === "null") { return null; } // Only convert to a number if it doesn't change the string if (data === +data + "") { return +data; } if (rbrace.test(data)) { return tryParseJson(data, element); } return data; } function tryParseJson(json, element) { try { return JSON.parse(json); } catch (err) { console.warn(element, 'error during parsing of json', json, err); return json; } } var processTask; return { initialize: register, process: function process(nodes) { if (!pendingForInitialization.length) { processTask = new Promise(function (resolve) { window.setTimeout(function () { Promise.all(processInit()).then(resolve); }, options.processInterval); }); } nodes.forEach(function (node) { context.emitter.emit("plugins:initializing", node); var plugins = context.scanner.scan(node); pendingForInitialization = pendingForInitialization.concat(plugins); processTask.then(function () { context.emitter.emit("plugins:loaded", node); }); }); return processTask; }, disconnect: function disconnect(nodes) { var _this = this; Array.prototype.forEach.call(nodes, function (element) { context.pluginManager.disconnect(element); // context.scanner.processed.delete(element); // var parent = element.parentElement; // while (parent !== null) { // context.scanner.processed["delete"](parent); // parent = parent.parentElement; // } element.children && _this.disconnect(element.children); }); }, cleanup: function cleanup() { var now = performance.now(); var released = context.pluginManager.release(function (i) { return i.stateChangedAt + options.idleTimeout * 1000 > now; }); released.forEach(function (instances, element) { instances.forEach(function (type) { delete element.initializedPlugins[type]; //context.scanner.processed.delete(element); }); }); } }; } function createObserver(context) { var observer = new MutationObserver(function (mutations, obs) { mutations.forEach(function (mutation, index) { if (mutation.type === 'childList') { processNodes(mutation.addedNodes, mutation.removedNodes); } }); }); function processNodes(added, removed) { context.processor.process(added); context.processor.disconnect(removed); } return { start: function start(node) { observer.observe(node, { childList: true, subtree: true }); }, stop: function stop() { observer.disconnect(); } }; } var setup = function setup(options) { var componentInstallers = [install$4, install$3, install, install$2, install$1], pluginsExtensions = [], systemExtensions = [], app = { debug: false, pluginManager: null }, systemEmitter = emitter(app); Object.keys(config).forEach(function (key) { if (typeof options[key] === 'undefined') { options[key] = config[key]; } }); var context = { moduleLoader: null, onCreate: function onCreate(fn) { pluginsExtensions.push(fn); }, onInitialize: function onInitialize(fn) { systemExtensions.push(fn); }, emitter: systemEmitter, app: app, scanner: null, processor: null, observer: null, pluginManager: null }; context.scanner = createScanner(config); context.processor = createProcessor(context, { processInterval: config.processInterval, idleTimeout: config.idleTimeout, onRegister: config.preconfigure, lazyLoadRootMargin: config.lazyLoadRootMargin, lazyLoadAttribute: config.lazyLoadAttribute, shouldLazyLoad: config.shouldLazyLoad }); context.observer = createObserver(context); componentInstallers.forEach(function (f) { return f(context); }); app.pluginManager = context.pluginManager = pluginManagerFactory.create(context, pluginsExtensions); var initialized = false, cleanTimer, plugins = {}; return { use: function use(install) { install(context); }, initialize: initialize }; function enableCleanup() { if (!cleanTimer) { cleanTimer = window.setInterval(context.processor.cleanup, config.cleanupInterval); } } function disableCleanup() { window.clearInterval(cleanTimer); cleanTimer = null; } function setupSystem() { var api = { enableCleanup: enableCleanup, disableCleanup: disableCleanup, observer: context.observer, processor: context.processor, application: app }; systemExtensions.forEach(function (extend) { extend(api); }); return api; } function run() { context.processor.process([document.querySelector("body")]); enableCleanup(); if (config.observeDocument && context.observer) { context.observer.start(document); } } function initialize() { if (initialized) { return plugins; } initialized = true; plugins = setupSystem(); run(); return plugins; } }; var main = { setup: setup }; return main; }));