qcubed/framework

View on GitHub
assets/js/qcubed.js

Summary

Maintainability
F
1 wk
Test Coverage
// BEWARE: this clears the $ variable!
var $j = jQuery.noConflict(),
    qcubed,
    qc;

$j.fn.extend({
    wait: function(time, type) {
        time = time || 1000;
        type = type || "fx";
        return this.queue(type, function() {
            var self = this;

            setTimeout(function() {
                $j(self).dequeue();
            }, time);
        });
    }
});

/**
 * Queued Ajax requests.
 * A new Ajax request won't be started until the previous queued
 * request has finished.
 * @param {function} a function that returns ajax options.
 * @param {boolean} blnAsync true to launch right away.
 */
$j.ajaxQueue = function(o, blnAsync) {
    if (typeof $j.ajaxq === "undefined" || blnAsync) {
        $j.ajax(o()); // fallback in case ajaxq is not here
    } else {
        var p = $j.ajaxq("qcu.be", o);
    }
};
$j.ajaxQueueIsRunning = function() {
    if ($j.ajaxq) {
        return $j.ajaxq.isRunning("qcu.be");
    }
    return false;
}


/**
 * @namespace qcubed
 */
qcubed = {
    /**
     * @param {string} strControlId
     * @param {string} strProperty
     * @param {string|Array|Object} strNewValue
     */
    recordControlModification: function(strControlId, strProperty, strNewValue) {
        if (!qcubed.controlModifications[strControlId]) {
            qcubed.controlModifications[strControlId] = {};
        }
        qcubed.controlModifications[strControlId][strProperty] = strNewValue;
    },
    /**
     * Given a control, returns the correct index to use in the formObjsModified array.
     * @param ctl
     * @private
     */
    _formObjChangeIndex: function (ctl) {
        var id = $j(ctl).attr('id');
        var strType = $j(ctl).prop("type");
        var name = $j(ctl).attr("name");

        if (((strType === 'checkbox') || (strType === 'radio')) &&
           id && ((indexOffset = id.lastIndexOf('_')) >= 0)) { // a member of a control list
            return id.substr(0, indexOffset); // use the id of the group
        }
        else if (id && strType === 'radio' && name !== id) { // a radio button with a group name
            return id; // these buttons are changed individually
        }
        else if (id && strType === 'hidden') { // a hidden field, possibly associated with a different widget
            if ((indexOffset = id.lastIndexOf('_')) >= 0) {
                return id.substr(0, indexOffset); // use the id of the parent control
            }
            return name;
        }
        return id;
    },
    /**
     * Records that a control has changed in order to synchronize the control with
     * the php version on the next request.
     * @param event
     */
    formObjChanged: function (event) {
        var ctl = event.target;
        var id = qc._formObjChangeIndex(ctl);
        var strType = $j(ctl).prop("type");
        var name = $j(ctl).attr("name");

        if (strType === 'radio' && name !== id) { // a radio button with a group name
            // since html does not submit a changed event on the deselected radio, we are going to invalidate all the controls in the group
            var group = $j('input[name=' + name + ']');
            if (group) {
                group.each(function () {
                    id = $j(this).attr('id');
                    qcubed.formObjsModified[id] = true;
                });
            }
        }
        else if (id) {
            qcubed.formObjsModified[id] = true;
        }
    },
    /**
     * Initialize form related scripts
     * @param strFormId
     */
    initForm: function (strFormId) {
        $j('#' + strFormId).on ('qformObjChanged', this.formObjChanged); // Allow any control, including hidden inputs, to trigger a change and post of its data.
        $j('#' + strFormId).submit(function(event) {
            if (!$j('#Qform__FormControl').val()) { // did postBack initiated the submit?
                // if not, prevent implicit form submission. This can happen in the rare case we have a single field and no submit button.
                event.preventDefault();
            }
        });
    },

    /**
     * @param {string} strForm The QForm Id, gets overwritten.
     * @param {string} strControl The Control Id.
     * @param {string} strEvent The Event.
     * @param {null|string|Array|Object} mixParameter
     */
    postBack: function(strForm, strControl, strEvent, mixParameter) {
        if (qc.blockEvents) return;   // We are waiting for a response from the server

        strForm = $j("#Qform__FormId").val();
        var $objForm = $j('#' + strForm);

        var checkableControls = $j('#' + strForm).find('input[type="checkbox"], input[type="radio"]');
        var checkableValues = this._checkableControlValues(strForm, $j.makeArray(checkableControls));

        $j('#Qform__FormControl').val(strControl);
        $j('#Qform__FormEvent').val(strEvent);
        $j('#Qform__FormCallType').val("Server");

        // Notify custom controls that we are about to post
        $objForm.trigger("qposting", "Server");

        if (mixParameter !== undefined) {
            $j('#Qform__FormParameter').val(JSON.stringify(mixParameter));
        }
        if (!$j.isEmptyObject(qcubed.controlModifications)) {
            $j('#Qform__FormUpdates').val(JSON.stringify(qcubed.controlModifications));
        }
        if (!$j.isEmptyObject(checkableValues)) {
            $j('#Qform__FormCheckableControls').val(JSON.stringify(checkableValues));
        }

        // add hidden control for additional values given
        // Will be decoded and assigned to the $_POST var in PHP.
        if (!$j.isEmptyObject(qcubed.additionalPostVars)) {
            var input = $j("<input>")
                .attr("type", "hidden")
                .attr("name", "Qform__AdditionalPostVars").val(JSON.stringify(qcubed.additionalPostVars));
            $objForm.append(input);
        }

        // have $j trigger the submit event (so it can catch all submit events)
        $objForm.trigger("submit");
    },
    /**
     * Custom controls should call this in response to the qposting trigger on the form if they need to add some
     * additional post variables. Multiple sets of the same value will overwrite previous value.
     *
     * @param {string} name Name to post. Should probably be the control id, but can be anything.
     * @param {null|number|string|Array|Object} val  Any value you want to send to PHP. Can be a string, array or simple object. Can also contain null
     * values and these will become nulls in PHP.
     */
    setAdditionalPostVar: function (name, val) {
        qcubed.additionalPostVars[name] = val;
    },
    /**
     * This function resolves the state of checkable controls into postable values.
     *
     * Checkable controls (checkboxes and radio buttons) can be problematic. We have the following issues to work around:
     * - On a submit, only the values of the checked items are submitted. Non-checked items are not submitted.
     * - QCubed may have checkboxes that are part of the form object, but not visible on the html page. In particular,
     *   this can happen when a grid is creating objects at render time, and then scrolls or pages so those objects
     *   are no longer "visible".
     * - Controls can be part of a group, and the group gets the value of the checked control(s), rather than individual
     *   items getting a true or false.
     *
     * To solve all of these issues, we post a value that has all the values of all visible checked items, either
     * true or false for individual items, or an array of values, single value, or null for groups. QCubed controls that
     * deal with checkable controls must look for this special posted variable to know how to update their internal state.
     *
     * Checkboxes that are part of a group will return an array of values, keyed by the group id.
     * Radio buttons that are part of a group will return a single value keyed by group id.
     * Checkboxes and radio buttons that are not part of a group will return a true or false keyed by the control id.
     * Note that for radio buttons, a group is defined by a common identifier in the id. Radio buttons with the same
     * name, but different ids, are not considered part of a group for purposes here, even though visually they will
     * act like they are part of a group. This allows you to create individual QRadioButton objects that each will
     * be updated with a true or false, but the browser will automatically make sure only one is checked.
     *
     * Any time an id has an underscore in it, that control is considered part of a group. The value after the underscore
     * will be the value returned, and before the last underscore will be id that will be used as the key for the value.
     *
     * @param {string} strForm   Form Id
     * @param {array} controls  Array of checkable controls. These must be checkable controls, it will not validate this.
     * @returns {object}  A hash of values keyed by control id
     * @private
     */
    _checkableControlValues: function(strForm, controls) {
        var values = {};

        if (!controls || controls.length == 0) {
            return {};
        }
        $j.each(controls, function() {
            var $element = $j(this),
                id = $element.attr("id"),
                strType = $element.prop("type"),
                index = null,
                offset;

            if (id &&
                (offset = id.lastIndexOf('_')) != -1) {
                // A control group
                index = id.substr(offset + 1);
                id = id.substr(0, offset);
            }
            switch (strType) {
                case "checkbox":
                    if (index !== null) {   // this is a group of checkboxes
                        var a = values[id];
                        if ($element.is(":checked")) {
                            if (a) {
                                a.push(index);
                            } else {
                                a = [index];
                            }
                            values[id] = a;
                        }
                        else {
                            if (!a) {
                                values[id] = null; // empty array to notify that the group has a null value, if nothing gets checked
                            }
                        }
                    } else {
                        values[id] = $element.is(":checked");
                    }
                    break;

                case "radio":
                    if (index !== null) {
                        if ($element.is(":checked")) {
                            values[id] = index;
                        }
                    } else {
                        // control name MIGHT be a group name, which we don't want here, so we use control id instead
                        values[id] = $element.is(":checked");
                    }
                    break;
            }
        });
        return values;
    },

    /**
     * Gets the data to be sent to an ajax call as post data. Note that once you call this, you MUST post this data, as
     * it has the side effect of resetting the cache of changed objects.
     *
     * @param {string} strForm The Form Id
     * @param {string} strControl The Control Id
     * @param {string} strEvent The Event
     * @param {null|string|array|object} mixParameter An array of parameters or a string value.
     * @param {string} strWaitIconControlId Not used, probably legacy code.
     * @return {object} Post Data
     */
    getAjaxData: function(strForm, strControl, strEvent, mixParameter, strWaitIconControlId) {
        var $form = $j('#' + strForm),
            $formElements = $form.find('input,select,textarea'),
            checkables = [],
            controls = [],
            postData = {};

        // Notify controls we are about to post.
        $form.trigger("qposting", "Ajax");

        // Filter and separate controls into checkable and non-checkable controls
        // We ignore controls that have not changed to reduce the amount of data sent in an ajax post.
        $formElements.each(function() {
            var $element = $j(this),
                id = $element.attr("id"),
                blnQform = (id && (id.substr(0, 7) === 'Qform__')),
                strType = $element.prop("type"),
                objChangeIndex = qc._formObjChangeIndex($element);


                if (!qcubed.inputSupport || // if not oninput support, then post all the controls, rather than just the modified ones
                qcubed.ajaxError || // Ajax error would mean that formObjsModified is invalid. We need to submit everything.
                (objChangeIndex && qcubed.formObjsModified[objChangeIndex]) ||
                blnQform) {  // all controls with Qform__ at the beginning of the id are always posted.

                switch (strType) {
                    case "checkbox":
                    case "radio":
                        checkables.push(this);
                        break;

                    default:
                        controls.push(this);
                }
            }
        });


        $j.each(controls, function() {
            var $element = $j(this),
                strType = $element.prop("type"),
                strControlId = $element.attr("id"),
                strControlName = $element.attr("name"),
                strPostValue = $element.val();

            switch (strType) {
                case "select-multiple":
                    var items = $element.find(':selected'),
                        values = [];
                    if (items.length) {
                        values = $j.map($j.makeArray(items), function(item) {
                            return $j(item).val();
                        });
                        postData[strControlId] = values;
                    }
                    else {
                        postData[strControlId] = null;    // mark it as set to nothing
                    }
                    break;

                default:
                    var strPostName = (strControlName ? strControlName: strControlId);
                    postData[strPostName] = strPostValue;
                    break;
            }
        });

        // Update most of the Qform__ parameters explicitly here. Others, like the state and form id will have been handled above.
        if (mixParameter !== undefined) {
            postData.Qform__FormParameter = JSON.stringify(mixParameter);
        }
        postData.Qform__FormControl = strControl;
        postData.Qform__FormEvent = strEvent;
        postData.Qform__FormCallType = "Ajax";

        if (!$j.isEmptyObject(qcubed.controlModifications)) {
            postData.Qform__FormUpdates = JSON.stringify(qcubed.controlModifications);
        }
        postData.Qform__FormCheckableControls = qcubed._checkableControlValues(strForm, checkables);

        if (!$j.isEmptyObject(qcubed.additionalPostVars)) {
            postData.Qform__AdditionalPostVars = JSON.stringify(qcubed.additionalPostVars);
            qcubed.additionalPostVars = {};
        }

        qcubed.ajaxError = false;
        qcubed.formObjsModified = {};
        qcubed.controlModifications = {};

        return postData;
    },

    /**
     * @param {string} strForm The QForm Id
     * @param {string} strControl The Control Id
     * @param {string} strEvent
     * @param {null|string|Object|Array} mixParameter
     * @param {string} strWaitIconControlId The id of the control's spinner.
     * @param {boolean} blnAsync Whether to queue the ajax requests and processes serially (default), or do them async.
     *                  See QAjaxAction comments for more info
     * @return {void}
     * @todo There is an eval() in here. We need to find a way around that.
     */
    postAjax: function(strForm, strControl, strEvent, mixParameter, strWaitIconControlId, blnAsync) {
        var objForm = $j('#' + strForm),
            strFormAction = objForm.attr("action"),
            qFormParams = {};

        if (qc.blockEvents) return;

        qFormParams.form = strForm;
        qFormParams.control = strControl;
        qFormParams.event = strEvent;
        qFormParams.param = mixParameter;
        qFormParams.waitIcon = strWaitIconControlId;

        if (strWaitIconControlId) {
            this.objAjaxWaitIcon = qcubed.getWrapper(strWaitIconControlId);
            if (this.objAjaxWaitIcon) {
                this.objAjaxWaitIcon.style.display = 'inline';
            }
        }

        // Use a modified ajax queue so ajax requests happen synchronously
        $j.ajaxQueue(function() {
            var data = qcubed.getAjaxData(
                qFormParams.form,
                qFormParams.control,
                qFormParams.event,
                qFormParams.param,
                qFormParams.waitIcon);

            return {
                url: strFormAction,
                type: "POST",
                qFormParams: qFormParams,
                data: data,
                error: function (XMLHttpRequest, textStatus, errorThrown) {
                    var result = XMLHttpRequest.responseText,
                        objErrorWindow,
                        $dialog;

                    qcubed.ajaxError = true;
                    qcubed.blockEvents = false;
                    if (XMLHttpRequest.status !== 0 || (result && result.length > 0)) {
                        if (result.substr(0, 15) === '<!DOCTYPE html>') {
                            alert("An error occurred.\r\n\r\nThe error response will appear in a new popup.");
                            objErrorWindow = window.open('about:blank', 'qcubed_error', 'menubar=no,toolbar=no,location=no,status=no,scrollbars=yes,resizable=yes,width=1000,height=700,left=50,top=50');
                            objErrorWindow.focus();
                            objErrorWindow.document.write(result);
                            return false;
                        } else {
                            var resultText = $j('<div>').html(result);
                            $dialog = $j('<div id="Qcubed_AJAX_Error" />')
                                .append('<h1 style="text-transform:capitalize">' + textStatus + '</h1>')
                                .append('<p>' + errorThrown + '</p>')
                                .append(resultText)
                                .append('<button onclick="$j(this).parent().hide()">OK</button>')
                                .appendTo('form');
                            return false;
                        }
                    }
                },
                success: function (json) {
                    qcubed._prevUpdateTime = new Date().getTime();
                    if (json.js) {
                        var deferreds = [];
                        // Load all javascript files before attempting to process the rest of the response, in case some things depend on the injected files
                        $j.each(json.js, function (i, v) {
                            deferreds.push(qcubed.loadJavaScriptFile(v));
                        });
                        qcubed.processImmediateAjaxResponse(json, qFormParams); // go ahead and begin processing things that will not depend on the javascript files to allow parallel processing
                        $j.when.apply($j, deferreds).then(
                            function () {
                                qcubed.processDeferredAjaxResponse(json);
                                qcubed.blockEvents = false;
                            }, // success
                            function () {
                                console.log('Failed to load a file');
                                qcubed.blockEvents = false;
                            } // failed to load a file. What to do?
                        );
                    } else {
                        qcubed.processImmediateAjaxResponse(json, qFormParams);
                        qcubed.processDeferredAjaxResponse(json);
                        qcubed.blockEvents = false;
                    }
                }
            };
        }, blnAsync);
    },

    /**
     * Start me up.
     */
    initialize: function() {

        ////////////////////////////////
        // Browser-related functionality
        ////////////////////////////////

        this.loadJavaScriptFile = function(strScript, objCallback) {
            if (strScript.indexOf("/") === 0) {
                strScript = qc.baseDir + strScript;
            } else if (strScript.indexOf("http") !== 0) {
                strScript = qc.jsAssets + "/" + strScript;
            }
            return $j.ajax({
                url: strScript,
                success: objCallback,
                dataType: "script",
                cache: true
            });
        };

        this.loadStyleSheetFile = function(strStyleSheetFile, strMediaType) {
            if (strStyleSheetFile.indexOf("/") === 0) {
                strStyleSheetFile = qc.baseDir + strStyleSheetFile;
            } else if (strStyleSheetFile.indexOf("http") !== 0) {
                strStyleSheetFile = qc.cssAssets + "/" + strStyleSheetFile;
            }
            if (strMediaType){
                strMediaType = " media="+strMediaType;
            }
            $j('head').append('<link rel="stylesheet"'+strMediaType+' href="' + strStyleSheetFile + '" type="text/css" />');
        };

        /////////////////////////////
        // QForm-related functionality
        /////////////////////////////

        this.wrappers = [];

        $j(window).on ("storage", function (o) {
            if (o.originalEvent.key == "qcubed.broadcast") {
                qcubed.updateForm();
            }
        });

        this.inputSupport = 'oninput' in document;

        // Detect browsers that do not correctly support the oninput event, even though they say they do.
        // IE 9 in particular has a major bug
        var ua = window.navigator.userAgent;
        var intIeOffset = ua.indexOf ('MSIE');
        if (intIeOffset > -1) {
            var intOffset2 = ua.indexOf ('.', intIeOffset + 5);
            var strVersion = ua.substr (intIeOffset + 5, intOffset2 - intIeOffset - 5);
            if (strVersion < 10) {
                this.inputSupport = false;
            }
        }

        $j( document ).ajaxComplete(function( event, request, settings ) {
            if (!$j.ajaxQueueIsRunning()) {
                qcubed.processFinalCommands();
            }
        });


        return this;
    },
    processImmediateAjaxResponse: function(json, qFormParams) {
        if (json.controls) $j.each(json.controls, function() {
            var strControlId = '#' + this.id,
                control = $j(strControlId),
                wrapper = $j(strControlId + '_ctl');

            if (this.value !== undefined) {
                control.val(this.value);
            }

            if (this.attributes !== undefined) {
                control.attr (this.attributes);
            }

            if (this.html !== undefined) {
                if (wrapper.length) {
                    // Control's wrapper was found, so fill it in
                    wrapper.html(this.html);
                }
                else if (control.length) {
                    // control was found without a wrapper, replace it in the same position it was in.
                    // remove related controls (error, name ...) for wrapper-less controls
                    var relSelector = "[data-qrel='" + strControlId + "']",
                        relItems = $j(relSelector),
                        $relParent;

                    if (relItems && relItems.length) {
                        // if the control is wrapped in a related control, we move the control outside the related controls
                        // before deleting the related controls
                        $relParent = control.parents(relSelector).last();
                        if ($relParent.length) {
                            control.insertBefore($relParent);
                        }
                        relItems.remove();
                    }

                    control.before(this.html).remove();
                }
                else {
                    // control is being injected at the top level, so put it at the end of the form.
                    var strForm = $j("#Qform__FormId").val();
                    var $objForm = $j('#' + strForm);

                    $objForm.append(this.html);
                }
            }
        });

        if (json.regc) {
            qcubed.registerControlArray (json.regc);
        }

        if (json.watcher && qFormParams.control) {
            qcubed.broadcastChange();
        }
        if (json.ss) {
            $j.each(json.ss, function (i,v) {
                qc.loadStyleSheetFile(v, "all");
            });
        }
        if (json.alert) {
            $j.each(json.alert, function (i,v) {
                alert(v);
            });
        }
    },
    processDeferredAjaxResponse: function(json) {
        if (json.commands) { // commands
            $j.each(json.commands, function (index, command) {
                if (command.final &&
                    $j.ajaxQueueIsRunning()) {

                    qcubed.enqueueFinalCommand(command);
                } else {
                    qcubed.processCommand(command);
                }
            });
        }
        if (json.winclose) {
            window.close();
        }
        if (json.loc) {
            if (json.loc == 'reload') {
                window.location.reload(true);
            } else {
                document.location = json.loc;
            }
        }

        if (qcubed.objAjaxWaitIcon) {
            $j(qcubed.objAjaxWaitIcon).hide();
        }
    },
    processCommand: function(command) {
        if (command.script) {
            /** @todo eval is evil, do no evil */
            eval (command.script);
        }
        else if (command.selector) {
            var params = qc.unpackArray(command.params);
            var objs;

            if (typeof command.selector === 'string') {
                objs = $j(command.selector);
            } else {
                objs = $j(command.selector[0], command.selector[1]);
            }

            // apply the function on each jQuery object found, using the found jQuery object as the context.
            objs.each (function () {
                var $item = $j(this);
                if ($item[command.func]) {
                    $item[command.func].apply($j(this), params);
                }
            });
        }
        else if (command.func) {
            var params = qc.unpackArray(command.params);

            // Find the function by name. Walk an object list in the process.
            var objs = command.func.split(".");
            var obj = window;
            var ctx = null;

            $j.each (objs, function (i, v) {
                ctx = obj;
                obj = obj[v];
            });
            // obj is now a function object, and ctx is the parent of the function object
            obj.apply(ctx, params);
        }

    },
    enqueueFinalCommand: function(command) {
        qcubed.finalCommands.push(command);
    },
    processFinalCommands: function() {
        while(qcubed.finalCommands.length) {
            var command = qcubed.finalCommands.pop();
            qcubed.processCommand(command);
        }
    },
    /**
     * Convert from JSON return value to an actual jQuery object. Certain structures don't work in JSON, like closures,
     * but can be part of a javascript object.
     * @param params
     * @returns {*}
     */
    unpackArray: function(params) {
        if (!params) {
            return null;
        }
        var newParams = [];

        $j.each(params, function (index, item){
            if ($j.type(item) == 'object') {
                if (item.qObjType) {
                    item = qcubed.unpackObj(item);  // top level special object
                }
                else {
                    // look for special objects inside top level objects.
                    var newItem = {};
                    $j.each (item, function (key, obj) {
                        newItem[key] = qcubed.unpackObj(obj);
                    });
                    item = newItem;
                }
            }
            else if ($j.type(item) == 'array') {
                item = qcubed.unpackArray (item);
            }
            newParams.push(item);
        });
        return newParams;
    },

    /**
     * Given an object coming from qcubed, will attempt to decode the object into a corresponding javascript object.
     * @param obj
     * @returns {*}
     */
    unpackObj: function (obj) {
        if ($j.type(obj) == 'object' &&
                obj.qObjType) {

            switch (obj.qObjType) {
                case 'qClosure':
                    if (obj.params) {
                        params = [];
                        $j.each (obj.params, function (i, v) {
                            params.push(qc.unpackObj(v)); // recurse
                        });

                        return new Function(params, obj.func);
                    } else {
                        return new Function(obj.func);
                    }
                    break;

                case 'qDateTime':
                    return new Date(obj.year, obj.month, obj.day, obj.hour, obj.minute, obj.second);
                    break;

                case 'qVarName':
                    // Find the variable value starting at the window context.
                    var vars = obj.varName.split(".");
                    var val = window;
                    $j.each (vars, function (i, v) {
                        val = val[v];
                    });
                    return val;

                case 'qFunc':
                    // Returns the result of the given function called immediately
                    // Find the function and context starting at the window context.
                    var target = window;
                    var params;
                    if (obj.context) {
                       var objects = obj.context.split(".");
                        $j.each (objects, function (i, v) {
                            target = target[v];
                        });
                    }

                    if (obj.params) {
                        params = [];
                        $j.each (obj.params, function (i, v) {
                            params.push(qc.unpackObj(v)); // recurse
                        });
                    }
                    var func = target[obj.func];

                    return func.apply(target, params);
            }
        }
        else if ($j.type(obj) == 'object') {
            var newItem = {};
            $j.each (obj, function (key, obj2) {
                newItem[key] = qcubed.unpackObj(obj2);
            });
            return newItem;
        }
        else if ($j.type(obj) == 'array') {
            return qcubed.unpackArray(obj);
        }
        return obj; // no change
    },
    setCookie: function(name, val, expires, path, dom, secure) {
            var cookie = name + "=" + encodeURIComponent(val) + "; ";

            if (expires) {
                cookie += "expires=" + expires.toUTCString() + "; ";
            }

            if (path) {
                cookie += "path=" + path + "; ";
            }
            if (dom) {
                cookie += "domain=" + dom + "; ";
            }
            if (secure) {
                cookie += "secure;";
            }

            document.cookie = cookie;
        }
};

