From e8a00bc50da435d3859118ca127d20a2eb9a0cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 9 Mar 2015 16:34:27 +0100 Subject: [REF] web: add module system to the web client This commit introduces a new module system for the javascript code. Instead of using global (namespaced) variables everywhere (like openerp.web.whatever, ...), each file declare one (or more) module with its dependencies. Each module will be loaded after its dependencies, and will be given a reference to the return values. This change is a massive departure from the way the client worked previously. It will sadly break some/most history, but I am sure that the future git ecosystem will allow you to easily browse the history past this revision (one can dream). This commit feature: * main namespace is now odoo instead of openerp * a way to define modules and dependencies (inspired by requirejs) * many files have been splitted in simpler subfiles * basic support for debugging in odoo.__DEBUG__ * no global variable (i.e. nothing in window.openerp) * many bug fixes, many new bugs Future vision: this modularization should simplify future code development, simplify debugging and will allow a serious testing effort --- addons/web/static/src/js/framework/ajax.js | 286 +++++ addons/web/static/src/js/framework/class.js | 154 +++ addons/web/static/src/js/framework/core.js | 191 ++++ .../web/static/src/js/framework/crash_manager.js | 195 ++++ addons/web/static/src/js/framework/data.js | 1117 ++++++++++++++++++++ addons/web/static/src/js/framework/dialog.js | 194 ++++ addons/web/static/src/js/framework/formats.js | 189 ++++ addons/web/static/src/js/framework/misc.js | 339 ++++++ addons/web/static/src/js/framework/mixins.js | 328 ++++++ addons/web/static/src/js/framework/model.js | 191 ++++ addons/web/static/src/js/framework/pyeval.js | 1018 ++++++++++++++++++ addons/web/static/src/js/framework/registry.js | 54 + addons/web/static/src/js/framework/session.js | 393 +++++++ addons/web/static/src/js/framework/time.js | 285 +++++ addons/web/static/src/js/framework/translation.js | 98 ++ addons/web/static/src/js/framework/utils.js | 383 +++++++ addons/web/static/src/js/framework/view.js | 239 +++++ addons/web/static/src/js/framework/widget.js | 367 +++++++ 18 files changed, 6021 insertions(+) create mode 100644 addons/web/static/src/js/framework/ajax.js create mode 100644 addons/web/static/src/js/framework/class.js create mode 100644 addons/web/static/src/js/framework/core.js create mode 100644 addons/web/static/src/js/framework/crash_manager.js create mode 100644 addons/web/static/src/js/framework/data.js create mode 100644 addons/web/static/src/js/framework/dialog.js create mode 100644 addons/web/static/src/js/framework/formats.js create mode 100644 addons/web/static/src/js/framework/misc.js create mode 100644 addons/web/static/src/js/framework/mixins.js create mode 100644 addons/web/static/src/js/framework/model.js create mode 100644 addons/web/static/src/js/framework/pyeval.js create mode 100644 addons/web/static/src/js/framework/registry.js create mode 100644 addons/web/static/src/js/framework/session.js create mode 100644 addons/web/static/src/js/framework/time.js create mode 100644 addons/web/static/src/js/framework/translation.js create mode 100644 addons/web/static/src/js/framework/utils.js create mode 100644 addons/web/static/src/js/framework/view.js create mode 100644 addons/web/static/src/js/framework/widget.js (limited to 'addons/web/static/src/js/framework') diff --git a/addons/web/static/src/js/framework/ajax.js b/addons/web/static/src/js/framework/ajax.js new file mode 100644 index 00000000000..00da4610f9c --- /dev/null +++ b/addons/web/static/src/js/framework/ajax.js @@ -0,0 +1,286 @@ +odoo.define('web.ajax', ['web.time'], function (require) { +"use strict"; + +var time = require('web.time'); + +function genericJsonRpc (fct_name, params, fct) { + var data = { + jsonrpc: "2.0", + method: fct_name, + params: params, + id: Math.floor(Math.random() * 1000 * 1000 * 1000) + }; + var xhr = fct(data); + var result = xhr.pipe(function(result) { + if (result.error !== undefined) { + console.error("Server application error", result.error); + return $.Deferred().reject("server", result.error); + } else { + return result.result; + } + }, function() { + //console.error("JsonRPC communication error", _.toArray(arguments)); + var def = $.Deferred(); + return def.reject.apply(def, ["communication"].concat(_.toArray(arguments))); + }); + // FIXME: jsonp? + result.abort = function () { if (xhr.abort) xhr.abort(); }; + return result; +} + +function jsonRpc(url, fct_name, params, settings) { + return genericJsonRpc(fct_name, params, function(data) { + return $.ajax(url, _.extend({}, settings, { + url: url, + dataType: 'json', + type: 'POST', + data: JSON.stringify(data, time.date_to_utc), + contentType: 'application/json' + })); + }); +}; + +function jsonpRpc(url, fct_name, params, settings) { + settings = settings || {}; + return genericJsonRpc(fct_name, params, function(data) { + var payload_str = JSON.stringify(data, time.date_to_utc); + var payload_url = $.param({r:payload_str}); + var force2step = settings.force2step || false; + delete settings.force2step; + var session_id = settings.session_id || null; + delete settings.session_id; + if (payload_url.length < 2000 && ! force2step) { + return $.ajax(url, _.extend({}, settings, { + url: url, + dataType: 'jsonp', + jsonp: 'jsonp', + type: 'GET', + cache: false, + data: {r: payload_str, session_id: session_id} + })); + } else { + var args = {session_id: session_id, id: data.id}; + var ifid = _.uniqueId('oe_rpc_iframe'); + var html = ""; + var $iframe = $(html); + var nurl = 'jsonp=1&' + $.param(args); + nurl = url.indexOf("?") !== -1 ? url + "&" + nurl : url + "?" + nurl; + var $form = $('
') + .attr('method', 'POST') + .attr('target', ifid) + .attr('enctype', "multipart/form-data") + .attr('action', nurl) + .append($('').attr('value', payload_str)) + .hide() + .appendTo($('body')); + var cleanUp = function() { + if ($iframe) { + $iframe.unbind("load").remove(); + } + $form.remove(); + }; + var deferred = $.Deferred(); + // the first bind is fired up when the iframe is added to the DOM + $iframe.bind('load', function() { + // the second bind is fired up when the result of the form submission is received + $iframe.unbind('load').bind('load', function() { + $.ajax({ + url: url, + dataType: 'jsonp', + jsonp: 'jsonp', + type: 'GET', + cache: false, + data: {session_id: session_id, id: data.id} + }).always(function() { + cleanUp(); + }).done(function() { + deferred.resolve.apply(deferred, arguments); + }).fail(function() { + deferred.reject.apply(deferred, arguments); + }); + }); + // now that the iframe can receive data, we fill and submit the form + $form.submit(); + }); + // append the iframe to the DOM (will trigger the first load) + $form.after($iframe); + if (settings.timeout) { + realSetTimeout(function() { + deferred.reject({}); + }, settings.timeout); + } + return deferred; + } + }); +}; + +// helper +function realSetTimeout (fct, millis) { + var finished = new Date().getTime() + millis; + var wait = function() { + var current = new Date().getTime(); + if (current < finished) { + setTimeout(wait, finished - current); + } else { + fct(); + } + }; + setTimeout(wait, millis); +} + +function loadCSS(url) { + if (!$('link[href="' + url + '"]').length) { + $('head').append($('', { + 'href': url, + 'rel': 'stylesheet', + 'type': 'text/css' + })); + } +}; + +function loadJS(url) { + var def = $.Deferred(); + if ($('script[src="' + url + '"]').length) { + def.resolve(); + } else { + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + script.onload = script.onreadystatechange = function() { + if ((script.readyState && script.readyState != "loaded" && script.readyState != "complete") || script.onload_done) { + return; + } + script.onload_done = true; + def.resolve(url); + }; + script.onerror = function () { + console.error("Error loading file", script.src); + def.reject(url); + }; + var head = document.head || document.getElementsByTagName('head')[0]; + head.appendChild(script); + } + return def; +}; + +/** + * Cooperative file download implementation, for ajaxy APIs. + * + * Requires that the server side implements an httprequest correctly + * setting the `fileToken` cookie to the value provided as the `token` + * parameter. The cookie *must* be set on the `/` path and *must not* be + * `httpOnly`. + * + * It would probably also be a good idea for the response to use a + * `Content-Disposition: attachment` header, especially if the MIME is a + * "known" type (e.g. text/plain, or for some browsers application/json + * + * @param {Object} options + * @param {String} [options.url] used to dynamically create a form + * @param {Object} [options.data] data to add to the form submission. If can be used without a form, in which case a form is created from scratch. Otherwise, added to form data + * @param {HTMLFormElement} [options.form] the form to submit in order to fetch the file + * @param {Function} [options.success] callback in case of download success + * @param {Function} [options.error] callback in case of request error, provided with the error body + * @param {Function} [options.complete] called after both ``success`` and ``error` callbacks have executed + */ +function get_file(options) { + // need to detect when the file is done downloading (not used + // yet, but we'll need it to fix the UI e.g. with a throbber + // while dump is being generated), iframe load event only fires + // when the iframe content loads, so we need to go smarter: + // http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx + var timer, token = new Date().getTime(), + cookie_name = 'fileToken', cookie_length = cookie_name.length, + CHECK_INTERVAL = 1000, id = _.uniqueId('get_file_frame'), + remove_form = false; + + + // iOS devices doesn't allow iframe use the way we do it, + // opening a new window seems the best way to workaround + if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) { + var params = _.extend({}, options.data || {}, {token: token}); + var url = this.url(options.url, params); + return window.open(url); + } + + var $form, $form_data = $('
'); + + var complete = function () { + if (options.complete) { options.complete(); } + clearTimeout(timer); + $form_data.remove(); + $target.remove(); + if (remove_form && $form) { $form.remove(); } + }; + var $target = $('