Skip to content

Commit 23ab7ee

Browse files
committed
initial
1 parent 4428698 commit 23ab7ee

3 files changed

Lines changed: 309 additions & 1 deletion

File tree

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,27 @@
1-
# SelectorObserver.js
1+
# SelectorObserver.js
2+
3+
Monitor DOM elements that match a CSS selector
4+
5+
## Ussage
6+
7+
```javascript
8+
9+
import { SelectorObserver } from 'https://cdn.jsdelivr.net/gh/u1ui/SelectorObserver.js@x.x.x/SelectorObserver.min.js';
10+
11+
let o = new SelectorObserver({
12+
on : el => console.log('added',el) ,
13+
off : el => console.log('removed',el) ,
14+
});
15+
o.observe('[data-trigger]');
16+
17+
```
18+
19+
## Options - "checkAnimation" (beta)
20+
21+
```javascript
22+
o.observer('.el', {checkAnimation: true});
23+
24+
```
25+
26+
Like this, you can event watch selectors like `.el:has(img:hover) > tr:nth-child(2)`
27+
`off` does not work right now.

SelectorObserver.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,183 @@
1+
const observers = new Set();
12

3+
// global mutation observer
4+
const muObserver = new MutationObserver(checkMutations);
5+
muObserver.observe(document, {
6+
childList: true,
7+
subtree: true,
8+
attributes: true
9+
});
10+
function addTree(el) {
11+
for (const observer of observers) observer._addTree(el);
12+
}
13+
function removeTree(el) {
14+
for (const observer of observers) observer._removeTree(el);
15+
}
16+
function treeModified(el) {
17+
for (const observer of observers) observer._treeModified(el);
18+
}
19+
function checkMutations(mutations) {
20+
for (const mutation of mutations) {
21+
if (mutation.type==='childList') {
22+
for (const target of mutation.addedNodes) {
23+
addTree(target);
24+
}
25+
for (const target of mutation.removedNodes) {
26+
removeTree(target);
27+
}
28+
}
29+
if (mutation.type==='attributes') {
30+
treeModified(mutation.target);
31+
}
32+
}
33+
}
34+
35+
36+
// animation observer
37+
let animationCounter = 0;
38+
let aObservers = new Set();
39+
class _animationObserver {
40+
constructor(selector, callback) {
41+
this.callback = callback;
42+
this.style = document.createElement('style');
43+
this.animationName = `observer-animation-${animationCounter++}`;
44+
45+
this.style.innerHTML = `
46+
@keyframes ${this.animationName} {}
47+
${selector} { animation: ${this.animationName} 1ms; }
48+
`;
49+
document.head.append(this.style);
50+
aObservers.add(this);
51+
}
52+
disconnect(){
53+
this.style.remove();
54+
aObservers.delete(this);
55+
}
56+
}
57+
document.addEventListener('animationstart', e => { // todo: remove/add listener by ussage
58+
for (const observer of aObservers) {
59+
if (e.animationName === observer.animationName) {
60+
observer.callback(e.target);
61+
}
62+
}
63+
});
64+
65+
66+
67+
68+
69+
70+
export class SelectorObserver {
71+
constructor({on, off}) {
72+
this.elements = new WeakSet(); // todo WeakRef: to be able to loop over elements
73+
this._on = on;
74+
this._off = off;
75+
}
76+
observe(selector, options={}) {
77+
this.selector = selector;
78+
this.options = options;
79+
const els = document.querySelectorAll(this.selector);
80+
for (const el of els) this._add(el);
81+
82+
if (options.checkMutations!==false) {
83+
observers.add(this);
84+
}
85+
86+
if (options && options.checkAnimation) {
87+
this.aniObserver = new _animationObserver(this.selector, el=>{
88+
el.matches(this.selector) ? this._add(el) : this._remove(el);
89+
});
90+
}
91+
}
92+
disconnect() {
93+
observers.delete(this);
94+
this.aniObserver && this.aniObserver.disconnect();
95+
}
96+
_add(el) {
97+
if (this.elements.has(el)) return;
98+
this.elements.add(el);
99+
this._on && this._on(el);
100+
}
101+
_remove(el) {
102+
if (!this.elements.has(el)) return;
103+
this.elements.delete(el);
104+
this._off && this._off(el);
105+
}
106+
107+
_addTree(target) {
108+
target.matches(this.selector) && this._add(target);
109+
if (document.readyState === 'complete') { // Before domready MutationObserver reports every node, ok?
110+
for (const el of target.querySelectorAll(this.selector)) this._add(el);
111+
}
112+
}
113+
_removeTree(target) {
114+
//if (!this._off) return; // performance
115+
this._remove(target);
116+
for (const el of target.querySelectorAll('*')) this._remove(el);
117+
}
118+
_treeModified(target) {
119+
target.matches(this.selector) ? this._add(target) : this._remove(target);
120+
// for the moment subtree not checked
121+
// if (this.options.deep) {
122+
// for (const el of this.elements) if (!el.matches(this.selector)) this._remove(el);
123+
// for (const el of target.querySelectorAll(this.selector)) this._add(el);
124+
// }
125+
}
126+
}
127+
128+
129+
130+
131+
132+
/*
133+
var listeners = [],
134+
//root = document.documentElement,
135+
root = document,
136+
Observer;
137+
138+
onElement = function(selector, options) {
139+
var listener = {
140+
selector: selector,
141+
onAdd: options.onAdd,
142+
elements: new WeakSet(),
143+
};
144+
var els = root.querySelectorAll(listener.selector), i=0, el;
145+
while (el = els[i++]) {
146+
listener.elements.add(el);
147+
listener.onAdd && listener.onAdd.call(el, el);
148+
}
149+
150+
listeners.push(listener);
151+
if (!Observer) {
152+
Observer = new MutationObserver(checkMutations);
153+
Observer.observe(root, {
154+
childList: true,
155+
subtree: true
156+
});
157+
}
158+
checkListener(listener);
159+
};
160+
function checkListener(listener, target) {
161+
var i=0, el, els = [];
162+
target && target.matches(listener.selector) && els.push(target);
163+
if (loaded) { // ok? check inside node on innerHTML - only when loaded
164+
Array.prototype.push.apply(els, (target||root).querySelectorAll(listener.selector));
165+
}
166+
while (el = els[i++]) {
167+
if (listener.elements.has(el)) continue;
168+
listener.elements.add(el);
169+
listener.onAdd && listener.onAdd.call(el, el);
170+
}
171+
}
172+
function checkListeners(inside) {
173+
var i=0, listener;
174+
while (listener = listeners[i++]) checkListener(listener, inside);
175+
}
176+
function checkMutations(mutations) {
177+
var j=0, i, mutation, nodes, target;
178+
while (mutation = mutations[j++]) {
179+
nodes = mutation.addedNodes, i=0;
180+
while (target=nodes[i++]) target.nodeType === 1 && checkListeners(target);
181+
}
182+
}
183+
*/

