├── .gitignore ├── spec ├── jshintrc.json ├── jasmine.tap_reporter.js └── test.js ├── package.json ├── GruntFile.js ├── LICENSE ├── index.html ├── insQ.min.js ├── README.md └── insQ.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .grunt/ 3 | _SpecRunner.html 4 | -------------------------------------------------------------------------------- /spec/jshintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "camelcase": true, 3 | "curly": true, 4 | "eqeqeq":true, 5 | "forin":true, 6 | "immed":true, 7 | "indent":4, 8 | "latedef":true, 9 | "noarg":true, 10 | "undef":true, 11 | "unused":false, 12 | "strict":true, 13 | "maxparams":10, 14 | "maxdepth":10, 15 | "maxstatements":20, 16 | 17 | "es5": true, 18 | "eqnull":true, 19 | "lastsemic":true, 20 | "scripturl":true, 21 | 22 | "globals": { 23 | "window": false, 24 | "document": false, 25 | "setTimeout": false, 26 | "clearTimeout": false, 27 | "module": false 28 | } 29 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "insertion-query", 3 | "version": "1.1.0", 4 | "description": "Non-dom-event way to catch DOM insertions. Works in IE10+ and filters by selectors.", 5 | "keywords": [ 6 | "DOM", 7 | "dom insertion", 8 | "dom events", 9 | "mutation observer", 10 | "browser" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/naugtur/insertionQuery" 15 | }, 16 | "author": "Zbyszek Tenerowicz (http://naugtur.pl/)", 17 | "main": "insQ.js", 18 | "license": "MIT", 19 | "dependencies": {}, 20 | "devDependencies": { 21 | "grunt": "^1.3.0", 22 | "grunt-contrib-jasmine": "^2.2.0", 23 | "grunt-contrib-jshint": "^2.1.0", 24 | "grunt-contrib-uglify": "^4.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /GruntFile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | conf: { 4 | testPath: 'spec' 5 | }, 6 | pkg: grunt.file.readJSON('package.json'), 7 | 8 | uglify: { 9 | options: { 10 | banner: 11 | '// <%= pkg.name %> v<%= pkg.version %> (<%= grunt.template.today("yyyy-mm-dd") %>) \n'+ 12 | '// license:<%= pkg.license %> \n'+ 13 | '// <%= pkg.author %> \n' 14 | }, 15 | dist: { 16 | src: 'insQ.js', 17 | dest: 'insQ.min.js' 18 | } 19 | }, 20 | 21 | 22 | jasmine: { 23 | src: 'insQ.js', 24 | options: { 25 | specs: '<%= conf.testPath %>/test.js' 26 | } 27 | }, 28 | 29 | jshint: { 30 | code: { 31 | options: { 32 | jshintrc: '<%= conf.testPath %>/jshintrc.json' 33 | }, 34 | src: [ 35 | 'insQ.js' 36 | ] 37 | } 38 | }, 39 | 40 | 41 | }); 42 | 43 | grunt.loadNpmTasks('grunt-contrib-jasmine'); 44 | grunt.loadNpmTasks('grunt-contrib-jshint'); 45 | grunt.loadNpmTasks('grunt-contrib-uglify'); 46 | 47 | grunt.registerTask('test', ['jshint', 'jasmine']); 48 | 49 | grunt.registerTask('dist', ['jshint', 'jasmine', 'uglify:dist']); 50 | 51 | grunt.registerTask('default', ['dist']); 52 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014-present Zbyszek Tenerowicz 4 | Eryk Napierała 5 | Askar Yusupov 6 | Dan Dascalescu 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | demo 7 | 8 | 9 | 21 | 22 | 23 | 24 | 25 |
26 | 27 |
28 | 29 |

Open the console and watch...

30 | 31 | 32 | 33 | 34 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /insQ.min.js: -------------------------------------------------------------------------------- 1 | // insertion-query v1.0.5 (2020-09-20) 2 | // license:MIT 3 | // Zbyszek Tenerowicz (http://naugtur.pl/) 4 | 5 | var insertionQ=function(){"use strict";var m=100,t=!1,u="animationName",d="",n="Webkit Moz O ms Khtml".split(" "),e="",i=document.createElement("div"),s={strictlyNew:!0,timeout:20,addImportant:!1};if(i.style.animationName&&(t=!0),!1===t)for(var o=0;o setTimeout(resolve, time)) 3 | } 4 | 5 | describe("Insertion Query lib", function () { 6 | 7 | 8 | it('should react to an insertion', async function () { 9 | insertionQ.config({ 10 | strictlyNew: true 11 | }); 12 | var callback = jasmine.createSpy('callback'); 13 | insertionQ('blockquote').every(callback); 14 | await waits(200); 15 | document.body.appendChild(document.createElement('blockquote')); 16 | await waits(200); //just to be sure 17 | console.log(callback) 18 | expect(callback.calls.count()).toEqual(1); 19 | 20 | }); 21 | 22 | 23 | it('should call the callbacks for two selectors accordingly', async function () { 24 | var callback1 = jasmine.createSpy('callback'), 25 | callback2 = jasmine.createSpy('callback'); 26 | insertionQ('#a').every(callback1); 27 | insertionQ('#b').every(callback2); 28 | await waits(200); 29 | var el = document.createElement('q'); 30 | el.id = "a"; 31 | document.body.appendChild(el); 32 | el = document.createElement('q'); 33 | el.id = "b"; 34 | document.body.appendChild(el); 35 | await waits(200); //just to be sure 36 | expect(callback1.calls.count()).toEqual(1); 37 | expect(callback2.calls.count()).toEqual(1); 38 | 39 | }); 40 | 41 | it('should react to a change that causes the element to match the selector', async function () { 42 | var callback = jasmine.createSpy('callback'), 43 | el; 44 | insertionQ('q.someFunnyClass').every(callback); 45 | await waits(200); 46 | el = document.createElement('q'); 47 | document.body.appendChild(el); 48 | await waits(100); 49 | el.setAttribute('class', 'someFunnyClass'); 50 | await waits(200); 51 | expect(callback.calls.count()).toEqual(1); 52 | 53 | }); 54 | 55 | it('should not react to old elements', async function () { 56 | var callback = jasmine.createSpy('callback'); 57 | document.body.appendChild(document.createElement('q')); 58 | await waits(200); 59 | insertionQ('q').every(callback); 60 | await waits(200); 61 | document.body.appendChild(document.createElement('q')); 62 | await waits(200); 63 | expect(callback.calls.count()).toEqual(1); 64 | 65 | }); 66 | it('should NOT react to old elements getting displayed just now (happened in webkit)', async function () { 67 | var callback = jasmine.createSpy('callback'), 68 | el = document.createElement('q'); 69 | el.style.display = 'none'; 70 | document.body.appendChild(el); 71 | await waits(200); 72 | insertionQ('q').every(function () { 73 | console.log('call'); 74 | callback(); 75 | }); 76 | await waits(200); 77 | el.style.display = 'inline'; 78 | await waits(200); 79 | expect(callback.calls.count()).toEqual(0); 80 | 81 | }); 82 | 83 | it('should pass the newly added node to the callback function', async function () { 84 | var el = document.createElement('q'), 85 | resultNode; 86 | insertionQ('q').every(function (node) { 87 | resultNode = node; 88 | }); 89 | await waits(200); 90 | document.body.appendChild(el); 91 | await waits(200); 92 | expect(resultNode).toBe(el); 93 | 94 | }); 95 | 96 | 97 | it('should call one summary callback for two nodes with common parent', async function () { 98 | var nodeArray, 99 | callback1 = jasmine.createSpy('callback1'); 100 | insertionQ('q').summary(function (a) { 101 | nodeArray = a; 102 | callback1(); 103 | }); 104 | await waits(200); 105 | var wrap = document.createElement('div'), 106 | el = document.createElement('q'); 107 | el.id = "z"; 108 | wrap.appendChild(el); 109 | el = document.createElement('q'); 110 | el.id = "x"; 111 | wrap.appendChild(el); 112 | document.body.appendChild(wrap); 113 | await waits(500); //just to be sure 114 | expect(callback1.calls.count()).toEqual(1); 115 | expect(nodeArray.length).toEqual(1); 116 | expect(nodeArray[0].nodeName).toBe('DIV'); 117 | 118 | }); 119 | 120 | 121 | it('should unbind everything when destroyed', async function () { 122 | var callback = jasmine.createSpy('callback'); 123 | insertionQ('q').every(callback).destroy(); 124 | await waits(20); 125 | document.body.appendChild(document.createElement('q')); 126 | await waits(200); 127 | expect(callback).not.toHaveBeenCalled(); 128 | }); 129 | 130 | it('should react to a disabled input insertion', async function () { 131 | var callback = jasmine.createSpy('callback'); 132 | insertionQ('input[type="checkbox"]').every(callback); 133 | await waits(20); 134 | var el = document.createElement('input'); 135 | el.type = 'checkbox'; 136 | el.disabled = true; 137 | document.body.appendChild(el); 138 | await waits(200); 139 | expect(callback).toHaveBeenCalled(); 140 | }); 141 | 142 | it('should work with strictlyNew set to false', async function () { 143 | var asyncErrorCaught; 144 | var onErrorBackup = window.onerror; 145 | window.onerror = function (err) { 146 | asyncErrorCaught = err 147 | } 148 | insertionQ.config({ 149 | strictlyNew: false 150 | }); 151 | var callback = jasmine.createSpy('callback'); 152 | document.body.appendChild(document.createElement('q')); 153 | insertionQ('q').summary(callback); 154 | await waits(200); 155 | insertionQ.config({ 156 | strictlyNew: true 157 | }); 158 | window.onerror = onErrorBackup 159 | if (asyncErrorCaught) { 160 | throw asyncErrorCaught 161 | } 162 | }); 163 | 164 | 165 | }); 166 | -------------------------------------------------------------------------------- /insQ.js: -------------------------------------------------------------------------------- 1 | var insertionQ = (function () { 2 | "use strict"; 3 | 4 | var sequence = 100, 5 | isAnimationSupported = false, 6 | animationstring = 'animationName', 7 | keyframeprefix = '', 8 | domPrefixes = 'Webkit Moz O ms Khtml'.split(' '), 9 | pfx = '', 10 | elm = document.createElement('div'), 11 | options = { 12 | strictlyNew: true, 13 | timeout: 20, 14 | addImportant: false 15 | }; 16 | 17 | if (elm.style.animationName) { 18 | isAnimationSupported = true; 19 | } 20 | 21 | if (isAnimationSupported === false) { 22 | for (var i = 0; i < domPrefixes.length; i++) { 23 | if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) { 24 | pfx = domPrefixes[i]; 25 | animationstring = pfx + 'AnimationName'; 26 | keyframeprefix = '-' + pfx.toLowerCase() + '-'; 27 | isAnimationSupported = true; 28 | break; 29 | } 30 | } 31 | } 32 | 33 | function isTagged(el) { 34 | return (options.strictlyNew && (el.QinsQ === true)); 35 | } 36 | 37 | function listen(selector, callback) { 38 | var styleAnimation, 39 | animationName = 'insQ_' + (sequence++), 40 | importantRule = options.addImportant ? " !important" : ""; 41 | 42 | var eventHandler = function (event) { 43 | if (event.animationName === animationName || event[animationstring] === animationName) { 44 | if (!isTagged(event.target)) { 45 | callback(event.target); 46 | } 47 | } 48 | }; 49 | 50 | styleAnimation = document.createElement('style'); 51 | styleAnimation.innerHTML = '@' + keyframeprefix + 'keyframes ' + animationName + ' { from { outline: 1px solid transparent } to { outline: 0px solid transparent } }' + 52 | "\n" + selector + ' { animation-duration: 0.001s' + importantRule + '; animation-name: ' + animationName + importantRule + '; ' + 53 | keyframeprefix + 'animation-duration: 0.001s' + importantRule + '; ' + keyframeprefix + 'animation-name: ' + animationName + importantRule + '; ' + 54 | ' } '; 55 | 56 | document.head.appendChild(styleAnimation); 57 | 58 | var bindAnimationLater = setTimeout(function () { 59 | document.addEventListener('animationstart', eventHandler, false); 60 | document.addEventListener('MSAnimationStart', eventHandler, false); 61 | document.addEventListener('webkitAnimationStart', eventHandler, false); 62 | //event support is not consistent with DOM prefixes 63 | }, options.timeout); //starts listening later to skip elements found on startup. this might need tweaking 64 | 65 | return { 66 | destroy: function () { 67 | clearTimeout(bindAnimationLater); 68 | if (styleAnimation) { 69 | document.head.removeChild(styleAnimation); 70 | styleAnimation = null; 71 | } 72 | document.removeEventListener('animationstart', eventHandler); 73 | document.removeEventListener('MSAnimationStart', eventHandler); 74 | document.removeEventListener('webkitAnimationStart', eventHandler); 75 | } 76 | }; 77 | } 78 | 79 | 80 | function tag(el) { 81 | el.QinsQ = true; //bug in V8 causes memory leaks when weird characters are used as field names. I don't want to risk leaking DOM trees so the key is not '-+-' anymore 82 | } 83 | 84 | function topmostUntaggedParent(el) { 85 | if (isTagged(el.parentNode) || el.nodeName === 'BODY') { 86 | return el; 87 | } else { 88 | return topmostUntaggedParent(el.parentNode); 89 | } 90 | } 91 | 92 | function tagAll(e) { 93 | if (!e) { return; } 94 | tag(e); 95 | e = e.firstChild; 96 | for (; e; e = e.nextSibling) { 97 | if (e !== undefined && e.nodeType === 1) { 98 | tagAll(e); 99 | } 100 | } 101 | } 102 | 103 | //aggregates multiple insertion events into a common parent 104 | function catchInsertions(selector, callback) { 105 | var insertions = []; 106 | //throttle summary 107 | var sumUp = (function () { 108 | var to; 109 | return function () { 110 | clearTimeout(to); 111 | to = setTimeout(function () { 112 | insertions.forEach(tagAll); 113 | callback(insertions); 114 | insertions = []; 115 | }, 10); 116 | }; 117 | })(); 118 | 119 | return listen(selector, function (el) { 120 | if (isTagged(el)) { 121 | return; 122 | } 123 | tag(el); 124 | var myparent = topmostUntaggedParent(el); 125 | if (insertions.indexOf(myparent) < 0) { 126 | insertions.push(myparent); 127 | } 128 | sumUp(); 129 | }); 130 | } 131 | 132 | //insQ function 133 | var exports = function (selector) { 134 | if (isAnimationSupported && selector.match(/[^{}]/)) { 135 | 136 | if (options.strictlyNew) { 137 | tagAll(document.body); //prevents from catching things on show 138 | } 139 | return { 140 | every: function (callback) { 141 | return listen(selector, callback); 142 | }, 143 | summary: function (callback) { 144 | return catchInsertions(selector, callback); 145 | } 146 | }; 147 | } else { 148 | return false; 149 | } 150 | }; 151 | 152 | //allows overriding defaults 153 | exports.config = function (opt) { 154 | for (var o in opt) { 155 | if (opt.hasOwnProperty(o)) { 156 | options[o] = opt[o]; 157 | } 158 | } 159 | }; 160 | 161 | return exports; 162 | })(); 163 | 164 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 165 | module.exports = insertionQ; 166 | } 167 | --------------------------------------------------------------------------------