///////////////////////////////
// Timers-related functionality
///////////////////////////////

qcubed._objTimers = {};

qcubed.clearTimeout = function(strTimerId) {
    if (qcubed._objTimers[strTimerId]) {
        clearTimeout(qcubed._objTimers[strTimerId]);
        qcubed._objTimers[strTimerId] = null;
    }
};

qcubed.setTimeout = function(strTimerId, action, intDelay) {
    qcubed.clearTimeout(strTimerId);
    qcubed._objTimers[strTimerId] = setTimeout(action, intDelay);
};

///////////////////////////////
// QWatcher support
///////////////////////////////
qcubed._prevUpdateTime = 0;
qcubed.minUpdateInterval = 1000; // milliseconds to limit broadcast updates. Feel free to change this.
qcubed.broadcastChange = function () {
    if ('localStorage' in window && window['localStorage'] !== null) {
        var newTime = new Date().getTime();
        localStorage.setItem("qcubed.broadcast", newTime); // must change value to induce storage event in other windows
    }
};

qcubed.updateForm = function() {
    // call this whenever you generally just need the form to update without a specific action.
    var newTime = new Date().getTime();

    // the following code prevents too many updates from happening in a short amount of time.
    // the default will update no faster than once per second.
    if (newTime - qcubed._prevUpdateTime >= qcubed.minUpdateInterval) {
        //refresh immediately
        var strForm = $j('#Qform__FormId').val();
        qcubed.postAjax (strForm, '', '', '', '');
        qcubed.clearTimeout ('qcubed.update');
    } else if (!qcubed._objTimers['qcubed.update']) {
        // delay to let multiple fast actions only trigger periodic refreshes
        qcubed.setTimeout ('qcubed.update', 'qcubed.updateForm', qcubed.minUpdateInterval);
    }
};

