diff --git a/components/flight/lib/advice.js b/components/flight/lib/advice.js index 0f46869..898c340 100644 --- a/components/flight/lib/advice.js +++ b/components/flight/lib/advice.js @@ -4,16 +4,14 @@ // http://opensource.org/licenses/MIT // ========================================== -"use strict"; - define( [ - './utils', './compose' ], - function (util, compose) { + function(compose) { + 'use strict'; var advice = { @@ -25,7 +23,7 @@ define( for (; i < l; i++) args[i + 1] = arguments[i]; return wrapped.apply(this, args); - } + }; }, before: function(base, before) { @@ -33,7 +31,7 @@ define( return function composedBefore() { beforeFn.apply(this, arguments); return base.apply(this, arguments); - } + }; }, after: function(base, after) { @@ -42,7 +40,7 @@ define( var res = (base.unbound || base).apply(this, arguments); afterFn.apply(this, arguments); return res; - } + }; }, // a mixin that allows other mixins to augment existing functions by adding additional @@ -53,10 +51,12 @@ define( compose.unlockProperty(this, method, function() { if (typeof this[method] == 'function') { - return this[method] = advice[m](this[method], fn); + this[method] = advice[m](this[method], fn); } else { - return this[method] = fn; + this[method] = fn; } + + return this[method]; }); }; diff --git a/components/flight/lib/base.js b/components/flight/lib/base.js new file mode 100644 index 0000000..f52e59e --- /dev/null +++ b/components/flight/lib/base.js @@ -0,0 +1,227 @@ +// ========================================== +// Copyright 2013 Twitter, Inc +// Licensed under The MIT License +// http://opensource.org/licenses/MIT +// ========================================== + +define( + + [ + './utils', + './registry', + './debug' + ], + + function(utils, registry, debug) { + 'use strict'; + + // common mixin allocates basic functionality - used by all component prototypes + // callback context is bound to component + var componentId = 0; + + function teardownInstance(instanceInfo){ + instanceInfo.events.slice().forEach(function(event) { + var args = [event.type]; + + event.element && args.unshift(event.element); + (typeof event.callback == 'function') && args.push(event.callback); + + this.off.apply(this, args); + }, instanceInfo.instance); + } + + function checkSerializable(type, data) { + try { + window.postMessage(data, '*'); + } catch(e) { + console.log('unserializable data for event',type,':',data); + throw new Error( + ['The event', type, 'on component', this.toString(), 'was triggered with non-serializable data'].join(' ') + ); + } + } + + function withBase() { + + // delegate trigger, bind and unbind to an element + // if $element not supplied, use component's node + // other arguments are passed on + // event can be either a string specifying the type + // of the event, or a hash specifying both the type + // and a default function to be called. + this.trigger = function() { + var $element, type, data, event, defaultFn; + var lastIndex = arguments.length - 1, lastArg = arguments[lastIndex]; + + if (typeof lastArg != 'string' && !(lastArg && lastArg.defaultBehavior)) { + lastIndex--; + data = lastArg; + } + + if (lastIndex == 1) { + $element = $(arguments[0]); + event = arguments[1]; + } else { + $element = this.$node; + event = arguments[0]; + } + + if (event.defaultBehavior) { + defaultFn = event.defaultBehavior; + event = $.Event(event.type); + } + + type = event.type || event; + + if (debug.enabled && window.postMessage) { + checkSerializable.call(this, type, data); + } + + if (typeof this.attr.eventData === 'object') { + data = $.extend(true, {}, this.attr.eventData, data); + } + + $element.trigger((event || type), data); + + if (defaultFn && !event.isDefaultPrevented()) { + (this[defaultFn] || defaultFn).call(this); + } + + return $element; + }; + + this.on = function() { + var $element, type, callback, originalCb; + var lastIndex = arguments.length - 1, origin = arguments[lastIndex]; + + if (typeof origin == 'object') { + //delegate callback + originalCb = utils.delegate( + this.resolveDelegateRules(origin) + ); + } else { + originalCb = origin; + } + + if (lastIndex == 2) { + $element = $(arguments[0]); + type = arguments[1]; + } else { + $element = this.$node; + type = arguments[0]; + } + + if (typeof originalCb != 'function' && typeof originalCb != 'object') { + throw new Error('Unable to bind to "' + type + '" because the given callback is not a function or an object'); + } + + callback = originalCb.bind(this); + callback.target = originalCb; + callback.context = this; + + $element.on(type, callback); + + // store every bound version of the callback + originalCb.bound || (originalCb.bound = []); + originalCb.bound.push(callback); + + return callback; + }; + + this.off = function() { + var $element, type, callback; + var lastIndex = arguments.length - 1; + + if (typeof arguments[lastIndex] == 'function') { + callback = arguments[lastIndex]; + lastIndex -= 1; + } + + if (lastIndex == 1) { + $element = $(arguments[0]); + type = arguments[1]; + } else { + $element = this.$node; + type = arguments[0]; + } + + if (callback) { + //set callback to version bound against this instance + callback.bound && callback.bound.some(function(fn, i, arr) { + if (fn.context && (this.identity == fn.context.identity)) { + arr.splice(i, 1); + callback = fn; + return true; + } + }, this); + } + + return $element.off(type, callback); + }; + + this.resolveDelegateRules = function(ruleInfo) { + var rules = {}; + + Object.keys(ruleInfo).forEach(function(r) { + if (!(r in this.attr)) { + throw new Error('Component "' + this.toString() + '" wants to listen on "' + r + '" but no such attribute was defined.'); + } + rules[this.attr[r]] = ruleInfo[r]; + }, this); + + return rules; + }; + + this.defaultAttrs = function(defaults) { + utils.push(this.defaults, defaults, true) || (this.defaults = defaults); + }; + + this.select = function(attributeKey) { + return this.$node.find(this.attr[attributeKey]); + }; + + this.initialize = function(node, attrs) { + attrs || (attrs = {}); + //only assign identity if there isn't one (initialize can be called multiple times) + this.identity || (this.identity = componentId++); + + if (!node) { + throw new Error('Component needs a node'); + } + + if (node.jquery) { + this.node = node[0]; + this.$node = node; + } else { + this.node = node; + this.$node = $(node); + } + + // merge defaults with supplied options + // put options in attr.__proto__ to avoid merge overhead + var attr = Object.create(attrs); + for (var key in this.defaults) { + if (!attrs.hasOwnProperty(key)) { + attr[key] = this.defaults[key]; + } + } + + this.attr = attr; + + Object.keys(this.defaults || {}).forEach(function(key) { + if (this.defaults[key] === null && this.attr[key] === null) { + throw new Error('Required attribute "' + key + '" not specified in attachTo for component "' + this.toString() + '".'); + } + }, this); + + return this; + }; + + this.teardown = function() { + teardownInstance(registry.findInstanceInfo(this)); + }; + } + + return withBase; + } +); diff --git a/components/flight/lib/component.js b/components/flight/lib/component.js index d9a5077..1ccd154 100644 --- a/components/flight/lib/component.js +++ b/components/flight/lib/component.js @@ -4,45 +4,34 @@ // http://opensource.org/licenses/MIT // ========================================== -"use strict"; - define( [ './advice', './utils', './compose', - './registry' + './base', + './registry', + './logger', + './debug' ], - function(advice, utils, compose, registry) { + function(advice, utils, compose, withBase, registry, withLogging, debug) { + 'use strict'; var functionNameRegEx = /function (.*?)\s?\(/; - var componentId = 0; - - function teardownInstance(instanceInfo){ - instanceInfo.events.slice().forEach(function(event) { - var args = [event.type]; - - event.element && args.unshift(event.element); - (typeof event.callback == 'function') && args.push(event.callback); - - this.off.apply(this, args); - }, instanceInfo.instance); - } - - function teardown() { - teardownInstance(registry.findInstanceInfo(this)); - } - - //teardown for all instances of this constructor + // teardown for all instances of this constructor function teardownAll() { var componentInfo = registry.findComponentInfo(this); componentInfo && Object.keys(componentInfo.instances).forEach(function(k) { var info = componentInfo.instances[k]; - info.instance.teardown(); + // It's possible that a previous teardown caused another component to teardown, + // so we can't assume that the instances object is as it was. + if (info && info.instance) { + info.instance.teardown(); + } }); } @@ -52,148 +41,11 @@ define( } catch(e) { console.log('unserializable data for event',type,':',data); throw new Error( - ["The event", type, "on component", this.toString(), "was triggered with non-serializable data"].join(" ") + ['The event', type, 'on component', this.toString(), 'was triggered with non-serializable data'].join(' ') ); } } - //common mixin allocates basic functionality - used by all component prototypes - //callback context is bound to component - function withBaseComponent() { - - // delegate trigger, bind and unbind to an element - // if $element not supplied, use component's node - // other arguments are passed on - // event can be either a string specifying the type - // of the event, or a hash specifying both the type - // and a default function to be called. - this.trigger = function() { - var $element, type, data, event, defaultFn; - var lastIndex = arguments.length - 1, lastArg = arguments[lastIndex]; - - if (typeof lastArg != "string" && !(lastArg && lastArg.defaultBehavior)) { - lastIndex--; - data = lastArg; - } - - if (lastIndex == 1) { - $element = $(arguments[0]); - event = arguments[1]; - } else { - $element = this.$node; - event = arguments[0]; - } - - if (event.defaultBehavior) { - defaultFn = event.defaultBehavior; - event = $.Event(event.type); - } - - type = event.type || event; - - if (window.DEBUG && window.DEBUG.enabled && window.postMessage) { - checkSerializable.call(this, type, data); - } - - if (typeof this.attr.eventData === 'object') { - data = $.extend(true, {}, this.attr.eventData, data); - } - - $element.trigger((event || type), data); - - if (defaultFn && !event.isDefaultPrevented()) { - (this[defaultFn] || defaultFn).call(this); - } - - return $element; - }; - - this.on = function() { - var $element, type, callback, originalCb; - var lastIndex = arguments.length - 1, origin = arguments[lastIndex]; - - if (typeof origin == "object") { - //delegate callback - originalCb = utils.delegate( - this.resolveDelegateRules(origin) - ); - } else { - originalCb = origin; - } - - if (lastIndex == 2) { - $element = $(arguments[0]); - type = arguments[1]; - } else { - $element = this.$node; - type = arguments[0]; - } - - if (typeof originalCb != 'function' && typeof originalCb != 'object') { - throw new Error("Unable to bind to '" + type + "' because the given callback is not a function or an object"); - } - - callback = originalCb.bind(this); - callback.target = originalCb; - - // if the original callback is already branded by jQuery's guid, copy it to the context-bound version - if (originalCb.guid) { - callback.guid = originalCb.guid; - } - - $element.on(type, callback); - - // get jquery's guid from our bound fn, so unbinding will work - originalCb.guid = callback.guid; - - return callback; - }; - - this.off = function() { - var $element, type, callback; - var lastIndex = arguments.length - 1; - - if (typeof arguments[lastIndex] == "function") { - callback = arguments[lastIndex]; - lastIndex -= 1; - } - - if (lastIndex == 1) { - $element = $(arguments[0]); - type = arguments[1]; - } else { - $element = this.$node; - type = arguments[0]; - } - - return $element.off(type, callback); - }; - - this.resolveDelegateRules = function(ruleInfo) { - var rules = {}; - - Object.keys(ruleInfo).forEach(function(r) { - if (!r in this.attr) { - throw new Error('Component "' + this.toString() + '" wants to listen on "' + r + '" but no such attribute was defined.'); - } - rules[this.attr[r]] = ruleInfo[r]; - }, this); - - return rules; - }; - - this.defaultAttrs = function(defaults) { - utils.push(this.defaults, defaults, true) || (this.defaults = defaults); - }; - - this.select = function(attributeKey) { - return this.$node.find(this.attr[attributeKey]); - }; - - this.initialize = $.noop; - this.teardown = teardown; - } - function attachTo(selector/*, options args */) { // unpacking arguments by hand benchmarked faster var l = arguments.length; @@ -201,20 +53,19 @@ define( for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; if (!selector) { - throw new Error("Component needs to be attachTo'd a jQuery object, native node or selector string"); + throw new Error('Component needs to be attachTo\'d a jQuery object, native node or selector string'); } var options = utils.merge.apply(utils, args); + var componentInfo = registry.findComponentInfo(this); $(selector).each(function(i, node) { - var rawNode = node.jQuery ? node[0] : node; - var componentInfo = registry.findComponentInfo(this) - if (componentInfo && componentInfo.isAttachedTo(rawNode)) { - //already attached + if (componentInfo && componentInfo.isAttachedTo(node)) { + // already attached return; } - new this(node, options); + (new this).initialize(node, options); }.bind(this)); } @@ -224,73 +75,38 @@ define( function define(/*mixins*/) { // unpacking arguments by hand benchmarked faster var l = arguments.length; - var mixins = new Array(l); + // add three for common mixins + var mixins = new Array(l + 3); for (var i = 0; i < l; i++) mixins[i] = arguments[i]; - Component.toString = function() { + var Component = function() {}; + + Component.toString = Component.prototype.toString = function() { var prettyPrintMixins = mixins.map(function(mixin) { if (mixin.name == null) { - //function name property not supported by this browser, use regex + // function name property not supported by this browser, use regex var m = mixin.toString().match(functionNameRegEx); - return (m && m[1]) ? m[1] : ""; + return (m && m[1]) ? m[1] : ''; } else { - return (mixin.name != "withBaseComponent") ? mixin.name : ""; + return (mixin.name != 'withBase') ? mixin.name : ''; } }).filter(Boolean).join(', '); return prettyPrintMixins; }; - if (window.DEBUG && window.DEBUG.enabled) { - Component.describe = Component.toString(); - } - - //'options' is optional hash to be merged with 'defaults' in the component definition - function Component(node, options) { - options = options || {}; - this.identity = componentId++; - - if (!node) { - throw new Error("Component needs a node"); - } - - if (node.jquery) { - this.node = node[0]; - this.$node = node; - } else { - this.node = node; - this.$node = $(node); - } - - this.toString = Component.toString; - if (window.DEBUG && window.DEBUG.enabled) { - this.describe = this.toString(); - } - - //merge defaults with supplied options - //put options in attr.__proto__ to avoid merge overhead - var attr = Object.create(options); - for (var key in this.defaults) { - if (!options.hasOwnProperty(key)) { - attr[key] = this.defaults[key]; - } - } - this.attr = attr; - - Object.keys(this.defaults || {}).forEach(function(key) { - if (this.defaults[key] === null && this.attr[key] === null) { - throw new Error('Required attribute "' + key + '" not specified in attachTo for component "' + this.toString() + '".'); - } - }, this); - - this.initialize.call(this, options); + if (debug.enabled) { + Component.describe = Component.prototype.describe = Component.toString(); } + // 'options' is optional hash to be merged with 'defaults' in the component definition Component.attachTo = attachTo; Component.teardownAll = teardownAll; // prepend common mixins to supplied list, then mixin all flavors - mixins.unshift(withBaseComponent, advice.withAdvice, registry.withRegistration); - + if (debug.enabled) { + mixins.unshift(withLogging); + } + mixins.unshift(withBase, advice.withAdvice, registry.withRegistration); compose.mixin(Component.prototype, mixins); return Component; diff --git a/components/flight/lib/compose.js b/components/flight/lib/compose.js index c1343e7..6d79d99 100644 --- a/components/flight/lib/compose.js +++ b/components/flight/lib/compose.js @@ -4,19 +4,18 @@ // http://opensource.org/licenses/MIT // ========================================== -"use strict"; - define( [ './utils', - '../tools/debug/debug' + './debug' ], - function(util, debug) { + function(utils, debug) { + 'use strict'; //enumerables are shims - getOwnPropertyDescriptor shim doesn't work - var canWriteProtect = debug.enabled && !util.isEnumerable(Object, 'getOwnPropertyDescriptor'); + var canWriteProtect = debug.enabled && !utils.isEnumerable(Object, 'getOwnPropertyDescriptor'); //whitelist of unlockable property names var dontLock = ['mixedIn']; diff --git a/components/flight/tools/debug/debug.js b/components/flight/lib/debug.js similarity index 76% rename from components/flight/tools/debug/debug.js rename to components/flight/lib/debug.js index b2e4fdf..0ed2f36 100644 --- a/components/flight/tools/debug/debug.js +++ b/components/flight/lib/debug.js @@ -1,31 +1,31 @@ -"use strict"; +// ========================================== +// Copyright 2013 Twitter, Inc +// Licensed under The MIT License +// http://opensource.org/licenses/MIT +// ========================================== define( - [ - '../../lib/registry', - '../../lib/utils' - ], + [], - function(registry, utils) { + function() { + 'use strict'; - var logFilter; - - //****************************************************************************************** + // ========================================== // Search object model - //****************************************************************************************** + // ========================================== function traverse(util, searchTerm, options) { - var options = options || {}; + options = options || {}; var obj = options.obj || window; - var path = options.path || ((obj==window) ? "window" : ""); + var path = options.path || ((obj==window) ? 'window' : ''); var props = Object.keys(obj); props.forEach(function(prop) { if ((tests[util] || util)(searchTerm, obj, prop)){ - console.log([path, ".", prop].join(""), "->",["(", typeof obj[prop], ")"].join(""), obj[prop]); + console.log([path, '.', prop].join(''), '->', ['(', typeof obj[prop], ')'].join(''), obj[prop]); } - if(Object.prototype.toString.call(obj[prop])=="[object Object]" && (obj[prop] != obj) && path.split(".").indexOf(prop) == -1) { - traverse(util, searchTerm, {obj: obj[prop], path: [path,prop].join(".")}); + if (Object.prototype.toString.call(obj[prop]) == '[object Object]' && (obj[prop] != obj) && path.split('.').indexOf(prop) == -1) { + traverse(util, searchTerm, {obj: obj[prop], path: [path,prop].join('.')}); } }); } @@ -34,28 +34,28 @@ define( if (!expected || typeof searchTerm == expected) { traverse(util, searchTerm, options); } else { - console.error([searchTerm, 'must be', expected].join(' ')) + console.error([searchTerm, 'must be', expected].join(' ')); } } var tests = { - 'name': function(searchTerm, obj, prop) {return searchTerm == prop}, - 'nameContains': function(searchTerm, obj, prop) {return prop.indexOf(searchTerm)>-1}, - 'type': function(searchTerm, obj, prop) {return obj[prop] instanceof searchTerm}, - 'value': function(searchTerm, obj, prop) {return obj[prop] === searchTerm}, - 'valueCoerced': function(searchTerm, obj, prop) {return obj[prop] == searchTerm} - } + 'name': function(searchTerm, obj, prop) {return searchTerm == prop;}, + 'nameContains': function(searchTerm, obj, prop) {return prop.indexOf(searchTerm) > -1;}, + 'type': function(searchTerm, obj, prop) {return obj[prop] instanceof searchTerm;}, + 'value': function(searchTerm, obj, prop) {return obj[prop] === searchTerm;}, + 'valueCoerced': function(searchTerm, obj, prop) {return obj[prop] == searchTerm;} + }; - function byName(searchTerm, options) {search('name', 'string', searchTerm, options);}; - function byNameContains(searchTerm, options) {search('nameContains', 'string', searchTerm, options);}; - function byType(searchTerm, options) {search('type', 'function', searchTerm, options);}; - function byValue(searchTerm, options) {search('value', null, searchTerm, options);}; - function byValueCoerced(searchTerm, options) {search('valueCoerced', null, searchTerm, options);}; - function custom(fn, options) {traverse(fn, null, options);}; + function byName(searchTerm, options) {search('name', 'string', searchTerm, options);} + function byNameContains(searchTerm, options) {search('nameContains', 'string', searchTerm, options);} + function byType(searchTerm, options) {search('type', 'function', searchTerm, options);} + function byValue(searchTerm, options) {search('value', null, searchTerm, options);} + function byValueCoerced(searchTerm, options) {search('valueCoerced', null, searchTerm, options);} + function custom(fn, options) {traverse(fn, null, options);} - //****************************************************************************************** + // ========================================== // Event logging - //****************************************************************************************** + // ========================================== var ALL = 'all'; //no filter @@ -105,7 +105,8 @@ define( eventNames: (window.localStorage && localStorage.getItem('logFilter_eventNames')) || defaultEventNamesFilter, actions: (window.localStorage && localStorage.getItem('logFilter_actions')) || defaultActionsFilter }; - //reconstitute arrays + + // reconstitute arrays Object.keys(result).forEach(function(k) { var thisProp = result[k]; if (typeof thisProp == 'string' && thisProp !== ALL) { @@ -154,4 +155,3 @@ define( }; } ); - diff --git a/components/flight/lib/index.js b/components/flight/lib/index.js index 1604b25..09806aa 100644 --- a/components/flight/lib/index.js +++ b/components/flight/lib/index.js @@ -15,7 +15,8 @@ define( './utils' ], - function (advice, component, compose, logger, registry, utils) { + function(advice, component, compose, logger, registry, utils) { + 'use strict'; return { advice: advice, diff --git a/components/flight/lib/logger.js b/components/flight/lib/logger.js index 2c89b1b..3773cd6 100644 --- a/components/flight/lib/logger.js +++ b/components/flight/lib/logger.js @@ -4,87 +4,87 @@ // http://opensource.org/licenses/MIT // ========================================== -"use strict"; - define( [ - './compose', './utils' ], - function (compose, util) { + function(utils) { + 'use strict'; var actionSymbols = { - on:'<-', + on: '<-', trigger: '->', off: 'x ' }; function elemToString(elem) { var tagStr = elem.tagName ? elem.tagName.toLowerCase() : elem.toString(); - var classStr = elem.className ? "." + (elem.className) : ""; + var classStr = elem.className ? '.' + (elem.className) : ''; var result = tagStr + classStr; return elem.tagName ? ['\'', '\''].join(result) : result; } function log(action, component, eventArgs) { - - var name, elem, fn, fnName, logFilter, toRegExp, actionLoggable, nameLoggable; + if (!window.DEBUG || !window.DEBUG.enabled) return; + var name, eventType, elem, fn, logFilter, toRegExp, actionLoggable, nameLoggable; if (typeof eventArgs[eventArgs.length-1] == 'function') { fn = eventArgs.pop(); - fn = fn.unbound || fn; //use unbound version if any (better info) - } - - if (typeof eventArgs[eventArgs.length - 1] == 'object') { - eventArgs.pop(); //trigger data arg - not logged right now + fn = fn.unbound || fn; // use unbound version if any (better info) } - if (eventArgs.length == 2) { - elem = eventArgs[0]; - name = eventArgs[1]; - } else { + if (eventArgs.length == 1) { elem = component.$node[0]; - name = eventArgs[0]; + eventType = eventArgs[0]; + } else if (eventArgs.length == 2) { + if (typeof eventArgs[1] == 'object' && !eventArgs[1].type) { + elem = component.$node[0]; + eventType = eventArgs[0]; + } else { + elem = eventArgs[0]; + eventType = eventArgs[1]; + } + } else { + elem = eventArgs[0]; + eventType = eventArgs[1]; } - if (window.DEBUG && window.DEBUG.enabled) { - logFilter = DEBUG.events.logFilter; - - // no regex for you, actions... - actionLoggable = logFilter.actions=="all" || (logFilter.actions.indexOf(action) > -1); - // event name filter allow wildcards or regex... - toRegExp = function(expr) { - return expr.test ? expr : new RegExp("^" + expr.replace(/\*/g, ".*") + "$"); - }; - nameLoggable = - logFilter.eventNames=="all" || - logFilter.eventNames.some(function(e) {return toRegExp(e).test(name)}); - - if (actionLoggable && nameLoggable) { - console.info( - actionSymbols[action], - action, - '[' + name + ']', - elemToString(elem), - component.constructor.toString(), - fn && (fnName = fn.name || fn.displayName) && '-> ' + fnName - ); - } + name = typeof eventType == 'object' ? eventType.type : eventType; + + logFilter = DEBUG.events.logFilter; + + // no regex for you, actions... + actionLoggable = logFilter.actions == 'all' || (logFilter.actions.indexOf(action) > -1); + // event name filter allow wildcards or regex... + toRegExp = function(expr) { + return expr.test ? expr : new RegExp('^' + expr.replace(/\*/g, '.*') + '$'); + }; + nameLoggable = + logFilter.eventNames == 'all' || + logFilter.eventNames.some(function(e) {return toRegExp(e).test(name);}); + + if (actionLoggable && nameLoggable) { + console.info( + actionSymbols[action], + action, + '[' + name + ']', + elemToString(elem), + component.constructor.describe.split(' ').slice(0,3).join(' ') // two mixins only + ); } } - function withLogging() { this.before('trigger', function() { - log('trigger', this, util.toArray(arguments)); + log('trigger', this, utils.toArray(arguments)); }); this.before('on', function() { - log('on', this, util.toArray(arguments)); + log('on', this, utils.toArray(arguments)); }); - this.before('off', function(eventArgs) { - log('off', this, util.toArray(arguments)); + this.before('off', function() { + log('off', this, utils.toArray(arguments)); }); } diff --git a/components/flight/lib/registry.js b/components/flight/lib/registry.js index baaa3b1..906a681 100644 --- a/components/flight/lib/registry.js +++ b/components/flight/lib/registry.js @@ -4,15 +4,12 @@ // http://opensource.org/licenses/MIT // ========================================== -"use strict"; - define( - [ - './utils' - ], + [], - function (util) { + function() { + 'use strict'; function parseEventArgs(instance, args) { var element, type, callback; @@ -71,22 +68,22 @@ define( this.attachedTo.push(instance.node); return instanceInfo; - } + }; this.removeInstance = function(instance) { delete this.instances[instance.identity]; var indexOfNode = this.attachedTo.indexOf(instance.node); (indexOfNode > -1) && this.attachedTo.splice(indexOfNode, 1); - if (!this.instances.length) { + if (!Object.keys(this.instances).length) { //if I hold no more instances remove me from registry registry.removeComponentInfo(this); } - } + }; this.isAttachedTo = function(node) { return this.attachedTo.indexOf(node) > -1; - } + }; } function InstanceInfo(instance) { @@ -104,7 +101,7 @@ define( this.events.splice(i, 1); } } - } + }; } this.addInstance = function(instance) { @@ -135,7 +132,7 @@ define( this.removeComponentInfo = function(componentInfo) { var index = this.components.indexOf(componentInfo); - (index > -1) && this.components.splice(index, 1); + (index > -1) && this.components.splice(index, 1); }; this.findComponentInfo = function(which) { @@ -151,18 +148,18 @@ define( }; this.findInstanceInfo = function(instance) { - return this.allInstances[instance.identity] || null; + return this.allInstances[instance.identity] || null; }; this.findInstanceInfoByNode = function(node) { - var result = []; - Object.keys(this.allInstances).forEach(function(k) { - var thisInstanceInfo = this.allInstances[k]; - if(thisInstanceInfo.instance.node === node) { - result.push(thisInstanceInfo); - } - }, this); - return result; + var result = []; + Object.keys(this.allInstances).forEach(function(k) { + var thisInstanceInfo = this.allInstances[k]; + if (thisInstanceInfo.instance.node === node) { + result.push(thisInstanceInfo); + } + }, this); + return result; }; this.on = function(componentOn) { @@ -183,26 +180,31 @@ define( } }; - this.off = function(el, type, callback) { + this.off = function(/*el, type, callback*/) { var event = parseEventArgs(this, arguments), instance = registry.findInstanceInfo(this); if (instance) { instance.removeBind(event); } + + //remove from global event registry + for (var i = 0, e; e = registry.events[i]; i++) { + if (matchEvent(e, event)) { + registry.events.splice(i, 1); + } + } }; - //debug tools may want to add advice to trigger - if (window.DEBUG && DEBUG.enabled) { - registry.trigger = new Function; - } + // debug tools may want to add advice to trigger + registry.trigger = function() {}; this.teardown = function() { registry.removeInstance(this); }; this.withRegistration = function() { - this.before('initialize', function() { + this.after('initialize', function() { registry.addInstance(this); }); @@ -210,7 +212,7 @@ define( this.after('off', registry.off); //debug tools may want to add advice to trigger window.DEBUG && DEBUG.enabled && this.after('trigger', registry.trigger); - this.after('teardown', {obj:registry, fnName:'teardown'}); + this.after('teardown', {obj: registry, fnName: 'teardown'}); }; } diff --git a/components/flight/lib/utils.js b/components/flight/lib/utils.js index 31af777..6e9fee5 100644 --- a/components/flight/lib/utils.js +++ b/components/flight/lib/utils.js @@ -4,13 +4,12 @@ // http://opensource.org/licenses/MIT // ========================================== -"use strict"; - define( [], - function () { + function() { + 'use strict'; var arry = []; var DEFAULT_INTERVAL = 100; @@ -92,14 +91,14 @@ define( if (base) { Object.keys(extra || {}).forEach(function(key) { if (base[key] && protect) { - throw Error("utils.push attempted to overwrite '" + key + "' while running in protected mode"); + throw new Error('utils.push attempted to overwrite "' + key + '" while running in protected mode'); } - if (typeof base[key] == "object" && typeof extra[key] == "object") { - //recurse + if (typeof base[key] == 'object' && typeof extra[key] == 'object') { + // recurse this.push(base[key], extra[key]); } else { - //no protect, so extra wins + // no protect, so extra wins base[key] = extra[key]; } }, this); @@ -112,9 +111,9 @@ define( return Object.keys(obj).indexOf(property) > -1; }, - //build a function from other function(s) - //util.compose(a,b,c) -> a(b(c())); - //implementation lifted from underscore.js (c) 2009-2012 Jeremy Ashkenas + // build a function from other function(s) + // utils.compose(a,b,c) -> a(b(c())); + // implementation lifted from underscore.js (c) 2009-2012 Jeremy Ashkenas compose: function() { var funcs = arguments; @@ -220,13 +219,41 @@ define( var target = $(e.target), parent; Object.keys(rules).forEach(function(selector) { - if ((parent = target.closest(selector)).length) { + if (!e.isPropagationStopped() && (parent = target.closest(selector)).length) { data = data || {}; data.el = parent[0]; return rules[selector].apply(this, [e, data]); } }, this); }; + }, + + // ensures that a function will only be called once. + // usage: + // will only create the application once + // var initialize = utils.once(createApplication) + // initialize(); + // initialize(); + // + // will only delete a record once + // var myHanlder = function () { + // $.ajax({type: 'DELETE', url: 'someurl.com', data: {id: 1}}); + // }; + // this.on('click', utils.once(myHandler)); + // + once: function(func) { + var ran, result; + + return function() { + if (ran) { + return result; + } + + result = func.apply(this, arguments); + ran = true; + + return result; + }; } }; diff --git a/requireMain.js b/requireMain.js index ce8aa51..8d07d61 100644 --- a/requireMain.js +++ b/requireMain.js @@ -7,16 +7,11 @@ requirejs.config({ require( [ - 'flight/lib/compose', - 'flight/lib/registry', - 'flight/lib/advice', - 'flight/lib/logger', - 'flight/tools/debug/debug' + 'flight/lib/debug' ], - function(compose, registry, advice, withLogging, debug) { + function(debug) { debug.enable(true); - compose.mixin(registry, [advice.withAdvice, withLogging]); require(['app/boot/page'], function(initialize) { initialize(); });