UNPKG

onmount

Version:

Run something when a DOM element appears and when it exits

391 lines (328 loc) 9.92 kB
/* global define */ void (function (root, factory) { if (typeof define === 'function' && define.amd) define(factory) else if (typeof exports === 'object') module.exports = factory() else { if (window.jQuery) window.jQuery.onmount = factory() else root.onmount = factory() } }(this, function ($) { /* * Internal: Registry. */ var handlers, behaviors, selectors /* * Internal: IDs for auto-incrementing. */ var bid = 0 /* behavior ID */ var cid = 0 /* component ID */ /** * (Module) Adds a behavior, or triggers behaviors. * * When no parameters are passed, it triggers all behaviors. When one * parameter is passed, it triggers the given behavior. Otherwise, it adds a * behavior. * * // define a behavior * $.onmount('.select-box', function () { * $(this).on('...') * }) * * // define a behavior with exit * $.onmount('.select-box', function () { * $(document).on('...') * }, function () { * $(document).off('...') * }) * * // retrigger a onmount * $.onmount('.select-box') * * // retriggers all behaviors * $.onmount() */ function onmount (selector, init, exit, options) { if (typeof exit === 'object') { options = exit exit = undefined } if (arguments.length === 0 || isjQuery(selector) || isEvent(selector)) { // onmount() - trigger all behaviors. Also account for cases such as // $($.onmount), where it's triggered with a jQuery event object. onmount.poll() } else if (arguments.length === 1) { // onmount(selector) - trigger for a given selector. onmount.poll(selector) } else { // onmount(sel, fn, [fn]) - register a new behavior. var be = new Behavior(selector, init, exit, options) behaviors.push(be) be.register() } return this } /* * Use jQuery (or a jQuery-like) when available. This will allow * the use of jQuery selectors. */ onmount.$ = window.jQuery || window.Zepto || window.Ender /* * Detect MutationObserver support for `onmount.observe()`. * You may even add a polyfill here via * `onmount.MutationObserver = require('mutation-observer')`. */ onmount.MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver /** * Internal: triggers behaviors for a selector or for all. * * onmount.poll() * onmount.poll('.js-button') */ onmount.poll = function poll (selector) { if (selector) selector = onmount.selectify(selector) var functions = (selector ? selectors[selector] : handlers) || [] each(functions, function (fn) { fn() }) } /** * Observes automatically using MutationObserver events. * * onmount.observe() */ onmount.observe = function observe () { var MutationObserver = onmount.MutationObserver if (typeof MutationObserver === 'undefined') return var obs = new MutationObserver(function (mutations) { each(behaviors, function (be) { each(mutations, function (mutation) { each(mutation.addedNodes, function (el) { if (matches(el, be.selector)) be.visitEnter(el) }) each(mutation.removedNodes, function (el) { if (matches(el, be.selector)) be.doExit(el) }) }) }) }) obs.observe(document, { subtree: true, childList: true }) onmount.observer = obs // trigger everything before going onmount() return true } /** * Turns off observation first issued by `onmount.observe()`. */ onmount.unobserve = function unobserve () { if (!this.observer) return this.observer.disconnect() delete this.observer } /** * Forces teardown of all behaviors currently applied. */ onmount.teardown = function teardown () { each(behaviors, function (be) { each(be.loaded, function (el, i) { if (el) be.doExit(el, i) }) }) } /** * Clears all behaviors. Useful for tests. * This will NOT call exit handlers. */ onmount.reset = function reset () { handlers = onmount.handlers = [] selectors = onmount.selectors = {} behaviors = onmount.behaviors = [] } /** * Internal: Converts `@role` to `[role~="role"]` if needed. You can override * this by reimplementing `onmount.selectify`. * * selectify('@hi') //=> '[role="hi"]' * selectify('.btn') //=> '.btn' */ onmount.selectify = function selectify (selector) { if (selector[0] === '@') { return '[role~="' + selector.substr(1).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"]' } return selector } /** * Internal: behavior class */ function Behavior (selector, init, exit, options) { this.id = 'b' + bid++ this.init = init this.exit = exit this.selector = onmount.selectify(selector) this.loaded = [] // keep track of dom elements loaded for this behavior this.key = '__onmount:' + bid // leave the state in el['__onmount:12'] this.detectMutate = options && options.detectMutate } /** * Internal: initialize this behavior by registering itself to the internal * `selectors` map. This allows you to call `onmount(selector)` later on. */ Behavior.prototype.register = function () { var be = this var loaded = this.loaded var selector = this.selector register(selector, function () { var list = query(selector) // This is the function invoked on `onmount(selector)`. // Clean up old ones (if they're not in the DOM anymore). each(loaded, function (element, i) { be.visitExit(element, i, list) }) // Clean up new ones (if they're not loaded yet). eachOf(list, function (element) { be.visitEnter(element) }) }) } /** * Internal: visits the element `el` and turns it on if applicable. */ Behavior.prototype.visitEnter = function (el) { if (el[this.key]) return var options = { id: 'c' + cid, selector: this.selector } if (this.init.call(el, options) !== false) { el[this.key] = options this.loaded.push(el) cid++ } } /** * Internal: visits the element `el` and sees if it needs its exit handler * called. */ Behavior.prototype.visitExit = function (el, i, list) { if (!el) return if (this.detectMutate) { if (!has(list, el)) return this.doExit(el, i) } else { if (!isAttached(el)) return this.doExit(el, i) } } /** * Internal: calls the exit handler for the behavior for element `el` (if * available), and marks the behavior/element as uninitialized. */ Behavior.prototype.doExit = function (el, i) { if (typeof i === 'undefined') i = this.loaded.indexOf(el) this.loaded[i] = undefined if (this.exit && this.exit.call(el, el[this.key]) !== false) { delete el[this.key] } } /** * Internal: check if an element is still attached to its document. */ function isAttached (el) { while (el) { if (el === document.documentElement) return true el = el.parentElement } } /** * Internal: reimplementation of `$('...')`. If jQuery is available, * use it (I guess to preserve IE compatibility and to enable special jQuery * attribute selectors). Use with `eachOf()` or `has()`. */ function query (selector, fn) { if (onmount.$) return onmount.$(selector) return document.querySelectorAll(selector) } /** * Internal: iterates through a `query()` result. */ function eachOf (list, fn) { if (onmount.$) return list.each(function (i) { fn(this, i) }) return each(list, fn) } /** * Interanl: checks if given element `el` is in the query result `list`. */ function has (list, el) { if (onmount.$) return list.index(el) > -1 return list.indexOf(el) > -1 } /** * Internal: registers a behavior handler for a selector. */ function register (selector, fn) { if (!selectors[selector]) selectors[selector] = [] selectors[selector].push(fn) handlers.push(fn) } /** * Checks if a given element `el` matches `selector`. * Compare with [$.fn.is](http://api.jquery.com/is/). * * var matches = require('dom101/matches'); * * matches(button, ':focus'); */ function matches (el, selector) { var _matches = el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector if (onmount.$) { return onmount.$(el).is(selector) } else if (_matches) { return _matches.call(el, selector) } else if (el.parentNode) { // IE8 and below var nodes = el.parentNode.querySelectorAll(selector) for (var i = nodes.length; i--; 0) { if (nodes[i] === el) return true } return false } } /** * Iterates through `list` (an array or an object). This is useful when dealing * with NodeLists like `document.querySelectorAll`. * * var each = require('dom101/each'); * var qa = require('dom101/query-selector-all'); * * each(qa('.button'), function (el) { * addClass('el', 'selected'); * }); */ function each (list, fn) { var i var len = list.length if (len === +len) { for (i = 0; i < len; i++) { fn(list[i], i) } } else { for (i in list) { if (list.hasOwnProperty(i)) fn(list[i], i) } } return list } /** * Internal: Check if a given object is jQuery */ function isjQuery ($) { return typeof $ === 'function' && $.fn && $.noConflict } function isEvent (e) { return typeof e === 'object' && e.target } /* * Export */ onmount.reset() return onmount }))