API Docs for:
Show:

File: loadingScreen.js

/**
@module LoadingScreen
*/

////////////////////////////////////////
//
//      NoScrollbar
//
////////////////////////////////////////
/**
A class for hiding the scrollbars in a browser window by writing style sheet rules.

*Typical Usage*

    // initialize class to operate on the active document
    var no_scrollbar = new NoScrollbar();

    // hide both horizontal and vertical scrollbars
    no_scrollbar.disableScrollbars();

    // make the scrollbars reappear
    no_scrollbar.reenableScrollbars();

@class NoScrollbar
@constructor

@param {Object} [document_obj=document] The document that the stylesheet will be attached
to. If not specified, the active document is the default.     
*/
function NoScrollbar(document_obj) {
    if( document_obj == null ) {
        document_obj = document;
    }

    this.style_id = "nosb_style";
    this.document_obj = document_obj;
}

/**
Set page style attributes to hide the browser scrollbars.

@method disableScrollbars

@param {String} [dimension] If a string of "x" is passed then only the horizontal
                            scrollbars will be hidden, or if "y" is passed only the
                            vertical scrollbars are hidden.  The default is for both 
                            horizontal and vertical scrollbars to be hidden.
*/
NoScrollbar.prototype.disableScrollbars = function(dimension) {
    var nosb_style = this.document_obj.getElementById(this.style_id);
    if( nosb_style == null ) {
        nosb_style = this.addStyleElement(this.document_obj.head);
    }

    var directive = (dimension == null ? "overflow" : "overflow-" + dimension);
    nosb_style.sheet.insertRule( "html { " + directive + ": hidden; }", 0);
}

/**
Remove the class-controlled stylesheet from the document, so the scrollbars will reappear.

@method reenableScrollbars
*/
NoScrollbar.prototype.reenableScrollbars = function() {
    var nosb_style = this.document_obj.getElementById(this.style_id);
    if( nosb_style != null ) {
        nosb_style.parentNode.removeChild(nosb_style);
    }
}

/**
Checks for existence of a style element in the associated document with an id equal
to this.style_id.  If it does not exist, it is created.  In all cases the style
element is returned.

@method addStyleElement
@private
*/
NoScrollbar.prototype.addStyleElement = function() {
    var nosb_style = this.document_obj.getElementById(this.style_id);
    if( nosb_style == null ) {
        nosb_style = this.document_obj.createElement("style");

        nosb_style.setAttribute("id", this.style_id);
        this.document_obj.head.appendChild(nosb_style);
    }
    return nosb_style;
}

////////////////////////////////////////
//
//      StopWatch
//
////////////////////////////////////////

/**
Implements a simple timer with methods to start the timer, and check to see how many seconds
have passed since it was started.

*Typical Usage*

    var stop_watch = new StopWatch();
    stop_watch.start();

    // ... time passes ...

    if( stop_watch.secondsElapsed() > 10 ) {
        // ... perform timeout processing ...
    } 

@class StopWatch
@constructor
*/
function StopWatch() {
}

/**
Start the StopWatch timer.

@method start
*/
StopWatch.prototype.start = function () {
    this.start_time = Math.floor(this.getClockTimeInMs()/1000);
}

/**
Returns the number of seconds that have elapsed since the timer was started.

If start() was not previously called on the StopWatch object, an exception is thrown.

@method secondsElapsed
*/
StopWatch.prototype.secondsElapsed = function () {
    if( typeof this.start_time === 'undefined' ) {
        throw "Object's start() method has not been called.";
    }
    var now = Math.floor(this.getClockTimeInMs()/1000);
    return now - this.start_time;
}

/**
A wrapper provided for testing purposes.  The client will not usually call this method directly.

@method getClockTimeInMs
@private
*/
StopWatch.prototype.getClockTimeInMs = function () {
    return new Date().getTime();
}

////////////////////////////////////////
//
//      ImageLoader
//
////////////////////////////////////////

