1 (function (global) {
  2 
  3     "use strict";
  4 
  5     if (global.Sfdc && global.Sfdc.canvas) {
  6         return;
  7     }
  8 
  9     // cached references
 10     //------------------
 11 
 12     var oproto = Object.prototype,
 13         aproto = Array.prototype,
 14         doc = global.document,
 15         /**
 16         * @class Canvas
 17         * @exports $ as Sfdc.canvas
 18         */
 19         // $ functions
 20         // The canvas global object is made available in the global scope.  The reveal to the global scope is done later.
 21         $ = {
 22 
 23             // type utilities
 24             //---------------
 25             
 26             /**
 27             * @description Checks whether an object contains an uninherited property.
 28             * @param {Object} obj The object to check
 29             * @param {String} prop The property name to check for
 30             * @returns {Boolean} <code>true</code> if the property exists for the object and isn't inherited; otherwise <code>false</code>
 31             */
 32             hasOwn: function (obj, prop) {
 33                 return oproto.hasOwnProperty.call(obj, prop);
 34             },
 35             
 36             /**
 37             * @description Checks whether an object is currently undefined.
 38             * @param {Object} value The object to check
 39             * @returns {Boolean} <code>true</code> if the object or value is of type undefined; otherwise <code>false</code>
 40             */
 41             isUndefined: function (value) {
 42                 var undef;
 43                 return value === undef;
 44             },
 45             
 46             /**
 47             * @description Checks whether an object is undefined, null, or an empty string.
 48             * @param {Object} value The object to check
 49             * @returns {Boolean} <code>true</code> if the object or value is of type undefined; otherwise <code>false</code>
 50             */
 51             isNil: function (value) {
 52                 return $.isUndefined(value) || value === null || value === "";
 53             },
 54             
 55             /**
 56             * @description Checks whether a value is a number. This function doesn't resolve strings to numbers.
 57             * @param {Object} value Object to check
 58             * @returns {Boolean} <code>true</code> if the object or value is a number; otherwise <code>false</code>
 59             */
 60             isNumber: function (value) {
 61                 return !!(value === 0 || (value && value.toExponential && value.toFixed));
 62             },
 63 
 64             /**
 65             * @description Checks whether an object is a function.
 66             * @param {Object} value The object to check
 67             * @returns {Boolean} <code>true</code> if the object or value is a function; otherwise <code>false</code>
 68             */
 69             isFunction: function (value) {
 70                 return !!(value && value.constructor && value.call && value.apply);
 71             },
 72             
 73             /**
 74             * @description Checks whether an object is an array.
 75             * @param {Object} value The object to check
 76             * @function
 77             * @returns {Boolean} <code>true</code> if the object or value is of type array; otherwise <code>false</code>
 78             */
 79             isArray: Array.isArray || function (value) {
 80                 return oproto.toString.call(value) === '[object Array]';
 81             },
 82             
 83             /**
 84             * @description Checks whether an object is the argument set for a function.
 85             * @param {Object} value The object to check
 86             * @returns {Boolean} <code>true</code> if the object or value is the argument set for a function; otherwise <code>false</code>
 87             */
 88             isArguments: function (value) {
 89                 return !!(value && $.hasOwn(value, 'callee'));
 90             },
 91             
 92             /**
 93             * @description Checks whether a value is of type object and isn't null.
 94             * @param {Object} value The object to check
 95             * @returns {Boolean} <code>true</code> if the object or value is of type object; otherwise <code>false</code>
 96             */
 97             isObject: function (value) {
 98                 return value !== null && typeof value === 'object';
 99             },
100 
101             /**
102              * @description Checks whether a value is of type string and isn't null.
103              * @param {Object} value The string to check
104              * @returns {Boolean} <code>true</code> if the string or value is of type string; otherwise <code>false</code>
105              */
106             isString: function(value) {
107                 return value !== null && typeof value == "string";
108             },
109 
110             /**
111              * @description Checks whether the value appears to be JSON.
112              * @param {String} value The JSON string to check
113              * @returns {Boolean} <code>true</code> if the string starts and stops with {} , otherwise <code>false</code>
114              */
115             appearsJson: function (value) {
116                 return (/^\{.*\}$/).test(value);
117             },
118 
119 
120             // common functions
121             //-----------------
122             
123             /**
124             * @description An empty or blank function.  
125             */
126             nop: function () {
127                 /* no-op */
128             },
129             
130             /**
131             * @description Runs the specified function.
132             * @param {Function} fn The function to run
133             */
134             invoker: function (fn) {
135                 if ($.isFunction(fn)) {
136                     fn();
137                 }
138             },
139             
140             /**
141             * @description Returns the argument.
142             * @param {Object} obj The object to return, untouched.
143             * @returns {Object} The argument used for this function call
144             */
145             identity: function (obj) {
146                 return obj;
147             },
148 
149             // @todo consider additional tests for: null, boolean, string, nan, element, regexp... as needed
150             /**
151             * @description Calls a defined function for each element in an object.
152             * @param {Object} obj The object to loop through.  
153                 The object can be an array, an array like object, or a map of properties.
154             * @param {Function} it The callback function to run for each element
155             * @param {Object} [ctx] The context object to be used for the callback function.
156                 Defaults to the original object if not provided.
157             */
158             each: function (obj, it, ctx) {
159                 if ($.isNil(obj)) {
160                     return;
161                 }
162                 var nativ = aproto.forEach, i = 0, l, key;
163                 l = obj.length;
164                 ctx = ctx || obj;
165                 // @todo: looks like native method will not break on return false; maybe throw breaker {}
166                 if (nativ && nativ === obj.forEach) {
167                     obj.forEach(it, ctx);
168                 }
169                 else if ($.isNumber(l)) { // obj is an array-like object
170                     while (i < l) {
171                         if (it.call(ctx, obj[i], i, obj) === false) {
172                             return;
173                         }
174                         i += 1;
175                     }
176                 }
177                 else {
178                     for (key in obj) {
179                         if ($.hasOwn(obj, key) && it.call(ctx, obj[key], key, obj) === false) {
180                             return;
181                         }
182                     }
183                 }
184             },
185             
186             /**
187             * @description Creates a new array with the results of calling the
188                 function on each element in the object.
189             * @param {Object} obj The object to use
190             * @param {Function} it The callback function to run for each element
191             * @param {Object} [ctx] The context object to be used for the callback function.
192                 Defaults to the original object if not provided.
193             * @returns {Array} The array that is created by calling the function on each
194                 element in the object.
195             */
196             map: function (obj, it, ctx) {
197                 var results = [], nativ = aproto.map;
198                 if ($.isNil(obj)) {
199                     return results;
200                 }
201                 if (nativ && obj.map === nativ) {
202                     return obj.map(it, ctx);
203                 }
204                 ctx = ctx || obj;
205                 $.each(obj, function (value, i, list) {
206                     results.push(it.call(ctx, value, i, list));
207                 });
208                 return results;
209             },
210             
211             /** 
212             * @description Creates an array containing all the elements of the given object.
213             * @param {Object} obj The source object used to create the array
214             * @returns {Array} An array containing all the elements in the object.
215             */
216             values: function (obj) {
217                 return $.map(obj, $.identity);
218             },
219             
220             /**
221             * @description Creates a new array containing the selected elements of the given array.
222             * @param {Array} array The array to subset
223             * @param {Integer} [begin=0] The index that specifies where to start the selection
224             * @param {Integer} [end = array.length] The index that specifies where to end the selection
225             * @returns {Array} A new array that contains the selected elements.
226             */
227             slice: function (array, begin, end) {
228                 /* FF doesn't like undefined args for slice so ensure we call with args */
229                 return aproto.slice.call(array, $.isUndefined(begin) ? 0 : begin, $.isUndefined(end) ? array.length : end);
230             },
231 
232             /**
233             * @description Creates an array from an object.
234             * @param {Object} iterable The source object used to create the array.
235             * @returns {Array} The new array created from the object.
236             */
237             toArray: function (iterable) {
238                 if (!iterable) {
239                     return [];
240                 }
241                 if (iterable.toArray) {
242                     return iterable.toArray;
243                 }
244                 if ($.isArray(iterable)) {
245                     return iterable;
246                 }
247                 if ($.isArguments(iterable)) {
248                     return $.slice(iterable);
249                 }
250                 return $.values(iterable);
251             },
252             
253             /**
254             * @description Calculates the number of elements in an object.
255             * @param {Object} obj The object to size
256             * @returns {Integer} The number of elements in the object.
257             */
258             size: function (obj) {
259                 return $.toArray(obj).length;
260             },
261             
262             /**
263             * @description Returns the location of an element in an array.
264             * @param {Array} array The array to check
265             * @param {Object} item The item to search for within the array
266             * @returns {Integer} The index of the element within the array.  
267                 Returns -1 if the element isn't found.
268             */            
269             indexOf: function (array, item) {
270                 var nativ = aproto.indexOf, i, l;
271                 if (!array) {
272                     return -1;
273                 }
274                 if (nativ && array.indexOf === nativ) {
275                     return array.indexOf(item);
276                 }
277                 for (i = 0, l = array.length; i < l; i += 1) {
278                     if (array[i] === item) {
279                         return i;
280                     }
281                 }
282                 return -1;
283             },
284             
285             /**
286             * @description Removes an element from an array.
287             * @param {Array} array The array to modify
288             * @param {Object} item The element to remove from the array
289             */
290             remove: function (array, item) {
291                 var i = $.indexOf(array, item);
292                 if (i >= 0) {
293                     array.splice(i, 1);
294                 }
295             },
296 
297             /**
298              * @description Serializes an object into a string that can be used as a URL query string.
299              * @param {Object|Array} a The array or object to serialize
300              * @param {Boolean} [encode=false] Indicates that the string should be encoded
301              * @returns {String} A string representing the object as a URL query string.
302              */
303             param: function (a, encode) {
304                 var s = [];
305 
306                 encode = encode || false;
307 
308                 function add( key, value ) {
309 
310                     if ($.isNil(value)) {return;}
311                     value = $.isFunction(value) ? value() : value;
312                     if ($.isArray(value)) {
313                         $.each( value, function(v, n) {
314                             add( key, v );
315                         });
316                     }
317                     else {
318                         if (encode) {
319                             s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value);
320                         }
321                         else {
322                             s[ s.length ] = key + "=" + value;
323                         }
324                     }
325                 }
326 
327                 if ( $.isArray(a)) {
328                     $.each( a, function(v, n) {
329                         add( n, v );
330                     });
331                 } else {
332                     for ( var p in a ) {
333                         if ($.hasOwn(a, p)) {
334                             add( p, a[p]);
335                         }
336                     }
337                 }
338                 return s.join("&").replace(/%20/g, "+");
339             },
340 
341             /**
342              * @description Converts a query string into an object.
343              * Note: this doesn't handle multi-value parameters.  For instance,
344              * passing in <code>?param=value1&​param=value2</code> will not return <code>['value1', 'value2']</code>
345              *
346              * @param {String} q ?param1=value1&param2=value2
347              * @return {Object} {param1 : 'value1', param2 : 'value2'}
348              */
349             objectify : function (q) {
350                 var o = {};
351                 q.replace(
352                     new RegExp("([^?=&]+)(=([^&]*))?", "g"),
353                     function($0, $1, $2, $3) { o[$1] = $3; }
354                 );
355                 return o;
356             },
357 
358             /**
359              * @description Strips out the URL to {scheme}://{host}:{port}.  Removes any path and query string information.
360              * @param {String} url The URL to be modified
361              * @returns {String} The {scheme}://{host}:{port} portion of the URL.
362              */
363             stripUrl : function(url) {
364                 return ($.isNil(url)) ? null : url.replace( /([^:]+:\/\/[^\/\?#]+).*/, '$1');
365             },
366 
367             /**
368              * @description Appends the query string to the end of the URL and removes any hash tag.
369              * @param {String} url The URL to be appended to
370              * @returns The URL with the query string appended.
371              */
372             query : function(url, q) {
373                 if ($.isNil(q)) {
374                     return url;
375                 }
376                 // Strip any old hash tags
377                 url = url.replace(/#.*$/, '');
378                 url += (/^\#/.test(q)) ? q  : (/\?/.test( url ) ? "&" : "?") + q;
379                 return url;
380             },
381 
382 
383             // strings
384             //--------
385             /**
386             * @description Adds the contents of two or more objects to
387                 a destination object.
388             * @param {Object} dest The destination object to modify
389             * @param {Object} mixin1-n An unlimited number of objects to add to the destination object
390             * @returns {Object} The modified destination object
391             */
392             extend: function (dest /*, mixin1, mixin2, ... */) {
393                 $.each($.slice(arguments, 1), function (mixin, i) {
394                     $.each(mixin, function (value, key) {
395                         dest[key] = value;
396                     });
397                 });
398                 return dest;
399             },
400 
401             /**
402              * @description Determines if a string ends with a particular suffix.
403              * @param {String} str The string to check
404              * @param {String} suffix The suffix to check for
405              * @returns {boolean} <code>true</code>, if the string ends with suffix; otherwise, <code>false</code>.
406              */
407             endsWith: function (str, suffix) {
408                 return str.indexOf(suffix, str.length - suffix.length) !== -1;
409             },
410 
411             capitalize: function(str) {
412                 return str.charAt(0).toUpperCase() + str.slice(1);
413             },
414 
415             uncapitalize: function(str) {
416                 return str.charAt(0).toLowerCase() + str.slice(1);
417             },
418 
419             // Events
420             //--------
421             /**
422              * @description Validates the event name.
423              * @param {String} name Name of the event; can include the namespace (namespace.name).
424              * @param {String} res Reserved namespace name to allow against default
425              * @returns {int} error code, 0 if valid
426              */
427              validEventName : function(name, res) {
428                 var ns, parts = name.split(/\./),
429                     regex = /^[$A-Z_][0-9A-Z_$]*$/i,
430                     reserved = {
431                         'sfdc':true, 'canvas':true,
432                         'force':true, 'salesforce':true, 'chatter':true
433                     };
434                 $.each($.isArray(res) ? res : [res], function (v) {
435                     reserved[v] = false;
436                 });
437                 if (parts.length > 2) {
438                     return 1;
439                 }
440                 if (parts.length === 2) {
441                     ns = parts[0].toLowerCase();
442                     if (reserved[ns]) {
443                         return 2;
444                     }
445                 }
446                 if (!regex.test(parts[0]) || !regex.test(parts[1])) {
447                     return 3;
448                 }
449                 return 0;
450             },
451 
452 
453             /**
454             * @name Sfdc.canvas.prototypeOf
455             * @function
456             * @description Returns the prototype of the specified object.
457             * @param {Object} obj The object for which to find the prototype
458             * @returns {Object} The object that is the prototype of the given object.
459             */
460             prototypeOf: function (obj) {
461                 var nativ = Object.getPrototypeOf,
462                     proto = '__proto__';
463                 if ($.isFunction(nativ)) {
464                     return nativ.call(Object, obj);
465                 }
466                 else {
467                     if (typeof {}[proto] === 'object') {
468                         return obj[proto];
469                     }
470                     else {
471                         return obj.constructor.prototype;
472                     }
473                 }
474             },
475 
476             /**
477             * @description Adds a module to the global.Sfdc.canvas object.
478             * @param {String} ns The namespace for the new module
479             * @decl {Object} The module to add.
480             * @returns {Object} The global.Sfdc.canvas object with a new module added.
481             */
482             module: function(ns, decl) {
483                 var parts = ns.split('.'), parent = global.Sfdc.canvas, i, length;
484 
485                 // strip redundant leading global
486                 if (parts[1] === 'canvas') {
487                     parts = parts.slice(2);
488                 }
489 
490                 length = parts.length;
491                 for (i = 0; i < length; i += 1) {
492                     // create a property if it doesn't exist
493                     if ($.isUndefined(parent[parts[i]])) {
494                         parent[parts[i]] = {};
495                     }
496                     parent = parent[parts[i]];
497                 }
498 
499                 if ($.isFunction(decl)) {
500                     decl = decl();
501                 }
502                 return $.extend(parent, decl);
503             },
504 
505             // dom
506             //----            
507             // Returns window.document element when invoked from a browser otherwise mocked document for
508             // testing. (Do not add JSDoc tags for this one)
509             document: function() {
510                 return doc;
511             },
512             /**
513             * @description Returns the DOM element with the given ID in the current document. 
514             * @param {String} id The ID of the DOM element
515             * @returns {DOMElement} The DOM element with the given ID.  Returns null if the element doesn't exist.
516             */
517             byId: function (id) {
518                 return doc.getElementById(id);
519             },
520             /**
521             * @description Returns a set of DOM elements with the given class names in the current document.
522             * @param {String} class The class names to find in the DOM; multiple
523                 classnames can be passed, separated by whitespace
524             * @returns {Array} Set of DOM elements that all have the given class name
525             */
526             byClass: function (clazz) {
527                 return doc.getElementsByClassName(clazz);
528             },
529             /**
530             * @description Returns the value for the given attribute name on the given DOM element.
531             * @param {DOMElement} el The element on which to check the attribute.
532             * @param {String} name The name of the attribute for which to find a value.
533             * @returns {String} The given attribute's value.
534             */
535             attr : function(el, name) {
536                 var a = el.attributes, i;
537                 for (i = 0; i < a.length; i += 1) {
538                     if (name === a[i].name) {
539                         return a[i].value;
540                     }
541                 }
542             },
543 
544             /**
545              * @description Registers a callback to be called after the DOM is ready.
546              * @param {Function} cb The callback function to be called
547              */
548             onReady : function(cb) {
549                 if ($.isFunction(cb)) {
550                     readyHandlers.push(cb);
551                 }
552             }            
553        },
554 
555         readyHandlers = [],
556 
557         ready = function () {
558             ready = $.nop;
559             $.each(readyHandlers, $.invoker);
560             readyHandlers = null;
561         },
562 
563         /**
564         * @description 
565         * @param {Function} cb The function to run when ready.
566         */
567         canvas = function (cb) {
568             if ($.isFunction(cb)) {
569                 readyHandlers.push(cb);
570             }
571         };
572 
573     (function () {
574         var ael = 'addEventListener',
575             tryReady = function () {
576                 if (doc && /loaded|complete/.test(doc.readyState)) {
577                     ready();
578                 }
579                 else if (readyHandlers) {
580                     if (!$.isNil(global.setTimeout)) {
581                         global.setTimeout(tryReady, 30);
582                     }
583                 }
584             };
585 
586         if (doc && doc[ael]) {
587             doc[ael]('DOMContentLoaded', ready, false);
588         }
589 
590         tryReady();
591 
592         if (global[ael]) {
593             global[ael]('load', ready, false);
594         }
595         else if (global.attachEvent) {
596             global.attachEvent('onload', ready);
597         }
598 
599     }());
600 
601     $.each($, function (fn, name) {
602         canvas[name] = fn;
603     });
604 
605     if (!global.Sfdc) {
606         global.Sfdc = {};
607     }
608 
609     global.Sfdc.canvas = canvas;
610 
611 
612 }(this));
613