├── .npmrc ├── test ├── index.js ├── package.json └── index.html ├── cjs ├── package.json └── index.js ├── .gitignore ├── .npmignore ├── rollup ├── es.config.js └── babel.config.js ├── LICENSE ├── es.js ├── min.js ├── package.json ├── README.md ├── esm └── index.js └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('../cjs'); -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | coverage/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .eslintrc.json 4 | .travis.yml 5 | coverage/ 6 | node_modules/ 7 | rollup/ 8 | test/ 9 | -------------------------------------------------------------------------------- /rollup/es.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import {terser} from 'rollup-plugin-terser'; 3 | import includePaths from 'rollup-plugin-includepaths'; 4 | export default { 5 | input: './esm/index.js', 6 | plugins: [ 7 | includePaths({ 8 | include: {}, 9 | }), 10 | nodeResolve(), 11 | terser() 12 | ], 13 | context: 'null', 14 | moduleContext: 'null', 15 | output: { 16 | esModule: false, 17 | exports: 'named', 18 | file: './es.js', 19 | format: 'iife', 20 | name: 'ElementObserver' 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /rollup/babel.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import babel from '@rollup/plugin-babel'; 3 | import includePaths from 'rollup-plugin-includepaths'; 4 | export default { 5 | input: './esm/index.js', 6 | plugins: [ 7 | includePaths({ 8 | include: {}, 9 | }), 10 | nodeResolve(), 11 | babel({ 12 | presets: ['@babel/preset-env'], 13 | babelHelpers: 'bundled' 14 | }) 15 | ], 16 | context: 'null', 17 | moduleContext: 'null', 18 | output: { 19 | esModule: false, 20 | exports: 'named', 21 | file: './index.js', 22 | format: 'iife', 23 | name: 'ElementObserver' 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /es.js: -------------------------------------------------------------------------------- 1 | self.ElementObserver=function(e){"use strict";const t="attributeChangedCallback",a="connectedCallback",n=new WeakMap,s=new WeakMap,o=(e,t)=>{const a=n.get(t);for(let t=0,{length:n}=e;t{e.disconnect(),n.delete(e)},c=(e,t,a)=>{for(let n=0,{length:s}=t;n{const s=n.get(t);for(let t=0,{length:n}=e;t 2 | 3 | 4 | 5 | 6 | element-observer 7 | 8 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webreflection/element-observer", 3 | "version": "0.0.5", 4 | "description": "A MutationObserver inspired observer for Custom Elements like mutations on any DOM element", 5 | "main": "./cjs/index.js", 6 | "scripts": { 7 | "build": "npm run cjs && npm run rollup:es && npm run rollup:babel && npm run min && npm run test", 8 | "cjs": "ascjs --no-default esm cjs", 9 | "rollup:es": "rollup --config rollup/es.config.js && sed -i.bck 's/^var /self./' es.js && rm -rf es.js.bck", 10 | "rollup:babel": "rollup --config rollup/babel.config.js && sed -i.bck 's/^var /self./; s/exports.default =/return/' index.js && rm -rf index.js.bck", 11 | "min": "terser index.js --comments='/^!/' -c -m -o min.js", 12 | "coveralls": "c8 report --reporter=text-lcov | coveralls", 13 | "test": "c8 node test/index.js" 14 | }, 15 | "keywords": [ 16 | "custom", 17 | "elements", 18 | "builtin", 19 | "lifecycle", 20 | "observer" 21 | ], 22 | "author": "Andrea Giammarchi", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "@babel/core": "^7.13.10", 26 | "@babel/preset-env": "^7.13.12", 27 | "@rollup/plugin-babel": "^5.3.0", 28 | "@rollup/plugin-node-resolve": "^11.2.0", 29 | "@ungap/degap": "^0.2.5", 30 | "ascjs": "^5.0.1", 31 | "c8": "^7.6.0", 32 | "coveralls": "^3.1.0", 33 | "rollup": "^2.42.4", 34 | "rollup-plugin-includepaths": "^0.2.4", 35 | "rollup-plugin-terser": "^7.0.2", 36 | "terser": "^5.6.1" 37 | }, 38 | "module": "./esm/index.js", 39 | "type": "module", 40 | "exports": { 41 | ".": { 42 | "import": "./esm/index.js", 43 | "default": "./cjs/index.js" 44 | }, 45 | "./package.json": "./package.json" 46 | }, 47 | "unpkg": "min.js" 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ElementObserver 2 | 3 | ## Update 4 | 5 | Don't miss out [Pretty Cool Elements](https://github.com/WebReflection/p-cool#readme), which is entirely based on *Custom Elements* primitives, and it's even easier to use, and graceful enhance, with any project out there 👍 6 | 7 | - - - 8 | 9 | A *MutationObserver* inspired observer for *Custom Elements* like mutations on any DOM element. 10 | 11 | ```js 12 | import ElementObserver from '@webreflection/element-observer'; 13 | 14 | const observer = new ElementObserver({ 15 | // optional, used only if options for attributes are used 16 | attributeChangedCallback(element, name, oldValue, newValue) { 17 | // all observed attributes will be triggered right away if present, 18 | // when the element is observed, and *before* connectedCallback, 19 | // as it is for Custom Elements 20 | }, 21 | 22 | // optional, used when the element is connected 23 | connectedCallback(element) { 24 | // if the element is already connected when observed, this is triggered. 25 | }, 26 | 27 | // optional, used when the element is disconnected 28 | disconnectedCallback(element) {} 29 | }); 30 | 31 | observer.observe( 32 | observedElement, 33 | // optional, if present is used to define attributes 34 | { 35 | // optional, if omitted will observe all attributes 36 | attributeFilter: ['only', 'these'], 37 | // optional, if omitted oldValue is always null 38 | attributeOldValue: true, 39 | // optional, if any of the previous properties are defined, 40 | // this is implicitly set as true 41 | attributes: true 42 | } 43 | ); 44 | 45 | observer.disconnect( 46 | // optional, if an element is passed, only that element 47 | // stops being observed, otherwise all observed elements 48 | // will immediately stop being observed 49 | observedElement 50 | ); 51 | ``` 52 | 53 | See [MutationObserver.observe()](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe) to better understand attributes properties. 54 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | const ATTRIBUTE_CHANGED_CALLBACK = 'attributeChangedCallback'; 2 | const CONNECTED_CALLBACK = 'connectedCallback'; 3 | const DISCONNECTED_CALLBACK = 'disconnectedCallback'; 4 | 5 | const observers = new WeakMap; 6 | const privates = new WeakMap; 7 | 8 | const attributeChangedCallback = (records, mo) => { 9 | const observed = observers.get(mo); 10 | for (let i = 0, {length} = records; i < length; i++) { 11 | const {target, attributeName, oldValue} = records[i]; 12 | observed.get(target)[ATTRIBUTE_CHANGED_CALLBACK]( 13 | target, attributeName, oldValue, target.getAttribute(attributeName) 14 | ); 15 | } 16 | }; 17 | 18 | const dropMutationObserver = mo => { 19 | mo.disconnect(); 20 | observers.delete(mo); 21 | }; 22 | 23 | const loopAndTrigger = (observed, nodes, method) => { 24 | for (let i = 0, {length} = nodes; i < length; i++) { 25 | if (observed.has(nodes[i])) { 26 | const handler = observed.get(nodes[i]); 27 | if (method in handler) 28 | handler[method](nodes[i]); 29 | } 30 | } 31 | }; 32 | 33 | const loopRecords = (records, mo) => { 34 | const observed = observers.get(mo); 35 | for (let i = 0, {length} = records; i < length; i++) { 36 | loopAndTrigger(observed, records[i].removedNodes, DISCONNECTED_CALLBACK); 37 | loopAndTrigger(observed, records[i].addedNodes, CONNECTED_CALLBACK); 38 | } 39 | }; 40 | 41 | export default class ElementObserver { 42 | /** 43 | * Create a new ElementObserver based on a specific handler. 44 | * @param {object} handler the context to use when 45 | * `connectedCallback(element)`, `disconnectedCallback(element)`, or 46 | * `attributeChangedCallback(element, name, old, value)` are invoked. 47 | */ 48 | constructor(handler) { 49 | const m = new MutationObserver(loopRecords); 50 | const o = new Map; 51 | observers.set(m, o); 52 | privates.set(this, { 53 | h: handler, 54 | a: new Map, 55 | m, o 56 | }); 57 | } 58 | 59 | /** 60 | * Like a MutationObserver, observe an element and, if already connected, 61 | * will trigger the `handler.connectedCallback(element)` right away. 62 | * @param {Element} element the DOM element to observe. 63 | * @param {object?} options an optional configuration for attributes. 64 | */ 65 | observe(element, options) { 66 | const _ = privates.get(this); 67 | if (_.o.has(element)) 68 | this.disconnect(element); 69 | if (!_.o.size) { 70 | _.m.observe(element.ownerDocument, { 71 | childList: true, 72 | subtree: true 73 | }); 74 | } 75 | _.o.set(element, _.h); 76 | if (options && ATTRIBUTE_CHANGED_CALLBACK in _.h) { 77 | const {attributes, attributeFilter, attributeOldValue} = options; 78 | const mo = new MutationObserver(attributeChangedCallback); 79 | mo.observe(element, {attributes, attributeFilter, attributeOldValue}); 80 | observers.set(mo, _.o); 81 | _.a.set(element, mo); 82 | for (let {attributes} = element, i = 0; i < attributes.length; i++) { 83 | const {name, value} = attributes[i]; 84 | if (!attributeFilter || -1 < attributeFilter.indexOf(name)) 85 | _.h[ATTRIBUTE_CHANGED_CALLBACK](element, name, null, value); 86 | } 87 | } 88 | if (element.isConnected && CONNECTED_CALLBACK in _.h) 89 | _.h[CONNECTED_CALLBACK](element); 90 | } 91 | 92 | /** 93 | * Like a MutationObserver, disconnect either all observed elements or, 94 | * differently from the native API, a single element. 95 | * @param {Element?} element the specific element to disconnect, or nothing 96 | * to clear all observed elements and their mutations. 97 | */ 98 | disconnect(element) { 99 | const _ = privates.get(this); 100 | if (element) { 101 | if (_.o.has(element)) { 102 | if (_.a.has(element)) { 103 | dropMutationObserver(_.a.get(element)); 104 | _.a.delete(element); 105 | } 106 | _.o.delete(element); 107 | if (_.o.size) 108 | return; 109 | } 110 | } 111 | else { 112 | _.a.forEach(dropMutationObserver); 113 | _.a.clear(); 114 | _.o.clear(); 115 | } 116 | _.m.disconnect(); 117 | } 118 | }; 119 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const ATTRIBUTE_CHANGED_CALLBACK = 'attributeChangedCallback'; 3 | const CONNECTED_CALLBACK = 'connectedCallback'; 4 | const DISCONNECTED_CALLBACK = 'disconnectedCallback'; 5 | 6 | const observers = new WeakMap; 7 | const privates = new WeakMap; 8 | 9 | const attributeChangedCallback = (records, mo) => { 10 | const observed = observers.get(mo); 11 | for (let i = 0, {length} = records; i < length; i++) { 12 | const {target, attributeName, oldValue} = records[i]; 13 | observed.get(target)[ATTRIBUTE_CHANGED_CALLBACK]( 14 | target, attributeName, oldValue, target.getAttribute(attributeName) 15 | ); 16 | } 17 | }; 18 | 19 | const dropMutationObserver = mo => { 20 | mo.disconnect(); 21 | observers.delete(mo); 22 | }; 23 | 24 | const loopAndTrigger = (observed, nodes, method) => { 25 | for (let i = 0, {length} = nodes; i < length; i++) { 26 | if (observed.has(nodes[i])) { 27 | const handler = observed.get(nodes[i]); 28 | if (method in handler) 29 | handler[method](nodes[i]); 30 | } 31 | } 32 | }; 33 | 34 | const loopRecords = (records, mo) => { 35 | const observed = observers.get(mo); 36 | for (let i = 0, {length} = records; i < length; i++) { 37 | loopAndTrigger(observed, records[i].removedNodes, DISCONNECTED_CALLBACK); 38 | loopAndTrigger(observed, records[i].addedNodes, CONNECTED_CALLBACK); 39 | } 40 | }; 41 | 42 | module.exports = class ElementObserver { 43 | /** 44 | * Create a new ElementObserver based on a specific handler. 45 | * @param {object} handler the context to use when 46 | * `connectedCallback(element)`, `disconnectedCallback(element)`, or 47 | * `attributeChangedCallback(element, name, old, value)` are invoked. 48 | */ 49 | constructor(handler) { 50 | const m = new MutationObserver(loopRecords); 51 | const o = new Map; 52 | observers.set(m, o); 53 | privates.set(this, { 54 | h: handler, 55 | a: new Map, 56 | m, o 57 | }); 58 | } 59 | 60 | /** 61 | * Like a MutationObserver, observe an element and, if already connected, 62 | * will trigger the `handler.connectedCallback(element)` right away. 63 | * @param {Element} element the DOM element to observe. 64 | * @param {object?} options an optional configuration for attributes. 65 | */ 66 | observe(element, options) { 67 | const _ = privates.get(this); 68 | if (_.o.has(element)) 69 | this.disconnect(element); 70 | if (!_.o.size) { 71 | _.m.observe(element.ownerDocument, { 72 | childList: true, 73 | subtree: true 74 | }); 75 | } 76 | _.o.set(element, _.h); 77 | if (options && ATTRIBUTE_CHANGED_CALLBACK in _.h) { 78 | const {attributes, attributeFilter, attributeOldValue} = options; 79 | const mo = new MutationObserver(attributeChangedCallback); 80 | mo.observe(element, {attributes, attributeFilter, attributeOldValue}); 81 | observers.set(mo, _.o); 82 | _.a.set(element, mo); 83 | for (let {attributes} = element, i = 0; i < attributes.length; i++) { 84 | const {name, value} = attributes[i]; 85 | if (!attributeFilter || -1 < attributeFilter.indexOf(name)) 86 | _.h[ATTRIBUTE_CHANGED_CALLBACK](element, name, null, value); 87 | } 88 | } 89 | if (element.isConnected && CONNECTED_CALLBACK in _.h) 90 | _.h[CONNECTED_CALLBACK](element); 91 | } 92 | 93 | /** 94 | * Like a MutationObserver, disconnect either all observed elements or, 95 | * differently from the native API, a single element. 96 | * @param {Element?} element the specific element to disconnect, or nothing 97 | * to clear all observed elements and their mutations. 98 | */ 99 | disconnect(element) { 100 | const _ = privates.get(this); 101 | if (element) { 102 | if (_.o.has(element)) { 103 | if (_.a.has(element)) { 104 | dropMutationObserver(_.a.get(element)); 105 | _.a.delete(element); 106 | } 107 | _.o.delete(element); 108 | if (_.o.size) 109 | return; 110 | } 111 | } 112 | else { 113 | _.a.forEach(dropMutationObserver); 114 | _.a.clear(); 115 | _.o.clear(); 116 | } 117 | _.m.disconnect(); 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | self.ElementObserver = (function (exports) { 2 | 'use strict'; 3 | 4 | function _classCallCheck(instance, Constructor) { 5 | if (!(instance instanceof Constructor)) { 6 | throw new TypeError("Cannot call a class as a function"); 7 | } 8 | } 9 | 10 | function _defineProperties(target, props) { 11 | for (var i = 0; i < props.length; i++) { 12 | var descriptor = props[i]; 13 | descriptor.enumerable = descriptor.enumerable || false; 14 | descriptor.configurable = true; 15 | if ("value" in descriptor) descriptor.writable = true; 16 | Object.defineProperty(target, descriptor.key, descriptor); 17 | } 18 | } 19 | 20 | function _createClass(Constructor, protoProps, staticProps) { 21 | if (protoProps) _defineProperties(Constructor.prototype, protoProps); 22 | if (staticProps) _defineProperties(Constructor, staticProps); 23 | return Constructor; 24 | } 25 | 26 | var ATTRIBUTE_CHANGED_CALLBACK = 'attributeChangedCallback'; 27 | var CONNECTED_CALLBACK = 'connectedCallback'; 28 | var DISCONNECTED_CALLBACK = 'disconnectedCallback'; 29 | var observers = new WeakMap(); 30 | var privates = new WeakMap(); 31 | 32 | var attributeChangedCallback = function attributeChangedCallback(records, mo) { 33 | var observed = observers.get(mo); 34 | 35 | for (var i = 0, length = records.length; i < length; i++) { 36 | var _records$i = records[i], 37 | target = _records$i.target, 38 | attributeName = _records$i.attributeName, 39 | oldValue = _records$i.oldValue; 40 | observed.get(target)[ATTRIBUTE_CHANGED_CALLBACK](target, attributeName, oldValue, target.getAttribute(attributeName)); 41 | } 42 | }; 43 | 44 | var dropMutationObserver = function dropMutationObserver(mo) { 45 | mo.disconnect(); 46 | observers["delete"](mo); 47 | }; 48 | 49 | var loopAndTrigger = function loopAndTrigger(observed, nodes, method) { 50 | for (var i = 0, length = nodes.length; i < length; i++) { 51 | if (observed.has(nodes[i])) { 52 | var handler = observed.get(nodes[i]); 53 | if (method in handler) handler[method](nodes[i]); 54 | } 55 | } 56 | }; 57 | 58 | var loopRecords = function loopRecords(records, mo) { 59 | var observed = observers.get(mo); 60 | 61 | for (var i = 0, length = records.length; i < length; i++) { 62 | loopAndTrigger(observed, records[i].removedNodes, DISCONNECTED_CALLBACK); 63 | loopAndTrigger(observed, records[i].addedNodes, CONNECTED_CALLBACK); 64 | } 65 | }; 66 | 67 | var ElementObserver = /*#__PURE__*/function () { 68 | /** 69 | * Create a new ElementObserver based on a specific handler. 70 | * @param {object} handler the context to use when 71 | * `connectedCallback(element)`, `disconnectedCallback(element)`, or 72 | * `attributeChangedCallback(element, name, old, value)` are invoked. 73 | */ 74 | function ElementObserver(handler) { 75 | _classCallCheck(this, ElementObserver); 76 | 77 | var m = new MutationObserver(loopRecords); 78 | var o = new Map(); 79 | observers.set(m, o); 80 | privates.set(this, { 81 | h: handler, 82 | a: new Map(), 83 | m: m, 84 | o: o 85 | }); 86 | } 87 | /** 88 | * Like a MutationObserver, observe an element and, if already connected, 89 | * will trigger the `handler.connectedCallback(element)` right away. 90 | * @param {Element} element the DOM element to observe. 91 | * @param {object?} options an optional configuration for attributes. 92 | */ 93 | 94 | 95 | _createClass(ElementObserver, [{ 96 | key: "observe", 97 | value: function observe(element, options) { 98 | var _ = privates.get(this); 99 | 100 | if (_.o.has(element)) this.disconnect(element); 101 | 102 | if (!_.o.size) { 103 | _.m.observe(element.ownerDocument, { 104 | childList: true, 105 | subtree: true 106 | }); 107 | } 108 | 109 | _.o.set(element, _.h); 110 | 111 | if (options && ATTRIBUTE_CHANGED_CALLBACK in _.h) { 112 | var attributes = options.attributes, 113 | attributeFilter = options.attributeFilter, 114 | attributeOldValue = options.attributeOldValue; 115 | var mo = new MutationObserver(attributeChangedCallback); 116 | mo.observe(element, { 117 | attributes: attributes, 118 | attributeFilter: attributeFilter, 119 | attributeOldValue: attributeOldValue 120 | }); 121 | observers.set(mo, _.o); 122 | 123 | _.a.set(element, mo); 124 | 125 | for (var _attributes = element.attributes, i = 0; i < _attributes.length; i++) { 126 | var _attributes$i = _attributes[i], 127 | name = _attributes$i.name, 128 | value = _attributes$i.value; 129 | if (!attributeFilter || -1 < attributeFilter.indexOf(name)) _.h[ATTRIBUTE_CHANGED_CALLBACK](element, name, null, value); 130 | } 131 | } 132 | 133 | if (element.isConnected && CONNECTED_CALLBACK in _.h) _.h[CONNECTED_CALLBACK](element); 134 | } 135 | /** 136 | * Like a MutationObserver, disconnect either all observed elements or, 137 | * differently from the native API, a single element. 138 | * @param {Element?} element the specific element to disconnect, or nothing 139 | * to clear all observed elements and their mutations. 140 | */ 141 | 142 | }, { 143 | key: "disconnect", 144 | value: function disconnect(element) { 145 | var _ = privates.get(this); 146 | 147 | if (element) { 148 | if (_.o.has(element)) { 149 | if (_.a.has(element)) { 150 | dropMutationObserver(_.a.get(element)); 151 | 152 | _.a["delete"](element); 153 | } 154 | 155 | _.o["delete"](element); 156 | 157 | if (_.o.size) return; 158 | } 159 | } else { 160 | _.a.forEach(dropMutationObserver); 161 | 162 | _.a.clear(); 163 | 164 | _.o.clear(); 165 | } 166 | 167 | _.m.disconnect(); 168 | } 169 | }]); 170 | 171 | return ElementObserver; 172 | }(); 173 | 174 | return ElementObserver; 175 | 176 | return exports; 177 | 178 | }({})); 179 | --------------------------------------------------------------------------------