/**
Initiates the asynchronous loading of image files by the browser.  Useful to start preloading images rather
than waiting to do it as the browser encounters them during the rendering of the page. 

The class maintains a list of images to load.  As each image is loaded, it is removed from the list.
If an image fails to load, it remains in the list until the client explicitly re-attempts to load it.

*Typical Usage*

    var image_loader = new ImageLoader();
    image_loader.queueImage("http://myserver.com/media/header.png");
    image_loader.queueImage("http://myserver.com/media/location_map.png");

    image_loader.loadAll(
        function(success) {
            if( success ) {
                console.log("Image was successfully loaded." );
            } else {
                console.log("Loading failed for image." );
            }
            console.log("Remaining images: " + image_loader.getImageList() )
        }
    );

@class ImageLoader
@constructor
*/
function ImageLoader() {
    this.image_queue = [];
    this.image_event_handler = function(success) {};
}

/**
@method getImageList
@return A list of images that are currently queued for loading.
*/
ImageLoader.prototype.getImageList = function () {
    return this.image_queue;
}

/**
Adds an image URL to the queue, to be loaded later by a call to loadAll().

@method queueImage

@param {String} src The URL of the image resource to queue.
*/
ImageLoader.prototype.queueImage = function(src) {
    this.image_queue.push(src);
}

/**
Invokes loadImage() once for each image URL in the current queue.

A callback function may be supplied which will be invoked whenever an image loads or
fails to load.  The function should receive a single boolean argument which is a flag
indicating whether loading was successful or not.

@method loadAll

@param {function} [event_handler] The optional callback function.
*/
ImageLoader.prototype.loadAll = function(event_handler) {
    var obj = this;

    // copy the queue here since loadImage() may modify this.image_queue 
    // which interferes with any traversal using forEach()
    process_queue = this.image_queue.slice(0);

    process_queue.forEach( function(element) {
        obj.loadImage(element, event_handler);
    });
}

/**
Loads a single image resource from the given URL.

A callback function may be supplied which will be invoked when the image loads or
fails to load.  The function should receive a single boolean argument which is a flag
indicating whether loading was successful or not.

If loading is successful and the given URL is found in the queue, it is removed.
Otherwise the URL is left in the queue and can be re-tried later by another call to loadImage()
or loadAll().

@method loadImage

@param {String} [src] URL of the image resource to load.
@param {function} [event_handler] The optional callback function.
@return The newly created <img> element.
*/
ImageLoader.prototype.loadImage = function(src, event_handler) {
    var img = null;
    if( event_handler == null ) {
        event_handler = this.image_event_handler;
    }
    try {
        img = this.initNewImage(event_handler); 
        img.src = src;
    } catch(err) {
        throw "loadImage() exception: " + err;
    }
    return img;
}

/**
Utility method invokes createImage() to create an <img> element, then attaches the supplied callback
to the element's onload property, wrapped with some logic to remove the URL from the queue following a
successful load.

If onload is invoked, the callback is called with an argument of true.
If onerror is invoked, the callback is called with an argument of false.

@method initNewImage
@private

@param {function} [event_handler] The callback function.
@return The newly created <img> element.
*/
ImageLoader.prototype.initNewImage = function(event_handler) {
    var new_img = this.createImage();
    new_img.loader_obj = this;
    new_img.onload = function() {
        var img_ndx = this.loader_obj.image_queue.indexOf(this.src);
        if( img_ndx > -1 ) {
            this.loader_obj.image_queue.splice(img_ndx, 1);
        }
        event_handler(true);
    };
    new_img.onerror = function() {
        event_handler(false);
    };
    return new_img;
}

/**
A wrapper provided for testing purposes.

@method createImage
@private

@return A new Image object.
*/
ImageLoader.prototype.createImage = function() {
    return new Image();
}

////////////////////////////////////////
//
//      LoadingScreen
//
////////////////////////////////////////