tests/test.html

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<!DOCTYPE html>
2+
<html lang=en>
3+
<head>
4+
<meta charset=utf-8>
5+
<meta name=viewport content="width=device-width">
6+
<title>u1-ico Test</title>
7+
<body>
8+
9+
10+
<div class=item id=el1>added before</div>
11+
12+
<script type=module>
13+
14+
import {SelectorObserver} from '../SelectorObserver.js';
15+
16+
let o = new SelectorObserver({
17+
on: el => console.log('added', el) ,
18+
off : el => console.log('removed', el),
19+
});
20+
o.observe('.item');
21+
22+
console.log('add element 2:');
23+
el1.insertAdjacentHTML('afterend', '<div class=item id=el2>element 2</div>');
24+
25+
setTimeout(()=>{
26+
console.log('removing class from element 2:');
27+
el2.classList.remove('item');
28+
}, 1000);
29+
30+
setTimeout(()=>{
31+
console.log('adding class to element 2:');
32+
el2.classList.add('item');
33+
}, 2000);
34+
35+
setTimeout(()=>{
36+
console.log('remove element 2:');
37+
el2.remove();
38+
}, 3000);
39+
40+
setTimeout(()=>{
41+
console.log('add wrapped:');
42+
el1.insertAdjacentHTML('afterend', '<div id=wrapped><div class="wrapped item">wrapped</div></div>');
43+
}, 4000);
44+
45+
setTimeout(()=>{
46+
console.log('remove wrapped:');
47+
wrapped.remove();
48+
}, 5000);
49+
50+
setTimeout(()=>{
51+
o.disconnect();
52+
console.log('add after disconnect:');
53+
el1.insertAdjacentHTML('afterend', '<div class=item>after disconnect</div>');
54+
}, 6000);
55+
56+
57+
58+
{
59+
let o = new SelectorObserver({
60+
on : el=>console.log('mouseover',el),
61+
off : el=>console.log('mouseout',el),
62+
});
63+
console.log('observer .item:hover')
64+
o.observe('.item:hover', {checkAnimation:true});
65+
}
66+
67+
68+
function addALotOfElements(){
69+
let html = '<div id=aLot style="font-size:7px" xhidden>';
70+
for (let i=0; i<10000; i++) {
71+
html += '<div class=item>item '+(i+1)+'</div>';
72+
}
73+
html += '</div>';
74+
document.body.insertAdjacentHTML('beforeend', html);
75+
}
76+
77+
78+
/*
79+
setTimeout(()=>{
80+
const time = performance.now();
81+
addALotOfElements();
82+
console.log('added 100000 elements', performance.now()-time);
83+
}, 6000);
84+
85+
setTimeout(()=>{
86+
o.disconnect();
87+
const time = performance.now();
88+
addALotOfElements();
89+
console.log('added 100000 elements after disconnect', performance.now()-time);
90+
}, 7000);
91+
*/
92+
93+
94+
</script>
95+
96+
<style>
97+
.item {
98+
background:yellow;
99+
}
100+
</style>

0 commit comments

Comments
 (0)