/////////////////////////////////////
// Drag and drop support
/////////////////////////////////////

qcubed.draggable = function (parentId, draggableId) {
    // we are working around some jQuery UI bugs here..
    jQuery('#' + parentId).on("dragstart", function () {
        var c = jQuery(this);
        c.data ("originalPosition", c.position());
    }).on("dragstop", function () {
        var c = jQuery(this);
        qcubed.recordControlModification(draggableId, "_DragData", {originalPosition: {left: c.data("originalPosition").left, top: c.data("originalPosition").top}, position: {left: c.position().left, top: c.position().top}});
    });
};

qcubed.droppable = function (parentId, droppableId) {
    jQuery('#' + parentId).on("drop", function (event, ui) {
        qcubed.recordControlModification(droppableId, "_DroppedId", ui.draggable.attr("id"));
    })
};

qcubed.resizable = function (parentId, resizeableId) {
    $j('#' + parentId).on("resizestart", function () {
        var c = jQuery(this);
        c.data ("oW", c.width());
        c.data ("oH", c.height());
    })
    .on("resizestop", function () {
        var c = jQuery(this);
        qcubed.recordControlModification(resizeableId, "_ResizeData", {originalSize: {width: c.data("oW"), height: c.data("oH")} , size:{width: c.width(), height: c.height()}});
    });
}

