transForm.plugin.js 11.6 KB
/*
//Defaults can be overwritten in $tyle
$.fn.transForm.defaults.useIdOnEmptyName = false;
//Methods can be chained
$('div').transForm('deserialize', {library: 'jQuery/Zepto'}).transForm('submit');
//Return value of serialize is an array of objects
var test = $('div').transForm('serialize');
//TODO:
- Tests
- MOAR $$$ functions!
*/

(function ($) {

    var methods = {
        serialize: serialize,
        deserialize: deserialize,
        clear: clear,
        submit: submit,
    };

    $.extend($.fn, {
        transForm: function (methodOrOptions) {
            if (methods[methodOrOptions]) {
                var args = Array.prototype.slice.call(arguments, 1);
                if (methodOrOptions === 'serialize') {
                    var result = [];
                    this.each(function () {
                        result.push(methods[methodOrOptions].apply(this, args));
                    });
                    return result;
                } else
                    return this.each(function () {
                        methods[methodOrOptions].apply(this, args);
                    });
            } else {
                error('Method ' + methodOrOptions + ' does not exist on transForm.js');
            }
        }
    });

    $.fn.transForm.defaults = {
        delimiter: '.',
        skipDisabled: true,
        skipReadOnly: false,
        skipFalsy: false,
        useIdOnEmptyName: true,
        triggerChange: false
    };

    /* Serialize */
    function serialize(options, nodeCallback) {
        var result = {},
            opts = getOptions(options),
            inputs = getFields(this, opts.skipDisabled, opts.skipReadOnly),
            skipFalsy = opts.skipFalsy,
            delimiter = opts.delimiter,
            useIdOnEmptyName = opts.useIdOnEmptyName;

        for (var i = 0, l = inputs.length; i < l; i++) {
            var input = inputs[i],
                key = input.name || useIdOnEmptyName && input.id;

            if (!key) continue;

            var entry = null;
            if (nodeCallback) entry = nodeCallback(input);
            if (!entry) entry = getEntryFromInput(input, key);

            if (typeof entry.value === 'undefined' || entry.value === null
                || (skipFalsy && (!entry.value || (isArray(entry.value) && !entry.value.length))))
                continue;
            saveEntryToResult(result, entry, input, delimiter);
        }
        return result;
    }

    function getEntryFromInput(input, key) {
        var nodeType = input.type && input.type.toLowerCase(),
            entry = { name: key, value: null };

        switch (nodeType) {
            case 'radio':
                if (input.checked)
                    entry.value = input.value === 'on' ? true : input.value;
                break;
            case 'checkbox':
                entry.value = input.checked ? (input.value === 'on' ? true : input.value) : false;
                break;
            case 'select-multiple':
                entry.value = [];
                for (var i = 0, l = input.options.length; i < l; i++)
                    if (input.options[i].selected) entry.value.push(input.options[i].value);
                break;
            case 'file':
                //Only interested in the filename (Chrome adds C:\fakepath\ for security anyway)
                entry.value = input.value.split('\\').pop();
                break;
            case 'button':
            case 'submit':
            case 'reset':
                break;
            default:
                entry.value = input.value;
        }
        return entry;
    }

    function parseString(str, delimiter) {
        var result = [],
            split = str.split(delimiter),
            len = split.length;
        for (var i = 0; i < len; i++) {
            var s = split[i].split('['),
                l = s.length;
            for (var j = 0; j < l; j++) {
                var key = s[j];
                if (!key) {
                    //if the first one is empty, continue
                    if (j === 0) continue;
                    //if the undefined key is not the last part of the string, throw error
                    if (j !== l - 1)
                        error('Undefined key is not the last part of the name > ' + str);
                }
                //strip "]" if its there
                if (key && key[key.length - 1] === ']')
                    key = key.slice(0, -1);
                result.push(key);
            }
        }
        return result;
    }

    function saveEntryToResult(parent, entry, input, delimiter) {
        //not not accept falsy values in array collections
        if (/\[\]$/.test(entry.name) && !entry.value) return;
        var parts = parseString(entry.name, delimiter);
        for (var i = 0, l = parts.length; i < l; i++) {
            var part = parts[i];
            //if last
            if (i === l - 1) {
                parent[part] = entry.value;
            } else {
                var index = parts[i + 1];
                if (!index || isNumber(index)) {
                    if (!isArray(parent[part]))
                        parent[part] = [];
                    //if second last
                    if (i === l - 2) {
                        parent[part].push(entry.value);
                    } else {
                        if (!isObject(parent[part][index]))
                            parent[part][index] = {};
                        parent = parent[part][index];
                    }
                    i++;
                } else {
                    if (!isObject(parent[part]))
                        parent[part] = {};
                    parent = parent[part];
                }
            }
        }
    }

    /* Deserialize */
    function deserialize(data, options, nodeCallback) {
        var opts = getOptions(options),
            triggerChange = opts.triggerChange,
            inputs = getFields(this, opts.skipDisabled, opts.skipReadOnly);

        if (!isObject(data)) {
            if (!isString(data)) return;
            try { //Try to parse the passed data as JSON
                data = JSON.parse(data);
            } catch (e) {
                error('Passed string is not a JSON string > ' + data);
            }
        }

        for (var i = 0, l = inputs.length; i < l; i++) {
            var input = inputs[i],
                key = input.name || opts.useIdOnEmptyName && input.id,
                value = getFieldValue(key, opts.delimiter, data);

            if (typeof value === 'undefined' || value === null) {
                clearInput(input, triggerChange);
                continue;
            }
            var mutated = nodeCallback && nodeCallback(input, value);
            if (!mutated) setValueToInput(input, value, triggerChange);
        }
    }

    function getFieldValue(key, delimiter, ref) {
        if (!key) return;
        var parts = parseString(key, delimiter);
        for (var i = 0, l = parts.length; i < l; i++) {
            if (!ref) return;
            var part = ref[parts[i]];

            if (typeof part === 'undefined' || part === null) return;
            //if last
            if (i === l - 1) {
                return part;
            } else {
                var index = parts[i + 1];
                if (index === '') {
                    return part;
                } else if (isNumber(index)) {
                    //if second last
                    if (i === l - 2)
                        return part[index];
                    else
                        ref = part[index];
                    i++;
                } else {
                    ref = part;
                }
            }
        }
    }

    function contains(array, value) {
        for (var i = array.length; i--;)
            if (array[i] == value) return true;
        return false;
    }

    function setValueToInput(input, value, triggerChange) {
        var nodeType = input.type && input.type.toLowerCase();

        switch (nodeType) {
            case 'radio':
                if (value == input.value) input.checked = true;
                break;
            case 'checkbox':
                input.checked = isArray(value)
                    ? contains(value, input.value)
                    : value === true || value == input.value;
                break;
            case 'select-multiple':
                if (isArray(value))
                    for (var i = input.options.length; i--;)
                        input.options[i].selected = contains(value, input.options[i].value);
                else
                    input.value = value;
                break;
            case 'button':
            case 'submit':
            case 'reset':
            case 'file':
                break;
            default:
                input.value = value;
        }
        if (triggerChange)
            $(input).change();
    }

    /* Clear */
    function clear(options) {
        var opts = getOptions(options),
            triggerChange = opts.triggerChange,
            inputs = getFields(this, opts.skipDisabled, opts.skipReadOnly);

        for (var i = 0, l = inputs.length; i < l; i++)
            clearInput(inputs[i], triggerChange);
    }

    function clearInput(input, triggerChange) {
        var nodeType = input.type && input.type.toLowerCase();

        switch (nodeType) {
            case 'select-one':
                input.selectedIndex = 0;
                break;
			case 'select-multiple':
				for (var i = input.options.length; i--;)
					input.options[i].selected = false;
				break;
            case 'radio':
            case 'checkbox':
                if (input.checked) input.checked = false;
                break;
            case 'button':
            case 'submit':
            case 'reset':
            case 'file':
                break;
            default:
                input.value = '';
        }
        if (triggerChange)
            $(input).change();
    }

    /* Submit */
    function submit(html5Submit) {
        var el = this;
        if (!html5Submit) {
            if (isFunction(el.submit))
                el.submit();
            return;
        }

        var clean, btn = el.querySelector('[type="submit"]');
        if (!btn) {
            clean = true;
            btn = document.createElement('button');
            btn.type = 'submit';
            btn.style.display = 'none';
            el.appendChild(btn);
        }
        $(btn).click();
        if (clean) el.removeChild(btn);
    }

    /* Helper functions */
    function isObject(obj) {
        return $.type(s) === 'object';
    }
    function isNumber(n) {
        return $.type(s) === 'number';
    }
    function isArray(arr) {
        return $.type(s) === 'array';
    }
    function isFunction(fn) {
        return $.type(s) === 'function';
    }
    function isString(s) {
        return $.type(s) === 'string';
    }

    function getFields(parent, skipDisabled, skipReadOnly) {
        var fields = ['input', 'select', 'textarea'];
        for (var i = 0; i < fields.length; i++) {
            var field = fields[i];
            if (skipDisabled) field += ':not([disabled])';
            if (skipReadOnly) field += ':not([readonly])';
            field += ':not([data-trans-form="ignore"])';
            fields[i] = field;
        }
        return parent.querySelectorAll(fields.join(','));
    }

    function getOptions(options) {
        var _defaults = $.fn.transForm.defaults;
        if (!isObject(options)) return _defaults;
        var o, opts = {};
        for (o in _defaults) opts[o] = _defaults[o];
        for (o in options) opts[o] = options[o];
        return opts;
    }

    function error(e) {
        $.error('transForm.js ♦ ' + e);
    }
})(window.Zepto || window.jQuery);