sentinel-js
Version:
JS library that detects new DOM nodes using CSS selectors
148 lines (123 loc) • 4.32 kB
JavaScript
if (!this.sentinel) (function(doc, ev) {
// define global object
sentinel = (function(){
var isArray = Array.isArray,
selectorToAnimationMap = {},
animationCallbacks = {},
styleEl,
styleSheet,
cssRules;
return {
/**
* Add watcher.
* @param {array} cssSelectors - List of CSS selector strings
* @param {Function} callback - The callback function
*/
on: function(cssSelectors, callback) {
if (!callback) return;
// initialize animationstart event listener
if (!styleEl) {
var doc = document,
head = doc.head;
// add animationstart event listener
doc.addEventListener('animationstart', function(ev, callbacks, l, i) {
callbacks = animationCallbacks[ev.animationName];
// exit if callbacks haven't been registered
if (!callbacks) return;
// stop other callbacks from firing
ev.stopImmediatePropagation();
// iterate through callbacks
l = callbacks.length;
for (i=0; i < l; i++) callbacks[i](ev.target);
}, true);
// add stylesheet to document
styleEl = doc.getElementById("sentinel-css");
if (!styleEl) {
styleEl = doc.createElement('style');
head.insertBefore(styleEl, head.firstChild);
}
styleSheet = styleEl.sheet;
cssRules = styleSheet.cssRules;
}
// listify argument and add css rules/ cache callbacks
(isArray(cssSelectors) ? cssSelectors : [cssSelectors])
.map(function(selector, animId, isCustomName) {
animId = selectorToAnimationMap[selector];
if (!animId) {
isCustomName = selector[0] == '!';
// define animation name and add to map
selectorToAnimationMap[selector] = animId =
isCustomName ? selector.slice(1) : 'sentinel-' +
Math.random().toString(16).slice(2);
// add keyframe rule
cssRules[styleSheet.insertRule(
'@keyframes ' + animId +
'{from{transform:none;}to{transform:none;}}',
cssRules.length)]
._id = selector;
// add selector animation rule
if (!isCustomName) {
cssRules[styleSheet.insertRule(
selector + '{animation-duration:0.0001s;animation-name:' +
animId + ';}',
cssRules.length)]
._id = selector;
}
// add to map
selectorToAnimationMap[selector] = animId;
}
// add to callbacks
(animationCallbacks[animId] = animationCallbacks[animId] || [])
.push(callback);
});
},
/**
* Remove watcher.
* @param {array} cssSelectors - List of CSS selector strings
* @param {Function} callback - The callback function (optional)
*/
off: function(cssSelectors, callback) {
// listify argument and iterate through rules
(isArray(cssSelectors) ? cssSelectors : [cssSelectors])
.map(function(selector, animId, callbackList, i) {
// get animId
if (!(animId = selectorToAnimationMap[selector])) return;
// get callbacks
callbackList = animationCallbacks[animId];
// remove callback from list
if (callback) {
i = callbackList.length;
while (i--) {
if (callbackList[i] === callback) callbackList.splice(i, 1);
}
} else {
callbackList = [];
}
// exit if callbacks still exist
if (callbackList.length) return;
// clear cache and remove css rules
i = cssRules.length;
while (i--) {
if (cssRules[i]._id == selector) styleSheet.deleteRule(i);
}
delete selectorToAnimationMap[selector];
delete animationCallbacks[animId];
});
},
/**
* Reset watchers and cache
*/
reset: function() {
selectorToAnimationMap = {};
animationCallbacks = {};
if (styleEl) styleEl.parentNode.removeChild(styleEl);
styleEl = 0;
}
};
})();
// dispatch load event
ev = doc.createEvent('HTMLEvents');
if (ev.initEvent) ev.initEvent('sentinel-load', false, false);
else ev = new Event('sentinel-load');
doc.dispatchEvent(ev);
})(document);