Source: main-1.0.1.js

/*!
 * JavaScript Movie Picker v1.0.1
 * http://wjbryant.com/projects/movie-picker/
 *
 * Copyright (c) 2013 Bill Bryant
 * Licensed under the MIT license
 * http://opensource.org/licenses/mit
 */

/**
 * @fileOverview The main program for the Movie Picker project
 * @author       Bill Bryant
 * @version      1.0.1
 */

/*jshint devel: true */
/*jslint browser: true, devel: true */
/*global MOVIEPICKERDATA, UTILS */

/**
 * The MOVIEPICKER namespace.
 * This isn't really a "namespace", but the closure in which the main Movie
 * Picker code is executed.
 *
 * @namespace MOVIEPICKER
 * @private
 */
(function () {
    'use strict';

    // detect browsers using the "browser inference" method
    // since the conditional compilation comment is in a function,
    // it will affect munging and the compression level in YUI Compressor
    // this method is better used outside of the closure for that reason
    //var isIE = /*@cc_on!@*/false,
    //    isOpera = Object.prototype.toString.call(window.opera) === '[object Opera]',

    // Define boolean values for various browsers in the form of isBrowserX.
    // For this project, only IE and Opera need to be detected for special
    // event handling.
    var ua = navigator.userAgent.toLowerCase(),
        isOpera = ua.indexOf('opera') !== -1,
        isIE = !isOpera && ua.indexOf('msie') !== -1,
        utils = UTILS, // "import" the UTILS library
        document = window.document,
        mpdata = MOVIEPICKERDATA,
        currChoice = mpdata,
        choicePath = [], // array of user choices
        selectsEnabled = true,
        done = false, // the user is "done" and a result was displayed
        header = document.createElement('h1'),
        form = document.createElement('form'),
        body = document.getElementsByTagName('body')[0],
        selects,
        displayDiv,
        btnStartOver,
        btnSend,
        resultText,
        bigDiv,
        moviePic,
        copyright,
        link,
        instr,
        nameDiv,
        nameLabel,
        nameText,
        emailDiv,
        emailLabel,
        emailText,
        commentsDiv,
        commentsLabel,
        commentsBox,
        sendDiv,
        pics = {},
        selectOnChange,
        selectOnFocus,
        selectOnKeyDown,
        selectOnClick,
        selectOnKeyUp,
        selectOnBlur,
        createSelect,
        checkLength,
        rewind = false, // whether a previous select was changed and the "choice path" must be reset
        loadingImg = document.createElement('img');

    /**
     * Toggles the disabled property of all select elements on the page.
     *
     * @param {boolean}  enable whether to enable all the selects
     *
     * @memberOf MOVIEPICKER
     * @private
     */
    function enableSelects(enable) {
        var i,
            len;
        // check if they are already in the state we want them to be
        if (enable !== selectsEnabled) {
            for (i = 0, len = selects.length; i < len; i += 1) {
                selects[i].disabled = !enable;
            }
            selectsEnabled = !selectsEnabled;
            body.focus(); // for IE6 to set focus to other elements
        }
    }

    /**
     * Determines the next course of action when the user makes a choice.
     * This function expects to be called in the context of a select element.
     * For most browsers, it is registered as a change event handler function
     * for each select element. For IE and Opera it is invoked through other
     * event handler functions of the select elements.
     *
     * @memberOf MOVIEPICKER
     * @private
     */
    selectOnChange = function () {
        var theSelect = this,
            i,
            cpLen,
            lastSelect,
            result,
            title;

        if (done) {
            // a choice was changed, so we're not done anymore
            done = false;
            enableSelects(false);

            // move the answer display off the screen
            utils.move(bigDiv, {
                to: [420, -500],
                callback: function () {
                    this.parentNode.removeChild(this);
                    selectOnChange.call(theSelect);
                }
            });
        } else {
            lastSelect = selects[selects.length - 1];

            if (theSelect !== lastSelect) {
                // if this is not the last choice,
                // remove the choices below this one before proceeding
                rewind = true;
                enableSelects(false);

                utils.move(lastSelect.parentNode, {
                    to: [-400, 60],
                    callback: function () {
                        this.parentNode.removeChild(this);
                        choicePath.pop();
                        selectOnChange.call(theSelect);
                    }
                });
            } else {
                if (rewind) {
                    rewind = false;

                    // reset currChoice
                    currChoice = mpdata;
                    for (i = 0, cpLen = choicePath.length; i < cpLen; i += 1) {
                        currChoice = currChoice[choicePath[i]];
                    }
                }

                // check if the user selected a choice
                // the first choice, 'Select one' has a value of ''
                if (theSelect.value) {
                    // check to see if there are more choices or an answer
                    if (currChoice[theSelect.value].choices) {
                        // more choices, so create new select
                        currChoice = currChoice[theSelect.value];
                        choicePath[choicePath.length] = theSelect.value;
                        createSelect();
                    } else {
                        // no more choices, we're done
                        done = true;
                        enableSelects(false);

                        // set up the result display
                        result = currChoice[theSelect.value];
                        title = result.title;
                        link.firstChild.nodeValue = title;
                        link.setAttribute('href', result.link);
                        link.setAttribute('title', 'IMDb page for ' + title);
                        copyright.firstChild.nodeValue = 'Copyright \u00A9 ' +
                            result.year + ' ' + result.studio;
                        commentsBox.value = 'Your recommended movie is:\n' + title + '.';
                        body.appendChild(bigDiv);

                        // displayDiv.getElementsByTagName('img')[0] must be referenced after the
                        // div is appended to the body or IE throws an exception
                        displayDiv.replaceChild(pics[title], displayDiv.getElementsByTagName('img')[0]);

                        // check for cookies
                        if (utils.getCookie('userName')) {
                            nameText.value = utils.getCookie('userName');
                            emailText.value = utils.getCookie('userEmail');
                        }

                        // animate into place
                        utils.move(bigDiv, {
                            to: [420, 60],
                            callback: function () {
                                enableSelects(true);
                                nameText.focus();
                                nameText.select();
                            }
                        });
                    }
                } else {
                    // 'Select one' was selected
                    enableSelects(true);
                    theSelect.blur(); // Opera needs this to refocus
                    theSelect.focus();
                }
            }
        }
    };

    // the following functions are only used for IE and Opera
    // see explaination in the createSelect function
    // they are defined here to avoid creating closures in the createSelect function

    /* begin IE and Opera corrective functions */

    // this code is somewhat fragile due to assumptions about browser behavior

    selectOnFocus = function () {
        this.start = this.selectedIndex;
    };

    // Opera fires mouse events even when a key is pressed on a select element
    // the keydown property on the select element is used to circumvent this
    selectOnKeyDown = function (e) {
        this.keydown = true;
        e = e || event;

        if (e.keyCode === 13 || e.keyCode === 9) {
            // IE doesn't fire the keyup event because the selectOnChange function removes
            // focus from the element before the event is fired
            // Opera doesn't fire mouse events when the enter or tab key is pressed, so this is safe
            // tab happens on keydown, so keyup is never fired on the element
            this.keydown = false;
            if (this.selectedIndex !== this.start) {
                this.start = this.selectedIndex; // fix for blur event handler
                selectOnChange.call(this);
                return false; // prevents tabbing away after change function executes
            }
        } else if (e.keyCode === 27) {
            // escape happens on keydown, so keyup is never fired on the element (in Opera)
            this.keydown = false;
            this.selectedIndex = this.start;
        }
    };

    selectOnClick = function () {
        if (!this.keydown && this.selectedIndex !== this.start) {
            this.start = this.selectedIndex; // fix for blur event handler
            selectOnChange.call(this);
        }
    };

    selectOnKeyUp = function () {
        this.keydown = false;
    };

    selectOnBlur = function () {
        if (this.selectedIndex !== this.start) {
            selectOnChange.call(this);
        }
    };

    /* end IE and Opera corrective functions */

    /**
     * Creates a select element, populates it with the appropriate options,
     * styles it and animates it into position on the screen.
     *
     * @memberOf MOVIEPICKER
     * @private
     */
    createSelect = function () {
        var i,
            chLen,
            choice,
            question = document.createElement('label'),
            select = document.createElement('select'),
            option,
            box = document.createElement('div'),
            selectId = currChoice.question.replace(/\s/g, '-'),
            title,
            img;

        // due to problems in IE and Opera with the select change event and keyboard
        // navigation, the change event handler is not used, instead, the change
        // detection is done with click, focus/blur and keydown/keyup event handlers and some
        // extra logic - using this method for all browsers would require adding more
        // complexity for dealing with browser differences in mouse events on select elements
        if (isIE || isOpera) {
            select.onfocus = selectOnFocus;
            select.onkeydown = selectOnKeyDown;
            select.onclick = selectOnClick;
            select.onkeyup = selectOnKeyUp;
            select.onblur = selectOnBlur;
        } else {
            select.onchange = selectOnChange;
        }

        question.setAttribute('for', selectId);
        question.appendChild(document.createTextNode(currChoice.question));
        box.appendChild(question);

        option = document.createElement('option');
        option.setAttribute('value', '');
        option.appendChild(document.createTextNode('Select one'));
        select.appendChild(option);
        select.setAttribute('id', selectId);
        select.className = 'selection';

        for (i = 0, chLen = currChoice.choices.length; i < chLen; i += 1) {
            choice = currChoice.choices[i];
            option = document.createElement('option');
            option.setAttribute('value', choice);
            option.appendChild(document.createTextNode(choice));
            select.appendChild(option);

            // look ahead to preload images
            title = currChoice[choice].title;
            if (title && !(pics.hasOwnProperty(title))) {
                img = moviePic.cloneNode(false);
                img.setAttribute('src', 'img/' + title.replace(/\s/g, '_') + '.jpg');
                img.setAttribute('alt', title + ' pic');
                pics[title] = img;
            }
        }

        box.appendChild(select);

        // set the box position off screen
        box.style.left = '-400px';
        box.style.top = '60px';
        box.className = 'selectContainer';
        body.appendChild(box);

        // disable the select boxes while they are moving
        enableSelects(false);

        // animate the box into position
        utils.move(box, {
            to: [50, 60],
            callback: function () {
                enableSelects(true);
                this.childNodes[1].focus();
            }
        });
    };

    /**
     * Truncates the length of the commentsBox to 500.
     * This function expects to be executed in the context of a textarea
     * element. It is registered as keyup and change event handlers for
     * the commentsBox.
     *
     * @memberOf MOVIEPICKER
     * @private
     */
    checkLength = function () {
        var text = this.value;

        if (text.length > 500) {
            this.value = text.slice(0, 500);

            // scroll to the bottom to prevent jumping to the
            // top of the textarea in Firefox
            // (all modern browsers support these properties)
            this.scrollTop = this.scrollHeight;
        }
    };

    /*
     * Validates the user input before sending.
     * If it does not validate, it will prompt the user to correct the input.
     * If everything validates, it will send the e-mail (disabled on server
     * for security) and display a message that the e-mail has been sent. This
     * function always returns false to prevent the form from being submitted.
     */
    form.onsubmit = function () {
        var twoWeeks,
            ajax,
            name = utils.trim(nameText.value),
            email = utils.trim(emailText.value),
            comments = utils.trim(commentsBox.value);

        if (!name) {
            alert('Please enter a name');
            nameText.focus();
        } else if (!email.match(/^\w+(?:[\-+.]\w+)*@\w+(?:[\-.]\w+)*\.[a-zA-Z]{2,6}$/)) {
            alert('Please enter a valid e-mail address');
            emailText.focus();
            emailText.select();
        } else {
            // prevent user from submitting more than once while waiting
            // for response - do this before the request is started
            // so it is disabled immediately - if Ajax is used it will be
            // reenabled when the request is complete, otherwise it will be
            // reenabled again after an error message is displayed
            btnSend.disabled = true;

            // display the loading image
            sendDiv.appendChild(loadingImg);

            // set cookies
            twoWeeks = new Date(new Date().getTime() + (14 * 24 * 60 * 60 * 1000));
            utils.setCookie('userName', nameText.value, twoWeeks);
            utils.setCookie('userEmail', emailText.value, twoWeeks);

            // send an Ajax request with the information to be sent
            ajax = utils.ajaxRequest({
                url: 'mail.php',
                data: {
                    name: name,
                    email: email,
                    comments: comments
                },
                timeout: 4000,
                onsuccess: function (req) {
                    var response;
                    try {
                        response = JSON.parse(req.responseText);
                        if (response.message) {
                            alert(response.message);
                        }
                    } catch (ignore) {}
                },
                onfailure: function (req) {
                    // check req.readyState
                    // if it's not 0, check req.status for more info
                    // otherwise, it was aborted (timed out) and req.status is not accessible
                    if (req.readyState) {
                        alert('Error: Server returned status code ' + req.status);
                    } else {
                        alert('Error: Request timed out');
                    }
                },
                oncomplete: function () {
                    sendDiv.removeChild(loadingImg);
                    btnSend.disabled = false;
                }
            });

            if (!ajax) {
                alert('Error: Request could not be sent');
                btnSend.disabled = false;
            }
        }

        // this is all JavaScript based - always prevent the form
        // from being submitted
        // normally we would return !ajax to allow the submission
        // if Ajax was not used
        return false;
    };

    loadingImg.setAttribute('src', 'img/circle.gif');
    loadingImg.setAttribute('alt', 'Loading...');
    loadingImg.className = 'loading';
    loadingImg.setAttribute('width', '16');
    loadingImg.setAttribute('height', '16');

    selects = body.getElementsByTagName('select');

    header.appendChild(document.createTextNode('Movie Picker'));
    header.style.left = '10px';
    header.style.top = '-200px';
    body.appendChild(header);

    // create the result container in memory
    displayDiv = document.createElement('div');
    displayDiv.setAttribute('id', 'results');

    btnStartOver = document.createElement('input');
    btnStartOver.setAttribute('id', 'startOver');
    btnStartOver.setAttribute('type', 'button');
    btnStartOver.setAttribute('value', 'Start Over');
    btnStartOver.onclick = function () {
        var firstSelect = selects[0];
        firstSelect.selectedIndex = 0;
        selectOnChange.call(firstSelect);
    };

    sendDiv = document.createElement('div');
    sendDiv.setAttribute('id', 'send');
    btnSend = document.createElement('input');
    btnSend.setAttribute('type', 'submit');
    btnSend.setAttribute('value', 'Send');
    sendDiv.appendChild(btnSend);

    link = document.createElement('a');

    // the movie title will be inserted into this text node
    link.appendChild(document.createTextNode(''));

    resultText = document.createElement('h2');
    resultText.appendChild(link);
    displayDiv.appendChild(resultText);

    bigDiv = document.createElement('div');
    bigDiv.setAttribute('id', 'resultsContainer');


    moviePic = document.createElement('img');
    moviePic.setAttribute('width', '200');
    moviePic.setAttribute('height', '150');
    displayDiv.appendChild(moviePic);

    copyright = document.createElement('span');
    copyright.setAttribute('id', 'copyright');
    copyright.appendChild(document.createTextNode(''));
    displayDiv.appendChild(copyright);

    instr = document.createElement('p');
    instr.appendChild(document.createTextNode('Email yourself details about this movie!'));
    instr.setAttribute('id', 'instructions');
    displayDiv.appendChild(instr);

    // this isn't really necessary,
    // but forms require an action attribute to be valid HTML
    form.setAttribute('action', '');

    nameDiv = document.createElement('div');
    nameLabel = document.createElement('label');
    nameLabel.setAttribute('for', 'txtName');
    nameLabel.appendChild(document.createTextNode('Name:'));
    nameDiv.appendChild(nameLabel);
    nameText = document.createElement('input');
    nameText.setAttribute('type', 'text');
    nameText.setAttribute('size', '20');
    nameText.setAttribute('id', 'txtName');
    nameText.setAttribute('name', 'name');
    nameText.setAttribute('maxlength', '100');
    nameDiv.appendChild(nameText);
    form.appendChild(nameDiv);

    emailDiv = document.createElement('div');
    emailLabel = document.createElement('label');
    emailLabel.setAttribute('for', 'txtEmail');
    emailLabel.appendChild(document.createTextNode('E-mail:'));
    emailDiv.appendChild(emailLabel);
    emailText = document.createElement('input');
    emailText.setAttribute('type', 'text');
    emailText.setAttribute('size', '20');
    emailText.setAttribute('id', 'txtEmail');
    emailText.setAttribute('name', 'email');
    emailText.setAttribute('maxlength', '100');
    emailDiv.appendChild(emailText);
    form.appendChild(emailDiv);

    commentsDiv = document.createElement('div');
    commentsLabel = document.createElement('label');
    commentsLabel.setAttribute('for', 'comments');
    commentsLabel.appendChild(document.createTextNode('Comments:'));
    commentsDiv.appendChild(commentsLabel);
    commentsBox = document.createElement('textarea');
    commentsBox.setAttribute('id', 'comments');
    commentsBox.setAttribute('name', 'comments');
    commentsBox.setAttribute('rows', '4');
    commentsBox.setAttribute('cols', '30');
    commentsBox.onkeyup = checkLength;
    commentsBox.onchange = checkLength;
    commentsDiv.appendChild(commentsBox);
    form.appendChild(commentsDiv);

    form.appendChild(sendDiv);

    displayDiv.appendChild(form);
    bigDiv.appendChild(displayDiv);
    bigDiv.appendChild(btnStartOver);

    bigDiv.style.left = '420px';
    bigDiv.style.top = '-500px';

    // animate the header into place
    // then create the first select box
    utils.move(header, {
        to: [10, 10],
        callback: function () {
            createSelect();
        }
    });
}());