blob: a1b4eb63553a4398d0c6ef62252db7761c608a78 [file] [log] [blame]
/* 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 initAttributes(attrs) {
var definedKeys = [], incomingKeys;
this.attr = new this.attrDef;
if (debug.enabled && window.console) {
for (var key in this.attrDef.prototype) {
definedKeys.push(key);
}
incomingKeys = Object.keys(attrs);
for (var i = incomingKeys.length - 1; i >= 0; i--) {
if (definedKeys.indexOf(incomingKeys[i]) == -1) {
console.warn('Passed unused attributes including "' + incomingKeys[i] +
'" to component "' + this.toString() + '".');
break;
}
}
}
for (var key in this.attrDef.prototype) {
if (typeof attrs[key] == 'undefined') {
if (this.attr[key] === null) {
throw new Error('Required attribute "' + key +
'" not specified in attachTo for component "' + this.toString() + '".');
}
} else {
this.attr[key] = attrs[key];
}
if (typeof this.attr[key] == 'function') {
this.attr[key] = this.attr[key].call(this);
}
}
}
function initDeprecatedAttributes(attrs) {
// 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);
}
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, event, data);
}
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);
$element.off(type, callback);
} else {
// Loop through the events of `this` instance
// and unbind using the callback
registry.findInstanceInfo(this).events.forEach(function (event) {
if (type == event.type) {
$element.off(type, event.callback);
}
});
}
return $element;
};
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.select = function(attributeKey) {
return this.$node.find(this.attr[attributeKey]);
};
// New-style attributes
this.attributes = function(attrs) {
var Attributes = function() {};
if (this.attrDef) {
Attributes.prototype = new this.attrDef;
}
for (var name in attrs) {
Attributes.prototype[name] = attrs[name];
}
this.attrDef = Attributes;
};
// Deprecated attributes
this.defaultAttrs = function(defaults) {
utils.push(this.defaults, defaults, true) || (this.defaults = defaults);
};
this.initialize = function(node, attrs) {
attrs = attrs || {};
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);
}
if (this.attrDef) {
initAttributes.call(this, attrs);
} else {
initDeprecatedAttributes.call(this, attrs);
}
return this;
};
this.teardown = function() {
teardownInstance(registry.findInstanceInfo(this));
};
}
return withBase;
}
);