1 //
  2 // Copyright 2012, Tridium, Inc. All Rights Reserved.
  3 //
  4 
  5 /**
  6  * Hooks for running BajaScript inside of a browser.
  7  *
  8  * @author Logan Byam
  9  * @version 1.0.0.0
 10  */
 11 
 12 /*jslint browser: true, white: true, plusplus: true, nomen: true */
 13 /*global baja, ActiveXObject*/ 
 14 (function browser() {
 15   "use strict";
 16   
 17   ////////////////////////////////////////////////////////////////
 18   // DOM Ready
 19   //////////////////////////////////////////////////////////////// 
 20   
 21   var domReadyTimer,
 22       domReadyCallbacks,
 23       domIsReady;
 24       
 25   if (!baja.comm) {
 26     throw new Error("comm.js required");
 27   }
 28   
 29   /**
 30    * Waits for the DOM to be initialized and then executes a callback.
 31    * 
 32    * @name baja.browser.comm.domReady
 33    * @function
 34    * @private
 35    * @inner
 36    *
 37    * @param {Function} func a function to be executed once the DOM is ready.
 38    */
 39   function domReady(func) {
 40     // isDOMReady function
 41     var isDOMReady = function () {
 42       if (domIsReady) {
 43         return false;
 44       }
 45       
 46       if (document && 
 47           document.getElementsByTagName && 
 48           document.getElementById && 
 49           document.body) {
 50         clearInterval(domReadyTimer);
 51         domReadyTimer = null;
 52         
 53         var i;
 54         for (i = 0; i < domReadyCallbacks.length; ++i) {
 55           domReadyCallbacks[i]();
 56         }
 57         
 58         domReadyCallbacks = null;
 59         domIsReady = true;
 60       }
 61     };
 62   
 63     // If already ready then invoke and return
 64     if (domIsReady) {
 65       func();
 66       return;
 67     }
 68 
 69     if (typeof domReadyTimer === "number") {
 70       // Register this callback if we need too
 71       domReadyCallbacks.push(func);
 72     }    
 73     else {
 74       // Register for dom ready events
 75       if (document && document.addEventListener) {
 76         document.addEventListener("DOMContentLoaded", isDOMReady, false);
 77       }
 78       else {
 79         window.onload = isDOMReady;
 80       }
 81       
 82       domReadyCallbacks = [func];
 83       
 84       // Use a timer as a fallback just in case!
 85       domReadyTimer = setInterval(isDOMReady, 13);
 86     }
 87   }
 88   
 89   /**
 90    * This namespace is for documentation purposes only and will not actually
 91    * be available to Bajascript apps. It details enhancements/decorations
 92    * applied to functions in <code>baja.comm</code> when Bajascript is deployed
 93    * to a web browser environment.
 94    * 
 95    * @namespace
 96    * @name baja.browser.comm
 97    */
 98   (function comm() {
 99     // Ensure the started callbacks only happen once the DOM is fully loaded.
100     var doStart = baja.comm.start;
101     
102     /**
103      * In a browser, <code>baja.comm.start</code> will wait for the DOM to be
104      * ready (as well as for communications to the station be established)
105      * before executing its callback.
106      * 
107      * @name baja.browser.comm.start
108      * @function
109      * @private
110      * @inner
111      */
112     baja.comm.start = function start(obj) {
113       var started = obj.started || baja.ok;
114       obj.started = function () {
115         domReady(started);
116       };
117       doStart(obj);
118     };
119     
120     /**
121      * In a browser, <code>baja.comm.reconnect</code> will simply reload the
122      * browser page.
123      *
124      * @name baja.browser.comm.reconnect
125      * @function
126      * @private
127      * @inner
128      */
129     baja.comm.reconnect = function reconnect() {
130       // Delay the reconnection slightly - just incase there's a problem and this triggers another refresh!
131       baja.clock.schedule(function () {
132         // If we can get to the login page then attempt a reconnection...
133         // Reconnect by refreshing the page...
134         if (location && location.reload) {
135           location.reload(/*forceget*/false);
136         }
137       }, 2500);
138     };
139     
140     ////////////////////////////////////////////////////////////////
141     // HTTP Comms
142     //////////////////////////////////////////////////////////////// 
143     
144     /**
145      * An Http Error.
146      * <p>
147      * A HTTP Error happens as a result of problem with HTTP communcation.
148      * 
149      * @class
150      * @private
151      * @param {XmlHttpRequest} x the object used for comms.
152      */
153     baja.comm.HttpError = function (x) {
154       var t = x.responseText || "Session disconnected",
155           status = x.status;
156       
157       this.name = "HttpError";
158       this.message = t + " err: " + x.status;
159       this.status = status;
160       
161       // Indicate to delay any reconnection if a 404 or no status is returned.
162       this.delayReconnect = (!status || status === 404);
163       
164     }.$extend(baja.comm.ServerError);
165     
166     /**
167      * Make an HTTP connection using XMLHttpRequest or its MS equivalents.
168      *
169      * @name baja.browser.comm.sendHttp
170      * @function
171      * @private
172      * @inner
173      *
174      * @param {String} method  the method to use (i.e. 'post' or 'get').
175      * @param {String} uri  the URI used in the connection.
176      * @param {Boolean} async  boolean flag indicating if this is an asynchronous or synchronous call.
177      * @param {Object} callback  this object must have 'ok' and 'fail' functions.
178      * @param {String} [body]  the body of the HTTP POST.
179      * @param {String} [contentType]  the Content Type for an HTTP POST.
180      */
181     function sendHttp(method, uri, async, callback, body, contentType) {  
182       // TODO: Do we need to handle timeouts? Or just rely on the browser failing?
183       var x = null,
184           handler,
185           ie = false; // Are we Internet Explorer?
186       
187       // HTTP Callback Handler
188       handler = function () {    
189         var st;  // Status
190 
191         if (x.readyState === 4) {
192           try {
193             st = parseInt(x.status, 10);
194             
195             if (st !== 200 && !baja.isStopping()) {
196               callback.fail(new baja.comm.HttpError(x));
197             }
198             else {
199               // HTTP 200 ok
200               callback.ok(x.responseText);
201             }
202           }
203           catch (error) {
204             callback.fail(error);
205           }
206           finally {
207             // Request is now dealt with so clean up here
208             x.onreadystatechange = baja.ok;
209             x = null;
210           }
211         }
212       };
213           
214       // Make AJAX network call
215       try {     
216                  
217         // Create XMLHttpRequest
218         try { 
219           x = new XMLHttpRequest(); 
220         }
221         catch (e) {
222           try {
223             x = new ActiveXObject("Msxml2.XMLHTTP");
224             ie = true;
225           }
226           catch (e2) {
227             try {
228               x = new ActiveXObject("Microsoft.XMLHTTP");
229               ie = true;
230             }
231             catch (e3) {
232               // No XMLHttpAvailable?
233               throw new Error("Failed to create XMLHttpRequest: " + e3);
234             }
235           }
236         }
237               
238         x.open(method, uri, async);     
239 
240         if (contentType) {      
241           x.setRequestHeader("Content-Type", contentType);
242         }
243         
244         // Set HTTP Headers  
245         if (ie === undefined) {
246           x.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
247         }
248                     
249         if (async) {
250           // Set callback handler
251           x.onreadystatechange = handler;
252         }
253         
254         // Send body
255         x.send(body);  
256       }
257       catch (error) {
258         callback.fail(error);
259         return;
260       }
261       
262       if (!async && x !== null) {
263         // If synchronous then just invoke the handler
264         // Please note: this is outside of the above exception handler because we
265         // don't want to catch any exceptions generated by the handler
266         handler.call(x);
267       }
268     }
269     
270     /**
271      * In a browser, <code>baja.comm.BoxFrame#send</code> will delegate to
272      * <code>baja.browser.comm.sendHttp</code>.
273      * 
274      * @private
275      * @inner
276      * @ignore
277      */
278     baja.comm.BoxFrame.prototype.send = function (async, callback) {
279       // Make the HTTP POST to the BOX Servlet
280       sendHttp("post", 
281                "/box/", 
282                async, 
283                callback, 
284                this.toString(), 
285                "application/json");
286     };
287   }());
288     
289   /**
290    * This namespace is for documentation purposes only and will not actually
291    * be available to Bajascript apps. It details enhancements/decorations
292    * applied to functions in <code>baja</code> when Bajascript is deployed
293    * to a web browser environment.
294    * 
295    * @namespace
296    * @name baja.browser
297    */
298   (function sysUtil() {
299     var _outln = baja.outln,
300         _clearOut = baja.clearOut,
301         _error = baja.error;
302         
303     /**
304      * In a browser, <code>baja.outln</code> will (in addition to calling
305      * <code>bajaJsPrint()</code>) look for a <code><pre></code> element 
306      * with ID <code>bajaScriptOut</code> and append the message to that 
307      * element as well.
308      * 
309      * @name baja.browser.outln
310      * @function
311      * @private
312      * @inner
313      */
314     baja.outln = function (msg) {
315       // If BajaScript has stopped then don't output anything else...
316       if (baja.isStopping()) {
317         return this;
318       }
319     
320       var bajaScriptOut, 
321           escapedMsg;
322       
323       msg += "\n";
324       
325       if (document && document.getElementById) {
326         bajaScriptOut = document.getElementById("bajaScriptOut");
327         if (bajaScriptOut !== null) {  
328           escapedMsg = msg.toString();
329           escapedMsg = msg.replace(/</g, "(").replace(/>/g, ")");
330                     
331           // Should work around IE for ignoring newlines in 'pre' and 'textarea' elements
332           escapedMsg = escapedMsg.replace(/\n/g, "\r\n");
333           bajaScriptOut.appendChild(document.createTextNode(escapedMsg));
334         }
335       }
336   
337       // If available, attempt to write out to Chrome's JavaScript window
338       if (window.console && window.console.log) {
339         window.console.log(msg);
340       }
341       
342       return _outln.call(this, msg);
343     };
344     
345     /**
346      * In a browser, <code>baja.clearOut</code> will look for a
347      * <code><pre></code> element with ID <code>bajaScriptOut</code> and
348      * wipe all text from it.
349      * 
350      * @name baja.browser.clearOut
351      * @function
352      * @private
353      * @inner
354      */
355     baja.clearOut = function () {
356       if (document && document.getElementById) {
357         var elem = document.getElementById("bajaScriptOut");
358         if (elem !== null) {
359           elem.innerHTML = "";
360         }
361       }
362       
363       return _clearOut.apply(this, arguments);
364     };
365     
366     /**
367      * In a browser, <code>baja.error</code> will (in addition to calling
368      * <code>baja.outln</code>) look for <code>window.console.error</code> and,
369      * if it exists, pass the error message to it.
370      * 
371      * @name baja.browser.error
372      * @function
373      * @private
374      * @inner
375      */
376     baja.error = function (msg) {
377       if (baja.isStopping()) {
378         return this;
379       }
380     
381       // If available, attempt to write out to Chrome's JavaScript Window
382       if (window.console && window.console.error) {
383         window.console.error(msg);
384       }
385       
386       return _error.apply(this, arguments);
387     };
388   }());
389   
390   
391   /**
392    * This namespace is for documentation purposes only and will not actually
393    * be available to Bajascript Apps. It details enhancements/decorations
394    * applied to functions in <code>baja.registry</code> when Bajascript is 
395    * deployed to a web browser environment.
396    * 
397    * @namespace
398    * @name baja.browser.registry
399    */
400   (function sysRegistry() { 
401     var webStorageKeyName = "bajaScriptReg";
402   
403     /**
404      * In a browser, this overrides <code>baja.registry.clearStorage</code>
405      * and will clear the web storage content (if the browser supports web storage).
406      * 
407      * @name baja.browser.registry.clearStorage
408      * @function
409      * @private
410      * @inner
411      */
412     baja.registry.clearStorage = function () { 
413       if (window.localStorage) {
414         // Always access local storage in try/catch...
415         try {
416           window.localStorage.removeItem(webStorageKeyName);
417         }
418         catch (ignore) {}
419       }
420     };
421     
422     /**
423      * In a browser, this overrides <code>baja.registry.saveToStorage</code>
424      * and will write its contents to web storage (if the browser supports web storage).
425      * 
426      * @name baja.browser.registry.save
427      * @function
428      * @private
429      * @inner
430      * 
431      * @param {Object} regStorage the BajaScript registry information to store
432      */
433     baja.registry.saveToStorage = function (regStorage) {
434       // If available, save any cached registry information to web storage   
435       if (regStorage && window.localStorage) {
436         // Always access local storage in try/catch...
437         try {
438           window.localStorage.setItem(webStorageKeyName, JSON.stringify(regStorage));
439         }
440         catch (ignore) {
441           baja.registry.clearStorage();
442         }
443       }
444     };
445     
446     /**
447      * In a browser, this overrides <code>baja.registry.loadFromStorage</code> 
448      * and will attempt to read any registry information previously stored in web storage
449      * (if the browser supports web storage).
450      * 
451      * @name baja.browser.registry.loadFromStorage
452      * @function
453      * @private
454      * @inner
455      */
456     baja.registry.loadFromStorage = function () {   
457       var regStorage = null;
458       if (window.localStorage) {
459         // Always access local storage in try/catch...
460         try {
461           regStorage = window.localStorage.getItem(webStorageKeyName);
462           
463           // If we have data then parse it
464           if (regStorage) {
465             regStorage = JSON.parse(regStorage);
466           }
467         }
468         catch (ignore) {
469           baja.registry.clearStorage();
470         }  
471       }
472       return regStorage;      
473     };
474   }());
475 }());