/////////////////////////////////////
// JQueryUI Support
/////////////////////////////////////

qcubed.dialog = function(controlId) {
    $j('#' + controlId).on ("keydown", "input,select", function(event) {
        // makes sure a return key fires the default button if there is one
        if (event.which == 13) {
            var b = $j(this).closest("[role=\'dialog\']").find("button[type=\'submit\']");
            if (b && b[0]) {
                b[0].click();
            }
            event.preventDefault();
        }
    });
};

qcubed.accordion = function(controlId) {
    $j('#' + controlId).on("accordionactivate", function(event, ui) {
        qcubed.recordControlModification(controlId, "_SelectedIndex", $j(this).accordion("option", "active"));
        $j(this).trigger("change");
    });
};

qcubed.progressbar = function(controlId) {
    $j('#' + controlId).on("progressbarchange", function (event, ui) {
        qcubed.recordControlModification(controlId, "_Value", $j(this).progressbar ("value"));
    });
};

qcubed.selectable = function(controlId) {
    $j('#' + controlId).on("selectablestop", function (event, ui) {
        var strItems;

        strItems = "";
        $j(".ui-selected", this).each(function() {
            strItems = strItems + "," + this.id;
        });

        if (strItems) {
            strItems = strItems.substring (1);
        }
        qcubed.recordControlModification(controlId, "_SelectedItems", strItems);

    });
};

