/* Copyright 2011 English360 */
/* Contains the commonly used JS code in the e360 platform, this file is loaded on every page */
/*jslint forin: true */
/*global jQuery, Math, document, alert, confirm, $, window, unescape, setTimeout, scroll*/
var E360 = E360 || {};
(function($) {
    // SPECIAL automatic init method stuff which we probably should get rid of
    E360.initializer = function() {
        for (var prop in this) {
            if (prop !== 'initializeE360' && typeof(this[prop].initializeE360) === 'function') {
                this[prop].initializeE360();
            }
        }
    };
    $(document).ready( function() {
        E360.initializer();
    });


    // **************** UTILITY **********************************************

    /**
     * Check if a string contains a substring, case sensitive
     * @param {String} str
     * @param {String} substring
     */
    E360.strContains = function(str, substring) {
        return str.length >= substring.length && str.indexOf(substring) > -1;
    };

    /**
     * Check if a string starts with a substring, case sensitive
     * @param {String} str
     * @param {String} substring
     */
    E360.strStartsWith = function(str, substring) {
        return str.length >= substring.length && str.indexOf(substring) === 0;
    };

    /**
     * Check if a string ends with a substring, case sensitive
     * @param {String} str
     * @param {String} substring
     */
    E360.strEndsWith = function(str, substring) {
        return str.length >= substring.length && str.substr(str.length - substring.length) === substring;
    };

    /**
     * Validate a variable is a number.
     *
     * @param n
     * @return boolean
     * @see http://stackoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric
     */
    E360.isNumber = function(n) {
        return !isNaN(parseFloat(n)) && isFinite(n);
    };

    /**
     * Validate a variable is an integer.
     *
     * @param n
     * @return boolean
     * @see http://stackoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric
     */
    E360.isInt = function(n) {
        return !isNaN(parseInt(n, 10)) && isFinite(n) && Math.round(n) === n;
    };

    /**
     * Wrapper of encodeURIComponent that considers apache special chars encoding issue
     *
     * @param uri the URI component to encode
     * @return encoded URI
     */
    E360.encodeURI = function (uri){
        var value = encodeURIComponent(uri);
        if (E360.Configure.ApacheWebServer) {// if webserver is apache then double encode url
            value = encodeURIComponent(value);
            value = value.replace(/%252f/i,'%20'); // replace slash with space, since it throws an error on apache servers (see 10050)
        }
        return value;
    };
    
    /**
     * Returns a json object set by a controller via $this->set('json', array('key' => 'value'));
     */
    E360.json = function (varName) {
        var value = $.evalJSON($('#json-object').text());
        if (varName) {
            value = value[varName];
        }
        return value;
    };


    // ************ i18n translation **********************************************

    /**
     * Assists with translating strings which exist in javascript by pulling the translated
     * strings from either the special E360.Configure.i18n array in the bootstrap:
     * Configure::write('i18n', ...
     * OR from the DOM via id keys,
     * the DOM translations follow the convention:
     * <div style="display: none;">
     *     <span id="i18n_invalid_tag_warning_message"><?php __('You entered an invalid tag') ?></span>
     *     <span id="i18n_empty_tag_warning_message"><?php __('You didn\'t enter a tag for course %name%') ?></span>
     * </div>
     * first key is "invalid_tag_warning_message"
     * first translated string is "You entered an invalid tag"
     * in the second example %name% is a replacement value which would have the current course name inserted
     * in the JS via the replacements array
     *
     * var translated1 = E360.i18n('invalid_tag_warning_message');
     * var translated2 = E360.i18n('empty_tag_warning_message', {'%name%': courseName});
     *
     * @param string key the translation key (should not have spaces or quotes)
     * @param array replacements [OPTIONAL] the replacement values for the translated string
     * @param bool prependDomId If false, no string will be prepended to the key name (for DOM lookup),
     *             otherwise string, or true will default to i18n_
     * @return string the translated string
     */
    E360.i18n = function(key, replacements, prependDomId) {
        var translatedString = '** Unknown i18n key: '+key+' **';
        prependDomId = typeof(prependDomId) === "undefined" ? true : prependDomId;
        if (prependDomId === true) {
            prependDomId = "i18n_";
        }
        if (key && typeof key === 'string') {
            var found = false;
            if (E360.Configure.i18n) {
                // try to get the string from the translations data
                if (E360.Configure.i18n[key]) {
                    translatedString = E360.Configure.i18n[key];
                    found = true;
                } else if (E360.Configure.i18n[key.toLowerCase()]) {
                    translatedString = E360.Configure.i18n[key.toLowerCase()];
                    found = true;
                }
            }
            if (!found) {
                // get the string from element content with id matching the key in the current page
                var $keyText = $("#" + (prependDomId ? prependDomId : "") + key.replace(/[ '"]/,'_')); // replace space and quotes with underscore
                if ($keyText.length > 0) {
                    translatedString = $keyText.text();
                    found = true;
                }
            }
            if (found && translatedString && replacements) {
                // replace the replacement values in the string with values from the replacements array
                for (var translateKey in replacements) {
                    if (replacements.hasOwnProperty(translateKey)) {
                        if (translateKey) {
                            translatedString = translatedString.replace(translateKey, replacements[translateKey]);
                        }
                    }
                }
            }
        }
        return translatedString;
    };


    // ************ UUIDS and URLS **********************************************

    function S4() {
       return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
    }

    /**
     * generate a ID
     *
     * @param string prefix
     * @return string id
     */
    E360.generateId = function(prefix) {
        return prefix+'_'+S4()+S4()+S4();
    };

    /**
     * Given a uuid and filename, compute a normalized path for the file.
     * We use the first two characters of the uuid as a sub dir so we
     * have at most 256 top level dirs.
     *
     * @param uuid
     * @param filename
     * @return string
     */
    E360.computeS3Path = function(uuid, filename) {
        return uuid.slice(0, 2) + '/' + uuid + '/' + filename;
    };


    /**
     * Return a URL to an item on the AWS CDN
     *
     * @param type One of assets, images, or media
     * @param path Path of the item
     * @return string
     */
    E360.cdnUrl = function(type, path) {
        // @todo CDN doesn't work on preprod, so disabling until we can fix.
        // if (E360.Configure.debug > 0) {
            return "/" + path;
        // }

        /*
        var bucket = E360.Configure.Cloud.buckets["bucket_"+type],
            cdn = window.location.protocol + "//" + bucket.remoteHost + "/";
        if (bucket.remotePrefix) {
            cdn += bucket.remotePrefix;
        }
        return cdn + path;
        */
    };

    /**
     * Ensure all external links open in new window
     *
     * @param {string} selector to find the a tags
     */
    E360.changeExternalLinkTargets = function(selector) {
        var links = $(selector);
        links.each(function() {
            if((this.href.indexOf('://') !== -1) && (this.href.indexOf('://' + document.domain) === -1)) {
                $(this).attr('target', '_blank');
            }
        });
    };


    // ************ CLEANUP **********************************************

    /**
     * Replaces fancy double and single quotes, and elipses, into pure ASCII equivalents.
     *
     * @param string
     * @return string
     */
    E360.replaceSmartQuotes = function(string) {
        return string
            .replace(/[\u8216\u8217\u2018\u2019]/g, "'")
            .replace(/[\u8220\u8221\u201c\u201d]/g, '"')
            .replace(/[\u8212\u8211\u2013\u2014]/g, '-')
            .replace(/\u8230/, '...');
    };

    /**
     * Encode HTML entities in a string.
     *
     * @param str String to convert
     * @return string
     *
     * @see http://www.toao.net/32-my-htmlspecialchars-function-for-javascript
     */
    E360.htmlspecialchars = function(str) {
        if (typeof(str) == "string") {
            str = str.replace(/&/g, "&amp;"); /* must do &amp; first */
            str = str.replace(/"/g, "&quot;");
            str = str.replace(/'/g, "&#039;");
            str = str.replace(/</g, "&lt;");
            str = str.replace(/>/g, "&gt;");
        }
        return str;
    };

    /**
     * Decodes HTML entities in a string.
     *
     * @param str String to convert
     * @return string
     *
     * @see http://www.toao.net/32-my-htmlspecialchars-function-for-javascript
     */
    E360.htmlspecialchars_decode = function(str) {
        if (typeof(str) == "string") {
            str = str.replace(/&gt;/ig, ">");
            str = str.replace(/&lt;/ig, "<");
            str = str.replace(/&#039;/g, "'");
            str = str.replace(/&quot;/ig, '"');
            str = str.replace(/&amp;/ig, '&'); /* must do &amp; last */
        }
        return str;
    };

    /**
     * Convert new lines to <br> tags.
     *
     * @param str String to convert
     * @return string
     */
    E360.nl2br = function(str) {
        if (typeof(str) == "string") {
            str = str.replace(/(\r\n|\n\r)/g, '<br>').replace(/(\r|\n)/g, '<br>');
        }
        return str;
    };

    /**
     * Truncate a string to the length and adds a suffix '...'
     *
     * @param str String to truncate
     * @param length maxsize allowed for the given string
     * @return string
     */
    E360.truncate = function(str,length) {
        if (typeof(str) == "string") {
            if (str.length>length) {
                str = str.substr(0,length-3) + '...';
            }
        }
        return str;
    };


    // ************ INTERFACE **********************************************

    /**
     * Show a spinner overlay on DOM elements.
     *
     * @param options
     *    useOffset [default: false] Use the container offset to position the
     *    spinner overlay. This may be needed depending on the layout.
     */
    $.fn.showSpinner = function(options) {
        var defaultOptions = {
            useOffset: false
        };
        options = $.extend(defaultOptions, options);

        var $overlay = $('<div class="loader-overlay"><div></div><!-- Keep empty div --></div>')
            // loader-large jqmOverlay"
            .appendTo(this)
            .css({
                'z-index': this.css('z-index') + 1,
                width: this.outerWidth(),
                height: this.outerHeight()
            });
        if (options.useOffset) {
            $overlay.css($(this).offset());
        }

        return this;
    };

    /**
     * Hide spinner overlay.
     */
    $.fn.hideSpinner = function() {
        $('.loader-overlay', this).remove();
        return this;
    };

    /**
     * Show the loading spinner when loading a paginated page.
     */
    E360.jpaginatorLoading = function(element) {
        $(element).wrap('<span class="loader" />');
        return true;
    };

    /**
     * Reload a paginated page created with jpaginator helper.
     *
     * You must create your pagination using jpaginator and use the jpaging element.
     *
     * @param string selector Selector or DOM object in which the jpaging element is contained.
     * @param string selector Selector or DOM object in which to load the page contents.
     */
    E360.reloadPaginatedPage = function(selector, update) {
        var url = $(selector).find('.e3-paging .current-page').attr('rel');
        $(update).load(url);
    };

    /**
     * This handles displaying notifications (messages) for the user in a standard way
     *
     * @param {Object} options should include at least:
     * status: info (default), warning, success, or error
     * message: a message to display to the user
     * and optionally can include:
     * title: any title to show the user (defaults to "Alert")
     * modal: true | false (defaults to false)
     * image: an image to include with the notification (defaults to none)
     */
    E360.notify = function(options) {
        if (options) {
            // only trigger if the options are set
            // STRING input is allowed for simple messages
            if (typeof options === 'string') {
                // assume it is a message
                options = {status: 'success', message: options};
            }
            if (typeof options === 'object' && options.message) {
                // INIT cleanup of options
                if (! options.status) {
                    options.status = "info"; // default status
                }
                if (! options.title) {
                    options.title = E360.i18n("Alert"); // default title
                }
                if (! options.modal) {
                    options.modal = false; // default modal setting (non-modal)
                }
                if (! options.image) {
                    options.image = '/img/icon_notify-' + options.status + '.png';
                }


                var handled = false;

                // GRITTER - http://boedesign.com/blog/2009/07/11/growl-for-jquery-gritter/
                if (!handled && !options.modal && $.gritter) {
                    // attempt to use gritter for non-modal if available
                    var unique_id = $.gritter.add({
                        title: options.title, // (string | mandatory) the heading of the notification
                        text: options.message, // (string | mandatory) the text inside the notification
                        image: options.image,
                        sticky:  options.sticky, // if true this requires a click to get rid of it
                        // (string | optional) the class name you want to apply to that specific message
                        class_name: 'e360-notification e360-notification-' + options.status,
                        fade_in_speed: 50, // how fast notifications fade in (string or int)
                        fade_out_speed: 50, // how fast the notices fade out
                        time: 8000 // hang on the screen for...
                    });
                    if (unique_id) {
                        handled = true;
                    }
                }
                // DEFAULT cheesy notification using alert
                if (!handled) {
                    handled = true;
                    alert(options.message);
                }
            } else {
                // should we do anything in the case that the input is invalid? (no message or not an object)
                alert(E360.i18n("warning_error"));
            }
        }
    };

    /**
     * This handles general modal confirmation dialogs for users,
     * will trigger execution of the ok or cancel callbacks depending on what the user chooses
     *
     * @param {Object} options should include at least:
     *   message: a message to display to the user
     *   and optionally include these:
     *   okCallback: this is run when the user clicks the confirm button or enter, the options are passed
     *   cancelCallback: this is run when the user clicks cancel or escape, the options are passed (default: close the confirm dialog)
     *   okMsg: the confirm text (default: OK),
     *   cancelMsg: the decline text (default: Cancel),
     *   modal: true | false (default: true)
     * @return bool
     */
    E360.confirm = function(options) {
        if (options) {
            // only trigger if the options are set
            if (typeof options === 'object' && options.message) {
                // INIT - cleanup
                if (! options.okMsg) {
                    options.okMsg = E360.i18n("OK"); // default ok
                }
                if (! options.cancelMsg) {
                    options.cancelMsg = E360.i18n("Cancel"); // default ok
                }
                if (!options.cancelCallback || typeof options.cancelCallback !== 'function') {
                    options.cancelCallback = null;
                }
                if (typeof options.modal !== 'boolean') {
                    options.modal = true;
                }

                var handled = false;

                // DEFAULT JS confirm handler
                if (!handled) {
                    handled = true;
                    if ( confirm(options.message) ) {
                        if (options.okCallback) {
                            options.okCallback(options);
                        }
                        return true;
                    } else {
                        if (options.cancelCallback) {
                            options.cancelCallback(options);
                        }
                        return false;
                    }
                }
            } else {
                // should we do anything in the case that the input is invalid? (no message or not an object)
                alert(E360.i18n("warning_error"));
            }
        }
        return true;
    };


    // ************ JQUERY **********************************************

    /**
     * This will center any jquery object it is executed on,
     * if the container cannot be found then this will produce a failure alert
     *
     * @param selector [OPTIONAL] specify the selector for the container to center within, DEFAULT: window
     */
    jQuery.fn.center = function (selector, debug) {
        var $container = $(window);
        var topPos = 0;
        var leftPos = 0;
        debug = (typeof selector === "undefined") ? false : debug;
        if ( !(typeof selector === "undefined" || selector === null) ) {
            $container = $(selector);
        }
        if ($container.length) {
            this.css("position","absolute"); // must switch the position to absolute first
            topPos = ( ( $container.height() - this.height() ) / 2 ) + $container.scrollTop();
            leftPos = ( ( $container.width() - this.width() ) / 2 ) + $container.scrollLeft();
            if (debug) {
                alert("thing: "+this.width()+"x"+this.height()+" window: "+$container.width()+"x"+$container.height()+" scroll: "+$container.scrollLeft()+"x"+$container.scrollTop()+" -> "+leftPos+"x"+topPos);
            }
            this.css("top", topPos + "px");
            this.css("left", leftPos + "px");
        } else {
            alert('ERROR: invalid container ('+selector+') for center');
        }
        return this;
    };

    /**
     * jQuery shuffle
     *
     * Copyright (c) 2008 Ca-Phun Ung <caphun at yelotofu dot com>
     * Dual licensed under the MIT (MIT-LICENSE.txt)
     * and GPL (GPL-LICENSE.txt) licenses.
     *
     * http://yelotofu.com/labs/jquery/snippets/shuffle/
     *
     * Shuffles an array or the children of a element container.
     * This uses the Fisher-Yates shuffle algorithm <http://jsfromhell.com/array/shuffle [v1.0]>
     */
    $.fn.shuffle = function() {
        return this.each(function(){
            var items = $(this).children().clone(true);
            return (items.length) ? $(this).html($.shuffle(items)) : this;
        });
    };
    $.shuffle = function(arr) {
        for(var j, x, i = arr.length; i; j = parseInt(Math.random() * i, 10), x = arr[--i], arr[i] = arr[j], arr[j] = x);
        return arr;
    };

    /**
     * Extend jquery.ui.tabs for e360's default AJAX tab behavior.
     *
     * Usage: Same as jquery.ui.tabs except:
     *   - Include a #tab_panel in your HTML.
     *   - Do not set the spinner option.
     */
    $.fn.ajaxTabs = function(options) {
        var defaults = {
            // cache tabs by default
            cache: true,
            // we'll do our own spinner
            spinner:'',
            // the panel where we'll load tabs
            panelContainer: '#tab_panels'
        };
        options = $.extend(defaults, options);

        var loadingId = options.panelContainer + '_loading';
        loadingId = loadingId.replace('#', '');
        var $panelContainer = $(options.panelContainer);
        $panelContainer.before('<div id="'+loadingId+'" class="block tab_loader result"><span class="loader-large"><em>Loading...</em></span></div>');
        var $loading = $('#'+loadingId);

        var loadingStart = function(event, ui) {
            $panelContainer.find('.ui-tabs-panel').not('.ui-tabs-hide').addClass('ui-tabs-hide');
            $loading.show();
        };

        var loadingEnd = function(event, ui) {
            $loading.hide();
        };

        var tabsShow = function (event, ui) {
            var $panel = $(ui.panel);
            if (!$panel.is(":empty")) { // shows the spinner only if the panel hasnt been loaded yet
                $('#tab_panels_loading').hide();
            }
        };
        
        return this.tabs(options)
            .bind('tabsselect', loadingStart)
            .bind('tabsshow', tabsShow)
            .bind('tabsload', loadingEnd);
    };

    /**
     * E360 customized autocomplete
     */
    $.fn.e360Autocomplete = function(options) {
        var thisAutocomplete = this.autocomplete(options);

        thisAutocomplete.each(function() {
            var autocomplete = $(this).data("autocomplete");
            autocomplete._renderItem = function( ul, item ) {
                var term = E360.htmlspecialchars(autocomplete.term),
                    label = E360.htmlspecialchars(item.label);
                label = label.replace(
                    new RegExp($.ui.autocomplete.escapeRegex(term), "gi"),
                    "<strong>$&</strong>"
                );
                return $( "<li></li>" )
                    .data( "item.autocomplete", item )
                    .append( "<a>" + label + " (ID " + item.value + ")</a>" )
                    .appendTo( ul );
            };
        });

        return thisAutocomplete;
    };



    // ************ CONTROL **********************************************

    /**
     * Disables the <Enter> key. In some forms, hitting the <Enter>
     * key can be problematic. e.g. When there are multiple buttons
     * on a form, <Enter> always engages the first.
     *
     * https://english360.fogbugz.com/default.asp?2830
     *
     * @param   selector
     * @param   submit    Whether to submit the parent form instead of
     *                    simply disabling the enter key.
     * @returns boolean
     */
    E360.disallowEnter = function( selector, submit ) {
      selector = selector || 'form';
      submit   = submit || false;

      jQuery(selector).keypress( function( e ) {
        var code  = e.keyCode || e.which;
        var allow = true;

        if( code === 13 ) {
          var target  = e.target || e.srcElement;
          var $target = jQuery(target);

          if( target.tagName.toUpperCase() !== 'TEXTAREA' && $target.attr('type') !== 'submit' ) {
            e.preventDefault();

            if( submit ) {
              $target.closest('form').submit();
            }
            allow = false;
          }
        }

        return allow;
      });
    };


    // ************ INIT COMPLEX **********************************************

    /**
     * This will make form controls found by the selector select (highlight) the internal content
     * as long as it has not changed
     *
     * @param {string} itemsSelector a selector to choose the items
     */
    E360.initSelectUnchanged = function(itemsSelector){
        $(itemsSelector).focus(function(){
            // only select if the text has not changed
            if (this.value === this.defaultValue) {
                this.select();
            }
        });
    };

    /**
     * Trigger show a block when click triggerd, can also hide another block
     *
     * @param {string} clickTriggerSelector the selector for the items which are triggers
     * @param {string} itemsToShowSelector the selector for the items to reveal
     * @param {string} itemsToHideSelector [optional] selector for the items to hide, if set then hide the selected items
     */
    E360.initClickToShow = function(clickTriggerSelector, itemsToShowSelector, itemsToHideSelector){
        $(clickTriggerSelector).click(function(){
            if (undefined !== itemsToHideSelector) {
                $(itemsToHideSelector).hide();
            }
            $(itemsToShowSelector).show(200);
            return false; // cancel the action
        });
    };

    /**
     * This will clear any form fields found by the selector if they have not changed and
     * will then submit the form found by the form selector
     *
     * @param {string} submitSelector selector for the submit button
     * @param {string} formSelector the selector for a form
     * @param {string} itemsSelector selects items within the form only
     * @param {string} defaultValueField What field holds the default value. Either 'defaultValue' or 'title'.
     */
    E360.initClearUnchangedAndSubmit = function(submitSelector, formSelector, itemsSelector, defaultValueField){
        var form = $(formSelector);
        if (!defaultValueField) {
            defaultValueField = 'defaultValue';
        }
        $(submitSelector).click(function(){
            form.find(itemsSelector).each(function(i){
                // clear out the value if they are defaults before sending
                if (this.value === this[defaultValueField]) {
                    this.value = "";
                }
            });
            form.submit();
            return false; // cancel the action
        });
    };

    E360.initClickTagAdd = function(formSelector, formInputSelector, tagsSelector){
        var form = $(formSelector);
        var formInput = form.find(formInputSelector);
        $(tagsSelector).click(function(){
            var tagName = $(this).text();
            tagName = jQuery.trim(tagName);
            formInput.val(tagName);
            form.submit();
            return false; // cancel the action
        });
    };

    /**
     * Initializes a multi-form handler which will create new things by
     * appending copies of the repeatTemplate inside the container when
     * the add item is clicked and removing the associated one when the delete item is clicked
     *
     * @param {string}  repeatTemplate a template (or selector for the template content) which will be copied into the parent,
     *                  all '{{i}}' values will be replaced by the current value of the counter
     * @param {string}  containerSelector the selector for a container where the new things should be appended
     * @param {string}  addSelector the selector for an item which causes the template to be replicated
     * @param {string}  deleteSelector the selector for an item which causes a replicated template to be removed (should be inside the temple)
     * @param {number}  initialCounter [optional] defaults to 1, set the starting position for the counter
     */
    E360.initMultiForm = function(repeatTemplate, containerSelector, addSelector, deleteSelector, initialCounter) {
        if (undefined === initialCounter) {
            initialCounter = 1;
        }
        var template = $(repeatTemplate).html();
        var counter = initialCounter;
        var container = $(containerSelector);
        $(addSelector).click( function () {
            container.append(template.replace(/\{\{i\}\}/g, counter));
            var newElement = container.find(":last-child");
            newElement.find(deleteSelector).click( function () {
                var parentItem = $(this).parent();
                parentItem.hide();
                parentItem.remove();
                return false;
            });
            var inputs = newElement.find('input').defaultText();
            counter = counter + 1;
            return false;
        });
    };

    /**
     * Initialize school switcher if present
     **/
    E360.initSchoolSwitcher = function() {
        var $schoolSwitcher = $('#school-switcher');
        if (!$schoolSwitcher.length) {
            return;
        }
        var $ps = $('p', $schoolSwitcher);
        $('#school-switcher-start').click(function(event) {
            event.preventDefault();
            $ps.toggle();
            $('#school-switcher-id').change(function() {
                var $form = $(this).parents('form');
                $form.submit();
            });
            $('#school-switcher-cancel').click(function() {
                event.preventDefault();
                $ps.toggle();
            });
        });
    };

    /**
     * Init search form handler, for encoding values for the URL
     **/
    E360.initSearchForm = function() {
        $('.global-search, #search-criteria').submit(function(evt) {
            var $input = $(this).find('#SearchBy');
            // add encoded value to a hidden input to override the posted data without changing display value
            $(this).append('<input type="hidden" value="' + E360.encodeURI($input.val()) + '" name="' + $input.attr('name') + '" />');
        });
    };

    /**
     * Initialize style pickers
     */
    E360.initStylePicker = function(id, opts) {
        var $stylePicker = $('#' + id + '_style_picker'),
            input = document.getElementById(id + 'Styles'),
            $input = $(input),
            options,
            defaults = {
                autoEnabledField : null
            },
            decorations = {
                bold: {'font-weight': 'bold'},
                italic: {'font-style': 'italic'},
                underline: {'text-decoration': 'underline'}
            };
        options = jQuery.extend({}, defaults, opts);
        input.data = $.evalJSON($input.val());
        if (input.data.length === 0) {
            input.data = {};
        }

        var update = function(property, value) {
            if (options.autoEnabledField) {
                $(options.autoEnabledField).val(1);
            }
            if (decorations[property]) {
                for (var key in decorations[property]) {
                    if (value) {
                        input.data[key] = decorations[property][key];
                    } else {
                        delete input.data[key];
                    }
                }
            } else {
                if (value.length === 0) {
                    delete input.data[property];
                } else {
                    input.data[property] = value;
                }
            }
            $input.val($.toJSON(input.data));
        };

        $('.revert-styles', $stylePicker).click(function(event) {
            event.preventDefault();
            var defaultStyles = $.evalJSON($(this).attr('rel'));
            $stylePicker.find('.colorBox').css('background-color', defaultStyles[$stylePicker.find('.picker').attr('rel')]);
            $stylePicker.find('.picker').val(defaultStyles[$stylePicker.find('.picker').attr('rel')]);
            $stylePicker.find('.sizePicker').val(defaultStyles['font-size']);
            $stylePicker.find('.sizePicker').val(defaultStyles['font-size']);
            $stylePicker.find('.btn_bold, .btn_italic, .btn_underline').each(function() {
                var $this = $(this);
                $this.removeClass('btn_' + $this.attr('rel') + '_selected');
            });
            input.data = {};
            $input.val($.toJSON(input.data));
            if (options.autoEnabledField) {
                $(options.autoEnabledField).val(0);
            }
        });
        $('.picker', $stylePicker).icolor({
            trigger: 'icolor',
            autoClose: false,
            slide: false,
            onSelect:function(c){
                this.$t.val(c);
                this.$t.parent().find('div').css('background-color',c);
                update(this.$t.attr('rel'), c);
                this.$t.focus();
                this.$t.data('icolor-clicked', true);
            }
        }).change(function() {
            var $this = $(this),
                c = $this.val()
            ;
            if (c.match(/^\#[A-Fa-f0-9]{6}$/)) {
                $this.parent().find('div').css('background-color', c);
                update($this.attr('rel'), c);
                $this.val(c.toUpperCase());
            }
        }).focus(function() {
            if ($(this).data('icolor').$layout.is(':hidden')) {
                $(this).trigger('icolor');
            }
        }).blur(function(event) {
            $this = $(this);
            setTimeout(function() {
                if (!$this.data('icolor-clicked') && !$this.data('icolor').$layout.is(':hidden')) {
                    $this.trigger('icolor');
                } else {
                    $this.data('icolor-clicked', false);
                }
            }, 100);
        });
        $('.sizePicker', $stylePicker).change(function() {
            update('font-size', $(this).val());
        });
        $('.btn_bold, .btn_italic, .btn_underline', $stylePicker).click(function(){
            var $this = $(this);
            $this.toggleClass('btn_' + $this.attr('rel') + '_selected');
            update($this.attr('rel'), $this.hasClass('btn_' + $this.attr('rel') + '_selected'));
        });
    };

    /**
     * Initialize enable/disable behavior of new global-style buttons
     *   - you can change enabled setting with jQuery().trigger('enable' or 'disable')
     *   - disabled buttons automatically stop click events or other handlers
     */
    E360.initButtonLinks = function() {
        jQuery('a.btn_global, a.btn_global-disabled')
            .click(function(event) {
                if (jQuery(this).hasClass('btn_global-disabled')) {
                    event.stopImmediatePropagation();
                    event.preventDefault();
                }
            })
            .bind('enable', function(event) {
                jQuery(this).addClass('btn_global').removeClass('btn_global-disabled');
            })
            .bind('disable', function(event) {
                jQuery(this).addClass('btn_global-disabled').removeClass('btn_global');
            });
    };



    // ************ OTHER COMPLEX **********************************************

    /**
     * Handle the 'Add to favourites' and 'Remove from favourites' links.
     */
    E360.Favorites = {

        // track if we are already processing to prevent double clicks
        processing: false,

        initializeE360: function() {
            $('a.favorite_link').bind('click', this.handleClick);
        },

        handleClick: function(event) {
            var self = this;
            var link = event.target;
            var $link = $(link);

            event.preventDefault();
            event.stopPropagation();
            if (self.processing) {
                return false;
            }
            self.processing = true;
            $link.addClass('favorite_link_loading');

            var action = link.rel;
            var idSplit = link.id.split('_');
            var model = idSplit[2];
            var modelId = idSplit[3];
            var ajaxURL = '/favorites/manage/'+action+'/'+model+'/'+modelId;

            $.ajax({
                type: 'POST',
                url: ajaxURL,

                // handle AJAX error, probably server communication issue
                error: function(request, status) {
                    E360.notify({
                        message: E360.i18n("failure_updating_favourites")+" "+status,
                        title: E360.i18n("Error"),
                        status: 'error'
                    });
                },

                // handle successful AJAX request
                success: function(response, status, request) {
                    var message;
                    // NOTE: jquery makes json responses into objects by default - AZ
                    try {
                        // only attempt to convert this if it is a json string
                        if (typeof response === 'string') {
                            response = $.evalJSON(response);
                        }
                    } catch(e) {
                        // trap the failure so we can deal with it later
                        response = {
                            status: E360.i18n("Error")+": "+e.name,
                            message: e.extMessage,
                            stack: e.stack
                        };
                    }
                    // handle errors encountered during process
                    if (!response || response.status === undefined || response.status !== 'success') {
                        message = E360.i18n("failure_updating_favourites");
                        if (response.message !== undefined) {
                            message = response.message;
                        }
                        E360.notify({
                            message: message,
                            title: E360.i18n("Error"),
                            status: 'error'
                        });
                        return false;
                    }

                    // handle success situation
                    // Switch the link to the opposite action
                    // If you change the text here, change it in the En360Helper::favoriteLink()
                    // @todo i18n invalid - this should NOT change the text of the link but should find a cleaner way to do this
                    switch (action) {
                        case 'add':
                            $link.html(E360.i18n('Remove from favourites')).attr('rel', 'remove');
                            message = E360.i18n('Added to your favourites');
                            break;
                        case 'remove':
                            $link.html(E360.i18n('Add to favourites')).attr('rel', 'add');
                            message = E360.i18n('Removed from your favourites');
                            break;
                        default:
                            break;
                    }

                    E360.notify({
                        message: message,
                        title: E360.i18n("Favourites")
                    });
                    $link.removeClass('favorite_link_'+action).addClass('favorite_link_'+link.rel);
                    return false;
                },

                // remove the spinner
                complete: function(response, status) {
                    $link.removeClass('favorite_link_loading');
                    self.processing = false;
                }
            }); // end ajax() call

            return false;
        }
    };


    E360.Dictionary = {

        lookupUrl: 'http://dictionary.cambridge.org/license/e360/search/british/?q=',
        windowTarget: 'e360_dictionary',
        popupOptions: 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=no,copyhistory=no,width=600,height=550,top=300,left=300',
        // maximum number of words in a phrase to lookup. We want
        // search larger than this to avoid very long selects
        // triggering this, which won't work anyway.
        maxAllowedWords: 5,

        /**
         * Clean the input box of the "Define a word..." text when clicked on
         *
         * @param event jQuery Event object
         */
        clearInputBox: function(event) {
            $(event.target)
                .val('')
                .removeClass('empty')
                .unbind('click.dictionary');
        },


        /**
         * Handle the submission of a word open the dictionary window.
         */
        handleSubmit: function(event) {
            event.stopPropagation();
            var word = E360.Dictionary.cleanWord(jQuery('input#dictionary_word').val());
            if (word !== null) {
                E360.Dictionary.openDictionary(word);
            }
            return false;
        },

        /**
         * Handle when a user selects some text to be defined.
         */
        handleSelect: function(event) {

            // Ignore areas with the restrict_dictionary class.
            if (jQuery(event.target).closest('.restrict_dictionary').length !== 0) {
                return;
            }

            // Cross-browser function to get selected text
            var word = '';
            if(window.getSelection) {
                word = window.getSelection().toString();
            } else if(document.getSelection) {
                word = document.getSelection();
            } else if(document.selection) {
                word = document.selection.createRange().text;
            }

            word = E360.Dictionary.cleanWord(word);

            $('#define_selected_word').remove();

            // remove popup if no word selected  OR
            // disable the double-click feature if the lookup string
            // exceeds the maximum number of allowable words
            if (word === null || word.split(/[ \-]/).length > E360.Dictionary.maxAllowedWords) {
                return;
            }

            // we are going to load the popup, stop all other actions on this event.
            event.preventDefault();

            //append the layer to the DOM only once
            var ext = 'png';
            // When we can, use pngs for the popups because they look better,
            // but because of lack of png transparency in IE6 we use a gif there.
            if ($.browser.msie && $.browser.version.substr(0,1)<7) {
                ext = 'gif';
            }
            $("body").append('<a id="define_selected_word"><img src="/img/btn_define-selected-word-default.'+ext+'" /></a>');
            var $define = $('#define_selected_word');
            $define.hover(
                // handleIn
                function(event) {
                    jQuery(event.target).attr('src', '/img/btn_define-selected-word-hover.'+ext);
                },
                // handleOut
                function (event) {
                    jQuery(event.target).attr('src', '/img/btn_define-selected-word-default.'+ext);
                }
            );

            //move the layer at the cursor position
            $define.map(function() {
                $(this).css({
                    'left' : event.pageX-50,
                    'top' : event.pageY-40
                });
            });

            //open the definition popup clicking on the layer
            $define.mouseup(function(event) {
                event.stopPropagation();
                $(this).remove();
                E360.Dictionary.openDictionary(word);
            });
        },


        /**
         * Open the dictionary window.
         *
         * @param word word to lookup.
         */
        openDictionary: function(word) {
            var searchUrl = this.lookupUrl + word;
            var popup = window.open(searchUrl, this.windowTarget, this.popupOptions);
            if (popup.focus) {
                popup.focus();
            }
        },

        /**
         * Clean spaces and other garbage from word to be defined.
         *
         * @param string Word to clean
         * @return string Cleaned word
         */
        cleanWord: function(word) {
            var newWord = word
                // Replace characters the dictionary ignores with spaces.
                .replace(/[\.\*\?;!()\+,\[:\]<>\^_`\[\]{}~\\\/\"\'=]/g, " ")
                // Replace multiple spaces with a single space
                .replace(/\s+/g, " ")
                // Trim leading and trailing spaces
                .replace(/^\s+/, '').replace(/\s+$/, '')
                // Remove words that are only hyphens
                .replace(/^[\-\s]+$/, '')
                ;

            if (newWord.length === 0) {
                return null;
            }

            return newWord;
        },

        init: function() {
            $(document).ready(function() {
                $('input#dictionary_word').bind('click.dictionary', E360.Dictionary.clearInputBox);
                $('form#dictionary_form').bind('submit.dictionary', E360.Dictionary.handleSubmit);
                $('.allow_dictionary').bind('mouseup.dictionary', E360.Dictionary.handleSelect);
            });
        }
    };


    // handle the convert-to-plaintext fallback for tinymce on iDevices
    $(document).ready(function() {
        $('.tinymce-convert-to-plaintext').bind('click', function(event) {
            event.preventDefault();
            var $target = $(event.currentTarget);
            var targetId = $target.attr('data-target');
            var html = $target.attr('data-editor-html');
            $('#'+targetId).replaceWith(html);
            $target.hide();
        });
    });


}(jQuery));


var Inflector = Inflector || {};
(function($) {
    Inflector.upperCaseWords = function(string) {
        var ucWords = '';

        for (var i=0, words = string.split(' '), limiti=words.length; i < limiti; i++) {
            ucWords = ucWords + words[i].substr(0, 1).toUpperCase() + words[i].substr(1);
        }

        return ucWords;
    };

    Inflector.classify = function(string) {
        return Inflector.upperCaseWords(string.replace(/_/g, ' '));
    };
}(jQuery));

// CDC OO like Class stuff below here -AZ **********

/**
 * Create a basic prototypal inheritance Class.
 *
 * Create new classes with Class.create({prototype object});
 * Extend a class with Class.extend({prototype object});
 * Tape on new methods to all existing instances with Class.implement({object});
 *
 * Classes can have an initializeClass() function which acts as a constructor for the prototype
 */
function Class (features) {
    var klass = function (noStart) {
        // run the initializeClass method when this "klass" is "constructed"
        if (typeof this.initializeClass === 'function' && noStart !== 'noInit') {
            // http://www.webreference.com/js/column26/apply.html
            return this.initializeClass.apply(this, arguments);
        }
        return this;
    };
    for (var key in this) {
        klass[key] = this[key];
    }
    klass.prototype = features;
    return klass;
}

Class.prototype.extend = function (features) {
    var oldProto, oldFunc, newFunc, func;
    oldProto = new this('noInit');

    var makeParent = function(parent, current) {
        return function () {
            this.parent = parent;
            return current.apply(this, arguments);
        };
    };

    for (var key in features) {
        oldFunc = oldProto[key];
        newFunc = features[key];
        if (typeof oldFunc != 'function' || typeof newFunc != 'function') {
            func = newFunc;
        } else {
            func = makeParent(oldFunc, newFunc);
        }
        oldProto[key] = func;
    }
    return new Class(oldProto);
};

Class.prototype.implement = function (features) {
    for (var key in features) {
        this.prototype[key] = features[key];
    }
};

/**
 * Empty function good for comparing to empty functions
 */
Class.empty = function () { };

/**
 * Add bind function to all functions.
 *
 * lets change 'this' in any function.
 */
if (typeof Function.prototype.bind !== 'function') {
    Function.prototype.bind = function (obj) {
        var method = this;
        return function() {
            return method.apply(obj, arguments);
        };
    };
}

//
// Template Class for parsing and building of Templates that can be evaluated with values
//
// @param string templateString  The template to be used later on.
// @param string  pattern  Regular Expression to change the template tags of the Template
//
var Template = function (templateString, pattern) {
    this._template = null;
    this._data = {};

    // Default Syntax matches {{ tagName }}
    this.pattern = /\\?\{\{([^}]+)\}\}/mg;
    this._template = unescape(templateString);
    this.pattern = pattern || this.pattern;
    return this;
};

Template.prototype = {
    /**
     * Evaluate a Template and replace the template tags with the data in the
     * Data object.
     *
     * @param Object dataset Data that will fill the template out.
     * @return String Template with datavalues replaced.
     */
    evaluate : function (dataset, removeComments) {
        var evaluated = this._template.replace(this.pattern, function(match, name){
            if (match.charAt(0) === '\\') {
                return match.slice(1);
            }
            var dataPath = dataset,
                path = name.split('.');
            for (var i in path) {
                dataPath = dataPath[path[i]];
                if(dataPath === undefined) {
                    return '';
                }
                if(typeof(dataPath) !== 'object') {
                    return dataPath;
                }
            }
            return '';
        });

        if (removeComments !== false) {
            evaluated = evaluated.replace(/<!--.+?-->/g, '');
        }

        return evaluated;
    }
};

//
// multi form Class
//
// Create forms widget sets with a template allowing many records to be
// created in one form.
//
//
// @param string template DOM ID of template Block.
// @param {Object} options
//
// Options object takes a number of options
//  container : the dom id of the container element for the added form elements.
//  addButton : the dom id of the create new form button.
//  deleteButton : set to true to have events attached to the delete button
//  deleteButtonClass : the class name of the delete button.
//  count : what to start the internal instance counter at. (default 0)
//  clearContent :  If true, will clear the original content when first form is added
//                  also when last form is removed will replace original content.
//  lineElementClass : the class name of each multi form element defaults to '.line-block'
//  onComplete : Call backs to run after Template block is added in. Scoped to filled TemplateBlock
//  onFirst : Callback to run on first template block creation. Scoped to filled TemplateBlock
//  max : maximum number of rows allowed
var MultiForm = function (template, options) {
    this.count = 0;
    this.originalContent = null;
    this.container =  null;

    this.options = {
        deleteButtonClass : '.button.delete',
        deleteButton : true,
        lineElement : '.line-block',
        container : '',
        addButton : '',
        isAddButtonPartOfItem : false,
        addButtonInContainer: true, // search for add button inside the container or outside
        count : 0,
        clearContent: true,
        onRemove : function (){return true;},
        afterRemove : function (){return true;},
        onComplete : function (){return true;},
        onFirst : function (){return true;},
        min : 0,
        max : 1000,
        customFeedback : '',
        useRTE : false
    };
    this.options = jQuery.extend({}, this.options, options);
    if (this.options.maxErrorMessage === undefined) {
        var maxItems = (this.options.max === 1) ? 'item' : 'items';
        this.options.maxErrorMessage = 'You have reached the limit of '+this.options.max+' '+maxItems+'.';
    }
    if (this.options.minErrorMessage === undefined) {
        var minItems = (this.options.min === 1) ? 'item' : 'items';
        this.options.minErrorMessage = 'You must have at least '+this.options.min+' '+minItems+'.';
    }

    this.template = new Template(template);
    this.container = jQuery(options.container);
    // count is always the current count of MultiForm elements on the page
    this.count = this.options.count || this.container.find(this.options.lineElement).length;
    // i is not decremented when deleting, to prevent subsequent input names from overlapping
    this.i = this.count;
    this.attachEvents();

    if (this.options.useRTE) {
        jQuery("textarea", this.container).each(function() {
            tinyMCE.execCommand('mceAddControl', false, jQuery(this).attr("id"));
        });
    }

    return this;
};

// Public prototype methods.
MultiForm.prototype = {
    // Button event for 'add button's
    buttonEvent : function (event) {
        var self = event.data.self;
        var templateData = {
            i : self.i,
            itemNumber : self.count + 1,
            uuid: E360.generateId('e360')
        };
        var filledTemplate = self.template.evaluate(templateData);
        if (self.count >= self.options.max) {
            E360.nofity({status: 'error', message: self.options.maxErrorMessage});
            return false;
        }
        if (self.count === 0 && self.options.clearContent) {
            self.originalContent = self.container.html();
            self.container.empty();
        }
        var el = jQuery(filledTemplate);

        self.container.append(el);

        if (self.options.useRTE) {
            jQuery("textarea", el).each(function() {
                tinyMCE.execCommand('mceAddControl', false, jQuery(this).attr("id"));
            });
        }

        self.count++;
        self.i++;
        if (self.count === 1) {
            self.options.onFirst.call(el);
        }

        self.attachEvents();

        self.options.onComplete.call(el);
        return false;
    },

    // Enable / disable the feedback forms
    controlFeedback : function(button) {
        var self = this,
            $this = jQuery(button),
            $parent = $this.closest(self.options.lineElement),
            $feedback = $parent.find('.e3-feedback');
        if ($this.is(':checked')) {
            $feedback.show().find('input').removeAttr('disabled');
            // set placeholder text on new inputs
            jQuery('input[type=text][title]').defaultText();
        } else {
            $feedback.hide().find('input').attr('disabled', 'disabled').val('');
        }
        return true;
    },

    // Run After block has been added, attach events to buttons
    attachEvents : function () {
        var self = this;
        var $container = jQuery(this.container);
        var addButtonFinder = (this.options.addButtonInContainer === true) ? this.container : jQuery('body');
        addButtonFinder.find(this.options.addButton).each(function() {
            button = jQuery(this);
            button.unbind('click').bind('click', {self : self}, self.buttonEvent);
        });

        $container.find(this.options.deleteButtonClass).each(function() {
            button = jQuery(this);
            button.unbind('click');
            var parentEl = button.parents(self.options.lineElement);
            button.bind('click', function(event) {
                event.preventDefault();

                self.options.onRemove.call();

                if ( (self.count-1) < self.options.min) {
                    if (self.options.minErrorMessage) {
                        E360.notify({status: 'error', message: self.options.minErrorMessage});
                    }
                    return false;
                }
                parentEl.remove();

                self.options.afterRemove.call();
                if (self.options.isAddButtonPartOfItem) {
                    jQuery(self.options.addButton).hide();
                    jQuery(self.options.addButton+":last").show();
                }

                self.count--;
                if (self.count === 0 && self.options.clearContent) {
                    self.container.html(self.originalContent);
                }
                return true;
            });
        });

        if (self.options.customFeedback.length > 0) {
            $container.find(this.options.customFeedback).each(function() {
                button = jQuery(this);
                button.unbind('click');
                var parentEl = button.parents(self.options.lineElement);
                button.bind('click', function(event) {
                    self.controlFeedback(this);
                    return true;
                });
                self.controlFeedback(this);
            });

        }

        if (this.options.isAddButtonPartOfItem) {
            jQuery(this.options.addButton).hide();
            jQuery(this.options.addButton+":last").show();
        }
    }
};

//
// Tag Picker Class, handles event binding for Tag Picker element
// @see elements/layout/tag_picker.ctp
//
// @param options Options to use for the object
var TagPicker = function (options) {
    this.options = jQuery.extend({}, this.defaults, options);

    var buttons = jQuery.makeArray(this.options.button);
    for (var i=0; i < buttons.length; i++) {
        this.createButton(buttons[i]);
    }
    this.input = jQuery(this.options.input);
    this.addTags(this.options.tagList);
    this.tagList.hide();
    this.bindChange(this.input);
};

//Public prototype methods.
TagPicker.prototype = {
    defaults : {
        button : ["a.btn_tags", "#predefined-tags"],
        input : '#TagPicker',
        tagList :  'div.tags-list',
        tagSelector : 'a'
    },
    createButton : function (button) {
        var self = this;

        if (button.fadeIn === undefined) {
            button = jQuery(button);
        }
        button.bind('click', function(event) {
            event.preventDefault();

            if (self.tagList.is(':visible')) {
                self.tagList.fadeOut();
            } else {
                self.highlightTags();
                self.tagList.fadeIn();
            }
        });
    },
    addTags : function (tagContainer) {
        var $tagContainer = jQuery(tagContainer);
        this.tagList = $tagContainer;
        var links = jQuery(this.options.tagSelector, $tagContainer);
        var self = this;

        function _addTagToList (event) {
            event.preventDefault();
            var tagText = jQuery(this).text();
            var currentTags = jQuery(self.input).val();
            if (currentTags.length === 0) {
                self.input.val(tagText);
            } else {
                self.input.val(currentTags + ", " + tagText);
            }
            var er = new RegExp('^' + tagText.replace(/\(/,'\\(').replace(/\)/,'\\)') + '$', 'i');
            jQuery(self.options.tagSelector, self.tagList).each(function() {
                if (jQuery(this).text().match(er)) {
                    jQuery(this).replaceWith('<b>' + tagText + '</b>');
                }
            });
        }

        links.unbind('click').bind('click', _addTagToList);
    },
    bindChange : function(input) {
        var self = this;

        input.bind('change', function(event) {
            event.preventDefault();

            self.relinkTags();
        });
    },
    relinkTags : function() {
        var self = this;
        var currentTags;
        var val = jQuery(self.input).val();
        if (val === undefined) {
            currentTags = [];
        } else {
            currentTags = val.split(',');
        }
        jQuery.each(currentTags, function(n, val) {
            currentTags[n] = jQuery.trim(val);
        });
        var tags = jQuery('li', this.tagList).children('b');
        tags.each(function() {
            var tag = jQuery(this);
            var tagText = tag.text();
            if (jQuery.inArray(tagText, currentTags) == -1) {
                tag.replaceWith('<a href="#">' + tagText + '</a>');
            }
        });
        self.addTags(this.tagList);
    },
    highlightTags : function () {
        var self = this;
        var currentTags;
        var val = jQuery(self.input).val();
        if (val === undefined) {
            currentTags = [];
        } else {
            currentTags = val.split(',');
        }
        var links = jQuery(this.options.tagSelector, this.tagList);
        links.each(function() {
            var tag = jQuery(this);
            var tagText = tag.text();
            jQuery(currentTags).each(function() {
                var currentTag = this.replace(/^\s+|\s+$/g, '');
                var er = new RegExp('^' + currentTag + '$', 'i');
                if (tagText.match(er)) {
                    tag.replaceWith('<b>' + tagText + '</b>');
                }
            });
        });
    }
};

//
// Feedback manager
// Handles fetching all feedback sets, and attaching events for forms if they are visible.
var FeedbackManager = function () {
    var self;
    var options;
    var buttons;
    var defaults = {
        submitUrl : window.basePath + "items/feedback/",
        getFeedback : ".lm-item-feedback",
        submitButton : ".btn_submit"
    };
    var feedbackId;
    var savingNow = false;

    return {
        initializeClass : function (opts) {
            self = this;
            // prevents FeedbackManager to be initialized more than once
            // since all the calls are live calls
            if (this.hasOwnProperty('initialized')) {
                return null;
            } else {
                this.initialized = true;
            }
            options = $.extend({}, defaults, opts);
            buttons = $(options.getFeedback);

            buttons.toggle(self.getFeedback, self.hideFeedback);
            if (window.location.href.match(/feedback\:all/)) {
                // Display all feedback if chosen
                self.queueFeedbackRequests();
            } else {
                feedbackId = window.location.href.match(/feedback\:(\d+)/);
                if (feedbackId) {
                    // Display a specific feedback
                    $("#lm-item-feedback-button-" + feedbackId[1]).trigger("click");
                }
            }
            var $comments = $(".comments-feedback");
            $(".delete",$comments).live("click", self.deleteFeedback);
            $(".feedback-save").live("click", self.addNewFeedback);
            $(".buttons .save").live("click", self.saveFeedback);
            $(".lm-item-feedback").live("click", self.getFeedback);
            $("a.view-all",$comments).live("click", self.getFeedback);
            $("a.edit",$comments).live("click", self.editFeedback);
            $("a.cancel",$comments).live("click", self.cancelFeedback);
            return true;
        },

        getFeedback : function (event) {
            event.preventDefault();
            var id = $(this).closest(".item").attr("data-item-id");
            var $target = $("#lm-feedback-"+id);
            $target.html('<div class="loading">' + E360.i18n('feedbackLoading') + '</div>');
            $target.load(event.currentTarget.href);
        },

        hideFeedback : function (event) {
            event.preventDefault();
            var id = $(this).closest("item").attr("data-item-id");
            $("#lm-feedback-"+id).empty();
        },

        updateButton : function(item) {
            var totalCount = $('.comments-feedback .more span.total',item);
            var feedbackCount = $(".feedback-list li", item).length;
            if (totalCount.length) {
                feedbackCount = totalCount.html();
            }
            $(options.getFeedback, item).text(E360.i18n('feedbackCount', {'%count%': String(feedbackCount)}));
        },

        addNewFeedback : function (event) {
            event.preventDefault();
            var $this = $(this),
                inputs = $this
                    .closest('.comments-feedback')
                    .find("input, textarea"),
                feedbackArea = $this.closest(".lm-feedback-area"),
                data = {},
                feedbackMessage = feedbackArea.find("span.feedback-message"),
                item = $(event.currentTarget).closest('.item');
            
            for (var i = 0, len = inputs.length; i < len; i++) {
                data[inputs.eq(i).attr("name")] = inputs.eq(i).val();
            }

            if(data["data[Feedback][text]"].match(/^\s*$/)) {
                feedbackMessage.html(E360.i18n('feedbackNoFeedback')).show();
                return false;
            } else {
                feedbackMessage.find("span.feedback-message").html("");
            }

            var $profile = $("#FeedbackProfileId"),
                itemId = data["data[Feedback][item_id]"],
                profileId = $profile.size() > 0 ? $profile.val() : false;
            if (savingNow) {
                return false;
            }
            $this.after('<span class="loading"></span>');
            savingNow = true;
            $.ajax({
                type : "post",
                data : data,
                url : options.submitUrl + itemId + (profileId ? "/" + profileId : ""),
                success : function (response, status) {
                    feedbackArea.html(response);
                    self.updateButton(item);
                    savingNow = false;
                }
            });
            return true;
        },

        editFeedback: function (event) {
            var editButton = $(event.currentTarget),
                buttons = editButton.closest('li').find('.feedback .buttons'),
                feedback = editButton.closest('li').find('.feedback .inner');

            event.preventDefault();
            feedback.data('original_text',feedback.html());
            feedback.wrapInner('<textarea>');
            buttons.show();
            editButton.closest('.actions').hide();
        },

        saveFeedback: function (event) {
            event.preventDefault();
            var saveButton = $(event.currentTarget),
                buttons = saveButton.closest('.buttons'),
                feedback = saveButton.closest('li').find('.feedback .inner'),
                actions = saveButton.closest('li').find('.actions');
            $('.feedback-message',feedback).remove();
            if ($('textarea',feedback).val().match(/^\s*$/)) {
                feedback.append('<span class="feedback-message">' + E360.i18n('feedbackNoFeedback') + '</span>');
                return false;
            }
            if (savingNow) {
                return false;
            }
            savingNow = true;
            buttons.append('<span class="loading"></span>');
            $.ajax({
                type : "post",
                data : {'data': {'text':$('textarea',feedback).val()}},
                url : saveButton.attr('href'),
                dataType: "json",
                success: function (response, status) {
                    savingNow = false;
                    // check for error
                    if (!response.status || response.status !== "success") {
                        E360.notify({
                            status: "error",
                            message: response.message
                        });
                        buttons.find('span.loading').remove();
                        return;
                    }
                    buttons.find('span.loading').remove();
                    buttons.hide();
                    actions.show();
                    feedback.html($('textarea',feedback).val());
                }
            });
            return false;
        },
        cancelFeedback: function (event){
           var cancelButton = $(event.currentTarget),
                feedback = cancelButton.closest('li').find('.feedback .inner'),
                buttons = cancelButton.closest('.buttons'),
                actions = cancelButton.closest('li').find('.actions');
            event.preventDefault();

            buttons.hide();
            actions.show();
            feedback.html(feedback.data('original_text'));
            return false;
        },

        deleteFeedback : function (event) {
            event.preventDefault();
            var item = $(event.currentTarget).closest('.item');

            E360.confirm({
                message: E360.i18n('feedbackConfirmDelete'),
                okCallback: function(options) {
                    $(event.currentTarget).before('<span class="loading"></span>');
                    $.ajax({
                        type: "post",
                        dataType: "json",
                        url: event.currentTarget.href + ".json",
                        success: function(response, status) {
                            // check for error
                            if (!response.status || response.status !== "success") {
                                E360.notify({
                                    status: "error",
                                    message: E360.i18n('feedbackErrorDelete') + " " + response.data.message
                                });
                                $(event.currentTarget).closest('span.loading').remove();
                                return;
                            }
                            // success, get feedback
                            var id = item.attr("data-item-id");
                            var $profile = $("#FeedbackProfileId");
                            var profileId = $profile.size() > 0 ? $profile.val() : false;

                            var $target = $("#lm-feedback-"+id);
                            $target.html('<div class="loading">' + E360.i18n('feedbackLoading') + '</div>');

                            $target.load('/items/feedback/' + id + (profileId ? "/" + profileId : ""), function (){
                                self.updateButton($target.closest('.item'));
                                E360.notify({status:"success", message: E360.i18n('feedbackDeleted')});
                                $(event.currentTarget).closest('span.loading').remove();
                            });

                            return;
                        }
                    });
                }
            });
        },

        queueFeedbackRequests: function () {
            $(options.getFeedback).each(function () {
                $(this).trigger("click");
            });
        }
    };
}();

//
// Global object to hold method to bind
// and create forms for the course contents when
// the user is a moderator.
// also handles event binding for folder show/hide
//
var CourseContents = {
    initializeClass: function () {
        this.initFolders();
        if (jQuery('#quick-create-page').length) {
            this.initForms();
        }
    },

    initFolders: function () {
        jQuery('#course-contents .course-pages').hide();
        jQuery('.current .course-pages', jQuery('.course-sidebar').filter('#course-contents') ).show().addClass('stayopen');
        function loadFolder($target, folderId) {
            if ( ! $target.hasClass('loaded') ) {
                $target.addClass('loading');
                var isDropDown = $target.closest('.nav_dropdown_menu').length > 0;
                var url = '/folders/pages/'+'0'+'/'+folderId;
                if (isDropDown) {
                    url = url+'/dropdown:1';
                }
                $target.load(url, function() {
                    $target.addClass('loaded').removeClass('loading');
                });
            }
        }

        $('.folder', $('.course-sidebar').filter('#course-contents')).not('.stayopen').unbind('click').bind('click', function(event) {
            event.preventDefault();

            var $parent = $(this).parents('li');
            var $coursePages = $('.course-pages', $parent);

            if ( $coursePages.hasClass('loading') ) {
                return;
            }

            $parent.toggleClass('current');
            $coursePages.toggle();
            var folderId = $parent.attr('data-folder-id');
            loadFolder($coursePages, folderId);
        });
        $('.nav_dropdown_menu a.folder').click(function (event) {
            event.preventDefault();

            var parentLi = $(this).parents('li');
            var folderId = parentLi.attr('data-folder-id');
            var $target = $('#folder-'+folderId+'-pages');
            if ( $target.hasClass('loading') ) {
                return false;
            }
            $(this).parents('ul').find('li').removeClass('active');
            parentLi.addClass('active');
            $('#course-contents div.secondary .course-pages').hide();
            $('#add-page-dropdown a.add-new-content').toggleClass('disabled', folderId == 'home');
            $target.show();
            loadFolder($target, folderId);
            return false;
        });
        // show the active content nav section
        $('.nav_dropdown_menu .course-folder .active').each(function() {
            $('#folder-'+$(this).attr('data-folder-id')+'-pages').show();
        });

        // scroll to current page items in course nav
        var $courseDropdown = $('.nav-content .nav.course');
        function scrollCourseNav() {
            $('.content.primary').scrollTo('.active', {over: -1.2});
            $('.content.secondary').scrollTo('.active', {over: -1.2});
        };
        $courseDropdown.bind('mouseenter', scrollCourseNav);
    },

    initForms: function () {
        this.attachPageAdd();
        this.attachFolderAdd();
    },

    clearQuickForms: function () {
        jQuery('.add-page, .add-folder, .rename-folder-form, .rename-page-form').each(function () {
            jQuery(this).parent().find('.add-new-content').show();
            jQuery(this).remove();
        });
    },

    attachPageAdd: function () {
        var PageAddTemplate = new Template(jQuery('#quick-create-page').html());
        jQuery('.add-page-button').live('click', function (event) {
            event.preventDefault();
            event.stopPropagation();

            CourseContents.clearQuickForms();
            var $this = jQuery(this);
            var folderId = $this.attr('id').split('-')[2];
            if (folderId === 'dropdown') {
                var activeFolder = jQuery('ul.course-folder li.active');
                if (!activeFolder.length) {
                    return;
                }
                folderId = activeFolder.attr('data-folder-id');
            }
            // If the add new page link is disabled but is still clicked, we won't have
            // a folder Id. This takes care of that.
            if (! folderId || folderId == 'home') {
                return;
            }
            $this.find('a.add-new-content').hide();
            var pageForm = jQuery(PageAddTemplate.evaluate({folderId: folderId}));
            pageForm.submit(function (event) {
                var holder = jQuery(this).find('p.blank-slate-message');
                holder.hide();
                var value = jQuery.trim(jQuery(this).find('input[type=text]').val());
                if (value === '' || value === 'Untitled Page') {
                    holder.show().html(E360.i18n('page_name_empty'));
                    event.preventDefault();
                    event.stopPropagation();
                }
            });

            $this.append(pageForm);
            pageForm.find('a.cancel').bind('click', function (event) {
                CourseContents.clearQuickForms();
                $this.find('a.add-new-content').show();
                return false;
            });
            pageForm.fadeIn('fast').bind('click', function (event) {
                event.stopPropagation();
            });
            pageForm.find('input[type=text]').trigger('focus');
        });
    },

    attachFolderAdd: function () {
        var FolderAddTemplate = new Template(jQuery('#quick-create-folder').html());//jQuery(folderAdd.html());
        jQuery('.add-folder-button').bind('click', function (event) {
            CourseContents.clearQuickForms();
            var $this = jQuery(this);
            $this.find('a.add-new-content').hide();
            event.preventDefault();
            event.stopPropagation();
            var folderAddForm = jQuery(FolderAddTemplate.evaluate());
            folderAddForm.submit(function(event) {
                var holder = jQuery(this).find('p.blank-slate-message');
                holder.hide();
                var value = jQuery.trim(jQuery(this).find('input[type=text]').val());
                if (value === '' || value === 'Untitled Folder') {
                    holder.show().html(E360.i18n('folder_name_empty'));
                    event.preventDefault();
                    event.stopPropagation();
                }
            });

            $this.append(folderAddForm);
            $this.find('a.cancel').bind('click', function (event) {
                CourseContents.clearQuickForms();
                $this.find('a.add-new-content').show();
                return false;
            });
            $this.find('form').fadeIn('fast').bind('click', function (event) {
                event.stopPropagation();
            });
            $this.find('input[type=text]').trigger('focus');
        });
    },

    attachRenamePage: function () {
        var FolderRenameTemplate = new Template(jQuery('#quick-page-rename').val());
        jQuery('a.rename-page-trigger').bind('click', function(event) {
            event.preventDefault();
            event.stopPropagation();

            jQuery('a#rename-page-' + E360.pageId).click();
        });
        jQuery('a.folder-page').filter(function() {
            return jQuery(this).html() === 'Untitled Page';
        }).click(function (event) {
            event.preventDefault();
            event.stopPropagation();

            jQuery(this).nextAll('a.rename-page').click();
        });
        jQuery('a.rename-page').bind('click', function (event) {
            CourseContents.clearQuickForms();
            event.preventDefault();
            event.stopPropagation();

            var $this = jQuery(this);
            var pageId = $this.attr('id').split('-')[2];
            var pageName = jQuery.trim($this.parent().find('a.folder-page').text());
            var pageForm = jQuery(FolderRenameTemplate.evaluate({pageId: pageId, name: pageName}));

            pageForm.submit(function (event) {
                var holder = jQuery(this).find('p.blank-slate-message');
                holder.hide();
                var value = jQuery.trim(jQuery(this).find('input[type=text]').val());
                if (value === '' || value === 'Untitled Page') {
                    holder.show().html('Please choose a title that describes the page content');
                    event.preventDefault();
                    event.stopPropagation();
                }
            });

            $this.parent().append(pageForm);
            pageForm.find('input#PageName').val(pageName);
            if(pageName === 'Untitled Page' || pageName === '') {
                pageForm.find('p.blank-slate-message').css('display', 'block');
            }
            pageForm.fadeIn('fast')
                .bind('click', function (event) {
                    event.stopPropagation();
                });
            pageForm.find('a.cancel').click(function (event) {
                jQuery(document).trigger('click', event);
                return false;
            });
            pageForm.find('input[type=text]').trigger('focus');
        });
    },

    attachRenameFolder: function () {
        var FolderRenameTemplate = new Template(jQuery('#quick-folder-rename').val());
        jQuery('a.rename-folder-trigger').bind('click', function(event) {
            event.preventDefault();
            event.stopPropagation();

            jQuery('a#rename-folder-' + E360.folderId).click();
        });
        jQuery('a.rename-folder').bind('click', function (event) {
            CourseContents.clearQuickForms();
            event.preventDefault();
            event.stopPropagation();

            var $this = jQuery(this);
            var folderId = $this.attr('id').split('-')[2];
            var folderName = $this.parent().find('a.folder').text();
            var folderForm = jQuery(FolderRenameTemplate.evaluate({folderId: folderId, name: folderName}));

            folderForm.submit(function(event) {
                var holder = jQuery(this).find('p.blank-slate-message');
                holder.hide();
                var value = jQuery.trim(jQuery(this).find('input[type=text]').val());
                if(value === '' || value === 'Untitled Folder') {
                    holder.show().html('Please choose a name for the folder');
                    event.preventDefault();
                    event.stopPropagation();
                }
            });

            $this.parent().append(folderForm);
            folderForm.fadeIn('fast')
                .bind('click', function (event) {
                    event.stopPropagation();
                });
            folderForm.find('a.cancel').bind('click', function (event) {
                jQuery(document).trigger('click', event);
                return false;
            });
            folderForm.find('input[type=text]').trigger('focus');
        });
    }
};

// Handles expansion and collapse of folders
var FolderManager = {
    initializeClass : function(folderClass) {
        if (typeof folderClass == "undefined" || folderClass == '') {
            folderClass = 'icon-folder';
        }
        // UNBIND click events and then rebind all handlers
        // expand all folders
        var $expander = $('a.js_expand_all_folders');
        var self = this;
        $expander.unbind('.folders');
        $expander.bind('click.folders', function(event) {
            self.expand_all_folders();
            event.preventDefault();
        });
        // collapse all folders
        var $collapser = $('a.js_collapse_all_folders');
        $collapser.unbind('.folders');
        $collapser.bind('click.folders', function(event) {
            self.collapse_all_folders();
            event.preventDefault();
        });
        var $folders = $('table.js_expand_folders tr a.' + folderClass);
        $folders.unbind('.folders');
        $folders.bind('click.folders', function(event) {
            var $icon_arrow_span = $(this).parent().find('span.js_icon_arrow');
            if ($icon_arrow_span.hasClass('icon-arrow-right')) {
                $("tr."+$(this).parents('tr').attr('id')).show();
                $icon_arrow_span.removeClass('icon-arrow-right').addClass('icon-arrow-down');
            } else {
                $("tr."+$(this).parents('tr').attr('id')).hide();
                $icon_arrow_span.removeClass('icon-arrow-down').addClass('icon-arrow-right');
            }
            event.preventDefault();
        });
    },
    expand_all_folders : function() {
        var $table = $('table.js_expand_folders');
        $table.find('tr.can-hide').show();
        $table.find('span.js_icon_arrow').removeClass('icon-arrow-right').addClass('icon-arrow-down');
    },
    collapse_all_folders : function() {
        var $table = $('table.js_expand_folders');
        $table.find('tr.can-hide').hide();
        $table.find('span.js_icon_arrow').removeClass('icon-arrow-down').addClass('icon-arrow-right');
    }
};

// Generic Form Behaviors

var GenericFormEvents = {
    selectRadioLi: function (event) {
        jQuery(this).parents('ul').find('li').removeClass('selected');
        jQuery(this).parents('li').addClass('selected');
    }
};

/**
 * Adds default text to a form input that is cleared on focus and submit.
 */
jQuery.fn.defaultText = function() {
    return this.each(function() {
        var self = this;
        var clearText = function() {
            // If there is text
            if (jQuery(self).val() === jQuery(self).attr('title')) {
                jQuery(self).val('');
            }
            // return true so submit event continues
            return true;
        };
        var addText = function() {
            if (jQuery(self).val() === '') {
                jQuery(self).val(jQuery(self).attr('title'));
            }
        };
        jQuery(self)
            // clear default text on focus
            .bind('focus', clearText)
            // add default text back if lose focus and still empty
            .bind('blur', addText)
            // Clear default text on submit
            .closest('form').bind('submit', clearText);
        addText.call(self);
    });
};


// Tag flyout Javascript.
// Handles positioning the fly out div so that users can add Need Tags to their profile.


// Flyout class (why is this a separate class?)
var Flyout = new Class({
    enabled: true,
    defaults: {
        template: '#tag-flyout-template',
        targets: '.hover-tags li'
    },
    flyout: null,
    initializeClass: function (options) {
        if (!this.enabled) {
            return false;
        }

        this.options = jQuery.extend({}, this.defaults, options);
        var templateContainer = jQuery(this.options.template);
        if (templateContainer.length === 0) {
            return true;
        }
        this.flyoutTemplate = new Template(templateContainer.html());
        this.bindEvents();

        return true;
    },
    bindEvents: function () {
        var self = this;
        if (!this.options || !this.options.targets) {
            return;
        }
        jQuery(this.options.targets).live('hover', function (event) {
            return self.mouseEnter.apply(this, [event, self]);
        }, function (event) {
            return self.mouseLeave.apply(this, [event, self]);
        }).live('click', function(event) {
            var result = self.click.apply(this, [event, self]);
            if (result === false && event.returnValue) {
                event.returnValue = result;
            }
            return result;
        });
    },
    mouseEnter: function (event, self) {
    },
    mouseLeave: function (event, self) {
    },
    click: function (event, self) {
    }
});

// TagFlyoutPopup - Subclass for Tag Flyouts.
var TagFlyoutPopup = Flyout.extend({
    defaults: {
        template: '#tag-flyout-popup-template',
        targets: '.hover-tags li'
    },
    click: function(event, self) {
        event.preventDefault();

        var $tagElement = jQuery(this);
        var weight = $tagElement.attr("class");
        var tagNameValue = $tagElement.find("a").html(); // get text and encode entities
        var keyNameValue = tagNameValue.toLowerCase().replace(/[^a-z0-9]/g, '');
        var tagId = $tagElement.find("a").attr("id").substr(4);
        var data = {
            'tagname': tagNameValue,
            'keyname': keyNameValue,
            'tagid': tagId
        };


        var $body = jQuery('body');

        var flyoutId = '#' + keyNameValue + '-flyout';
        self.flyout = $body.find(flyoutId);
        if (self.flyout.length === 0) {
            var flyoutHtml = jQuery(self.flyoutTemplate.evaluate(data));
            $body.append(flyoutHtml);
            self.flyout = $body.find(flyoutId);
        }
        self.flyout.tagElement = $tagElement;
        self.status();
        self.flyout
            .css('top', (jQuery(window).scrollTop() + 100).toString() + 'px')
            .jqm({
                modal: true,
                closeClass: 'close-dialog',
                toTop: true,
                overlay: 50
            })
            .jqmShow();

        var cookieData = jQuery.cookies.get('needTags');
        var needTags = {};
        if (cookieData !== null) {
            if (typeof(cookieData) === 'string') {
                // only parse the cookieData if it is a json string
                needTags = jQuery.evalJSON(cookieData);
            } else if (typeof(cookieData) === 'object') {
                needTags = cookieData;
            }
        }

        var deleteLink = jQuery("a.c-delete", this.flyout).hide();

        jQuery("input.radio", self.flyout).click(function() {
            var level = parseInt(jQuery(this).attr("class").replace(/^.*\bc(\d+)\b.*$/, '$1'), 10);
            if (isNaN(level)) {
                return false;
            }

            return self.setNeed(level);
        });

        deleteLink.click(function() {
            self.removeNeed();
            return false;
        });
        if (needTags && needTags[keyNameValue] && needTags[keyNameValue] !== '0') {
            self.setNeed(needTags[keyNameValue], false);
        }

        return false;
    },
    status: function(type) {
        var self = this;
        jQuery("div.status-messages .status-message", this.flyout).hide();
        if (type) {
            var status = jQuery("div.status-messages .status-message-" + type, this.flyout);
            jQuery("div.status-messages", this.flyout).show();
            status.show();
            if (type === "added" || type === "removed") {
                setTimeout(function() {
                    status.fadeOut("fast", function() {
                        jQuery("div.status-messages", self.flyout).hide();
                    });
                }, 2000);
            }
        } else {
            jQuery("div.status-messages", this.flyout).hide();
        }
    },
    setNeed: function(level, post) {
        post = post === undefined || post;
        var input = jQuery("input.c" + level, this.flyout);
        var url = input.val();
        if (!url) {
            return false;
        }

        if (post) {
            this.status("loading");
        } else {
            input.attr("checked", true);
        }

        if (!post || this.postUrl(url)) {
            if (post) {

                /* update needs cloud on welcome/dashboard */
                var sameNeedTag = null;
                if(jQuery('#your-needs')) {
                    if(this.flyout.tagElement && this.flyout.tagElement.parent('#your-needs').length) {
                        this.flyout.tagElement.show().removeClass().addClass('c' + level);
                    } else if((sameNeedTag = jQuery('#your-needs #' + this.flyout.tagElement.find('a').attr('id')).parent('li')) && sameNeedTag.length) {
                        this.flyout.tagElement = sameNeedTag.show().removeClass().addClass('c' + level);
                    } else {
                        jQuery('#your-needs ul.hover-tags').append('<li class="c' + level + '">' + this.flyout.tagElement.html() + '</li>');
                        this.flyout.tagElement = jQuery('#your-needs a#' + this.flyout.tagElement.find('a').attr('id')).parent('li');
                    }
                }

                this.status("added");
            }
            jQuery("a.c-delete", this.flyout).show();
        } else {
            input.attr("checked", false);
            this.status("failed");
        }
        return true;
    },
    removeNeed: function() {
        var $delete = jQuery("a.c-delete", this.flyout);
        var url = $delete.attr("href");
        if (!url) {
            return false;
        }

        this.status("loading");
        if (this.postUrl(url)) {

            /* update needs cloud on welcome/dashboard */
            var sameNeedTag = null;
            if(jQuery('#your-needs')) {
                if(this.flyout.tagElement && this.flyout.tagElement.parent('#your-needs').length) {
                    this.flyout.tagElement.hide();
                } else if((sameNeedTag = jQuery('#your-needs #' + this.flyout.tagElement.find('a').attr('id')).parent('li')) && sameNeedTag.length) {
                    sameNeedTag.hide();
                }
            }

            this.status("removed");
            jQuery("input.radio", this.flyout).attr("checked", false);
            $delete.hide();
        } else {
            this.status("failed");
        }
        return true;
    },
    postUrl: function(url, data) {
        var success = false;
        jQuery.ajax({
            async: false,
            type: "POST",
            url: url,
            //dataType: "json",
            data: data || null,
            success: function(data, textStatus) {
                if (!textStatus || textStatus !== "success") {
                    success = false;
                } else {
                    success = true;
                    if (data.data) {
                        success = data.data;
                    }
                }
            }
        });
        return success;
    }
});

// TagFlyout - Subclass for Tag Flyouts.
var TagFlyout = Flyout.extend({
    enabled: false,
    mouseEnter: function (event, self) {
        var $tagElement = jQuery(this);
        var weight = $tagElement.attr("class");
        var tagNameValue = $tagElement.find("a").text();
        var keyNameValue = tagNameValue.toLowerCase().replace(/[^a-z0-9]/g, '');
        var tagId = $tagElement.find("a").attr("id").substr(4);
        var data = {
            'tagname': tagNameValue,
            'keyname': keyNameValue,
            'tagid': tagId
        };

        var flyoutHtml = jQuery(self.flyoutTemplate.evaluate(data));
        //var flyoutPosition = self.calcPosition($tagElement, flyoutHtml);
        var flyoutPosition = self.calcMousePosition(event, flyoutHtml);
        flyoutHtml.css('top', flyoutPosition.top);
        flyoutHtml.css('left', flyoutPosition.left);
        var $body = jQuery('body');
        $body.append(flyoutHtml);
        var $flyout = $body.find('#' + keyNameValue + '-flyout');

        // delay the popup appearing, the popup is removed on mouse out so no need to handle the case of running over multiple items
        setTimeout( function() {
            $flyout.fadeIn("normal");
            $flyout.mouseenter(function(){
                // sets the attribute to keep this alive when we leave the tag li
                jQuery(this).addClass('keepflyoutalive');
            });
            $flyout.mouseleave(function(){
                jQuery(this).remove();
            });
        }, 300);

        var cookieData = jQuery.cookies.get('needTags');
        var needTags = {};
        if (cookieData !== null) {
            if (typeof(cookieData) === 'string') {
                // only parse the cookieData if it is a json string
                needTags = jQuery.evalJSON(cookieData);
            } else if (typeof(cookieData) === 'object') {
                needTags = cookieData;
            }
        }

        if (needTags[keyNameValue] && needTags[keyNameValue] !== '0') {
            var $flyoutBold = $flyout.find("span a.c" + needTags[keyNameValue]);
            $flyoutBold.replaceWith("<b>" + $flyoutBold.text() + "</b>");
        } else {
            var $flyoutHidden = $flyout.find("span a.c-delete");
            $flyoutHidden.parent().remove();
        }
    },

    mouseLeave: function (event, self) {
        var $flyout = jQuery('body').find(".flyout");
        // delay the popup kill to see if we just wandered into the flyout
        setTimeout( function() {
            if (!$flyout.hasClass('keepflyoutalive')) {
                // no class so kill this
                $flyout.remove();
            }
        }, 60);
    },

    // calculate the position to place the popout
    calcMousePosition: function (event, flyoutElement) {
        var flyoutDimensions = {
            width: flyoutElement.outerWidth(),
            height: flyoutElement.outerHeight()
        };
        var flyoutCaretCenter = 26;
        var correction = -2;

        // changed this based on recommendation by Remy Sharp - he also says we should try jquery plugins for this but none seem to do what we need
        // http://codylindley.com/blogstuff/js/jtip/
        // http://docs.jquery.com/Plugins/Tooltip

        // this will be the absolute position relative to the document
        var topOffset = event.pageY - correction;
        var leftOffset = event.pageX - flyoutCaretCenter;
        var newPosition = {
            top: topOffset,
            left: leftOffset
        };

        // this attempts to ensure the popout is not off the screen
        var totalWidth = jQuery(window).width();
        if ((newPosition.left - flyoutCaretCenter + flyoutDimensions.width) >= totalWidth) {
            newPosition.left = totalWidth - flyoutDimensions.width;
        }
        return newPosition;
    },

    // calculate the position to place the popout
    calcPosition: function (containerElement, flyoutElement) {
        var containerDimensions = {
            width: containerElement.outerWidth(),
            height: containerElement.height()
        };
        var flyoutDimensions = {
            width: flyoutElement.outerWidth(),
            height: flyoutElement.outerHeight()
        };

        var flyoutCaretCenter = 26;
        // this will be the position relative to the containerElement
        var newPosition = {
            left: ((containerDimensions.width / 2) - flyoutCaretCenter) + "px",
            top: (containerDimensions.height - 2) + "px"
        };

        // this attempts to ensure the popout is not off the screen
        var containerOffset = containerElement.offset();
        var totalWidth = jQuery(window).width();
        if ((containerOffset.left + flyoutDimensions.width) >= totalWidth) {
            newPosition.left = totalWidth - (containerOffset.left + flyoutDimensions.width) - flyoutCaretCenter;
        }
        return newPosition;
    }
});


// e360_transloadit jquery function
(function($) {
    var defaultOptions = {
        file_field: "input[type=file]:first",
        filename_field: null,
        display_selector: null,
        allowed_extensions: /.*/,
        unknown_format_message: "Unknown format.",
        max_size: null
    };

    function Uploader() {
        this.$form = null;
        this.options = null;
        this.$file = null;
        this.$filename = null;
        this.$display = null;
        this.$overlay = null;
        this.transloadit_working = false;
    }

    Uploader.prototype.init = function(form, options) {
        this.$form = $(form);
        this.options = $.extend(defaultOptions, options);
        this.$file = $(options.file_field, this.$form);
        this.$filename = $(options.filename_field);
        this.$display = $(options.display_selector);

        $(options.file_field, this.$form).bind("change", $.proxy(this.onFileFieldChange, this));

        return this.$form.transloadit({
            autoSubmit: false,
            wait: true,
            modal: false,
            debug: (E360.Configure.debug > 0),
            onProgress: $.proxy(this.onProgress, this),
            onSuccess: $.proxy(this.onSuccess, this),
            onCancel: $.proxy(this.onCancel, this),
            onError: $.proxy(this.onError, this)
        });
    };

    Uploader.prototype.showError = function(message) {
        $(this.options.error_selector)
            .addClass('error-message')
            .text(message)
            .show();
        if (tinyMCEPopup) {
            tinyMCEPopup.resizeToInnerSize();
        }
    };

    Uploader.prototype.hideError = function() {
        $(this.options.error_selector).hide();
        if (tinyMCEPopup) {
            tinyMCEPopup.resizeToInnerSize();
        }
    };

    Uploader.prototype.stop = function() {
        var ret = this.$form.transloadit("stop");
        this.endUpload();
        return ret;
    };

    Uploader.prototype.working = function() {
        return this.transloadit_working;
    };

    Uploader.prototype.onFileFieldChange = function(event) {
        this.hideError();
        var errorMessage = null;
        if ( ! $(event.target).val().match(this.options.allowed_extensions) ) {
            errorMessage = this.options.unknown_format_message;
        }
        if (this.options.max_size && typeof FileReader !== "undefined") {
            if ($(event.target).get(0).files[0].size > this.options.max_size) {
                errorMessage = "Maximum file size is " + (this.options.max_size / 1024 / 1024 / 1024) + "GB";
            }
        }
        if (errorMessage) {
            this.showError(errorMessage);
            this.clearFileField();
            $(event.target).val("");
            return;
        }
        this.startUpload();
        this.$form.submit();
    };

    Uploader.prototype.startUpload = function() {
        var self = this;
        this.transloadit_working = true;
        this.$overlay =
            $('<div class="transloadit-upload">' +
                  '<p class="status"></p>' +
                  '<div class="progress">' +
                      '<div class="progressbar">' +
                          // '<div class="ui-progressbar-value"></div>' +
                      '</div>' +
                  '</div>' +
                  '<a class="close">Close</a>' +
              '</div>'
            //).appendTo(this.$display)
            ).appendTo('body')
            .css({
                "z-index": this.$display.css("z-index") + 1,
                width: this.$display.outerWidth(),
                height: this.$display.outerHeight()
            })
            .css(this.$display.offset());
        $(".progressbar", this.$overlay).progressbar({value: 0});
        $(".status", this.$overlay).text(E360.i18n('uploading'));
        $(".close", this.$overlay).bind("click", function(event) {
            event.preventDefault();
            self.stop();
        });
    };

    Uploader.prototype.clearFileField = function() {
        this.$file.val("");
    };

    Uploader.prototype.endUpload = function(error) {
        this.clearFileField();
        this.transloadit_working = false;
        if (error) {
            this.showError(error);
        } else {
            this.hideError();
        }
        if (this.$overlay) {
            this.$overlay.remove();
            this.$overlay = null;
        }
    };

    Uploader.prototype.onSuccess = function(assembly) {
        if (!assembly.results.encode) {
            this.endUpload(this.options.unknown_format_message);
            return;
        }
        this.hideError();
        var encode = assembly.results.encode[0];
        var original = assembly.results[":original"];
        var meta = this.$form.data("meta");
        $.extend(meta, {
            width:  encode.meta.width,
            height:  encode.meta.height,
            duration: encode.meta.duration,
            original_filename: original[0].name
        });
        this.$form
            .data("meta", meta)
            .data("url", encode.url);
        this.$filename.text(meta.original_filename);
        this.endUpload();
    };

    Uploader.prototype.onProgress = function(bytesReceived, bytesExpected) {
        $(".progressbar", this.$overlay).progressbar({
            value: Math.floor((bytesReceived / bytesExpected)*100)
        });
        var progressbarVal = $(".progressbar").attr('aria-valuenow');
        if (bytesReceived === bytesExpected && progressbarVal === '100') {
            $(".status", this.$overlay).text(E360.i18n('uploadComplete'));
        }
    };

    Uploader.prototype.onError = function(assembly) {
        this.endUpload("Upload error: "+assembly.error);
    };

    /**
     * Cleanup when transloadit is canceled.
     */
    Uploader.prototype.onCancel = function() {
        this.stop();
    };

    $.fn.e360_transloadit = function() {
        var args = Array.prototype.slice.call(arguments);
        var method;
        var uploader;
        var r;

        if (args.length === 1 && typeof args[0] === "object" || args[0] === undefined) {
            // if called with no options or an object we want to initialize
            args.unshift("init");
        }

        method = args.shift();
        if (method === "init") {
            uploader = new Uploader();
            // add the form element as the first argument
            args.unshift(this);
            this.data("e360_transloadit.uploader", uploader);
        } else {
            uploader = this.data("e360_transloadit.uploader");
        }

        if (!uploader) {
            throw new Error("Element is not initialized for e360_transloadit!");
        }

        r = uploader[method].apply(uploader, args);
        return (r === undefined) ? this : r;
    };

}(jQuery));


/**
 * Show and update progress bar.
 *
 * Usage: In the app use the ProgressBar component to set progress from 0 to 100. 
 * Be sure to set to 'complete' when done.
 *
 * This function is dumb in that we can only have one progress bar at a time.
 */
(function($) {
    $.fn.e360_progress = function(model, id, options) {
        var self = $(this);
        var $this = $(this);
        var defaultOptions = {
            updateInterval: 3000,
            success: function(data, textStatus, xhr) {},
            error: function(xhr, textStatus, errorThrown) {}
        };
        options = $.extend(defaultOptions, options);

        $this.progressbar({value: 0});

        var updateProgress = function() {
            $.ajax({
                url: '/progressbar/progress/'+model+'/'+id+'.json',
                dataType: 'json',
                error: options.error,
                success: function(data, textStatus, xhr) {
                    $this.progressbar({value: data.data.percentage});
                    if (data.data.percentage == 100) {
                        options.success.call(self, data, textStatus, xhr);
                        return;
                    }
                    setTimeout(updateProgress, options.updateInterval);
                }
            });
        };

        setTimeout(updateProgress, options.updateInterval);

        return this;
    };
}(jQuery));


// our customizations for validator
(function($) {
    if ($.validator) {
        $.validator.setDefaults({
            // method to resize the window if needed after errors are updated
            resizeMethod: function() {},
            // Message for the error summary
            errorSummaryMessage: "Please correct all errors before continuing.",
            errorElement: "div",
            errorClass: "error-message",
            showErrors: function(errorMap, errorList) {
                // this is validator object
                if (errorList.length > 0) {
                    $("#error-summary")
                        .html(this.settings.errorSummaryMessage)
                        .addClass("error-message");
                    this.defaultShowErrors();
                }
                this.settings.resizeMethod.apply(this);
            },
            errorPlacement: function(error, element) {
                // this is settings
                var id = $(error).attr("for");
                $('#error-'+id).show();
                $("#error-"+id).append(error);
                this.resizeMethod.apply(this);
            }
        });

        // match between 1-100% or 1 and 1,000 px (px optional)
        $.validator.addMethod("dimension", function(value, element, param) {
            return this.optional(element) || (/^0*((1000|[1-9][0-9]{0,2})(px)?|(100|[1-9][0-9]{0,1})%)$/i).test(value);
        }, "Please enter a valid dimension.");
    }
}(jQuery));


// run this on page load
jQuery(function() {
    // Add selected class to parent li.
    jQuery('ul.radios input').click(GenericFormEvents.selectRadioLi);

    // Toggle default input values
    jQuery('input[type=text][title]').defaultText();

    // load up the tags flyout handling
    if (typeof TagFlyout !== 'undefined') {
        var pageTagFlyout = new TagFlyout();
    }

    if (typeof TagFlyoutPopup !== 'undefined') {
        var pageTagFlyoutPopup = new TagFlyoutPopup();
    }

    // activate "Return to Top" link
    jQuery('#return_to_top').click(function() {
        scroll(0,0);
        return false;
    });

    E360.initButtonLinks();
    E360.initSchoolSwitcher();
    E360.initSearchForm();


    // Handle the admin_upgrade_alert
    jQuery('#upgrade_alert a.close').bind('click', function(event) {
        jQuery.ajax({
            type: 'POST',
            url: event.target.href,
            success: function(data, textStatus, request) {
                jQuery('#upgrade_alert').fadeOut('fast');
            }
        });
        return false;
    });

    // Handle the user agreements modal dialogs in footer
    jQuery('#js-term-dialogs div.modalTerm').each(function () {
        try {
            var term_type = $(this).attr('id').split('_')[1];
            var $dialog = $("#dialog_"+term_type);
            $dialog.jqm({
                modal: true,
                onShow : function (jqm) {
                    $dialog.center();
                    jqm.w.fadeIn("fast");
                },
                onHide : function (jqm) {
                    jqm.w.hide();
                    jqm.o.hide();
                }
            });
            $dialog.jqmAddTrigger('.'+term_type+'_agreement_trigger');
        } catch (e) {
            // TODO should this be blank?
        }
    });
    
    jQuery(document.documentElement).keypress(function (event) {
        // Firefox cancels any network request (including AJAX) if 'ESC' key pressed @see case 11293
        if (event.keyCode === 27) {
            event.preventDefault();
        }
    });
});

