diff options
Diffstat (limited to 'third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib')
7 files changed, 1042 insertions, 0 deletions
diff --git a/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/advice.js b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/advice.js new file mode 100644 index 0000000000..0f4686929a --- /dev/null +++ b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/advice.js @@ -0,0 +1,69 @@ +// ========================================== +// Copyright 2013 Twitter, Inc +// Licensed under The MIT License +// http://opensource.org/licenses/MIT +// ========================================== + +"use strict"; + +define( + + [ + './utils', + './compose' + ], + + function (util, compose) { + + var advice = { + + around: function(base, wrapped) { + return function composedAround() { + // unpacking arguments by hand benchmarked faster + var i = 0, l = arguments.length, args = new Array(l + 1); + args[0] = base.bind(this); + for (; i < l; i++) args[i + 1] = arguments[i]; + + return wrapped.apply(this, args); + } + }, + + before: function(base, before) { + var beforeFn = (typeof before == 'function') ? before : before.obj[before.fnName]; + return function composedBefore() { + beforeFn.apply(this, arguments); + return base.apply(this, arguments); + } + }, + + after: function(base, after) { + var afterFn = (typeof after == 'function') ? after : after.obj[after.fnName]; + return function composedAfter() { + 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 + // code before, after or around. + withAdvice: function() { + ['before', 'after', 'around'].forEach(function(m) { + this[m] = function(method, fn) { + + compose.unlockProperty(this, method, function() { + if (typeof this[method] == 'function') { + return this[method] = advice[m](this[method], fn); + } else { + return this[method] = fn; + } + }); + + }; + }, this); + } + }; + + return advice; + } +); diff --git a/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/component.js b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/component.js new file mode 100644 index 0000000000..d9a5077ac9 --- /dev/null +++ b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/component.js @@ -0,0 +1,308 @@ +// ========================================== +// 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; + } +); diff --git a/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/compose.js b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/compose.js new file mode 100644 index 0000000000..c1343e7fe8 --- /dev/null +++ b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/compose.js @@ -0,0 +1,86 @@ +// ========================================== +// Copyright 2013 Twitter, Inc +// Licensed under The MIT License +// http://opensource.org/licenses/MIT +// ========================================== + +"use strict"; + +define( + + [ + './utils', + '../tools/debug/debug' + ], + + function(util, debug) { + + //enumerables are shims - getOwnPropertyDescriptor shim doesn't work + var canWriteProtect = debug.enabled && !util.isEnumerable(Object, 'getOwnPropertyDescriptor'); + //whitelist of unlockable property names + var dontLock = ['mixedIn']; + + if (canWriteProtect) { + //IE8 getOwnPropertyDescriptor is built-in but throws exeption on non DOM objects + try { + Object.getOwnPropertyDescriptor(Object, 'keys'); + } catch(e) { + canWriteProtect = false; + } + } + + function setPropertyWritability(obj, isWritable) { + if (!canWriteProtect) { + return; + } + + var props = Object.create(null); + + Object.keys(obj).forEach( + function (key) { + if (dontLock.indexOf(key) < 0) { + var desc = Object.getOwnPropertyDescriptor(obj, key); + desc.writable = isWritable; + props[key] = desc; + } + } + ); + + Object.defineProperties(obj, props); + } + + function unlockProperty(obj, prop, op) { + var writable; + + if (!canWriteProtect || !obj.hasOwnProperty(prop)) { + op.call(obj); + return; + } + + writable = Object.getOwnPropertyDescriptor(obj, prop).writable; + Object.defineProperty(obj, prop, { writable: true }); + op.call(obj); + Object.defineProperty(obj, prop, { writable: writable }); + } + + function mixin(base, mixins) { + base.mixedIn = base.hasOwnProperty('mixedIn') ? base.mixedIn : []; + + mixins.forEach(function(mixin) { + if (base.mixedIn.indexOf(mixin) == -1) { + setPropertyWritability(base, false); + mixin.call(base); + base.mixedIn.push(mixin); + } + }); + + setPropertyWritability(base, true); + } + + return { + mixin: mixin, + unlockProperty: unlockProperty + }; + + } +); diff --git a/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/index.js b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/index.js new file mode 100644 index 0000000000..1604b253b9 --- /dev/null +++ b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/index.js @@ -0,0 +1,30 @@ +// ========================================== +// Copyright 2013 Twitter, Inc +// Licensed under The MIT License +// http://opensource.org/licenses/MIT +// ========================================== + +define( + + [ + './advice', + './component', + './compose', + './logger', + './registry', + './utils' + ], + + function (advice, component, compose, logger, registry, utils) { + + return { + advice: advice, + component: component, + compose: compose, + logger: logger, + registry: registry, + utils: utils + }; + + } +); diff --git a/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/logger.js b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/logger.js new file mode 100644 index 0000000000..2c89b1bd20 --- /dev/null +++ b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/logger.js @@ -0,0 +1,93 @@ +// ========================================== +// Copyright 2013 Twitter, Inc +// Licensed under The MIT License +// http://opensource.org/licenses/MIT +// ========================================== + +"use strict"; + +define( + + [ + './compose', + './utils' + ], + + function (compose, util) { + + var actionSymbols = { + on:'<-', + trigger: '->', + off: 'x ' + }; + + function elemToString(elem) { + var tagStr = elem.tagName ? elem.tagName.toLowerCase() : elem.toString(); + 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 (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 + } + + if (eventArgs.length == 2) { + elem = eventArgs[0]; + name = eventArgs[1]; + } else { + elem = component.$node[0]; + name = eventArgs[0]; + } + + 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 + ); + } + } + } + + + function withLogging() { + this.before('trigger', function() { + log('trigger', this, util.toArray(arguments)); + }); + this.before('on', function() { + log('on', this, util.toArray(arguments)); + }); + this.before('off', function(eventArgs) { + log('off', this, util.toArray(arguments)); + }); + } + + return withLogging; + } +); diff --git a/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/registry.js b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/registry.js new file mode 100644 index 0000000000..baaa3b10a7 --- /dev/null +++ b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/registry.js @@ -0,0 +1,220 @@ +// ========================================== +// Copyright 2013 Twitter, Inc +// Licensed under The MIT License +// http://opensource.org/licenses/MIT +// ========================================== + +"use strict"; + +define( + + [ + './utils' + ], + + function (util) { + + function parseEventArgs(instance, args) { + var element, type, callback; + var end = args.length; + + if (typeof args[end - 1] === 'function') { + end -= 1; + callback = args[end]; + } + + if (typeof args[end - 1] === 'object') { + end -= 1; + } + + if (end == 2) { + element = args[0]; + type = args[1]; + } else { + element = instance.node; + type = args[0]; + } + + return { + element: element, + type: type, + callback: callback + }; + } + + function matchEvent(a, b) { + return ( + (a.element == b.element) && + (a.type == b.type) && + (b.callback == null || (a.callback == b.callback)) + ); + } + + function Registry() { + + var registry = this; + + (this.reset = function() { + this.components = []; + this.allInstances = {}; + this.events = []; + }).call(this); + + function ComponentInfo(component) { + this.component = component; + this.attachedTo = []; + this.instances = {}; + + this.addInstance = function(instance) { + var instanceInfo = new InstanceInfo(instance); + this.instances[instance.identity] = instanceInfo; + 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 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) { + this.instance = instance; + this.events = []; + + this.addBind = function(event) { + this.events.push(event); + registry.events.push(event); + }; + + this.removeBind = function(event) { + for (var i = 0, e; e = this.events[i]; i++) { + if (matchEvent(e, event)) { + this.events.splice(i, 1); + } + } + } + } + + this.addInstance = function(instance) { + var component = this.findComponentInfo(instance); + + if (!component) { + component = new ComponentInfo(instance.constructor); + this.components.push(component); + } + + var inst = component.addInstance(instance); + + this.allInstances[instance.identity] = inst; + + return component; + }; + + this.removeInstance = function(instance) { + var index, instInfo = this.findInstanceInfo(instance); + + //remove from component info + var componentInfo = this.findComponentInfo(instance); + componentInfo && componentInfo.removeInstance(instance); + + //remove from registry + delete this.allInstances[instance.identity]; + }; + + this.removeComponentInfo = function(componentInfo) { + var index = this.components.indexOf(componentInfo); + (index > -1) && this.components.splice(index, 1); + }; + + this.findComponentInfo = function(which) { + var component = which.attachTo ? which : which.constructor; + + for (var i = 0, c; c = this.components[i]; i++) { + if (c.component === component) { + return c; + } + } + + return null; + }; + + this.findInstanceInfo = function(instance) { + 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; + }; + + this.on = function(componentOn) { + var instance = registry.findInstanceInfo(this), boundCallback; + + // unpacking arguments by hand benchmarked faster + var l = arguments.length, i = 1; + var otherArgs = new Array(l - 1); + for (; i < l; i++) otherArgs[i - 1] = arguments[i]; + + if (instance) { + boundCallback = componentOn.apply(null, otherArgs); + if (boundCallback) { + otherArgs[otherArgs.length-1] = boundCallback; + } + var event = parseEventArgs(this, otherArgs); + instance.addBind(event); + } + }; + + this.off = function(el, type, callback) { + var event = parseEventArgs(this, arguments), + instance = registry.findInstanceInfo(this); + + if (instance) { + instance.removeBind(event); + } + }; + + //debug tools may want to add advice to trigger + if (window.DEBUG && DEBUG.enabled) { + registry.trigger = new Function; + } + + this.teardown = function() { + registry.removeInstance(this); + }; + + this.withRegistration = function() { + this.before('initialize', function() { + registry.addInstance(this); + }); + + this.around('on', registry.on); + 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'}); + }; + + } + + return new Registry; + } +); diff --git a/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/utils.js b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/utils.js new file mode 100644 index 0000000000..31af777348 --- /dev/null +++ b/third_party/webkit/PerformanceTests/Speedometer/resources/flightjs-example-app/components/flight/lib/utils.js @@ -0,0 +1,236 @@ +// ========================================== +// Copyright 2013 Twitter, Inc +// Licensed under The MIT License +// http://opensource.org/licenses/MIT +// ========================================== + +"use strict"; + +define( + + [], + + function () { + + var arry = []; + var DEFAULT_INTERVAL = 100; + + var utils = { + + isDomObj: function(obj) { + return !!(obj.nodeType || (obj === window)); + }, + + toArray: function(obj, from) { + return arry.slice.call(obj, from); + }, + + // returns new object representing multiple objects merged together + // optional final argument is boolean which specifies if merge is recursive + // original objects are unmodified + // + // usage: + // var base = {a:2, b:6}; + // var extra = {b:3, c:4}; + // merge(base, extra); //{a:2, b:3, c:4} + // base; //{a:2, b:6} + // + // var base = {a:2, b:6}; + // var extra = {b:3, c:4}; + // var extraExtra = {a:4, d:9}; + // merge(base, extra, extraExtra); //{a:4, b:3, c:4. d: 9} + // base; //{a:2, b:6} + // + // var base = {a:2, b:{bb:4, cc:5}}; + // var extra = {a:4, b:{cc:7, dd:1}}; + // merge(base, extra, true); //{a:4, b:{bb:4, cc:7, dd:1}} + // base; //{a:2, b:6} + + merge: function(/*obj1, obj2,....deepCopy*/) { + // unpacking arguments by hand benchmarked faster + var l = arguments.length, + i = 0, + args = new Array(l + 1); + for (; i < l; i++) args[i + 1] = arguments[i]; + + if (l === 0) { + return {}; + } + + //start with empty object so a copy is created + args[0] = {}; + + if (args[args.length - 1] === true) { + //jquery extend requires deep copy as first arg + args.pop(); + args.unshift(true); + } + + return $.extend.apply(undefined, args); + }, + + // updates base in place by copying properties of extra to it + // optionally clobber protected + // usage: + // var base = {a:2, b:6}; + // var extra = {c:4}; + // push(base, extra); //{a:2, b:6, c:4} + // base; //{a:2, b:6, c:4} + // + // var base = {a:2, b:6}; + // var extra = {b: 4 c:4}; + // push(base, extra, true); //Error ("utils.push attempted to overwrite 'b' while running in protected mode") + // base; //{a:2, b:6} + // + // objects with the same key will merge recursively when protect is false + // eg: + // var base = {a:16, b:{bb:4, cc:10}}; + // var extra = {b:{cc:25, dd:19}, c:5}; + // push(base, extra); //{a:16, {bb:4, cc:25, dd:19}, c:5} + // + push: function(base, extra, protect) { + 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"); + } + + if (typeof base[key] == "object" && typeof extra[key] == "object") { + //recurse + this.push(base[key], extra[key]); + } else { + //no protect, so extra wins + base[key] = extra[key]; + } + }, this); + } + + return base; + }, + + isEnumerable: function(obj, property) { + 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 + compose: function() { + var funcs = arguments; + + return function() { + var args = arguments; + + for (var i = funcs.length-1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + + return args[0]; + }; + }, + + // Can only unique arrays of homogeneous primitives, e.g. an array of only strings, an array of only booleans, or an array of only numerics + uniqueArray: function(array) { + var u = {}, a = []; + + for (var i = 0, l = array.length; i < l; ++i) { + if (u.hasOwnProperty(array[i])) { + continue; + } + + a.push(array[i]); + u[array[i]] = 1; + } + + return a; + }, + + debounce: function(func, wait, immediate) { + if (typeof wait != 'number') { + wait = DEFAULT_INTERVAL; + } + + var timeout, result; + + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + + clearTimeout(timeout); + timeout = setTimeout(later, wait); + + if (callNow) { + result = func.apply(context, args); + } + + return result; + }; + }, + + throttle: function(func, wait) { + if (typeof wait != 'number') { + wait = DEFAULT_INTERVAL; + } + + var context, args, timeout, throttling, more, result; + var whenDone = this.debounce(function(){ + more = throttling = false; + }, wait); + + return function() { + context = this; args = arguments; + var later = function() { + timeout = null; + if (more) { + result = func.apply(context, args); + } + whenDone(); + }; + + if (!timeout) { + timeout = setTimeout(later, wait); + } + + if (throttling) { + more = true; + } else { + throttling = true; + result = func.apply(context, args); + } + + whenDone(); + return result; + }; + }, + + countThen: function(num, base) { + return function() { + if (!--num) { return base.apply(this, arguments); } + }; + }, + + delegate: function(rules) { + return function(e, data) { + var target = $(e.target), parent; + + Object.keys(rules).forEach(function(selector) { + if ((parent = target.closest(selector)).length) { + data = data || {}; + data.el = parent[0]; + return rules[selector].apply(this, [e, data]); + } + }, this); + }; + } + + }; + + return utils; + } +); |