Speedometer: Update the Flight.js implementation to a more recent library version
[WebKit-https.git] / PerformanceTests / Speedometer / resources / todomvc / dependency-examples / flight / flight / node_modules / flight / lib / base.js
1 /* Copyright 2013 Twitter, Inc. Licensed under The MIT License. http://opensource.org/licenses/MIT */
2
3 define(
4
5   [
6     './utils',
7     './registry',
8     './debug'
9   ],
10
11   function(utils, registry, debug) {
12     'use strict';
13
14     // common mixin allocates basic functionality - used by all component prototypes
15     // callback context is bound to component
16     var componentId = 0;
17
18     function teardownInstance(instanceInfo) {
19       instanceInfo.events.slice().forEach(function(event) {
20         var args = [event.type];
21
22         event.element && args.unshift(event.element);
23         (typeof event.callback == 'function') && args.push(event.callback);
24
25         this.off.apply(this, args);
26       }, instanceInfo.instance);
27     }
28
29     function checkSerializable(type, data) {
30       try {
31         window.postMessage(data, '*');
32       } catch (e) {
33         console.log('unserializable data for event',type,':',data);
34         throw new Error(
35           ['The event', type, 'on component', this.toString(), 'was triggered with non-serializable data'].join(' ')
36         );
37       }
38     }
39
40     function initAttributes(attrs) {
41       var definedKeys = [], incomingKeys;
42
43       this.attr = new this.attrDef;
44
45       if (debug.enabled && window.console) {
46         for (var key in this.attrDef.prototype) {
47           definedKeys.push(key);
48         }
49         incomingKeys = Object.keys(attrs);
50
51         for (var i = incomingKeys.length - 1; i >= 0; i--) {
52           if (definedKeys.indexOf(incomingKeys[i]) == -1) {
53             console.warn('Passed unused attributes including "' + incomingKeys[i] +
54                          '" to component "' + this.toString() + '".');
55             break;
56           }
57         }
58       }
59
60       for (var key in this.attrDef.prototype) {
61         if (typeof attrs[key]  == 'undefined') {
62           if (this.attr[key] === null) {
63             throw new Error('Required attribute "' + key +
64                             '" not specified in attachTo for component "' + this.toString() + '".');
65           }
66         } else {
67           this.attr[key] = attrs[key];
68         }
69
70         if (typeof this.attr[key] == 'function') {
71           this.attr[key] = this.attr[key].call(this);
72         }
73       }
74
75     }
76
77     function initDeprecatedAttributes(attrs) {
78       // merge defaults with supplied options
79       // put options in attr.__proto__ to avoid merge overhead
80       var attr = Object.create(attrs);
81
82       for (var key in this.defaults) {
83         if (!attrs.hasOwnProperty(key)) {
84           attr[key] = this.defaults[key];
85         }
86       }
87
88       this.attr = attr;
89
90       Object.keys(this.defaults || {}).forEach(function(key) {
91         if (this.defaults[key] === null && this.attr[key] === null) {
92           throw new Error('Required attribute "' + key +
93                           '" not specified in attachTo for component "' + this.toString() + '".');
94         }
95       }, this);
96     }
97
98     function proxyEventTo(targetEvent) {
99       return function(e, data) {
100         $(e.target).trigger(targetEvent, data);
101       };
102     }
103
104     function withBase() {
105
106       // delegate trigger, bind and unbind to an element
107       // if $element not supplied, use component's node
108       // other arguments are passed on
109       // event can be either a string specifying the type
110       // of the event, or a hash specifying both the type
111       // and a default function to be called.
112       this.trigger = function() {
113         var $element, type, data, event, defaultFn;
114         var lastIndex = arguments.length - 1, lastArg = arguments[lastIndex];
115
116         if (typeof lastArg != 'string' && !(lastArg && lastArg.defaultBehavior)) {
117           lastIndex--;
118           data = lastArg;
119         }
120
121         if (lastIndex == 1) {
122           $element = $(arguments[0]);
123           event = arguments[1];
124         } else {
125           $element = this.$node;
126           event = arguments[0];
127         }
128
129         if (event.defaultBehavior) {
130           defaultFn = event.defaultBehavior;
131           event = $.Event(event.type);
132         }
133
134         type = event.type || event;
135
136         if (debug.enabled && window.postMessage) {
137           checkSerializable.call(this, type, data);
138         }
139
140         if (typeof this.attr.eventData === 'object') {
141           data = $.extend(true, {}, this.attr.eventData, data);
142         }
143
144         $element.trigger((event || type), data);
145
146         if (defaultFn && !event.isDefaultPrevented()) {
147           (this[defaultFn] || defaultFn).call(this, event, data);
148         }
149
150         return $element;
151       };
152
153
154       this.on = function() {
155         var $element, type, callback, originalCb;
156         var lastIndex = arguments.length - 1, origin = arguments[lastIndex];
157
158         if (typeof origin == 'object') {
159           //delegate callback
160           originalCb = utils.delegate(
161             this.resolveDelegateRules(origin)
162           );
163         } else if (typeof origin == 'string') {
164           originalCb = proxyEventTo(origin);
165         } else {
166           originalCb = origin;
167         }
168
169         if (lastIndex == 2) {
170           $element = $(arguments[0]);
171           type = arguments[1];
172         } else {
173           $element = this.$node;
174           type = arguments[0];
175         }
176
177         if (typeof originalCb != 'function' && typeof originalCb != 'object') {
178           throw new Error('Unable to bind to "' + type +
179                           '" because the given callback is not a function or an object');
180         }
181
182         callback = originalCb.bind(this);
183         callback.target = originalCb;
184         callback.context = this;
185
186         $element.on(type, callback);
187
188         // store every bound version of the callback
189         originalCb.bound || (originalCb.bound = []);
190         originalCb.bound.push(callback);
191
192         return callback;
193       };
194
195       this.off = function() {
196         var $element, type, callback;
197         var lastIndex = arguments.length - 1;
198
199         if (typeof arguments[lastIndex] == 'function') {
200           callback = arguments[lastIndex];
201           lastIndex -= 1;
202         }
203
204         if (lastIndex == 1) {
205           $element = $(arguments[0]);
206           type = arguments[1];
207         } else {
208           $element = this.$node;
209           type = arguments[0];
210         }
211
212         if (callback) {
213           //this callback may be the original function or a bound version
214           var boundFunctions = callback.target ? callback.target.bound : callback.bound || [];
215           //set callback to version bound against this instance
216           boundFunctions && boundFunctions.some(function(fn, i, arr) {
217             if (fn.context && (this.identity == fn.context.identity)) {
218               arr.splice(i, 1);
219               callback = fn;
220               return true;
221             }
222           }, this);
223           $element.off(type, callback);
224         } else {
225           // Loop through the events of `this` instance
226           // and unbind using the callback
227           registry.findInstanceInfo(this).events.forEach(function (event) {
228             if (type == event.type) {
229               $element.off(type, event.callback);
230             }
231           });
232         }
233
234         return $element;
235       };
236
237       this.resolveDelegateRules = function(ruleInfo) {
238         var rules = {};
239
240         Object.keys(ruleInfo).forEach(function(r) {
241           if (!(r in this.attr)) {
242             throw new Error('Component "' + this.toString() + '" wants to listen on "' + r + '" but no such attribute was defined.');
243           }
244           rules[this.attr[r]] = (typeof ruleInfo[r] == 'string') ? proxyEventTo(ruleInfo[r]) : ruleInfo[r];
245         }, this);
246
247         return rules;
248       };
249
250       this.select = function(attributeKey) {
251         return this.$node.find(this.attr[attributeKey]);
252       };
253
254       // New-style attributes
255
256       this.attributes = function(attrs) {
257
258         var Attributes = function() {};
259
260         if (this.attrDef) {
261           Attributes.prototype = new this.attrDef;
262         }
263
264         for (var name in attrs) {
265           Attributes.prototype[name] = attrs[name];
266         }
267
268         this.attrDef = Attributes;
269       };
270
271       // Deprecated attributes
272
273       this.defaultAttrs = function(defaults) {
274         utils.push(this.defaults, defaults, true) || (this.defaults = defaults);
275       };
276
277       this.initialize = function(node, attrs) {
278         attrs = attrs || {};
279         this.identity || (this.identity = componentId++);
280
281         if (!node) {
282           throw new Error('Component needs a node');
283         }
284
285         if (node.jquery) {
286           this.node = node[0];
287           this.$node = node;
288         } else {
289           this.node = node;
290           this.$node = $(node);
291         }
292
293         if (this.attrDef) {
294           initAttributes.call(this, attrs);
295         } else {
296           initDeprecatedAttributes.call(this, attrs);
297         }
298
299         return this;
300       };
301
302       this.teardown = function() {
303         teardownInstance(registry.findInstanceInfo(this));
304       };
305     }
306
307     return withBase;
308   }
309 );