Source: winjscontrib.ui.dataform.js

/* 
 * WinJS Contrib v2.1.0.6
 * licensed under MIT license (see http://opensource.org/licenses/MIT)
 * sources available at https://github.com/gleborgne/winjscontrib
 */

(function () {
    'use strict';
    
    var DataFormState = WinJS.Class.mix(WinJS.Class.define(function () {
        this._initObservable();
    }, {
    }), WinJS.Binding.mixin, WinJS.Binding.expandProperties({ isValid: false, updated: false }));

    WinJS.Namespace.define("WinJSContrib.UI", {
        DataForm: WinJS.Class.mix(WinJS.Class.define(
            /**
             * @class WinJSContrib.UI.DataForm
             * @classdesc
             * This control enhance data form management by adding validation mecanism and form state helpers. It must be placed on a form element.
             * Input fields must use {@link WinJSContrib.UI.DataFormBinding} to bind object properties to input
             * @param {HTMLElement} element DOM element containing the control
             * @param {Object} options
             */
            function ctor(element, options) {
                var ctrl = this;
                this.element = element || document.createElement('FORM');
                //$(this.element).submit(function (e) {
                //    e.preventDefault()
                //})
                options = options || {};
                this.groups = options.groups;
                this.messages = options.messages;
                this.rules = options.rules;
                /**
                 * state of the form
                 * @field
                 * @type {Object}
                 */
                this.state = new DataFormState();
                this.state.item = {};
                this.allowTooltip = options.allowTooltip || true;
                this.workOnCopy = options.workOnCopy || true;
                this.tooltipDelay = options.tooltipDelay || 4000;
                this.tooltipPosition = options.tooltipPosition || 'right';
                this.tooltipTheme = options.tooltipTheme || 'tooltipster-error';
                this.initValidator();
                this.element.winControl = this;
                this.element.mcnDataForm = true;
                this.element.classList.add('win-disposable');
                if (WinJSContrib.CrossPlatform && WinJSContrib.CrossPlatform.crossPlatformClass)
                    WinJSContrib.CrossPlatform.crossPlatformClass(this.element);
                WinJS.UI.setOptions(this, options);
                WinJS.UI.processAll(this.element).done(function () {
                    WinJS.Binding.processAll(ctrl.element, ctrl.state);
                });

                $('.mcn-dataform-cancel', this.element).click(function (arg) {
                    arg.preventDefault();
                    ctrl.cancel();
                });
            },
            /**
             * @lends WinJSContrib.UI.DataForm.prototype
             */
        {
            /**
             * @member {Array}
             */
            messages: {
                get: function () {
                    return this._messages;
                },
                set: function (val) {
                    this._messages = val;
                }
            },
            /**
             * @member {Array}
             */
            rules: {
                get: function () {
                    return this._rules;
                },
                set: function (val) {
                    this._rules = val;
                }
            },
            /**
             * @member {Array}
             */
            groups: {
                get: function () {
                    return this._groups;
                },
                set: function (val) {
                    this._groups = val;
                }
            },
            /**
             * object bound to data form
             * @member {Object}
             */
            item: {
                get: function () {
                    return this.state.item;
                },
                set: function (val) {
                    var dataform = this;
                    if (dataform.workOnCopy) {
                        dataform.state.item = $.extend(true, {}, val);
                        dataform.state.refItem = val;
                    }
                    else {
                        dataform.state.item = val;
                    }

                    
                    dataform.validator.resetForm();

                    dataform.autobindFields();
                    WinJS.Binding.processAll(this.element, this.state).done(function () {
                        var tooltips = dataform.allowTooltip;
                        dataform.allowTooltip = false;
                        dataform.validator.form();
                        dataform.allowTooltip = tooltips;
                        dataform.checkState();
                        dataform.initValidator();
                        dataform.state.updated = false;
                    });
                    dataform.dispatchEvent("itemchanged", { dataform: this, item: val });
                }
            },
            /**
             * indicate if form has updates
             * @member {boolean}
             */
            updated: {
                get: function () {
                    return this.state.updated;
                },
                set: function (val) {
                    this.state.updated = val;
                    this.dispatchEvent("haschanges");
                }
            },

            autobindFields: function () {
                var ctrl = this;
                $('[data-formfield]', ctrl.element).each(function () {
                    var fieldElt = this;
                    var srcProperty = $(fieldElt).data('formfield');
                    var destFieldType = 'value';
                    if (fieldElt.type == 'checkbox') {
                        destFieldType = 'checked';
                    }
                    WinJSContrib.UI.DataFormBinding(ctrl.state.item, srcProperty.split('.'), fieldElt, [destFieldType]);
                });
            },

            /**
             * check form state
             */
            checkState: function () {
                var nbInvalids = this.validator.numberOfInvalids();
                this.state.isValid = nbInvalids == 0;
            },

            /**
             * cancel updates on form item
             */
            cancel: function () {
                var dataform = this;
                if (dataform.workOnCopy) {
                    dataform.item = $.extend(true, {}, dataform.state.refItem);
                }
            },

            /**
             * apply changes to source object (relevant only if using workOnCopy)
             */
            save: function () {
                var dataform = this;
                if (dataform.workOnCopy) {
                    dataform.state.updated = false;
                    dataform.state.refItem = $.extend(true, {}, dataform.state.item);
                }
            },

            initValidator: function () {
                var dataform = this;

                this.validator = $(this.element).validate({
                    groups: this.groups,
                    rules: this.rules,
                    submitHandler: function (form) {
                        dataform.dispatchEvent('submitted', { item: dataform.state.item });
                    },

                    invalidHandler: function (form, validator) {
                        dataform.checkState();
                    },

                    errorPlacement: function (error, element) {
                        dataform.checkState();
                        if (!dataform.allowTooltip)
                            return;

                        var $e = $(element);
                        $e.tooltipster({
                            trigger: 'custom',
                            onlyOne: false,
                            position: dataform.tooltipPosition,
                            theme: dataform.tooltipTheme
                        });

                        var lastError = $e.data('lastError'),
                            newError = $(error).text();

                        $e.data('lastError', newError);

                        if (newError !== '' && newError !== lastError) {
                            $e.tooltipster('content', newError);
                            $e.tooltipster('show');
                            $e[0].tooltipsterValidationTimeout = setTimeout(function () {
                                $e[0].tooltipsterValidationTimeout = null;
                                if ($e.hasClass('tooltipster'))
                                    $e.tooltipster('hide');
                            }, dataform.tooltipDelay);
                        }
                    },

                    success: function (label, element) {
                        dataform.checkState();
                        var $e = $(element);
                        if ($e[0].tooltipsterValidationTimeout) {
                            clearTimeout($e[0].tooltipsterValidationTimeout);
                            $e[0].tooltipsterValidationTimeout = null;
                        }

                        if ($e.hasClass('tooltipstered')) {
                            $e.tooltipster('hide');
                        }
                    }

                });
            },

            /**
             * validate form
             */
            validate: function () {
                var res = this.validator.form();
                return res;
            },

            /**
             * release form
             */
            dispose: function () {
                WinJS.Utilities.disposeSubTree(this.element);
            }
        },
        /**
         * @lends WinJSContrib.UI.DataForm
         */
        {
            /**
             * @namespace WinJSContrib.UI.DataForm.defaultBindingOptions
             */
            DefaultBindingOptions: {
                trimText: true,
                convertEmptyToNull: true,
            },

            /**
             * @namespace WinJSContrib.UI.DataForm.Converters
             */
            Converters: {
                /**
                 * @member
                 */
                "none": {
                    fromObject: function (val, options) {
                        return val.toString();
                    },
                    fromInput: function (val, options) {
                        return val;
                    }
                },
                /**
                 * @member
                 */
                "text": {
                    fromObject: function (val, options) {
                        if (typeof val === "undefined" || val === null)
                            return '';

                        var res = val.toString();
                        
                        return res;
                    },
                    fromInput: function (val, options) {
                        var res = val;
                        if (res && options && options.trimText) {
                            res = res.trim();
                        }
                        if (options && options.convertEmptyToNull) {
                            res = (res === '') ? null : res;
                        }
                        return res;
                    }
                },
                /**
                 * @member
                 */
                "number": {
                    fromObject: function (val, options) {
                        if (typeof val !== "number")
                            return '';

                        return val.toString();
                    },
                    fromInput: function (val, options) {
                        if (typeof val !== "undefined" && val !== null)
                          //return parseFloat(val);
                          return parseFloat(val.toString().replace(',', '.').replace(' ', ''));

                        return null;
                    }
                },
                /**
                 * @member
                 */
                "boolean": {
                    fromObject: function (val, options) {
                        if (typeof val === "undefined" || val === null)
                            return '';

                        return val.toString();
                    },
                    fromInput: function (val, options) {
                        if (val === 'true')
                            return true;
                        if (val === 'false')
                            return false;

                        return null;
                    }
                },
                /**
                 * @member
                 */
                "object": {
                    fromObject: function (val, options) {
                        return val;
                    },
                    fromInput: function (val, options) {
                        return val;
                    }
                },
                /**
                 * @member
                 */
                "stringifiedObject": {
                    fromObject: function (val, options) {
                        return JSON.stringify(val);
                    },
                    fromInput: function (val, options) {
                        return JSON.parse(val);
                    }
                }
            }
        }),
        WinJS.UI.DOMEventMixin,
        WinJS.Utilities.createEventProperties("itemchanged", "haschanges", "submitted")),

        parentDataForm: function (element) {
            var current = element.parentNode;

            while (current) {
                if (current.mcnDataForm) {
                    return current.winControl;
                }
                current = current.parentNode;
            }
        },

        

        /**
         * bi-directional binding for working with input fields and custom input controls. This binding expect a {@link WinJSContrib.UI.DataForm} to be found on the parent form
         * @function WinJSContrib.UI.DataFormBinding
         * @param {Object} source object owning data
         * @param {string[]} sourceProperty path to object data
         * @param {HTMLElement} dest DOM element targeted by binding
         * @param {string[]} destProperty path to DOM element property targeted by binding
         */
        DataFormBinding: WinJS.Binding.initializer(function (source, sourceProperty, dest, destProperty) {
            //if (dest.binded && dest.winControl)
            //    dest.winControl.dispose();

            var dataform = WinJSContrib.UI.parentDataForm(dest);
            var options = WinJSContrib.UI.DataForm.DefaultBindingOptions;
            var optionsText = dest.getAttribute("data-formfield-options");
            if (optionsText) {
                options = WinJS.UI.optionsParser(optionsText, window);
            }
            var inputType = "";
            var inputOption = dest.getAttribute("data-forminput");
            if (inputOption) {
                inputType = inputOption;
            }

            var fieldUpdated = false;
            dest.classList.add('mcn-dataform-field');

            //si le noeud n'est pas un champ input html, on renseigne la propriété form, sinon plantage de jquery validate
            if (!dest.form) {
                dest.form = dataform.element;
            }

            var inputType = 'text';
            if ($(dest).data('formfield-type')) {
                inputType = $(dest).data('formfield-type');
            }
            else if (dest.nodeName !== "TEXTAREA" && typeof dest.type !== "undefined") {
                inputType = dest.type;
            }
            var converter = WinJSContrib.UI.DataForm.Converters[inputType] || WinJSContrib.UI.DataForm.Converters['text'];

            function updateInputFromObject() {
                var data = WinJSContrib.Utils.readProperty(source, sourceProperty);
                if (typeof data === "undefined")
                    data = null;

                if (dest.nodeName == "INPUT" && dest.type == "radio") {
                    var fieldname = dest.name;
                    if (dest.value == data) {
                        dest.checked = true;
                    }
                } else {
                    data = converter.fromObject(data, options);
                    WinJSContrib.Utils.writeProperty(dest, destProperty, data);
                }
            }

            function updateObjectFromInput() {
                dataform.checkState();
                if (!dest.id || dataform.validator.element(dest)) {
                    var val = null;
                    if (dest.nodeName == "INPUT" && dest.type == "radio") {
                        var fieldname = dest.name;
                        if (dest.form && dest.form[fieldname] && dest.form[fieldname].length) {
                            for (var i = 0, l = dest.form[fieldname].length; i < l ; i++) {
                                var field = dest.form[fieldname][i];
                                if (field.checked){
                                    val = field.value;
                                    break;
                                }
                            }
                        }
                    } else {
                        val = WinJSContrib.Utils.getProperty(dest, destProperty).propValue;
                    }

                    if (val !== undefined)
                        val = converter.fromInput(val, options);

                    WinJSContrib.Utils.writeProperty(source, sourceProperty, val);
                }
                dataform.updated = true;
            }

            function validateObjectOnBlur() {
                if (fieldUpdated)
                    dataform.validator.element(dest);
            }
            if (!dest.binded)
                dest.binded = true;


            if (!dest.winControl) {
                dest.classList.add('win-disposable');
                dest.winControl = {
                    dispose: function () {
                        dest.removeEventListener("change", dest.winControl.updateObjectFromInput);
                        dest.removeEventListener("blur", dest.winControl.validateObjectOnBlur);
                        if (inputType) {
                            dest.removeEventListener(inputType, dest.winControl.updateObjectFromInput);
                        }
                    }
                }
            }
            dest.winControl.updateObjectFromInput = updateObjectFromInput;
            dest.winControl.validateObjectOnBlur = validateObjectOnBlur;

            if (inputType) {
                dest.addEventListener(inputType, dest.winControl.updateObjectFromInput);
            }
            dest.addEventListener("change", dest.winControl.updateObjectFromInput);
            if (dest.id) {
                dest.addEventListener("blur", validateObjectOnBlur);
            }

            var bindingDesc = {
            };

            bindingDesc[sourceProperty] = updateInputFromObject;
            return WinJS.Binding.bind(source, bindingDesc);
        })
    });
})();