YUI.add('moodle-core-formchangechecker', function (Y, NAME) {

/**
 * A utility to check for form changes before navigating away from a page.
 *
 * @module moodle-core-formchangechecker
 */

/**
 * A utility to check for form changes before navigating away from a page.
 *
 * To initialise, call M.core_formchangechecker.init({formid: 'myform'}); or perhaps
 *
 * Y.use('moodle-core-formchangechecker', function() {
 *     M.core_formchangechecker.init({formid: 'myform'});
 * });
 *
 * If you have some fields in your form that you don't want to have tracked, then add
 * a data-formchangechecker-ignore-dirty to the field, or any parent element, and it
 * will be ignored.
 *
 * If you have a submit button in your form that does not actually save the data,
 * then add a data-formchangechecker-ignore-submit attribute to it.
 *
 * @class M.core.formchangechecker
 * @constructor
 */

var FORMCHANGECHECKERNAME = 'core-formchangechecker',

    FORMCHANGECHECKER = function() {
        FORMCHANGECHECKER.superclass.constructor.apply(this, arguments);
    };

Y.extend(FORMCHANGECHECKER, Y.Base, {

        // The delegated listeners we need to detach after the initial value has been stored once
        initialvaluelisteners: [],

        /**
         * Initialize the module
         *
         * @method initializer
         */
        initializer: function() {
            var formid = 'form#' + this.get('formid'),
                currentform = Y.one(formid);

            if (!currentform) {
                // If the form was not found, then we can't check for changes.
                return;
            }

            if (!M.core_formchangechecker.stateinformation.formchanged) {
                M.core_formchangechecker.stateinformation.formchanged = this.get('initialdirtystate');
            }

            // Add a listener here for an editor restore event.
            Y.on(M.core.event.EDITOR_CONTENT_RESTORED, M.core_formchangechecker.reset_form_dirty_state, this);

            // Add change events to the form elements
            currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'input', this);
            currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'textarea', this);
            currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'select', this);

            // Add a focus event to check for changes which are made without triggering a change event
            this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'input', this));
            this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'textarea', this));
            this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'select', this));

            currentform.delegate('click', function() {
                currentform.setData('ignoreSubmission', true);
            }, '[data-formchangechecker-ignore-submit]');

            // We need any submit buttons on the form to set the submitted flag.
            Y.one(formid).on('submit', function() {
                if (currentform.getData('ignoreSubmission')) {
                    // But not if we have been told to ignore this button.
                    currentform.clearData('ignoreSubmission');
                    return;
                }
                M.core_formchangechecker.set_form_submitted();
            }, this);

            // YUI doesn't support onbeforeunload properly so we must use the DOM to set the onbeforeunload. As
            // a result, the has_changed must stay in the DOM too
            window.onbeforeunload = M.core_formchangechecker.report_form_dirty_state;
        },

        /**
         * Store the initial value of the currently focussed element
         *
         * If an element has been focussed and changed but not yet blurred, the on change
         * event won't be fired. We need to store it's initial value to compare it in the
         * get_form_dirty_state function later.
         *
         * @method store_initial_value
         * @param {EventFacade} e
         */
        store_initial_value: function(e) {
            var thisevent;

            // Don't warn on elements we have been told to ignore.
            if (e.target.ancestor('.ignoredirty', true) ||
                    e.target.ancestor('[data-formchangechecker-ignore-dirty]', true)) {
                return;
            }

            if (M.core_formchangechecker.get_form_dirty_state()) {
                // Detach all listen events to prevent duplicate initial value setting
                while (this.initialvaluelisteners.length) {
                    thisevent = this.initialvaluelisteners.shift();
                    thisevent.detach();
                }

                return;
            }

            // Make a note of the current element so that it can be interrogated and
            // compared in the get_form_dirty_state function
            M.core_formchangechecker.stateinformation.focused_element = {
                element: e.target,
                initial_value: e.target.get('value')
            };
        }
    },
    {
        NAME: FORMCHANGECHECKERNAME,
        ATTRS: {
            formid: {
                'value': ''
            },
            initialdirtystate: {
                'value': false
            }
        }
    }
);

M.core_formchangechecker = M.core_formchangechecker || {};

// We might have multiple instances of the form change protector
M.core_formchangechecker.instances = M.core_formchangechecker.instances || [];
M.core_formchangechecker.init = function(config) {
    var formchangechecker = new FORMCHANGECHECKER(config);
    M.core_formchangechecker.instances.push(formchangechecker);
    return formchangechecker;
};

// Store state information
M.core_formchangechecker.stateinformation = [];

/*
 * Set the form changed state to true
 */
M.core_formchangechecker.set_form_changed = function(e) {
    // Don't warn on elements we have been told to ignore.
    if (e && e.target && (e.target.ancestor('.ignoredirty', true) ||
            e.target.ancestor('[data-formchangechecker-ignore-dirty]', true))) {
        return;
    }

    M.core_formchangechecker.stateinformation.formchanged = 1;

    // Once the form has been marked as dirty, we no longer need to keep track of form elements
    // which haven't yet blurred
    delete M.core_formchangechecker.stateinformation.focused_element;
};

/*
 * Set the form submitted state to true
 */
M.core_formchangechecker.set_form_submitted = function() {
    M.core_formchangechecker.stateinformation.formsubmitted = 1;
};

/*
 * Attempt to determine whether the form has been modified in any way and
 * is thus 'dirty'
 *
 * @return Integer 1 is the form is dirty; 0 if not
 */
M.core_formchangechecker.get_form_dirty_state = function() {
    var state = M.core_formchangechecker.stateinformation,
        editor;

    // If the form was submitted, then return a non-dirty state
    if (state.formsubmitted) {
        return 0;
    }

    // If any fields have been marked dirty, return a dirty state
    if (state.formchanged) {
        return 1;
    }

    // If a field has been focused and changed, but still has focus then the browser won't fire the
    // onChange event. We check for this eventuality here
    if (state.focused_element) {
        if (state.focused_element.element.get('value') !== state.focused_element.initial_value) {
            return 1;
        }
    }

    // Handle TinyMCE editor instances
    // We can't add a listener in the initializer as the editors may not have been created by that point
    // so we do so here instead
    if (typeof window.tinyMCE !== 'undefined' && window.tinyMCE.editors) {
        for (editor in window.tinyMCE.editors) {
            if (window.tinyMCE.editors[editor].isDirty()) {
                return 1;
            }
        }
    }

    // If we reached here, then the form hasn't met any of the dirty conditions
    return 0;
};

/*
 * Reset the form state
 */
M.core_formchangechecker.reset_form_dirty_state = function() {
    M.core_formchangechecker.stateinformation.formsubmitted = false;
    M.core_formchangechecker.stateinformation.formchanged = false;
};

/*
 * Return a suitable message if changes have been made to a form
 */
M.core_formchangechecker.report_form_dirty_state = function(e) {
    if (!M.core_formchangechecker.get_form_dirty_state()) {
        // the form is not dirty, so don't display any message
        return;
    }

    // This is the error message that we'll show to browsers which support it
    var warningmessage = M.util.get_string('changesmadereallygoaway', 'moodle');

    if (M.cfg.behatsiterunning) {
        // If the behat site is running we don't want browser alerts.
        return;
    }

    // Most browsers are happy with the returnValue being set on the event
    // But some browsers do not consistently pass the event
    if (e) {
        e.returnValue = warningmessage;
    }

    // But some require it to be returned instead
    return warningmessage;
};


}, '@VERSION@', {"requires": ["base", "event-focus", "moodle-core-event"]});