qcubed.slider = function(controlId) {
    $j('#' + controlId).on("slidechange", function (event, ui) {
        if (ui.values && ui.values.length) {
            qcubed.recordControlModification(controlId, "_Values", ui.values[0] + ',' +  ui.values[1]);
        } else {
            qcubed.recordControlModification(controlId, "_Value", ui.value);
        }
    });
};

qcubed.tabs = function(controlId) {
    $j('#' + controlId).on("tabsactivate", function(event, ui) {
        var i = $j(this).tabs( "option", "active" );
        var id = ui.newPanel ? ui.newPanel.attr("id") : null;
        qcubed.recordControlModification(controlId, "_active", [i,id]);
    });
};

qcubed.datagrid2 = function(controlId) {
    $j('#' + controlId).on("click", "thead tr th a", function(event, ui) {
        var cellIndex = $j(this).parent()[0].cellIndex;
        $j(this).trigger('qdg2sort', cellIndex); // Triggers the QDataGrid_SortEvent
    });
};

qcubed.dialog = function(controlId) {
    $j('#' + controlId).on("tabsactivate", function(event, ui) {
        var i = $j(this).tabs( "option", "active" );
        var id = ui.newPanel ? ui.newPanel.attr("id") : null;
        qcubed.recordControlModification(controlId, "_active", [i,id]);
    });
}