/**
Implements a loading screen for a webpage - that is, an opaque screen that obscures the page
contents until it has finished loading, while displaying a short text message such as "Loading...".

This is implemented by adding a new &lt;div&gt; element to the associated document that is styled to
fill the entire browser window.  Any scroll bars are also disabled until the loading screen is
closed.

*Timeout Features*

The screen features two timeout values:

1. A close-screen button timeout which is initially hidden, but appears a number of seconds
   after the screen is displayed, given by the property this.close_button_timeout_in_s.
2. A clear screen timeout after which the screen will be automatically closed.  The timeout
   is specified as a number of seconds given by this.clear_screen_timeout_in_s.

The screen may be dismissed before the timeouts expire if the loadingIsFinished() method returns
true.  The default implementation of this method must be replaced to use this feature.

*Typical Usage*

    var loading_screen = new LoadingScreen();   // applied to active document

    loading_screen.default_text = "We will be with you shortly...";
    loading_screen.default_bg_colour = "HotPink";
    loading_screen.default_text_colour = "white";

    loading_screen.loadingIsFinished = function() {
        return areAllImagesLoaded();
    }

    loading_screen.show();     // displays the loading screen
    loading_screen.startPolling();

    // ...

    if( escape_key_was_pressed ) {
        // remove the loading screen in special circumstances
        // normally this method does not need to be explicitly called
        loading_screen.clear();
    }

The following properties can be set to customize the behaviour of the loading screen:
 * this.default_bg_colour - the colour of the screen
 * this.default_text - the message that is displayed on the loading screen
 * this.default_text_colour - the colour of the message and close button
 * this.default_button_text - the text string that is displayed inside the close button
 * this.close_button_timeout_in_s - explained above
 * this.clear_screen_timeout_in_s - explained above
 * this.polling_frequency_in_ms - how often the timeouts are checked

@class LoadingScreen
@constructor

@param {function} [id] A text id that is given to the screen's base &lt;div&gt; element. By default,
                       "load_screen".
@param {function} [doc] The document to which the loading screen is applied.  By default, the 
                        active document.
*/
function LoadingScreen(id, doc) {
    this.doc = doc || document;
    this.no_scrollbar = new NoScrollbar(this.doc);
    this.stop_watch = new StopWatch();
    this.default_bg_colour = "rgb(126,123,190)";
    this.default_text = "Loading...";
    this.default_text_colour = "rgb(210,237,114)";
    this.default_button_text = "X";
    this.id = id || "load_screen";
    this.clear_screen_timeout_in_s = 10;
    this.close_button_timeout_in_s = 3;
    this.polling_frequency_in_ms = 500;
    this.loadingIsFinished = function ()  { return false; }
}

/**
Main method to construct and display the loading screen, and disable any scrollbars.

@method show

@param {String} [id_of_element_to_insert_before] See addElementToDocument().
@param {function} [retrieve_element] A function which will return an element that will be used as the
                                     loading screen.  If omitted, the loading screen element
                                     is the return value of this.combineScreenElements().
*/
LoadingScreen.prototype.show = function(id_of_element_to_insert_before, retrieve_element) {
    var loadingscreen_base_elt = null;
    if(retrieve_element ) {
        loadingscreen_base_elt = retrieve_element();
    } else { 
        loadingscreen_base_elt = this.combineScreenElements();
    }
    this.addElementToDocument(loadingscreen_base_elt, id_of_element_to_insert_before);

    this.no_scrollbar.disableScrollbars();
}

/**
Combines all child elements of the loading screen under a single root element, which gets returned.

@method combineScreenElements
@private

@param {Element} [text_elt] A text element to use for the display text. Default is the result of calling
                            this.createText()
@param {Element} [button_elt] A button element to use for the close button. Default is the result of calling
                              this.createCloseButton()
@return Root element of loading screen.
*/
LoadingScreen.prototype.combineScreenElements = function(text_elt, button_elt) {
    text_elt = text_elt || this.createText();
    button_elt = button_elt || this.createCloseButton();

    var background_element = this.createBackground();
    background_element.appendChild(text_elt);
    background_element.appendChild(button_elt);
    return background_element;
}

