page-parser-tree
Version:
Library to find elements in a dynamic web page
130 lines (116 loc) • 4.15 kB
Flow
/* @flow */
import LiveSet from 'live-set';
import type { ElementContext } from '../internalTypes';
export default function watchFilteredChildren(
input: LiveSet<ElementContext>,
condFn: (el: HTMLElement) => boolean
): LiveSet<ElementContext> {
return new LiveSet({
scheduler: input.getScheduler(),
read() {
throw new Error();
},
listen(setValues, controller) {
setValues(new Set());
const inputEntries: Map<
ElementContext,
{ observer: MutationObserver, removedNode: * }
> = new Map();
const outputEcs: Map<HTMLElement, ElementContext> = new Map();
function newEc(ec: ElementContext) {
function addedNode(child: Node) {
if (child.nodeType !== 1) return;
/*:: if (!(child instanceof HTMLElement)) throw new Error() */
if (condFn(child)) {
const childEc = { el: child, parents: ec.parents };
outputEcs.set(child, childEc);
controller.add(childEc);
}
}
function removedNode(child: Node) {
if (child.nodeType !== 1) return;
/*:: if (!(child instanceof HTMLElement)) throw new Error() */
const childEc = outputEcs.get(child);
if (!childEc) return;
outputEcs.delete(child);
controller.remove(childEc);
}
function changesHandler(mutations) {
if (mutations.length > 1) {
// If any removals are followed by a re-add, then drop the pair.
const removedEls = new Set();
const addedEls = [];
mutations.forEach(({ addedNodes, removedNodes }) => {
for (let i = 0, len = removedNodes.length; i < len; i++) {
const el = removedNodes[i];
if (el.nodeType !== 1) continue;
removedEls.add(removedNodes[i]);
}
for (let i = 0, len = addedNodes.length; i < len; i++) {
const el = addedNodes[i];
if (el.nodeType !== 1) continue;
if (removedEls.has(el)) {
removedEls.delete(el);
} else {
addedEls.push(el);
}
}
});
addedEls.forEach(addedNode);
removedEls.forEach(removedNode);
} else {
mutations.forEach(mutation => {
Array.prototype.forEach.call(mutation.addedNodes, addedNode);
Array.prototype.forEach.call(mutation.removedNodes, removedNode);
});
}
}
Array.prototype.forEach.call(ec.el.children, addedNode);
const observer = new MutationObserver(changesHandler);
observer.observe(ec.el, { childList: true });
inputEntries.set(ec, { observer, removedNode });
}
function removedEc(ec: ElementContext) {
const entry = inputEntries.get(ec);
if (!entry)
throw new Error('Should not happen: Unseen ElementContext removed');
entry.observer.takeRecords().forEach(mutation => {
Array.prototype.forEach.call(
mutation.removedNodes,
entry.removedNode
);
});
entry.observer.disconnect();
Array.prototype.forEach.call(ec.el.children, entry.removedNode);
inputEntries.delete(ec);
}
const sub = input.subscribe({
start() {
input.values().forEach(newEc);
},
next(changes) {
changes.forEach(change => {
if (change.type === 'add') {
newEc(change.value);
} else if (change.type === 'remove') {
removedEc(change.value);
}
});
}
});
return {
unsubscribe() {
sub.unsubscribe();
inputEntries.forEach(({ observer }) => {
observer.disconnect();
});
},
pullChanges() {
sub.pullChanges();
// Don't bother doing observer.takeRecords(), we don't need that in
// PageParserTree for how we use pullChanges().
}
};
}
});
}