/////////////////////////////////
// Controls-related functionality
/////////////////////////////////

qcubed.getControl = function(mixControl) {
    if (typeof mixControl === 'string') {
        return document.getElementById(mixControl);
    } else {
        return mixControl;
    }
};

qcubed.getWrapper = function(mixControl) {
    var objControl = qcubed.getControl(mixControl);

    if (!objControl) {
        //maybe it doesn't have a child control, just the wrapper
        if (typeof mixControl === 'string') {
            return this.getControl(mixControl + "_ctl");
        }
        return null;
    } else if (objControl.wrapper) {
        return objControl.wrapper;
    }

    return objControl; //a wrapper-less control, return the control itself
};

/**
 * Radio buttons are a little tricky to set if they are part of a group
 * @param strControlId
 */
qcubed.setRadioInGroup = function(strControlId) {
    var $objControl = $j('#' + strControlId);
    if ($objControl) {
        var groupName = $objControl.prop('name');
        if (groupName) {
            var $radios = $objControl.closest('form').find('input[type=radio][name=' + groupName + ']');
            $radios.val([strControlId]);  // jquery does the work here of setting just the one control
            $radios.trigger('qformObjChanged'); // send the new values back to the form
        }
    }
};

/////////////////////////////
// Register Control - General
/////////////////////////////