/**
Creates the base &lt;div&gt; element which serves as the root of the loading screen's HTML sub-tree.
The element is styled with an inline "style" attribute. The created element is returned.

@method createBackground
@private

@param {String} [bg_colour] CSS colour string. Defaults to value of this.default_bg_colour
@return The newly-created &lt;div&gt; element.
*/
LoadingScreen.prototype.createBackground = function (bg_colour) {
    if( bg_colour == null ) {
        bg_colour = this.default_bg_colour;
    }
    background_div = this.doc.createElement("div");
    background_div.setAttribute('id', this.id);
    var loading_screen_div_css =
      "width: 100%;" +
      "height: 100%;" +
      "position: fixed;" +
      "left: 0;" +
      "top: 0;" +
      "z-index: 10;" +
      "background-color: " + bg_colour + ";";
    background_div.setAttribute('style', loading_screen_div_css );
    return background_div;
}

/**
Create a new &lt;div&gt; element containing the display text for the loading Screen.
The element is styled with an inline style attribute and returned.

@method createText
@private

@param {String} [display_text] Display text defaults to this.default_text.
@param {List} [position] Defaults to ["50%", "50%"]. See customElementStyleString()
@param {String} [text_colour] Defaults to this.default_text_colour.
                              See colour argument of customElementStyleString()
@param {String} [style_string] See extra_style argument of customElementStyleString()
@return The newly-created &lt;div&gt; element.
*/
LoadingScreen.prototype.createText = function (display_text, position, text_colour, style_string) {
    display_text = display_text || this.default_text;
    position = position || ["50%", "50%"];
    text_colour = text_colour || this.default_text_colour;
    style_string = style_string || "";

    var default_styles =
      "font: 15px Verdana, sans-serif;" +
      "font-style: italic;" +
      "width: 0;" +
      "height: 0;" +
      "position: absolute;" +
      "text-align: center;";

    var full_style_string = default_styles +
                            this.customElementStyleString(position, text_colour, style_string);

    var loading_screen_text_div = this.doc.createElement("div");
    var loading_screen_text = this.doc.createTextNode(display_text);
    loading_screen_text_div.setAttribute('style', full_style_string );
    loading_screen_text_div.appendChild(loading_screen_text);

    return loading_screen_text_div;
}

/**
Create a new &lt;button&gt; element to serve as the loading Screen close button.
The element is styled with an inline style attribute and returned.

@method createCloseButton
@private

@param {List} [pos] Defaults to ["50%", "50%"].
                    See position argument of customElementStyleString()
@param {String} [colour] Defaults to this.default_text_colour.
                         See customElementStyleString()
@param {String} [extra_style] See extra_style argument of customElementStyleString()
@param {String} [text] Display text defaults to this.default_button_text.
*/
LoadingScreen.prototype.createCloseButton = function (pos, colour, extra_style, text) {
    pos = pos || ["90%", "50px"];
    colour = colour || this.default_text_colour;
    extra_style = extra_style || ""; 
    text = text || this.default_button_text;

    var loading_screen_close_btn = this.doc.createElement("button");
    loading_screen_close_btn.setAttribute('id', this.id + "_close");
    loading_screen_close_btn.appendChild(this.doc.createTextNode(text));
    loading_screen_close_btn.setAttribute( 'style',
      "visibility: hidden;" +
      "disabled: true;" +
      "padding: 5px;" +
      "position: absolute;" +
      "font-weight: bold;" +
      "border-radius: 18px;" +
      "height:36px;" +
      "width: 36px;" + 
      "background-color: rgb(126,123,190);" +
      "border: 2px solid rgb(210,237,114);" +
      this.customElementStyleString(pos, colour, extra_style)
    );
    loading_screen_close_btn.setAttribute('onClick', 'this.clear()');
    return loading_screen_close_btn;
}

