fabiommendes/django-bricks

View on GitHub
src/bricks/app/static/js/bricks/core.js

Summary

Maintainability
D
3 days
Test Coverage
var bricks = (function ($) {
    var json = bricks$json;
    var actions = bricks$actions;
    var util = bricks$util;
    var byId = util.byId;

    // Main entry point
    function bricks() {
        return bricks_call(arguments, {
            program: true,
            converter: function (x) {
                return x.result
            }
        });
    }


    // The synchronous version of the bricks() function.
    bricks.sync = function () {
        return bricks_call(arguments, {
            async: false,
            program: true,
            converter: function (x) {
                return x.result
            }
        });
    };


    // Like the regular bricks function, but it does not run any program
    // returned by the server.
    bricks.call = function () {
        return bricks_call(arguments, {
            program: false,
            converter: function (x) {
                return x.result
            }
        });
    };


    // Consumes and execute javascript
    bricks.js = function () {
        return bricks_call(arguments, {
            method: 'js',
            converter: function (x) {
                return x.data
            }
        }).then(processProgram);
    };


    /**
     Retrieve HTML data from a registered bricks template by passing the
     given arguments.

     In Django, functions are registered using the @bricks.html decorator::

     .. code:: python
     import bricks

     @bricks.html
     def js_maker(request, arg1, arg2, arg3, ...):
     return string_of_html_source()

     */
    bricks.html = function (api) {
        return bricks_call(arguments, {
            bricks: 'html',
            converter: function (x) {
                return x.data
            }
        });
    };


    /**
     Similar to bricks.html. However, the second argument is a CSS selector
     for the elements that will receive the resulting html code.
     */
    bricks.htmlTo = function () {
        var selector = arguments[2];
        arguments.splice(1, 1);

        return bricks.html.apply(this, arguments).then(function (html) {
            $(selector).html(html);
        });

    };

    /**
     Form processing using bricks: the form is converted into the arguments
     passed to a bricks function which is then executed.
     */
    bricks.form = function (api, form) {
        var args = getFormData(form);
        if (args.__pending !== undefined) {
            var pending = args.__pending;
            var send = function () {
                if (pending.__size === 0) {
                    delete args.__pending;
                    return bricks(api, args);
                } else {
                    setTimeout(send, 50);
                }
            };
            setTimeout(send, 50);
        } else {
            return bricks(api, args);
        }
    };

    /**
     Bind api element to form submit event.
     */
    bricks.bindForm = function (api, form) {
        $(form).submit(function (event) {
            event.preventDefault();
            bricks.form(api, this);
        });
    };

    bricks.bindClick = function (api, elem) {
        $(elem).click(function (event) {
            event.preventDefault();
            bricks.click(api, this);
        })
    };

    bricks.bind = function (api, elem, event) {
        if (event == undefined) {
            return bindAuto(api, elem);
        }

        // We map event names to events
        return {
            form: bricks.bindForm,
            click: bricks.bindClick
        }[event](api, elem);
    };

    function bindAuto(api, element) {
        var query;
        if (element === undefined) {
            query = $('[bricks-bind]');
        } else {
            query = $('[bricks-bind]', element);
        }
        bindForms(api, query);
        bindClickable(api, query);
    }

    // Bind all bricks form elements to the submit event
    function bindForms(api, query) {
        query.filter('form').each(function () {
            if (api === undefined) {
                api = $(this).attr('bricks-bind');
            }
            var transform = getBoundTransform(this);
            bricks.bindForm(api, this, transform);
        });
    }

    // Bind all clickable elements to bricks
    function bindClickable(api, query) {
        query.filter('a, button, input[type=button]').each(function () {
            if (api == undefined) {
                api = $(this).attr('bricks-bind');
            }
            var transform = getBoundTransform(this);
            bricks.bindClick(api, this, transform);
        });
    }

    function getBoundTransform(elem) {
        var data = $(elem).attr('bricks-transform');
        return undefined;
    }


    function getFormData(form) {
        var files = {};
        var pending = {__size: 0};
        var formData = {fileData: files, __pending: pending};
        var params = $(form).serializeArray();

        // We add all files in the array as a text file. Maybe in the future we
        // will add support for binary data
        $(form).find('input[type=file]').each(function (i, item) {
            var itemFileList = [];
            formData[item.name] = itemFileList;

            for (var i = 0; i < item.files.length; i++) {
                var file = item.files[i];
                var reader = new FileReader();
                pending[file.name] = true;
                itemFileList[itemFileList.length] = file.name;
                pending.__size += 1;
                reader.readAsText(file);
                reader.onloadend = function () {
                    files[file.name] = reader.result;
                    delete pending[file.name];
                    pending.__size -= 1;
                };
            }
        });
        if (pending.__size === 0) {
            delete formData.__pending;
        }

        // Convert array into dictionary. We iterate backwards to preserve the
        // first occurrence of a given name.
        for (var i = params.length - 1; i >= 0; i--) {
            var item = params[i];
            formData[item.name] = item.value;
        }
        return formData;
    }


    // Workhorse implementation for bricks(), bricks.call(), etc.
    bricks.rpc = function (args) {
        // Initialize parameters
        args = $.extend({
            api: undefined,
            params: {},
            async: true,
            program: true,
            errors: true,
            timeout: 5,
            bricks: 'api',
            converter: function (x) {
                return x
            }
        }, args);

        // Check consistency
        if (args.api === undefined) {
            throw TypeError('must define an api function');
        }

        // Create the payload
        var payload = json.dumps({
            jsonrpc: '2.0',
            method: args.api,
            params: args.params,
            id: Math.random(),
        });

        // Create ajax promise object
        var promise = $.ajax({
            url: args.api,
            data: payload,
            contentType: 'application/json',
            dataType: 'json',
            async: args.async,
            method: 'POST',
            converters: {
                "text json": function (x) {
                    var raw = $.parseJSON(x);
                    var data = json.decode(raw);
                    args.errors && processErrors(data.error);
                    if (data.result.constructor == json.JsAction) {
                        processProgram(data.result.js);
                        data.result = data.result.result;
                    }
                    return args.converter(data.result);
                }
            }
        });

        return (args.async) ? promise : promise.responseJSON;
    };

    function processProgram(program) {
        if (program !== undefined) {
            Function(program)();
        }
    }


    function processErrors(error) {
        if (error !== undefined) {
            var errormsg = error.error + ': ' + error.message + '\n\n' + error.traceback;
            bricks.dialog({html: errormsg});
            throw Error(errormsg);
        }
    }

    // Auxiliary function used to normalize input arguments to many bricks
    // methods.
    function bricks_call(args, options) {
        var api_url = args[0];
        args = Array.prototype.slice.call(args, 1);

        if (api_url[api_url.length - 1] === '*') {
            kwargs = {}
        } else if (args.length == 0) {
            args = [];
            kwargs = {};
        } else if (args.length == 1 && args[0] instanceof Object) {
            kwargs = args[0];
            args = [];
        } else {
            kwargs = {};
        }

        // Normalize api name
        if (api_url[api_url.length - 1] !== '/') {
            api_url = api_url + '/';
        }

        // Create promise and attach value() method
        if (args.length != 0) {
            kwargs['*args'] = args
        }
        var promise = bricks.rpc($.extend({
            api: api_url,
            params: kwargs,
        }, options));

        return promise;
    }

    // Support CSRF protection for AJAX requests in Django.
    // This recipe was taken from Django's documentation.
    function getCookie(name) {
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }

    function csrfSafeMethod(method) {
        // these HTTP methods do not require CSRF protection
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }

    function sameOrigin(url) {
        // test that a given url is a same-origin URL
        // url could be relative or scheme relative or absolute
        var host = document.location.host; // host + port
        var protocol = document.location.protocol;
        var sr_origin = '//' + host;
        var origin = protocol + sr_origin;
        // Allow absolute or scheme relative URLs to same origin
        return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
            (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
                // or any other URL that isn't scheme relative or absolute i.e relative.
            !(/^(\/\/|http:|https:).*/.test(url));
    }

    $.ajaxSetup({
        beforeSend: function (xhr, settings) {
            if (!csrfSafeMethod(settings.type) && sameOrigin(settings.url)) {
                // Send the token to same-origin, relative URLs only.
                // Send the token only if the method warrants CSRF protection
                // Using the CSRFToken value acquired earlier
                xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
            }
        }
    });


    // Configure bricks function and register it in jQuery.
    $.bricks = bricks;
    bricks.bricksURI = '/api/';
    bricks.do = bricks$actions;
    bricks.json = json;
    return bricks;
})(jQuery);


// Bind all [bricks-bind] elements in the bubbling phase of document load.
window.addEventListener('load', function () {
    bricks.bind();
}, false);