// ========================================== // 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 proxyEventTo(targetEvent) { return function(e, data) { $(e.target).trigger(targetEvent, data); }; } 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 if (typeof origin == 'string') { originalCb = proxyEventTo(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) { //this callback may be the original function or a bound version var boundFunctions = callback.target ? callback.target.bound : callback.bound || []; //set callback to version bound against this instance boundFunctions && boundFunctions.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]] = (typeof ruleInfo[r] == 'string') ? proxyEventTo(ruleInfo[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; } );