UNPKG

autocreate.js

Version:

autocreate.js contains a small function that watches for the creation of elements matching a given selector

435 lines (366 loc) 9.75 kB
/* * Copyright (c) 2017 Simon Schoenenberger * https://github.com/detomon/autocreate.js * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ ;(function (root, factory) { 'use strict'; if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof module === 'object' && module.exports) { module.exports = factory(); } else { root.autocreate = factory(); } }(this || window, function () { 'use strict'; /** * Random data attribute name. */ var key = 'ac' + randomString(); /** * Data attribute name. */ var dataKey = 'data-' + key; /** * Data attribute selector. */ var dataSelector = '[' + dataKey + ']'; /** * The topmost DOM element. */ var dom = document.documentElement; /** * The DOM MutationObserver watching for removed nodes. */ var domObserver; /** * Element prototype. */ var elementPrototype = Element.prototype; /** * Element matches method. */ var elementMatches = elementPrototype.matches || elementPrototype.matchesSelector || elementPrototype.msMatchesSelector || elementPrototype.mozMatchesSelector || elementPrototype.webkitMatchesSelector || elementPrototype.oMatchesSelector; /** * Create random string. * * @return String */ function randomString() { return Math.random().toString(36).substring(2, 12); } /** * Make unique list by removing duplicate items. * * @param Array array A list of arbitary items. * @return Array A list with unique elements. */ function arrayUnique(array) { return array.sort().reduce(function (list, value) { if (list[list.length - 1] !== value) { list.push(value); } return list; }, []); } /** * Loop over given NodeList. * * @param NodeList A node list. * @param Function func A callback function. */ function forEachNode(nodes, func) { for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; if (node.nodeType === Node.ELEMENT_NODE) { func(node); } } } /** * Throw error with given string. */ function error(string) { throw new Error(string); } /** * Get or create element context. * * @param Element element A DOM element. * @return Object The element context. */ function elementContext(element) { var ctx = element[key]; if (!ctx) { ctx = element[key] = new Context(element); } return ctx; }; /** * Initialize DOM observer. */ function init() { if (domObserver) { return; } domObserver = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { forEachNode(mutation.removedNodes, function (element) { var elements = element.querySelectorAll(dataSelector); forEachNode(elements, function (element) { var ctx = elementContext(element); if (ctx) { ctx.destroy(); } }); var ctx = elementContext(element); if (ctx) { ctx.destroy(); } }); }); // TODO: removing delayed }); domObserver.observe(dom, {childList: true, subtree: true}); } /** * Construct Module. * * @param Object options The module options: * String selector: A selector (e.g. `.wrapper .module`). * Function create: A create callback `function (element) {}`. * Function destroy: An optional destroy callback `function (element) {}`. * Array parents: An optional list of parents. */ var Module = function (options) { options = options || {}; var selector = options.selector || error('Selector cannot be empty'); var parents = options.parents || dom; // convert to array if not array-like object if (parents.length === undefined) { parents = [parents]; } parents = arrayUnique(parents); this.id = randomString(); this.selector = selector; this.parents = parents; this.createElement = options.create || function () {}; this.destroyElement = options.destroy || function () {}; this.destroyDelayed = !!options.destroyDelayed; this.elements = {}; this.parents.forEach(function (parent) { elementContext(parent).addModule(this); this.createAll(parent); }.bind(this)); } /** * Create instance for given element. * * @param Element element A DOM element. */ Module.prototype.create = function (element) { var moduleId = this.id; var ctx = elementContext(element); var data = ctx.data; if (!data[moduleId]) { var moduleCtx = data[moduleId] = { module: this, userCtx: {}, }; var userCtx = moduleCtx.userCtx; this.elements[ctx.id] = element; this.createElement.call(userCtx, element, userCtx); } }; /** * Destroy module and call destroy callback for every matching element. */ Module.prototype.destroy = function () { var moduleId = this.id; var elements = this.elements; var parents = this.parents; var destroy = this.destroyElement; for (var i in elements) { if (elements.hasOwnProperty(i)) { var element = elements[i]; var ctx = elementContext(element); ctx.removeModule(this); } } for (var i in parents) { if (parents.hasOwnProperty(i)) { var parent = parents[i]; var ctx = elementContext(parent); ctx.removeModule(this); } } this.elements = {}; this.parents = []; }; /** * Create instances for target element and matching child elements. * * @param Element target A DOM element. */ Module.prototype.createAll = function (target) { var self = this; var selector = this.selector; if (elementMatches.call(target, selector)) { self.create(target); } forEachNode(target.querySelectorAll(selector), function (element) { self.create(element); }); }; /** * Construct element Context. * * @param Element element A DOM element. */ var Context = function (element) { this.id = randomString(); this.data = {}; this.element = element; element.setAttribute(dataKey, ''); }; /** * Add MutationObserver to context. */ Context.prototype.addObserver = function () { if (!this.observer) { var self = this; var observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { forEachNode(mutation.addedNodes, function (element) { self.createAll(element); }); }); }); this.modules = {}; this.observer = observer; observer.observe(this.element, {childList: true, subtree: true}); } }; /** * Remove MutationObserver from context. */ Context.prototype.removeObserver = function () { if (this.observer) { this.observer.disconnect(); delete this.modules; delete this.observer; } }; /** * Add module. * * @param Module module The module to add to the context. */ Context.prototype.addModule = function (module) { this.addObserver(); this.modules[module.id] = module; }; /** * Remove module. * * @param Module module The module to remove from the context. */ Context.prototype.removeModule = function (module) { var moduleId = module.id; var element = this.element; var data = this.data; var modules = this.modules; var modulesLength = 0; if (data[moduleId]) { var userCtx = data[moduleId].userCtx; module.destroyElement.call(userCtx, element, userCtx); delete data[moduleId]; } if (modules) { delete modules[moduleId]; modulesLength = Object.keys(modules).length; } if (!modulesLength) { this.removeObserver(); if (!Object.keys(data).length) { this.destroy(); } } }; /** * Create instances for target element and matching child elements. * * @param Element target A DOM element. */ Context.prototype.createAll = function (target) { var modules = this.modules; for (var i in modules) { if (modules.hasOwnProperty(i)) { modules[i].createAll(target); } } }; /** * Destroy context. */ Context.prototype.destroy = function () { var element = this.element; var data = this.data; // before element destruction call to prevent possible recursion this.removeObserver(); for (var i in data) { if (data.hasOwnProperty(i)) { var moduleCtx = data[i]; var userCtx = moduleCtx.userCtx; moduleCtx.module.destroyElement.call(userCtx, element, userCtx); } } element.removeAttribute(dataKey); delete element[key]; }; /** * Create instance of Module. * * @param Object options Options for Module constructor. * @return Module */ function autocreate(options) { init(); return new Module(options); } var $ = window.jQuery || window.ujs; // Define jQuery or u.js plugin function if present if ($) { $.fn.autocreate = function (options) { options = options || {}; options.parents = this; return autocreate(options); }; } return autocreate; }));