1 /**
  2 * Copyright (c) 2014, salesforce.com, inc.
  3 * All rights reserved.
  4 *
  5 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
  6 * that the following conditions are met:
  7 *
  8 * Redistributions of source code must retain the above copyright notice, this list of conditions and the
  9 * following disclaimer.
 10 *
 11 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
 12 * the following disclaimer in the documentation and/or other materials provided with the distribution.
 13 *
 14 * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or
 15 * promote products derived from this software without specific prior written permission.
 16 *
 17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
 18 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 19 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 20 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 21 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 22 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 23 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 24 * POSSIBILITY OF SUCH DAMAGE.
 25 */
 26 
 27 /*jslint bitwise:false */
 28 (function (global) {
 29 
 30     "use strict";
 31 
 32     if (global.Sfdc && global.Sfdc.canvas && global.Sfdc.canvas.module) {
 33         return;
 34     }
 35 
 36     // Preserve any external modules created on canvas
 37     // (This is the case with controller.js)
 38     var extmodules = {};
 39     if (global.Sfdc && global.Sfdc.canvas) {
 40         for (var key in global.Sfdc.canvas) {
 41             if (global.Sfdc.canvas.hasOwnProperty(key)) {
 42                 extmodules[key] = global.Sfdc.canvas[key];
 43             }
 44         }
 45     }
 46 
 47     // cached references
 48     //------------------
 49 
 50     var oproto = Object.prototype,
 51         aproto = Array.prototype,
 52         doc = global.document,
 53         keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
 54 
 55         /**
 56         * @class Canvas
 57         * @exports $ as Sfdc.canvas
 58         */
 59         // $ functions
 60         // The Sfdc.canvas global object is made available in the global scope.  The reveal to the global scope is done later.
 61         $ = {
 62 
 63             // type utilities
 64             //---------------
 65             
 66             /**
 67             * @description Checks whether an object contains an uninherited property.
 68             * @param {Object} obj The object to check
 69             * @param {String} prop The property name to check for
 70             * @returns {Boolean} <code>true</code> if the property exists for the object and isn't inherited; otherwise <code>false</code>
 71             */
 72             hasOwn: function (obj, prop) {
 73                 return oproto.hasOwnProperty.call(obj, prop);
 74             },
 75             
 76             /**
 77             * @description Checks whether an object is currently undefined.
 78             * @param {Object} value The object to check
 79             * @returns {Boolean} <code>true</code> if the object or value is of type undefined; otherwise <code>false</code>
 80             */
 81             isUndefined: function (value) {
 82                 var undef;
 83                 return value === undef;
 84             },
 85             
 86             /**
 87             * @description Checks whether an object is undefined, null, or an empty string.
 88             * @param {Object} value The object to check
 89             * @returns {Boolean} <code>true</code> if the object or value is of type undefined; otherwise <code>false</code>
 90             */
 91             isNil: function (value) {
 92                 return $.isUndefined(value) || value === null || value === "";
 93             },
 94             
 95             /**
 96             * @description Checks whether a value is a number. This function doesn't resolve strings to numbers.
 97             * @param {Object} value Object to check
 98             * @returns {Boolean} <code>true</code> if the object or value is a number; otherwise <code>false</code>
 99             */
100             isNumber: function (value) {
101                 return !!(value === 0 || (value && value.toExponential && value.toFixed));
102             },
103 
104             /**
105             * @description Checks whether an object is a function.
106             * @param {Object} value The object to check
107             * @returns {Boolean} <code>true</code> if the object or value is a function; otherwise <code>false</code>
108             */
109             isFunction: function (value) {
110                 return !!(value && value.constructor && value.call && value.apply);
111             },
112             
113             /**
114             * @description Checks whether an object is an array.
115             * @param {Object} value The object to check
116             * @function
117             * @returns {Boolean} <code>true</code> if the object or value is of type array; otherwise <code>false</code>
118             */
119             isArray: Array.isArray || function (value) {
120                 return oproto.toString.call(value) === '[object Array]';
121             },
122             
123             /**
124             * @description Checks whether an object is the argument set for a function.
125             * @param {Object} value The object to check
126             * @returns {Boolean} <code>true</code> if the object or value is the argument set for a function; otherwise <code>false</code>
127             */
128             isArguments: function (value) {
129                 return !!(value && $.hasOwn(value, 'callee'));
130             },
131             
132             /**
133             * @description Checks whether a value is of type object and isn't null.
134             * @param {Object} value The object to check
135             * @returns {Boolean} <code>true</code> if the object or value is of type object; otherwise <code>false</code>
136             */
137             isObject: function (value) {
138                 return value !== null && typeof value === 'object';
139             },
140 
141             /**
142              * @description Checks whether a value is of type string and isn't null.
143              * @param {Object} value The string to check
144              * @returns {Boolean} <code>true</code> if the string or value is of type string; otherwise <code>false</code>
145              */
146             isString: function(value) {
147                 return value !== null && typeof value == "string";
148             },
149 
150             /**
151              * @description Checks whether the value appears to be JSON.
152              * @param {String} value The JSON string to check
153              * @returns {Boolean} <code>true</code> if the string starts and stops with {} , otherwise <code>false</code>
154              */
155             appearsJson: function (value) {
156                 return (/^\{.*\}$/).test(value);
157             },
158 
159             // common functions
160             //-----------------
161             
162             /**
163             * @description An empty or blank function.  
164             */
165             nop: function () {
166                 /* no-op */
167             },
168             
169             /**
170             * @description Runs the specified function.
171             * @param {Function} fn The function to run
172             */
173             invoker: function (fn) {
174                 if ($.isFunction(fn)) {
175                     fn();
176                 }
177             },
178             
179             /**
180             * @description Returns the argument.
181             * @param {Object} obj The object to return, untouched.
182             * @returns {Object} The argument used for this function call
183             */
184             identity: function (obj) {
185                 return obj;
186             },
187 
188             // @todo consider additional tests for: null, boolean, string, nan, element, regexp... as needed
189             /**
190             * @description Calls a defined function for each element in an object.
191             * @param {Object} obj The object to loop through.  
192                 The object can be an array, an array like object, or a map of properties.
193             * @param {Function} it The callback function to run for each element
194             * @param {Object} [ctx] The context object to be used for the callback function.
195                 Defaults to the original object if not provided.
196             */
197             each: function (obj, it, ctx) {
198                 if ($.isNil(obj)) {
199                     return;
200                 }
201                 var nativ = aproto.forEach, i = 0, l, key;
202                 l = obj.length;
203                 ctx = ctx || obj;
204                 // @todo: looks like native method will not break on return false; maybe throw breaker {}
205                 if (nativ && nativ === obj.forEach) {
206                     obj.forEach(it, ctx);
207                 }
208                 else if ($.isNumber(l)) { // obj is an array-like object
209                     while (i < l) {
210                         if (it.call(ctx, obj[i], i, obj) === false) {
211                             return;
212                         }
213                         i += 1;
214                     }
215                 }
216                 else {
217                     for (key in obj) {
218                         if ($.hasOwn(obj, key) && it.call(ctx, obj[key], key, obj) === false) {
219                             return;
220                         }
221                     }
222                 }
223             },
224             
225             /**
226              * @description Convenience method to prepend a method with a fully qualified url, if the
227              * method does not begin with http protocol.
228              * @param {String} orig The original url to check
229              * @param {String} newUrl The new url to use if it does not begin with http(s) protocol.
230              * @returns {String} orig if the url begins with http, or newUrl if it does not.
231              */
232             startsWithHttp: function(orig, newUrl) {
233                 return  !$.isString(orig) ? orig : (orig.substring(0, 4) === "http") ? orig : newUrl;
234             },
235 
236             
237             /**
238             * @description Creates a new array with the results of calling the
239                 function on each element in the object.
240             * @param {Object} obj The object to use
241             * @param {Function} it The callback function to run for each element
242             * @param {Object} [ctx] The context object to be used for the callback function.
243                 Defaults to the original object if not provided.
244             * @returns {Array} The array that is created by calling the function on each
245                 element in the object.
246             */
247             map: function (obj, it, ctx) {
248                 var results = [], nativ = aproto.map;
249                 if ($.isNil(obj)) {
250                     return results;
251                 }
252                 if (nativ && obj.map === nativ) {
253                     return obj.map(it, ctx);
254                 }
255                 ctx = ctx || obj;
256                 $.each(obj, function (value, i, list) {
257                     results.push(it.call(ctx, value, i, list));
258                 });
259                 return results;
260             },
261             
262             /** 
263             * @description Creates an array containing all the elements of the given object.
264             * @param {Object} obj The source object used to create the array
265             * @returns {Array} An array containing all the elements in the object.
266             */
267             values: function (obj) {
268                 return $.map(obj, $.identity);
269             },
270             
271             /**
272             * @description Creates a new array containing the selected elements of the given array.
273             * @param {Array} array The array to subset
274             * @param {Integer} [begin=0] The index that specifies where to start the selection
275             * @param {Integer} [end = array.length] The index that specifies where to end the selection
276             * @returns {Array} A new array that contains the selected elements.
277             */
278             slice: function (array, begin, end) {
279                 /* FF doesn't like undefined args for slice so ensure we call with args */
280                 return aproto.slice.call(array, $.isUndefined(begin) ? 0 : begin, $.isUndefined(end) ? array.length : end);
281             },
282 
283             /**
284             * @description Creates an array from an object.
285             * @param {Object} iterable The source object used to create the array.
286             * @returns {Array} The new array created from the object.
287             */
288             toArray: function (iterable) {
289                 if (!iterable) {
290                     return [];
291                 }
292                 if (iterable.toArray) {
293                     return iterable.toArray;
294                 }
295                 if ($.isArray(iterable)) {
296                     return iterable;
297                 }
298                 if ($.isArguments(iterable)) {
299                     return $.slice(iterable);
300                 }
301                 return $.values(iterable);
302             },
303             
304             /**
305             * @description Calculates the number of elements in an object.
306             * @param {Object} obj The object to size
307             * @returns {Integer} The number of elements in the object.
308             */
309             size: function (obj) {
310                 return $.toArray(obj).length;
311             },
312             
313             /**
314             * @description Returns the location of an element in an array.
315             * @param {Array} array The array to check
316             * @param {Object} item The item to search for within the array
317             * @returns {Integer} The index of the element within the array.  
318                 Returns -1 if the element isn't found.
319             */            
320             indexOf: function (array, item) {
321                 var nativ = aproto.indexOf, i, l;
322                 if (!array) {
323                     return -1;
324                 }
325                 if (nativ && array.indexOf === nativ) {
326                     return array.indexOf(item);
327                 }
328                 for (i = 0, l = array.length; i < l; i += 1) {
329                     if (array[i] === item) {
330                         return i;
331                     }
332                 }
333                 return -1;
334             },
335             
336             /**
337              * @description Returns true if the object is null, or the object has no 
338              * enumerable properties/attributes.
339              * @param {Object} obj The object to check
340              * @returns {Boolean} <code>true</code> if the object or value is null, or is an object with
341              * no enumerable properties/attributes.
342              */            
343             isEmpty: function(obj){
344                 if (obj === null){
345                     return true;
346                 }
347                 if ($.isArray(obj) || $.isString(obj)){
348                 	return obj.length === 0;
349                 }
350                 for (var key in obj){
351                     if ($.hasOwn(obj, key)){
352                         return false;
353                     }
354                 } 
355                 return true;
356             },
357             
358             /**
359             * @description Removes an element from an array.
360             * @param {Array} array The array to modify
361             * @param {Object} item The element to remove from the array
362             */
363             remove: function (array, item) {
364                 var i = $.indexOf(array, item);
365                 if (i >= 0) {
366                     array.splice(i, 1);
367                 }
368             },
369 
370             /**
371              * @description Serializes an object into a string that can be used as a URL query string.
372              * @param {Object|Array} a The array or object to serialize
373              * @param {Boolean} [encode=false] Indicates that the string should be encoded
374              * @returns {String} A string representing the object as a URL query string.
375              */
376             param: function (a, encode) {
377                 var s = [];
378 
379                 encode = encode || false;
380 
381                 function add( key, value ) {
382 
383                     if ($.isNil(value)) {return;}
384                     value = $.isFunction(value) ? value() : value;
385                     if ($.isArray(value)) {
386                         $.each( value, function(v, n) {
387                             add( key, v );
388                         });
389                     }
390                     else {
391                         if (encode) {
392                             s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value);
393                         }
394                         else {
395                             s[ s.length ] = key + "=" + value;
396                         }
397                     }
398                 }
399 
400                 if ( $.isArray(a)) {
401                     $.each( a, function(v, n) {
402                         add( n, v );
403                     });
404                 } else {
405                     for ( var p in a ) {
406                         if ($.hasOwn(a, p)) {
407                             add( p, a[p]);
408                         }
409                     }
410                 }
411                 return s.join("&").replace(/%20/g, "+");
412             },
413 
414             /**
415              * @description Converts a query string into an object.
416              * @param {String} q param1=value1&param1=value2&param2=value2
417              * @return {Object} {param1 : ['value1', 'value2'], param2 : 'value2'}
418              */
419             objectify : function (q) {
420                 var arr, obj = {}, i, p, n, v, e;
421                 if ($.isNil(q)) {return obj;}
422                 if (q.substring(0, 1) == '?') {
423                     q = q.substring(1);
424                 }
425                 arr = q.split('&');
426                 for (i = 0; i < arr.length; i += 1) {
427                     p = arr[i].split('=');
428                     n = p[0];
429                     v = p[1];
430                     e = obj[n];
431                     if (!$.isNil(e)) {
432                         if ($.isArray(e)) {
433                             e[e.length] = v;
434                         }
435                         else {
436                             obj[n] = [];
437                             obj[n][0] = e;
438                             obj[n][1] = v;
439                         }
440                     }
441                     else {
442                         obj[n] = v;
443                     }
444                 }
445                 return obj;
446             },
447 
448             /**
449              * @description Strips out the URL to {scheme}://{host}:{port}.  Removes any path and query string information.
450              * @param {String} url The URL to be modified
451              * @returns {String} The {scheme}://{host}:{port} portion of the URL.
452              */
453             stripUrl : function(url) {
454                 return ($.isNil(url)) ? null : url.replace( /([^:]+:\/\/[^\/\?#]+).*/, '$1');
455             },
456 
457             /**
458              * @description Appends the query string to the end of the URL and removes any hash tag.
459              * @param {String} url The URL to be appended to
460              * @returns The URL with the query string appended.
461              */
462             query : function(url, q) {
463                 if ($.isNil(q)) {
464                     return url;
465                 }
466                 // Strip any old hash tags
467                 url = url.replace(/#.*$/, '');
468                 url += (/^\#/.test(q)) ? q  : (/\?/.test( url ) ? "&" : "?") + q;
469                 return url;
470             },
471 
472 
473             // strings
474             //--------
475             /**
476             * @description Adds the contents of two or more objects to
477                 a destination object.
478             * @param {Object} dest The destination object to modify
479             * @param {Object} mixin1-n An unlimited number of objects to add to the destination object
480             * @returns {Object} The modified destination object
481             */
482             extend: function (dest /*, mixin1, mixin2, ... */) {
483                 $.each($.slice(arguments, 1), function (mixin, i) {
484                     $.each(mixin, function (value, key) {
485                         dest[key] = value;
486                     });
487                 });
488                 return dest;
489             },
490 
491             /**
492              * @description Determines if a string ends with a particular suffix.
493              * @param {String} str The string to check
494              * @param {String} suffix The suffix to check for
495              * @returns {boolean} <code>true</code>, if the string ends with suffix; otherwise, <code>false</code>.
496              */
497             endsWith: function (str, suffix) {
498                 return str.indexOf(suffix, str.length - suffix.length) !== -1;
499             },
500 
501             capitalize: function(str) {
502                 return str.charAt(0).toUpperCase() + str.slice(1);
503             },
504 
505             uncapitalize: function(str) {
506                 return str.charAt(0).toLowerCase() + str.slice(1);
507             },
508 
509             /**
510              * @description decode a base 64 string.
511              * @param {String} str - base64 encoded string
512              * @return decoded string
513              */
514             decode : function(str) {
515                 var output = [], chr1, chr2, chr3 = "", enc1, enc2, enc3, enc4 = "", i = 0;
516                 str = str.replace(/[^A-Za-z0-9\+\/\=]/g, "");
517 
518                 do {
519                     enc1 = keyStr.indexOf(str.charAt(i++));
520                     enc2 = keyStr.indexOf(str.charAt(i++));
521                     enc3 = keyStr.indexOf(str.charAt(i++));
522                     enc4 = keyStr.indexOf(str.charAt(i++));
523                     chr1 = (enc1 << 2) | (enc2 >> 4);
524                     chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
525                     chr3 = ((enc3 & 3) << 6) | enc4;
526                     output.push(String.fromCharCode(chr1));
527                     if (enc3 !== 64) {
528                         output.push(String.fromCharCode(chr2));
529                     }
530                     if (enc4 !== 64) {
531                         output.push(String.fromCharCode(chr3));
532                     }
533                     chr1 = chr2 = chr3 = "";
534                     enc1 = enc2 = enc3 = enc4 = "";
535                 } while (i < str.length);
536                 return output.join('');
537             },
538 
539 
540             // Events
541             //--------
542             /**
543              * @description Validates the event name.
544              * @param {String} name Name of the event; can include the namespace (namespace.name).
545              * @param {String} res Reserved namespace name to allow against default
546              * @returns {int} error code, 0 if valid
547              */
548              validEventName : function(name, res) {
549                 var ns, parts = name.split(/\./),
550                     regex = /^[$A-Z_][0-9A-Z_$]*$/i,
551                     reserved = {
552                         'sfdc':true, 'canvas':true,
553                         'force':true, 'salesforce':true, 'chatter':true
554                     };
555                 $.each($.isArray(res) ? res : [res], function (v) {
556                     reserved[v] = false;
557                 });
558                 if (parts.length > 2) {
559                     return 1;
560                 }
561                 if (parts.length === 2) {
562                     ns = parts[0].toLowerCase();
563                     if (reserved[ns]) {
564                         return 2;
565                     }
566                 }
567                 if (!regex.test(parts[0]) || !regex.test(parts[1])) {
568                     return 3;
569                 }
570                 return 0;
571             },
572 
573 
574             /**
575             * @name Sfdc.canvas.prototypeOf
576             * @function
577             * @description Returns the prototype of the specified object.
578             * @param {Object} obj The object for which to find the prototype
579             * @returns {Object} The object that is the prototype of the given object.
580             */
581             prototypeOf: function (obj) {
582                 var nativ = Object.getPrototypeOf,
583                     proto = '__proto__';
584                 if ($.isFunction(nativ)) {
585                     return nativ.call(Object, obj);
586                 }
587                 else {
588                     if (typeof {}[proto] === 'object') {
589                         return obj[proto];
590                     }
591                     else {
592                         return obj.constructor.prototype;
593                     }
594                 }
595             },
596 
597             /**
598             * @description Adds a module to the global.Sfdc.canvas object.
599             * @param {String} ns The namespace for the new module
600             * @decl {Object} The module to add.
601             * @returns {Object} The global.Sfdc.canvas object with a new module added.
602             */
603             module: function(ns, decl) {
604                 var parts = ns.split('.'), parent = global.Sfdc.canvas, i, length;
605 
606                 // strip redundant leading global
607                 if (parts[1] === 'canvas') {
608                     parts = parts.slice(2);
609                 }
610 
611                 length = parts.length;
612                 for (i = 0; i < length; i += 1) {
613                     // create a property if it doesn't exist
614                     if ($.isUndefined(parent[parts[i]])) {
615                         parent[parts[i]] = {};
616                     }
617                     parent = parent[parts[i]];
618                 }
619 
620                 if ($.isFunction(decl)) {
621                     decl = decl();
622                 }
623                 return $.extend(parent, decl);
624             },
625 
626             // dom
627             //----            
628             // Returns window.document element when invoked from a browser otherwise mocked document for
629             // testing. (Do not add JSDoc tags for this one)
630             document: function() {
631                 return doc;
632             },
633             /**
634             * @description Returns the DOM element with the given ID in the current document. 
635             * @param {String} id The ID of the DOM element
636             * @returns {DOMElement} The DOM element with the given ID.  Returns null if the element doesn't exist.
637             */
638             byId: function (id) {
639                 return doc.getElementById(id);
640             },
641             /**
642             * @description Returns a set of DOM elements with the given class names in the current document.
643             * @param {String} class The class names to find in the DOM; multiple
644                 classnames can be passed, separated by whitespace
645             * @returns {Array} Set of DOM elements that all have the given class name
646             */
647             byClass: function (clazz) {
648                 return doc.getElementsByClassName(clazz);
649             },
650             /**
651             * @description Returns the value for the given attribute name on the given DOM element.
652             * @param {DOMElement} el The element on which to check the attribute.
653             * @param {String} name The name of the attribute for which to find a value.
654             * @returns {String} The given attribute's value.
655             */
656             attr : function(el, name) {
657                 var a = el.attributes, i;
658                 for (i = 0; i < a.length; i += 1) {
659                     if (name === a[i].name) {
660                         return a[i].value;
661                     }
662                 }
663             },
664 
665             /**
666              * @description Registers a callback to be called after the DOM is ready.
667              * @param {Function} cb The callback function to be called
668              */
669             onReady : function(cb) {
670                 if ($.isFunction(cb)) {
671                     readyHandlers.push(cb);
672                 }
673             },
674 
675             console : (function() {
676 
677                 var enabled = false;
678 
679                 // Prevent errors in browsers without console.log
680                 if (window && !window.console) {window.console = {};}
681                 if (window && !window.console.log) {window.console.log = function(){};}
682                 if (window && !window.console.error) {window.console.error = function(){};}
683 
684                 function isSessionStorage() {
685                     try {
686                         return 'sessionStorage' in window && window.sessionStorage !== null;
687                     } catch (e) {
688                         return false;
689                     }
690                 }
691 
692                 /**
693                 * @description Writes a message to the console. You may pass as many arguments as you'd like.
694                 * The first argument to log may be a string containing printf-like string substitution patterns.
695                 * Note: this function will be ignored for versions of IE that don't support console.log
696                 *
697                 * @public
698                 * @name Sfdc.canvas.console#log
699                 * @function
700                 * @param {Object} arguments Objects(s) to pass to the logger
701                 * @example
702                 * // Log a simple string to the console if the logger is enbabled.
703                 * Sfdc.canvas.console.log("Hello world");
704                 *
705                 * @example
706                 * // Log a formatted string to the console if the logger is enbabled.
707                 * Sfdc.canvas.console.log("Hello %s", "world");
708                 *
709                 * @example
710                 * // Log an object to the console if the logger is enbabled.
711                 * Sfdc.canvas.console.log({hello : "Hello", world : "World"});
712                 *
713                 */
714                 function log() {}
715 
716                 /**
717                 * @description Writes an error message to the console. You may pass as many arguments as you'd like.
718                 * The first argument to log may be a string containing printf-like string substitution patterns.
719                 * Note: this function will be ignored for versions of IE that don't support console.error
720                 *
721                 * @public
722                 * @name Sfdc.canvas.console#error
723                 * @function
724                 * @param {Object} arguments Objects(s) to pass to the logger
725 
726                  * @example
727                 * // Log a simple string to the console if the logger is enbabled.
728                 * Sfdc.canvas.console.error("Something wrong");
729                 *
730                 * @example
731                 * // Log a formatted string to the console if the logger is enbabled.
732                 * Sfdc.canvas.console.error("Bad Status %i", 404);
733                 *
734                 * @example
735                 * // Log an object to the console if the logger is enbabled.
736                 * Sfdc.canvas.console.error({text : "Not Found", status : 404});
737                 *
738                 */
739                 function error() {}
740 
741                 function activate() {
742                     if (Function.prototype.bind) {
743                         log = Function.prototype.bind.call(console.log, console);
744                         error = Function.prototype.bind.call(console.error, console);
745                     }
746                     else {
747                         log = function() {
748                             Function.prototype.apply.call(console.log, console, arguments);
749                         };
750                         error = function() {
751                             Function.prototype.apply.call(console.error, console, arguments);
752                         };
753                     }
754                 }
755 
756                 function deactivate() {
757                     log = function() {};
758                     error = function() {};
759                 }
760 
761                 /**
762                 * @description Enable logging. subsequent calls to log() or error() will be displayed on the javascript console.
763                 * This command can be typed from the javascript console.
764                 *
765                 * @example
766                 * // Enable logging
767                 * Sfdc.canvas.console.enable();
768                 */
769                 function enable() {
770                     enabled = true;
771                     if (isSessionStorage()) {sessionStorage.setItem("canvas_console", "true");}
772                     activate();
773                 }
774 
775                 /**
776                 * @description Disable logging. Subsequent calls to log() or error() will be ignored. This command can be typed
777                 * from the javascript console.
778                 *
779                 * @example
780                 * // Disable logging
781                 * Sfdc.canvas.console.disable();
782                 */
783                 function disable() {
784                     enabled = false;
785                     if (isSessionStorage()) {sessionStorage.setItem("canvas_console", "false");}
786                     deactivate();
787                 }
788 
789                 // Survive page refresh, if enabled or disable previously honor it.
790                 // This is only called once when the page is loaded
791                 enabled = (isSessionStorage() && sessionStorage.getItem("canvas_console") === "true");
792                 if (enabled) {activate();} else {deactivate();}
793 
794                 return {
795                     enable : enable,
796                     disable : disable,
797                     log : log,
798                     error : error
799                 };
800             }())
801         },
802 
803         readyHandlers = [],
804 
805         /**
806          * @description
807          * @param {Function} cb The function to run when ready.
808          */
809         canvas = function (cb) {
810             if ($.isFunction(cb)) {
811                 readyHandlers.push(cb);
812             }
813         };
814 
815         /**
816          * Provide a consistent/performant DOMContentLoaded across all browsers
817          * Implementation was based off of the following tutorial
818          * http://javascript.info/tutorial/onload-ondomcontentloaded?fromEmail=1
819          */
820         (function () {
821 
822             var called = false, isFrame, fn;
823 
824             function ready() {
825                 if (called) {return;}
826                 called = true;
827                 ready = $.nop;
828                 $.each(readyHandlers, $.invoker);
829                 readyHandlers = [];
830             }
831 
832             function tryScroll(){
833                 if (called) {return;}
834                 try {
835                     document.documentElement.doScroll("left");
836                     ready();
837                 } catch(e) {
838                     setTimeout(tryScroll, 30);
839                 }
840             }
841 
842             if ( document.addEventListener ) { // native event
843                 document.addEventListener( "DOMContentLoaded", ready, false );
844             } else if ( document.attachEvent ) {  // IE
845 
846                 try {
847                     isFrame = self !== top;
848                 } catch(e) {}
849 
850                 // IE, the document is not inside a frame
851                 if ( document.documentElement.doScroll && !isFrame ) {
852                     tryScroll();
853                 }
854 
855                 // IE, the document is inside a frame
856                 document.attachEvent("onreadystatechange", function(){
857                     if ( document.readyState === "complete" ) {
858                         ready();
859                     }
860                 });
861             }
862 
863             // Old browsers
864             if (window.addEventListener) {
865                 window.addEventListener('load', ready, false);
866             } else if (window.attachEvent) {
867                 window.attachEvent('onload', ready);
868             } else {
869                 fn = window.onload; // very old browser, copy old onload
870                 window.onload = function() { // replace by new onload and call the old one
871                     if (fn) {fn();}
872                     ready();
873                 };
874             }
875         }());
876 
877     $.each($, function (fn, name) {
878         canvas[name] = fn;
879     });
880 
881     // Add those external modules back in
882     $.each(extmodules, function (fn, name) {
883         canvas[name] = fn;
884     });
885 
886 
887     (function () {
888         var method;
889         var noop = function () { };
890         var methods = [
891             'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error',
892             'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log',
893             'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd',
894             'timeStamp', 'trace', 'warn'
895         ];
896         var length = methods.length;
897         var console = (typeof window !== 'undefined' && window.console) ? window.console : {};
898 
899         while (length--) {
900             method = methods[length];
901 
902             // Only stub undefined methods.
903             if (!console[method]) {
904                 console[method] = noop;
905             }
906         }
907 
908     }());
909 
910 
911     if (!global.Sfdc) {
912         global.Sfdc = {};
913     }
914 
915     global.Sfdc.canvas = canvas;
916 
917 
918 }(this));
919