qcubed.controlModifications = {};
qcubed.javascriptStyleToQcubed = {};
qcubed.formObjsModified = {};
qcubed.additionalPostVars = {};
qcubed.ajaxError = false;
qcubed.inputSupport = true;
qcubed.javascriptStyleToQcubed.backgroundColor = "BackColor";
qcubed.javascriptStyleToQcubed.borderColor = "BorderColor";
qcubed.javascriptStyleToQcubed.borderStyle = "BorderStyle";
qcubed.javascriptStyleToQcubed.border = "BorderWidth";
qcubed.javascriptStyleToQcubed.height = "Height";
qcubed.javascriptStyleToQcubed.width = "Width";
qcubed.javascriptStyleToQcubed.text = "Text";

qcubed.javascriptWrapperStyleToQcubed = {};
qcubed.javascriptWrapperStyleToQcubed.position = "Position";
qcubed.javascriptWrapperStyleToQcubed.top = "Top";
qcubed.javascriptWrapperStyleToQcubed.left = "Left";

qcubed.blockEvents = false;
qcubed.finalCommands = [];

qcubed.registerControl = function(mixControl) {
    var objControl = qcubed.getControl(mixControl),
        objWrapper;

    if (!objControl) {
        return;
    }

    // detect changes to objects before any changes trigger other events
    if (objControl.type === 'checkbox' || objControl.type === 'radio') {
        // clicks are equivalent to changes for checkboxes and radio buttons, but some browsers send change way after a click. We need to capture the click first.
        $j(objControl).on ('click', this.formObjChanged);
    }
    $j(objControl).on ('change input', this.formObjChanged);
    $j(objControl).on ('change input', 'input, select, textarea', this.formObjChanged);   // make sure we get to bubbled events before later attached handlers


    // Link the Wrapper and the Control together
    objWrapper = this.getControl(objControl.id + "_ctl");
    if (!objWrapper) {
        objWrapper = objControl; //wrapper-less control
    } else {
        objWrapper.control = objControl;
        objControl.wrapper = objWrapper;

        // Add the wrapper to the global qcubed wrappers array
        qcubed.wrappers[objWrapper.id] = objWrapper;
    }

    // track change events


    // Create New Methods, etc.
    // Like: objWrapper.something = xyz;

    /**
     * This function was originally intended to be used by javascript to manipulate QControl objects and have the result
     * reported back to the PHP side. Modern jQuery objects now have events that can be hooked to catch changes to
     * objects, and using those events is probably a better approach in most cases. Various jQuery UI base QControls
     * use this method. In any case, you can use this as a model for how to use the recordControlModification function
     * to send results to PHP objects.
     *
     * @param strStyleName
     * @param strNewValue
     */
    objWrapper.updateStyle = function(strStyleName, strNewValue) {
        var objControl = (this.control) ? this.control : this,
            objNewParentControl,
            objParentControl,
            $this;

        switch (strStyleName) {
            case "className":
                objControl.className = strNewValue;
                qcubed.recordControlModification(objControl.id, "CssClass", strNewValue);
                break;

            case "parent":
                if (strNewValue) {
                    objNewParentControl = qcubed.getControl(strNewValue);
                    objNewParentControl.appendChild(this);
                    qcubed.recordControlModification(objControl.id, "Parent", strNewValue);
                } else {
                    objParentControl = this.parentNode;
                    objParentControl.removeChild(this);
                    qcubed.recordControlModification(objControl.id, "Parent", "");
                }
                break;

            case "displayStyle":
                objControl.style.display = strNewValue;
                qcubed.recordControlModification(objControl.id, "DisplayStyle", strNewValue);
                break;

            case "display":
                $this = $j(this);
                if (strNewValue) {
                    $this.show();
                    qcubed.recordControlModification(objControl.id, "Display", "1");
                } else {
                    $this.hide();
                    qcubed.recordControlModification(objControl.id, "Display", "0");
                }
                break;

            case "enabled":
                if (strNewValue) {
                    objControl.disabled = false;
                    qcubed.recordControlModification(objControl.id, "Enabled", "1");
                } else {
                    objControl.disabled = true;
                    qcubed.recordControlModification(objControl.id, "Enabled", "0");
                }
                break;

            case "width":
            case "height":
                objControl.style[strStyleName] = strNewValue;
                if (qcubed.javascriptStyleToQcubed[strStyleName]) {
                    qcubed.recordControlModification(objControl.id, qcubed.javascriptStyleToQcubed[strStyleName], strNewValue);
                }
                /* ???
                if (this.handle) {
                    this.updateHandle();
                }*/
                break;

            case "text":
                objControl.innerHTML = strNewValue;
                qcubed.recordControlModification(objControl.id, "Text", strNewValue);
                break;

            default:
                if (qcubed.javascriptWrapperStyleToQcubed[strStyleName]) {
                    this.style[strStyleName] = strNewValue;
                    qcubed.recordControlModification(objControl.id, qcubed.javascriptWrapperStyleToQcubed[strStyleName], strNewValue);
                } else {
                    objControl.style[strStyleName] = strNewValue;
                    if (qcubed.javascriptStyleToQcubed[strStyleName]) {
                        qcubed.recordControlModification(objControl.id, qcubed.javascriptStyleToQcubed[strStyleName], strNewValue);
                    }
                }
                break;
        }
    };

    // Positioning-related functions

    objWrapper.getAbsolutePosition = function() {
        var objControl = (this.control) ? this.control : this,
            pos = $j(objControl).offset();

        return {x: pos.left, y: pos.top};
    };

    objWrapper.setAbsolutePosition = function(intNewX, intNewY, blnBindToParent) {
        var objControl = this.offsetParent;

        while (objControl) {
            intNewX -= objControl.offsetLeft;
            intNewY -= objControl.offsetTop;
            objControl = objControl.offsetParent;
        }

        if (blnBindToParent) {
            if (this.parentNode.nodeName.toLowerCase() !== 'form') {
                // intNewX and intNewY must be within the parent's control
                intNewX = Math.max(intNewX, 0);
                intNewY = Math.max(intNewY, 0);

                intNewX = Math.min(intNewX, this.offsetParent.offsetWidth - this.offsetWidth);
                intNewY = Math.min(intNewY, this.offsetParent.offsetHeight - this.offsetHeight);
            }
        }

        this.updateStyle("left", intNewX + "px");
        this.updateStyle("top", intNewY + "px");
    };

    // Toggle Display / Enabled
    objWrapper.toggleDisplay = function(strShowOrHide) {
        var strDisplay = "display";
        // Toggles the display/hiding of the entire control (including any design/wrapper HTML)
        // If ShowOrHide is blank, then we toggle
        // Otherwise, we'll execute a "show" or a "hide"
        if (strShowOrHide) {
            if (strShowOrHide === "show") {
                this.updateStyle(strDisplay, true);
            } else {
                this.updateStyle(strDisplay, false);
            }
        } else
            this.updateStyle(strDisplay, (this.style.display === "none"));
    };

    objWrapper.toggleEnabled = function(strEnableOrDisable) {
        var objControl = (this.control) ? this.control : this,
            strEnabled = "enabled";

        if (strEnableOrDisable) {
            if (strEnableOrDisable === "enable") {
                this.updateStyle(strEnabled, true);
            } else {
                this.updateStyle(strEnabled, false);
            }
        } else {
            this.updateStyle(strEnabled, objControl.disabled);
        }
    };

    objWrapper.registerClickPosition = function(objEvent) {
        var objControl = (this.control) ? this.control : this,
            intX = objEvent.pageX - objControl.offsetLeft,
            intY = objEvent.pageY - objControl.offsetTop;

        $j('#' + objControl.id + "_x").val(intX);
        $j('#' + objControl.id + "_y").val(intY);
        $j(objControl).trigger('qformObjChanged');
    };

    // Focus
    if (objWrapper.control) {
        objWrapper.focus = function() {
            $j(this.control).focus();
        };
    }

    // Select All (will only work for textboxes)
    if (objWrapper.control) {
        objWrapper.select = function() {
            $j(this.control).select();
        };
    }

    // Blink
    objWrapper.blink = function(strFromColor, strToColor) {
        var objControl = (this.control) ? this.control : this;

        $j(objControl)
            .css('background-color', '' + strFromColor)
            .animate({backgroundColor: '' + strToColor}, 500);
    };
};

qcubed.registerControlArray = function(mixControlArray) {
    var intLength = mixControlArray.length,
        intIndex;

    for (intIndex = 0; intIndex < intLength; intIndex++) {
        this.registerControl(mixControlArray[intIndex]);
    }
};

////////////////////////////////
// QCubed Shortcuts and Initialize
////////////////////////////////

qc = qcubed;
qc.pB = qc.postBack;
qc.pA = qc.postAjax;
qc.getC = qc.getControl;
qc.getW = qc.getWrapper;
qc.regC = qc.registerControl;
qc.regCA = qc.registerControlArray;
qc.recCM = qc.recordControlModification;

qc.initialize();