lifecycle-events
Version:
Enable lifecycle events for DOM-elements: attached, detached
666 lines (530 loc) • 16.7 kB
JavaScript
require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({"../":[function(require,module,exports){
var on = require('emmy/on');
var emit = require('emmy/emit');
var off = require('emmy/off');
var getElements = require('tiny-element');
var doc = document, win = window;
/**
* @module lifecycle-events
*
* @todo Work out tolerance issue (whether it needs to be passed as an option - sometimes useful, like to detect an element being fully visible)
*
* @todo Optimize enabled selectors. For example, avoid extra enabling if you have '*' enabled. And so on.
* @todo Testling table.
* @todo Ignore native CustomElements lifecycle events
*
* @note Nested queryselector ten times faster than doc.querySelector:
* http://jsperf.com/document-vs-element-queryselectorall-performance/2
* @note Multiple observations to an extent faster than one global observer:
* http://jsperf.com/mutation-observer-cases
*/
var lifecycle = module.exports = enable;
lifecycle.enable = enable;
lifecycle.disable = disable;
/** Defaults can be changed outside */
lifecycle.attachedCallbackName = 'attached';
lifecycle.detachedCallbackName = 'detached';
/** One observer to observe a lot of nodes */
var MO = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
var observer = new MO(mutationHandler);
/** Set of targets to observe */
var mTargets = [];
/** Attached items set */
var attachedItemsSet = new WeakSet;
/**
* Observer targets
*
* @param {(string|Node|NodeList|document)} query Target pointer
* @param {Object} within Settings for observer
*/
function enable(query, within) {
if (!query) query = '*';
within = getElements(within || doc);
//save cached version of target
mTargets.push(query);
//make observer observe one more target
observer.observe(within, {subtree: true, childList: true});
//ignore not bound nodes
if (query instanceof Node && !doc.contains(query)) return;
//check initial nodes
checkAddedNodes(getElements.call(within, query, true));
}
/**
* Stop observing items
*/
function disable(target) {
var idx = mTargets.indexOf(target);
if (idx >= 0) {
mTargets.splice(idx,1);
}
}
/**
* Handle a mutation passed
*/
function mutationHandler(mutations) {
mutations.forEach(function(mutation) {
checkAddedNodes(mutation.addedNodes);
checkRemovedNodes(mutation.removedNodes);
});
}
/**
* Check nodes list to call attached
*/
function checkAddedNodes(nodes) {
var newItems = false, node;
//find attached evt targets
for (var i = nodes.length; i--;) {
node = nodes[i];
if (node.nodeType !== 1) continue;
//find options corresponding to the node
if (!attachedItemsSet.has(node)) {
node = getObservee(node);
//if observee found within attached items - add it to set
if (node) {
if (!newItems) {
newItems = true;
}
attachedItemsSet.add(node);
emit(node, lifecycle.attachedCallbackName, null, true);
}
}
}
}
/**
* Check nodes list to call detached
*/
function checkRemovedNodes(nodes) {
//handle detached evt
for (var i = nodes.length; i--;) {
var node = nodes[i];
if (node.nodeType !== 1) continue;
//find options corresponding to the node
if (attachedItemsSet.has(node)) {
emit(node, lifecycle.detachedCallbackName, null, true);
attachedItemsSet.delete(node);
}
}
}
/**
* Check whether node is observing
*
* @param {Node} node An element to check on inclusion to target list
*/
function getObservee(node) {
//check queries
for (var i = mTargets.length, target; i--;) {
target = mTargets[i];
if (node === target) return node;
if (typeof target === 'string' && node.matches(target)) return node;
//return innermost target
if (node.contains(target)) return target;
}
}
},{"emmy/emit":1,"emmy/off":11,"emmy/on":12,"tiny-element":14}],1:[function(require,module,exports){
/**
* @module emmy/emit
*/
var icicle = require('icicle');
var listeners = require('./listeners');
var slice = require('sliced');
var redirect = require('./src/redirect');
module.exports = emit;
//TODO: think to pass list of args to `emit`
/** detect env */
var $ = typeof jQuery === 'undefined' ? undefined : jQuery;
var doc = typeof document === 'undefined' ? undefined : document;
var win = typeof window === 'undefined' ? undefined : window;
/**
* Emit an event, optionally with data or bubbling
*
* @param {string} eventName An event name, e. g. 'click'
* @param {*} data Any data to pass to event.details (DOM) or event.data (elsewhere)
* @param {bool} bubbles Whether to trigger bubbling event (DOM)
*
*
* @return {target} a target
*/
function emit(target, eventName, data, bubbles){
//parse args
if (redirect(emit, arguments, true)) return;
var emitMethod, evt = eventName;
//Create proper event for DOM objects
if (target.nodeType || target === doc || target === win) {
//NOTE: this doesnot bubble on off-DOM elements
if (eventName instanceof Event) {
evt = eventName;
} else {
//IE9-compliant constructor
evt = document.createEvent('CustomEvent');
evt.initCustomEvent(eventName, bubbles, true, data);
//a modern constructor would be:
// var evt = new CustomEvent(eventName, { detail: data, bubbles: bubbles })
}
emitMethod = target.dispatchEvent;
}
//create event for jQuery object
else if ($ && target instanceof $) {
//TODO: decide how to pass data
evt = $.Event( eventName, data );
evt.detail = data;
//FIXME: reference case where triggerHandler needed (something with multiple calls)
emitMethod = bubbles ? targte.trigger : target.triggerHandler;
}
//detect target events
else {
emitMethod = target['emit'] || target['trigger'] || target['fire'] || target['dispatchEvent'];
}
//use locks to avoid self-recursion on objects wrapping this method
if (emitMethod) {
if (icicle.freeze(target, 'emit' + eventName)) {
//use target event system, if possible
emitMethod.call(target, evt, data, bubbles);
icicle.unfreeze(target, 'emit' + eventName);
return;
}
//if event was frozen - probably it is Emitter instance
//so perform normal callback
}
//fall back to default event system
//ignore if no event specified
var evtCallbacks = listeners(target, evt);
//copy callbacks to fire because list can be changed by some callback (like `off`)
var fireList = slice(evtCallbacks);
var args = slice(arguments, 2);
for (var i = 0; i < fireList.length; i++ ) {
fireList[i] && fireList[i].apply(target, args);
}
return;
}
},{"./listeners":2,"./src/redirect":13,"icicle":3,"sliced":9}],2:[function(require,module,exports){
/**
* A storage of per-target callbacks.
* For now weakmap is used as the most safe solution.
*
* @module emmy/listeners
*/
var cache = new WeakMap;
module.exports = listeners;
/**
* Get listeners for the target/evt (optionally)
*
* @param {object} target a target object
* @param {string}? evt an evt name, if undefined - return object with events
*
* @return {(object|array)} List/set of listeners
*/
function listeners(target, evt){
var listeners = cache.get(target);
if (!evt) return listeners || {};
return listeners && listeners[evt] || [];
}
/**
* Save new listener
*/
listeners.add = function(target, evt, cb){
//ensure set of callbacks for the target exists
if (!cache.has(target)) cache.set(target, {});
var targetCallbacks = cache.get(target);
//save a new callback
(targetCallbacks[evt] = targetCallbacks[evt] || []).push(cb);
};
},{}],3:[function(require,module,exports){
/**
* @module Icicle
*/
module.exports = {
freeze: lock,
unfreeze: unlock,
isFrozen: isLocked
};
/** Set of targets */
var lockCache = new WeakMap;
/**
* Set flag on target with the name passed
*
* @return {bool} Whether lock succeeded
*/
function lock(target, name){
var locks = lockCache.get(target);
if (locks && locks[name]) return false;
//create lock set for a target, if none
if (!locks) {
locks = {};
lockCache.set(target, locks);
}
//set a new lock
locks[name] = true;
//return success
return true;
}
/**
* Unset flag on the target with the name passed.
*
* Note that if to return new value from the lock/unlock,
* then unlock will always return false and lock will always return true,
* which is useless for the user, though maybe intuitive.
*
* @param {*} target Any object
* @param {string} name A flag name
*
* @return {bool} Whether unlock failed.
*/
function unlock(target, name){
var locks = lockCache.get(target);
if (!locks || !locks[name]) return false;
locks[name] = null;
return true;
}
/**
* Return whether flag is set
*
* @param {*} target Any object to associate lock with
* @param {string} name A flag name
*
* @return {Boolean} Whether locked or not
*/
function isLocked(target, name){
var locks = lockCache.get(target);
return (locks && locks[name]);
}
},{}],4:[function(require,module,exports){
var isString = require('./is-string');
var isArray = require('./is-array');
var isFn = require('./is-fn');
//FIXME: add tests from http://jsfiddle.net/ku9LS/1/
module.exports = function (a){
return isArray(a) || (a && !isString(a) && !a.nodeType && (typeof window != 'undefined' ? a != window : true) && !isFn(a) && typeof a.length === 'number');
}
},{"./is-array":5,"./is-fn":6,"./is-string":8}],5:[function(require,module,exports){
module.exports = function(a){
return a instanceof Array;
}
},{}],6:[function(require,module,exports){
module.exports = function(a){
return !!(a && a.apply);
}
},{}],7:[function(require,module,exports){
/**
* @module mutype/is-object
*/
//TODO: add st8 tests
//isPlainObject indeed
module.exports = function(a){
// return obj === Object(obj);
return a && a.constructor && a.constructor.name === "Object";
};
},{}],8:[function(require,module,exports){
module.exports = function(a){
return typeof a === 'string' || a instanceof String;
}
},{}],9:[function(require,module,exports){
module.exports = exports = require('./lib/sliced');
},{"./lib/sliced":10}],10:[function(require,module,exports){
/**
* An Array.prototype.slice.call(arguments) alternative
*
* @param {Object} args something with a length
* @param {Number} slice
* @param {Number} sliceEnd
* @api public
*/
module.exports = function (args, slice, sliceEnd) {
var ret = [];
var len = args.length;
if (0 === len) return ret;
var start = slice < 0
? Math.max(0, slice + len)
: slice || 0;
if (sliceEnd !== undefined) {
len = sliceEnd < 0
? sliceEnd + len
: sliceEnd
}
while (len-- > start) {
ret[len - start] = args[len];
}
return ret;
}
},{}],11:[function(require,module,exports){
/**
* @module emmy/off
*/
module.exports = off;
var icicle = require('icicle');
var listeners = require('./listeners');
var redirect = require('./src/redirect');
/**
* Remove listener[s] from the target
*
* @param {[type]} evt [description]
* @param {Function} fn [description]
*
* @return {[type]} [description]
*/
function off(target, evt, fn){
//parse args
if (redirect(off, arguments)) return;
var callbacks, i;
//unbind all listeners if no fn specified
if (fn === undefined) {
//try to use target removeAll method, if any
var allOff = target['removeAll'] || target['removeAllListeners'];
//call target removeAll
if (allOff) {
allOff.call(target, evt, fn);
}
//then forget own callbacks, if any
callbacks = listeners(target);
//unbind all if no evtRef defined
if (evt === undefined) {
for (var evtName in callbacks) {
off(target, evtName, callbacks[evtName]);
}
}
else if (callbacks[evt]) {
off(target, evt, callbacks[evt]);
}
return;
}
//target events (string notation to advanced_optimizations)
var offMethod = target['off'] || target['removeEventListener'] || target['removeListener'];
//use target `off`, if possible
if (offMethod) {
//avoid self-recursion from the outside
if (icicle.freeze(target, 'off' + evt)){
offMethod.call(target, evt, fn);
icicle.unfreeze(target, 'off' + evt);
}
//if it’s frozen - ignore call
else {
return;
}
}
//forget callback
var evtCallbacks = listeners(target, evt);
//remove specific handler
for (i = 0; i < evtCallbacks.length; i++) {
//once method has original callback in .fn
if (evtCallbacks[i] === fn || evtCallbacks[i].fn === fn) {
evtCallbacks.splice(i, 1);
break;
}
}
return;
}
},{"./listeners":2,"./src/redirect":13,"icicle":3}],12:[function(require,module,exports){
/**
* @module emmy/on
*/
module.exports = on;
var icicle = require('icicle');
var listeners = require('./listeners');
var redirect = require('./src/redirect');
/**
* Bind fn to the target
*
* @param {string} evt An event name
* @param {Function} fn A callback
* @param {Function}? condition An optional filtering fn for a callback
* which accepts an event and returns callback
*
* @return {object} A target
*/
function on(target, evt, fn, condition){
//parse args
if (redirect(on, arguments)) return;
//get target on method, if any
var onMethod = target['on'] || target['addEventListener'] || target['addListener'];
var cb;
//apply condition wrapper
if (condition) {
cb = function(){
if (condition.apply(this, arguments)) {
return fn.apply(this, arguments);
}
};
cb.fn = fn;
} else {
cb = fn;
}
//use target event system, if possible
if (onMethod) {
//avoid self-recursions
//if it’s frozen - ignore call
if (icicle.freeze(target, 'on' + evt)){
onMethod.call(target, evt, cb);
icicle.unfreeze(target, 'on' + evt);
}
else {
return;
}
}
//save the callback anyway
listeners.add(target, evt, cb);
return;
}
},{"./listeners":2,"./src/redirect":13,"icicle":3}],13:[function(require,module,exports){
/**
* Iterate method for args.
* Ensure that final method is called with single arguments,
* so that any list/object argument is iterated.
*
* Supposed to be used internally by emmy.
*
* @module emmy/redirect
*/
var isArrayLike = require('mutype/is-array-like');
var isObject = require('mutype/is-object');
var isFn = require('mutype/is-fn');
var slice = require('sliced');
module.exports = function(method, args, ignoreFn){
var target = args[0], evt = args[1], fn = args[2], param = args[3];
//batch events
if (isObject(evt)){
for (var evtName in evt){
method(target, evtName, evt[evtName]);
}
return true;
}
//Swap params, if callback & param are changed places
if (isFn(param) && !isFn(fn)) {
method.apply(this, [target, evt, param, fn].concat(slice(args, 4)));
return true;
}
//bind all callbacks, if passed a list (and no ignoreFn flag)
if (isArrayLike(fn) && !ignoreFn){
args = slice(args, 3);
for (var i = fn.length; i--;){
// method(target, evt, fn[i]);
method.apply(this, [target, evt, fn[i]].concat(args));
}
return true;
}
//bind all events, if passed a list
if (isArrayLike(evt)) {
args = slice(args, 2);
for (var i = evt.length; i--;){
// method(target, evt[i], fn);
method.apply(this, [target, evt[i]].concat(args));
}
return true;
}
//bind all targets, if passed a list
if (isArrayLike(target)) {
args = slice(args, 1);
for (var i = target.length; i--;){
// method(target[i], evt, fn);
method.apply(this, [target[i]].concat(args));
}
return true;
}
};
},{"mutype/is-array-like":4,"mutype/is-fn":6,"mutype/is-object":7,"sliced":9}],14:[function(require,module,exports){
var slice = [].slice;
module.exports = function (selector, multiple) {
var ctx = this === window ? document : this;
return (typeof selector == 'string')
? (multiple) ? slice.call(ctx.querySelectorAll(selector), 0) : ctx.querySelector(selector)
: (selector instanceof Node || selector === window || !selector.length) ? (multiple ? [selector] : selector) : slice.call(selector, 0);
};
},{}]},{},[]);