UNPKG

lifecycle-events

Version:

Enable lifecycle events for DOM-elements: attached, detached

666 lines (530 loc) 16.7 kB
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); }; },{}]},{},[]);