| // ========================================== |
| // Copyright 2013 Twitter, Inc |
| // Licensed under The MIT License |
| // http://opensource.org/licenses/MIT |
| // ========================================== |
| |
| "use strict"; |
| |
| define( |
| |
| [ |
| './advice', |
| './utils', |
| './compose', |
| './registry' |
| ], |
| |
| function(advice, utils, compose, registry) { |
| |
| 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 |
| function teardownAll() { |
| var componentInfo = registry.findComponentInfo(this); |
| |
| componentInfo && Object.keys(componentInfo.instances).forEach(function(k) { |
| var info = componentInfo.instances[k]; |
| info.instance.teardown(); |
| }); |
| } |
| |
| 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(" ") |
| ); |
| } |
| } |
| |
| //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; |
| var args = new Array(l - 1); |
| 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"); |
| } |
| |
| var options = utils.merge.apply(utils, args); |
| |
| $(selector).each(function(i, node) { |
| var rawNode = node.jQuery ? node[0] : node; |
| var componentInfo = registry.findComponentInfo(this) |
| if (componentInfo && componentInfo.isAttachedTo(rawNode)) { |
| //already attached |
| return; |
| } |
| |
| new this(node, options); |
| }.bind(this)); |
| } |
| |
| // define the constructor for a custom component type |
| // takes an unlimited number of mixin functions as arguments |
| // typical api call with 3 mixins: define(timeline, withTweetCapability, withScrollCapability); |
| function define(/*mixins*/) { |
| // unpacking arguments by hand benchmarked faster |
| var l = arguments.length; |
| var mixins = new Array(l); |
| for (var i = 0; i < l; i++) mixins[i] = arguments[i]; |
| |
| Component.toString = function() { |
| var prettyPrintMixins = mixins.map(function(mixin) { |
| if (mixin.name == null) { |
| //function name property not supported by this browser, use regex |
| var m = mixin.toString().match(functionNameRegEx); |
| return (m && m[1]) ? m[1] : ""; |
| } else { |
| return (mixin.name != "withBaseComponent") ? 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); |
| } |
| |
| Component.attachTo = attachTo; |
| Component.teardownAll = teardownAll; |
| |
| // prepend common mixins to supplied list, then mixin all flavors |
| mixins.unshift(withBaseComponent, advice.withAdvice, registry.withRegistration); |
| |
| compose.mixin(Component.prototype, mixins); |
| |
| return Component; |
| } |
| |
| define.teardownAll = function() { |
| registry.components.slice().forEach(function(c) { |
| c.component.teardownAll(); |
| }); |
| registry.reset(); |
| }; |
| |
| return define; |
| } |
| ); |