/**
Utility method to generate and return a valid CSS style string from the given arguments.

@method customElementStyleString
@private

@param {List} [position] Position coordinates in the format [x,y], where x and y are valid
                         CSS coordinate strings (eg. "10%" or "90px"). The position rule is omitted
                         if not specified.
@param {String} [colour] CSS colour string. The colour rule is omitted if not specified.
@param {String} [extra_style] CSS style string is appended to the returned string if specified.
@return Full CSS style string generated from the arguments.
*/
LoadingScreen.prototype.customElementStyleString = function(position, colour, extra_style) {
    var style_string = "";
    if( position != null ) {
        style_string += "top: " + position[1] + ";";
        style_string += "left: " + position[0] + ";";
    }
    if( colour != null ) {
        style_string += "color: " + colour + ";";
    }
    if( extra_style != null ) {
        style_string += extra_style;
    }
    return style_string;
}

/**
Inserts a provided element into the associated document.

@method addElementToDocument
@private

@param {Element} elt_to_add The Element object to add to the associated document.
@param {String} [id_of_elt_to_insert_before] String id of the existing element that will immediately follow
                                             the inserted element in the document order.  If not specified
                                             the element is inserted at the end of the document.
*/
LoadingScreen.prototype.addElementToDocument = function(elt_to_add, id_of_elt_to_insert_before) {
    var elt_to_insert_before = this.doc.getElementById(id_of_elt_to_insert_before);
    if( elt_to_insert_before ) {
        elt_to_insert_before.parentNode.insertBefore(elt_to_add, elt_to_insert_before);
    }
}

/**
Begins polling to check for expiration of the loading screen timeout values.

@method startPolling
*/
LoadingScreen.prototype.startPolling = function () {
    this.stop_watch.start();
    this.setupPollEvent();
}

/**
Schedules this.pollEventHandler() to get called after a timeout given by this.polling_frequency_in_ms.

@method setupPollEvent
@private
*/
LoadingScreen.prototype.setupPollEvent = function () {
    var calling_object = this;
    setTimeout( function() {
                    calling_object.pollEventHandler();
                },
                this.polling_frequency_in_ms);
}

/**
Callback method that is invoked whever a poll event is triggered.

If the close button timeout has elapsed, this.showCloseButton() is called.
If the clear screen timeout has elapsed, this.clear() is called.
If this.loadingIsFinished() returns true, this.clear() is called.

@method pollEventHandler
@private
*/
LoadingScreen.prototype.pollEventHandler = function() {
    var time_elapsed = this.stop_watch.secondsElapsed();

    if( this.loadingIsFinished() ||
        time_elapsed >= this.clear_screen_timeout_in_s )  {
        this.clear();
    } else {
        if( time_elapsed >= this.close_button_timeout_in_s ) {
            this.showCloseButton();
        }
        this.setupPollEvent();
    }
}

/**
Styles the pre-existing close button element to be visible.

@method showCloseButton
@private
*/
LoadingScreen.prototype.showCloseButton = function() {
    var close_btn = this.getCloseButton();
    close_btn.style.disabled = "false";
    close_btn.style.visibility = "visible";
}

/**
Utility method. 

@method getCloseButton
@private

@return The element implementing the loading screen close button.
*/
LoadingScreen.prototype.getCloseButton = function() {
    return this.doc.getElementById(this.id + "_close");
}

/**
Removes the loading screen from the associated document, and reinstates any scroll bars.

@method clear
*/
LoadingScreen.prototype.clear = function() {
    loadingscreen_base_element = this.doc.getElementById(this.id);
    loadingscreen_base_element.parentNode.removeChild(loadingscreen_base_element);

    this.no_scrollbar.reenableScrollbars();
}