diff options
| author | Xavier Morel <xmo@openerp.com> | 2012-04-19 10:12:13 +0200 |
|---|---|---|
| committer | Xavier Morel <xmo@openerp.com> | 2012-04-19 10:12:13 +0200 |
| commit | e7ab44929ff4aa3add8d00bab97fc3e879b9b91c (patch) | |
| tree | a600579e5c3c00c473550db8da591b6634fcbe0f /addons/web/static/src/js | |
| parent | 45c48d684270f9a88af668f99a1d027df57f68d3 (diff) | |
| parent | f5c37eabe94a04ca62d08f46c1cf3a084fa77f19 (diff) | |
[MERGE] from trunk
bzr revid: xmo@openerp.com-20120419081213-m3fhofkgydj6gp1m
Diffstat (limited to 'addons/web/static/src/js')
| -rw-r--r-- | addons/web/static/src/js/boot.js | 21 | ||||
| -rw-r--r-- | addons/web/static/src/js/chrome.js | 465 | ||||
| -rw-r--r-- | addons/web/static/src/js/corelib.js (renamed from addons/web/static/src/js/core.js) | 1306 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 142 | ||||
| -rw-r--r-- | addons/web/static/src/js/data.js | 120 | ||||
| -rw-r--r-- | addons/web/static/src/js/data_export.js | 10 | ||||
| -rw-r--r-- | addons/web/static/src/js/data_import.js | 16 | ||||
| -rw-r--r-- | addons/web/static/src/js/dates.js | 14 | ||||
| -rw-r--r-- | addons/web/static/src/js/formats.js | 86 | ||||
| -rw-r--r-- | addons/web/static/src/js/search.js | 1496 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_editor.js | 68 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_form.js | 2829 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 303 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list_editable.js | 114 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_page.js | 288 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_tree.js | 21 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 521 |
17 files changed, 4157 insertions, 3663 deletions
diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index 282435f2a71..1251975f41c 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -47,26 +47,15 @@ /*--------------------------------------------------------- * OpenERP Web web module split *---------------------------------------------------------*/ - -/** - * @namespace - */ -openerp.web = function(instance) { - openerp.web.core(instance); - if (openerp.web.dates) { - openerp.web.dates(instance); - } - openerp.web.formats(instance); - openerp.web.chrome(instance); - openerp.web.data(instance); - var files = ["views","search","list","form", "page","list_editable","web_mobile","view_tree","data_export","data_import","view_editor"]; +openerp.web = function(session) { + var files = ["corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import","view_editor"]; for(var i=0; i<files.length; i++) { if(openerp.web[files[i]]) { - openerp.web[files[i]](instance); + openerp.web[files[i]](session); } } - instance.log = function() { - if (instance.connection.debug && window.console) { + session.log = function() { + if (session.connection.debug && window.console) { console.log.apply(console, arguments); } } diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 7743e2b5777..f1d3faef367 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -1,18 +1,17 @@ /*--------------------------------------------------------- * OpenERP Web chrome *---------------------------------------------------------*/ -openerp.web.chrome = function(openerp) { -var QWeb = openerp.web.qweb, - _t = openerp.web._t; +openerp.web.chrome = function(instance) { +var QWeb = instance.web.qweb, + _t = instance.web._t; -openerp.web.Notification = openerp.web.OldWidget.extend(/** @lends openerp.web.Notification# */{ +instance.web.Notification = instance.web.Widget.extend({ template: 'Notification', - init: function() { this._super.apply(this, arguments); - openerp.notification = this; + // move to instance.web.notification + instance.notification = this; }, - start: function() { this._super.apply(this, arguments); this.$element.notify({ @@ -42,24 +41,16 @@ openerp.web.Notification = openerp.web.OldWidget.extend(/** @lends openerp.web. text: text }, opts); } - }); -openerp.web.dialog = function(element) { +instance.web.dialog = function(element) { var result = element.dialog.apply(element, _.rest(_.toArray(arguments))); result.dialog("widget").addClass("openerp"); return result; } -openerp.web.Dialog = openerp.web.OldWidget.extend(/** @lends openerp.web.Dialog# */{ +instance.web.Dialog = instance.web.Widget.extend({ dialog_title: "", - /** - * @constructs openerp.web.Dialog - * @extends openerp.web.OldWidget - * - * @param parent - * @param options - */ init: function (parent, options, content) { var self = this; this._super(parent); @@ -92,7 +83,7 @@ openerp.web.Dialog = openerp.web.OldWidget.extend(/** @lends openerp.web.Dialog# if (this.dialog_options.autoOpen) { this.open(); } else { - openerp.web.dialog(this.$element, this.get_options()); + instance.web.dialog(this.$element, this.get_options()); } }, get_options: function(options) { @@ -128,10 +119,10 @@ openerp.web.Dialog = openerp.web.OldWidget.extend(/** @lends openerp.web.Dialog# open: function(options) { // TODO fme: bind window on resize if (this.template) { - this.$element.html(this.render()); + this.$element.html(this.renderElement()); } var o = this.get_options(options); - openerp.web.dialog(this.$element, o).dialog('open'); + instance.web.dialog(this.$element, o).dialog('open'); if (o.height === 'auto' && o.max_height) { this.$element.css({ 'max-height': o.max_height, 'overflow-y': 'auto' }); } @@ -149,14 +140,13 @@ openerp.web.Dialog = openerp.web.OldWidget.extend(/** @lends openerp.web.Dialog# //openerp.log("Dialog resized to %d x %d", this.$element.width(), this.$element.height()); }, destroy: function () { - // Destroy widget this.close(); this.$element.dialog('destroy'); this._super(); } }); -openerp.web.CrashManager = openerp.web.CallbackEnabled.extend({ +instance.web.CrashManager = instance.web.CallbackEnabled.extend({ on_rpc_error: function(error) { if (error.data.fault_code) { var split = ("" + error.data.fault_code).split('\n')[0].split(' -- '); @@ -172,7 +162,7 @@ openerp.web.CrashManager = openerp.web.CallbackEnabled.extend({ } }, on_managed_error: function(error) { - openerp.web.dialog($('<div>' + QWeb.render('CrashManagerWarning', {error: error}) + '</div>'), { + instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), { title: "OpenERP " + _.str.capitalize(error.type), buttons: [ {text: _t("Ok"), click: function() { $(this).dialog("close"); }} @@ -182,14 +172,14 @@ openerp.web.CrashManager = openerp.web.CallbackEnabled.extend({ on_traceback: function(error) { var self = this; var buttons = {}; - if (openerp.connection.openerp_entreprise) { + if (instance.connection.openerp_entreprise) { buttons[_t("Send OpenERP Enterprise Report")] = function() { var $this = $(this); var issuename = $('#issuename').val(); var explanation = $('#explanation').val(); var remark = $('#remark').val(); // Call the send method from server to send mail with details - new openerp.web.DataSet(self, 'publisher_warranty.contract').call_and_eval('send', [error.data,explanation,remark,issuename]).then(function(result){ + new instance.web.DataSet(self, 'publisher_warranty.contract').call_and_eval('send', [error.data,explanation,remark,issuename]).then(function(result){ if (result === false) { alert('There was a communication error.') } else { @@ -205,7 +195,7 @@ openerp.web.CrashManager = openerp.web.CallbackEnabled.extend({ $(this).dialog("close"); }; } - var dialog = new openerp.web.Dialog(this, { + var dialog = new instance.web.Dialog(this, { title: "OpenERP " + _.str.capitalize(error.type), width: '80%', height: '50%', @@ -213,19 +203,12 @@ openerp.web.CrashManager = openerp.web.CallbackEnabled.extend({ min_height: '600px', buttons: buttons }).open(); - dialog.$element.html(QWeb.render('CrashManagerError', {session: openerp.connection, error: error})); + dialog.$element.html(QWeb.render('CrashManager.error', {session: instance.connection, error: error})); } }); -openerp.web.Loading = openerp.web.OldWidget.extend(/** @lends openerp.web.Loading# */{ +instance.web.Loading = instance.web.Widget.extend({ template: 'Loading', - /** - * @constructs openerp.web.Loading - * @extends openerp.web.OldWidget - * - * @param parent - * @param element_id - */ init: function(parent) { this._super(parent); this.count = 0; @@ -258,10 +241,9 @@ openerp.web.Loading = openerp.web.OldWidget.extend(/** @lends openerp.web.Loadin this.count += increment; if (this.count > 0) { - $(".loading",this.$element).text(_.str.sprintf( - _t("Loading (%d)"), this.count)); - $(".loading",this.$element).show(); - this.getParent().$element.addClass('loading'); + this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count)); + this.$element.show(); + this.getParent().$element.addClass('oe_wait'); } else { this.count = 0; clearTimeout(this.long_running_timer); @@ -270,73 +252,60 @@ openerp.web.Loading = openerp.web.OldWidget.extend(/** @lends openerp.web.Loadin this.blocked_ui = false; $.unblockUI(); } - $(".loading",this.$element).fadeOut(); - this.getParent().$element.removeClass('loading'); + this.$element.fadeOut(); + this.getParent().$element.removeClass('oe_wait'); } } }); -openerp.web.Database = openerp.web.OldWidget.extend(/** @lends openerp.web.Database# */{ - template: "DatabaseManager", - /** - * @constructs openerp.web.Database - * @extends openerp.web.OldWidget - * - * @param parent - * @param element_id - * @param option_id - */ - init: function(parent, element_id, option_id) { - this._super(parent, element_id); +instance.web.DatabaseManager = instance.web.Widget.extend({ + init: function(parent) { + this._super(parent); this.unblockUIFunction = $.unblockUI; $.validator.addMethod('matches', function (s, _, re) { return new RegExp(re).test(s); }, _t("Invalid database name")); }, start: function() { - this.$option_id = $("#oe_db_options"); - var self = this; var fetch_db = this.rpc("/web/database/get_list", {}).pipe( - function(result) { self.db_list = result.db_list; }, - function (_, ev) { ev.preventDefault(); self.db_list = null; }); - var fetch_langs = this.rpc("/web/session/get_lang_list", {}, function(result) { - if (result.error) { - self.display_error(result); - return; - } + function(result) { + self.db_list = result.db_list; + }, + function (_, ev) { + ev.preventDefault(); + self.db_list = null; + }); + var fetch_langs = this.rpc("/web/session/get_lang_list", {}).then(function(result) { self.lang_list = result.lang_list; }); - $.when(fetch_db, fetch_langs).then(function () {self.do_create();}); - - this.$element.find('#db-create').click(this.do_create); - this.$element.find('#db-drop').click(this.do_drop); - this.$element.find('#db-backup').click(this.do_backup); - this.$element.find('#db-restore').click(this.do_restore); - this.$element.find('#db-change-password').click(this.do_change_password); - this.$element.find('#back-to-login').click(function() { - self.hide(); + return $.when(fetch_db, fetch_langs).then(self.do_render); + }, + do_render: function() { + var self = this; + self.$element.html(QWeb.render("DatabaseManager", { widget : self })); + self.$element.find(".oe_database_manager_menu").tabs(); + self.$element.find("form[name=create_db_form]").validate({ submitHandler: self.do_create }); + self.$element.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop }); + self.$element.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup }); + self.$element.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore }); + self.$element.find("form[name=change_pwd_form]").validate({ + messages: { + old_pwd: "Please enter your previous password", + new_pwd: "Please enter your new password", + confirm_pwd: { + required: "Please confirm your new password", + equalTo: "The confirmation does not match the password" + } + }, + submitHandler: self.do_change_password }); + self.$element.find("#back_to_login").click(self.do_exit); }, destroy: function () { - this.hide(); - this.$option_id.empty(); - - this.$element - .find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login') - .unbind('click') - .end() - .empty(); + this.$element.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty(); this._super(); }, - show: function () { - this.$element.closest(".login") - .addClass("database_block"); - }, - hide: function () { - this.$element.closest(".login") - .removeClass("database_block") - }, /** * Converts a .serializeArray() result into a dict. Does not bother folding * multiple identical keys into an array, last key wins. @@ -375,7 +344,7 @@ openerp.web.Database = openerp.web.OldWidget.extend(/** @lends openerp.web.Datab * @param {String} error.error message of the error dialog */ display_error: function (error) { - return openerp.web.dialog($('<div>'), { + return instance.web.dialog($('<div>'), { modal: true, title: error.title, buttons: [ @@ -383,157 +352,96 @@ openerp.web.Database = openerp.web.OldWidget.extend(/** @lends openerp.web.Datab ] }).html(error.error); }, - do_create: function() { + do_create: function(form) { var self = this; - self.$option_id.html(QWeb.render("Database.CreateDB", self)); - self.$option_id.find("form[name=create_db_form]").validate({ - submitHandler: function (form) { - var fields = $(form).serializeArray(); - self.rpc("/web/database/create", {'fields': fields}, function(result) { - if (self.db_list) { - self.db_list.push(self.to_object(fields)['db_name']); - self.db_list.sort(); - self.getParent().set_db_list(self.db_list); - } - - var form_obj = self.to_object(fields); - self.getParent().do_login( - form_obj['db_name'], - 'admin', - form_obj['create_admin_pwd']); - self.destroy(); - }); - } + var fields = $(form).serializeArray(); + self.rpc("/web/database/create", {'fields': fields}, function(result) { + var form_obj = self.to_object(fields); + self.getParent().do_login( form_obj['db_name'], 'admin', form_obj['create_admin_pwd']); + self.destroy(); }); + }, - do_drop: function() { + do_drop: function(form) { var self = this; - self.$option_id.html(QWeb.render("DropDB", self)); - self.$option_id.find("form[name=drop_db_form]").validate({ - submitHandler: function (form) { - var $form = $(form), - fields = $form.serializeArray(), - $db_list = $form.find('[name=drop_db]'), - db = $db_list.val(); - - if (!db || !confirm("Do you really want to delete the database: " + db + " ?")) { - return; - } - self.rpc("/web/database/drop", {'fields': fields}, function(result) { - if (result.error) { - self.display_error(result); - return; - } - $db_list.find(':selected').remove(); - if (self.db_list) { - self.db_list.splice(_.indexOf(self.db_list, db, true), 1); - self.getParent().set_db_list(self.db_list); - } - self.do_notify("Dropping database", "The database '" + db + "' has been dropped"); - }); + var $form = $(form), + fields = $form.serializeArray(), + $db_list = $form.find('[name=drop_db]'), + db = $db_list.val(); + if (!db || !confirm("Do you really want to delete the database: " + db + " ?")) { + return; + } + self.rpc("/web/database/drop", {'fields': fields}, function(result) { + if (result.error) { + self.display_error(result); + return; } + self.do_notify("Dropping database", "The database '" + db + "' has been dropped"); + self.start(); }); }, - do_backup: function() { + do_backup: function(form) { var self = this; - self.$option_id - .html(QWeb.render("BackupDB", self)) - .find("form[name=backup_db_form]").validate({ - submitHandler: function (form) { - self.blockUI(); - self.session.get_file({ - form: form, - success: function () { - self.do_notify(_t("Backed"), - _t("Database backed up successfully")); - }, - error: openerp.webclient.crashmanager.on_rpc_error, - complete: function() { - self.unblockUI(); - } - }); + self.blockUI(); + self.session.get_file({ + form: form, + success: function () { + self.do_notify(_t("Backed"), _t("Database backed up successfully")); + }, + error: instance.webclient.crashmanager.on_rpc_error, + complete: function() { + self.unblockUI(); } }); }, - do_restore: function() { + do_restore: function(form) { var self = this; - self.$option_id.html(QWeb.render("RestoreDB", self)); + self.blockUI(); + $(form).ajaxSubmit({ + url: '/web/database/restore', + type: 'POST', + resetForm: true, + success: function (body) { + // If empty body, everything went fine + if (!body) { return; } - self.$option_id.find("form[name=restore_db_form]").validate({ - submitHandler: function (form) { - self.blockUI(); - $(form).ajaxSubmit({ - url: '/web/database/restore', - type: 'POST', - resetForm: true, - success: function (body) { - // TODO: ui manipulations - // note: response objects don't work, but we have the - // HTTP body of the response~~ - - // If empty body, everything went fine - if (!body) { return; } - - if (body.indexOf('403 Forbidden') !== -1) { - self.display_error({ - title: 'Access Denied', - error: 'Incorrect super-administrator password' - }) - } else { - self.display_error({ - title: 'Restore Database', - error: 'Could not restore the database' - }) - } - }, - complete: function() { - self.unblockUI(); - self.do_notify(_t("Restored"), _t("Database restored successfully")); - } - }); + if (body.indexOf('403 Forbidden') !== -1) { + self.display_error({ + title: 'Access Denied', + error: 'Incorrect super-administrator password' + }) + } else { + self.display_error({ + title: 'Restore Database', + error: 'Could not restore the database' + }) + } + }, + complete: function() { + self.unblockUI(); + self.do_notify(_t("Restored"), _t("Database restored successfully")); } }); }, - do_change_password: function() { + do_change_password: function(form) { var self = this; - self.$option_id.html(QWeb.render("Change_DB_Pwd", self)); - - self.$option_id.find("form[name=change_pwd_form]").validate({ - messages: { - old_pwd: "Please enter your previous password", - new_pwd: "Please enter your new password", - confirm_pwd: { - required: "Please confirm your new password", - equalTo: "The confirmation does not match the password" - } - }, - submitHandler: function (form) { - self.rpc("/web/database/change_password", { - 'fields': $(form).serializeArray() - }, function(result) { - if (result.error) { - self.display_error(result); - return; - } - self.do_notify("Changed Password", "Password has been changed successfully"); - }); + self.rpc("/web/database/change_password", { + 'fields': $(form).serializeArray() + }, function(result) { + if (result.error) { + self.display_error(result); + return; } + self.do_notify("Changed Password", "Password has been changed successfully"); }); + }, + do_exit: function () { } }); -openerp.web.Login = openerp.web.OldWidget.extend(/** @lends openerp.web.Login# */{ - remember_credentials: true, - +instance.web.Login = instance.web.Widget.extend({ template: "Login", - /** - * @constructs openerp.web.Login - * @extends openerp.web.OldWidget - * - * @param parent - * @param element_id - */ - + remember_credentials: true, init: function(parent) { this._super(parent); this.has_local_storage = typeof(localStorage) != 'undefined'; @@ -550,29 +458,35 @@ openerp.web.Login = openerp.web.OldWidget.extend(/** @lends openerp.web.Login# }, start: function() { var self = this; - this.database = new openerp.web.Database(this); - this.database.appendTo(this.$element); - this.$element.find('#oe-db-config').click(function() { - self.database.show(); - }); + self.$element.find("form").submit(self.on_submit); - this.$element.find("form").submit(this.on_submit); - - this.rpc("/web/database/get_list", {}, function(result) { + self.$element.find('.oe_login_manage_db').click(function() { + self.$element.find('.oe_login_bottom').hide(); + self.$element.find('.oe_login_pane').hide(); + self.databasemanager = new instance.web.DatabaseManager(self); + self.databasemanager.appendTo(self.$element); + self.databasemanager.do_exit.add_last(function() { + self.databasemanager.destroy(); + self.$element.find('.oe_login_bottom').show(); + self.$element.find('.oe_login_pane').show(); + self.load_db_list(); + }) + }); + self.load_db_list(); + }, + load_db_list: function () { + var self = this; + self.rpc("/web/database/get_list", {}, function(result) { self.set_db_list(result.db_list); - }, - function(error, event) { + }, function(error, event) { if (error.data.fault_code === 'AccessDenied') { event.preventDefault(); } }); - }, set_db_list: function (list) { - this.$element.find("[name=db]").replaceWith( - openerp.web.qweb.render('Login_dblist', { - db_list: list, selected_db: this.selected_db})) + this.$element.find("[name=db]").replaceWith(instance.web.qweb.render('Login.dblist', { db_list: list, selected_db: this.selected_db})) }, on_submit: function(ev) { if(ev) { @@ -598,15 +512,15 @@ openerp.web.Login = openerp.web.OldWidget.extend(/** @lends openerp.web.Login# */ do_login: function (db, login, password) { var self = this; - this.$element.removeClass('login_invalid'); + this.$element.removeClass('oe_login_invalid'); this.session.on_session_invalid.add({ callback: function () { - self.$element.addClass("login_invalid"); + self.$element.addClass("oe_login_invalid"); }, unique: true }); this.session.session_authenticate(db, login, password).then(function() { - self.$element.removeClass("login_invalid"); + self.$element.removeClass("oe_login_invalid"); if (self.has_local_storage) { if(self.remember_credentials) { localStorage.setItem('last_db_login_success', db); @@ -624,13 +538,7 @@ openerp.web.Login = openerp.web.OldWidget.extend(/** @lends openerp.web.Login# } }); -openerp.web.Menu = openerp.web.Widget.extend(/** @lends openerp.web.Menu# */{ - /** - * @constructs openerp.web.Menu - * @extends openerp.web.Widget - * - * @param parent - */ +instance.web.Menu = instance.web.Widget.extend({ template: 'Menu', init: function() { this._super.apply(this, arguments); @@ -774,14 +682,8 @@ openerp.web.Menu = openerp.web.Widget.extend(/** @lends openerp.web.Menu# */{ } }); -openerp.web.UserMenu = openerp.web.Widget.extend(/** @lends openerp.web.UserMenu# */{ +instance.web.UserMenu = instance.web.Widget.extend({ template: "UserMenu", - /** - * @constructs openerp.web.UserMenu - * @extends openerp.web.Widget - * - * @param parent - */ init: function(parent) { this._super(parent); this.update_promise = $.Deferred().resolve(); @@ -805,7 +707,7 @@ openerp.web.UserMenu = openerp.web.Widget.extend(/** @lends openerp.web.UserMen }, change_password :function() { var self = this; - this.dialog = new openerp.web.Dialog(this, { + this.dialog = new instance.web.Dialog(this, { title: _t("Change Password"), width : 'auto' }).open(); @@ -819,14 +721,14 @@ openerp.web.UserMenu = openerp.web.Widget.extend(/** @lends openerp.web.UserMen self.display_error(result); return; } else { - openerp.webclient.on_logout(); + instance.webclient.on_logout(); } }); } }); }, display_error: function (error) { - return openerp.web.dialog($('<div>'), { + return instance.web.dialog($('<div>'), { modal: true, title: error.title, buttons: [ @@ -841,10 +743,10 @@ openerp.web.UserMenu = openerp.web.Widget.extend(/** @lends openerp.web.UserMen $avatar.attr('src', $avatar.data('default-src')); if (!self.session.uid) return; - var func = new openerp.web.Model("res.users").get_func("read"); + var func = new instance.web.Model("res.users").get_func("read"); return func(self.session.uid, ["name", "company_id"]).pipe(function(res) { // TODO: Show company if multicompany is in use - var topbar_name = _.str.sprintf("%s (%s)", res.name, openerp.connection.db, res.company_id[1]); + var topbar_name = _.str.sprintf("%s (%s)", res.name, instance.connection.db, res.company_id[1]); self.$element.find('.oe_topbar_name').text(topbar_name); var avatar_src = _.str.sprintf('%s/web/binary/image?session_id=%s&model=res.users&field=avatar&id=%s', self.session.prefix, self.session.session_id, self.session.uid); $avatar.attr('src', avatar_src); @@ -858,7 +760,7 @@ openerp.web.UserMenu = openerp.web.Widget.extend(/** @lends openerp.web.UserMen shortcut_load :function(){ var self = this, sc = self.session.shortcuts, - shortcuts_ds = new openerp.web.DataSet(this, 'ir.ui.view_sc'); + shortcuts_ds = new instance.web.DataSet(this, 'ir.ui.view_sc'); self.$element.find('.oe_dropdown_options a[data-menu=shortcut]').each(function() { $(this).parent().remove(); }); @@ -905,11 +807,12 @@ openerp.web.UserMenu = openerp.web.Widget.extend(/** @lends openerp.web.UserMen }, on_menu_settings: function() { var self = this; - var action_manager = new openerp.web.ActionManager(this); - var dataset = new openerp.web.DataSet (this,'res.users',this.context); + var action_manager = new instance.web.ActionManager(this); + var dataset = new instance.web.DataSet (this,'res.users',this.context); dataset.call ('action_get','',function (result){ self.rpc('/web/action/load', {action_id:result}, function(result){ action_manager.do_action(_.extend(result['result'], { + target: 'inline', res_id: self.session.uid, res_model: 'res.users', flags: { @@ -922,7 +825,7 @@ openerp.web.UserMenu = openerp.web.Widget.extend(/** @lends openerp.web.UserMen })); }); }); - this.dialog = new openerp.web.Dialog(this,{ + this.dialog = new instance.web.Dialog(this,{ title: _t("Preferences"), width: '700px', buttons: [ @@ -941,7 +844,7 @@ openerp.web.UserMenu = openerp.web.Widget.extend(/** @lends openerp.web.UserMen ] }).open(); action_manager.appendTo(this.dialog.$element); - action_manager.render(this.dialog); + action_manager.renderElement(this.dialog); }, on_menu_about: function() { var self = this; @@ -952,7 +855,7 @@ openerp.web.UserMenu = openerp.web.Widget.extend(/** @lends openerp.web.UserMen window.location = $.param.querystring( window.location.href, 'debug'); }); - openerp.web.dialog($help, {autoOpen: true, + instance.web.dialog($help, {autoOpen: true, modal: true, width: 960, title: _t("About")}); }); }, @@ -968,21 +871,17 @@ openerp.web.UserMenu = openerp.web.Widget.extend(/** @lends openerp.web.UserMen } }); -openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClient */{ - /** - * @constructs openerp.web.WebClient - * @extends openerp.web.Widget - */ +instance.web.WebClient = instance.web.Widget.extend({ init: function(parent) { var self = this; this._super(parent); - openerp.webclient = this; + instance.webclient = this; this.querystring = '?' + jQuery.param.querystring(); this._current_state = null; }, start: function() { var self = this; - this.$element.addClass("openerp openerp2"); + this.$element.addClass("openerp openerp-web-client-container"); if (jQuery.param != undefined && jQuery.deparam(jQuery.param.querystring()).kitten != undefined) { this.$element.addClass("kitten-mode-activated"); this.$element.delegate('img.oe-record-edit-link-img', 'hover', function(e) { @@ -1001,7 +900,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie self.menu.do_reload(); if(self.action_manager) self.action_manager.destroy(); - self.action_manager = new openerp.web.ActionManager(self); + self.action_manager = new instance.web.ActionManager(self); self.action_manager.appendTo(self.$element.find('.oe_application')); self.bind_hashchange(); var version_label = _t("OpenERP - Unsupported/Community Version"); @@ -1018,7 +917,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie var self = this; this.destroy_content(); this.show_common(); - self.login = new openerp.web.Login(self); + self.login = new instance.web.Login(self); self.login.appendTo(self.$element); }, show_application: function() { @@ -1027,10 +926,10 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie this.show_common(); self.$table = $(QWeb.render("WebClient", {})); self.$element.append(self.$table); - self.menu = new openerp.web.Menu(self); + self.menu = new instance.web.Menu(self); self.menu.replace(this.$element.find('.oe_menu_placeholder')); self.menu.on_action.add(this.proxy('on_menu_action')); - self.user_menu = new openerp.web.UserMenu(self); + self.user_menu = new instance.web.UserMenu(self); self.user_menu.replace(this.$element.find('.oe_user_menu_placeholder')); self.user_menu.on_menu_logout.add(this.proxy('on_logout')); self.user_menu.on_action.add(this.proxy('on_menu_action')); @@ -1038,8 +937,8 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie show_common: function() { var self = this; if (!this.crashmanager) { - this.crashmanager = new openerp.web.CrashManager(); - openerp.connection.on_rpc_error.add(this.crashmanager.on_rpc_error); + this.crashmanager = new instance.web.CrashManager(); + instance.connection.on_rpc_error.add(this.crashmanager.on_rpc_error); window.onerror = function (message, file, line) { self.crashmanager.on_traceback({ type: _t("Client Error"), @@ -1048,9 +947,9 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie }); } } - this.notification = new openerp.web.Notification(this); + this.notification = new instance.web.Notification(this); this.notification.appendTo(this.$element); - this.loading = new openerp.web.Loading(this); + this.loading = new instance.web.Loading(this); this.loading.appendTo(this.$element); }, destroy_content: function() { @@ -1062,7 +961,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie do_reload: function() { var self = this; return this.session.session_reload().pipe(function () { - openerp.connection.load_modules(true).pipe( + instance.connection.load_modules(true).pipe( self.menu.proxy('do_reload')); }); }, @@ -1126,16 +1025,15 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie } }); -openerp.web.EmbeddedClient = openerp.web.OldWidget.extend({ +instance.web.EmbeddedClient = instance.web.Widget.extend({ template: 'EmptyComponent', - init: function(action_id, options) { - this._super(); + init: function(parent, action_id, options) { + this._super(parent); // TODO take the xmlid of a action instead of its id this.action_id = action_id; this.options = options || {}; - this.am = new openerp.web.ActionManager(this); + this.am = new instance.web.ActionManager(this); }, - start: function() { var self = this; this.am.appendTo(this.$element.addClass('openerp')); @@ -1152,10 +1050,9 @@ openerp.web.EmbeddedClient = openerp.web.OldWidget.extend({ self.am.do_action(action); }); } - }); -openerp.web.embed = function (origin, dbname, login, key, action, options) { +instance.web.embed = function (origin, dbname, login, key, action, options) { $('head').append($('<link>', { 'rel': 'stylesheet', 'type': 'text/css', @@ -1166,9 +1063,9 @@ openerp.web.embed = function (origin, dbname, login, key, action, options) { var sc = document.getElementsByTagName('script'); currentScript = sc[sc.length-1]; } - openerp.connection.session_bind(origin).then(function () { - openerp.connection.session_authenticate(dbname, login, key, true).then(function () { - var client = new openerp.web.EmbeddedClient(action, options); + instance.connection.session_bind(origin).then(function () { + instance.connection.session_authenticate(dbname, login, key, true).then(function () { + var client = new instance.web.EmbeddedClient(null, action, options); client.insertAfter(currentScript); }); }); diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/corelib.js index 44e15f6dd13..cbd28aca8a3 100644 --- a/addons/web/static/src/js/core.js +++ b/addons/web/static/src/js/corelib.js @@ -1,60 +1,134 @@ -/*--------------------------------------------------------- - * OpenERP Web core - *--------------------------------------------------------*/ -var console; -if (!console) { - console = {log: function () {}}; -} -if (!console.debug) { - console.debug = console.log; -} +/* + * Copyright (c) 2012, OpenERP S.A. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ -openerp.web.core = function(openerp) { +openerp.web.corelib = function(instance) { -// a function to override the "extend()" method of JR's inheritance, allowing -// the usage of "include()" -oe_override_class = function(claz){ - var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? - /\b_super\b/ : /.*/; - - // Create a new Class that inherits from this class - claz.extend = function(prop) { +/** + * Improved John Resig's inheritance, based on: + * + * Simple JavaScript Inheritance + * By John Resig http://ejohn.org/ + * MIT Licensed. + * + * Adds "include()" + * + * Defines The Class object. That object can be used to define and inherit classes using + * the extend() method. + * + * Example: + * + * var Person = instance.web.Class.extend({ + * init: function(isDancing){ + * this.dancing = isDancing; + * }, + * dance: function(){ + * return this.dancing; + * } + * }); + * + * The init() method act as a constructor. This class can be instancied this way: + * + * var person = new Person(true); + * person.dance(); + * + * The Person class can also be extended again: + * + * var Ninja = Person.extend({ + * init: function(){ + * this._super( false ); + * }, + * dance: function(){ + * // Call the inherited version of dance() + * return this._super(); + * }, + * swingSword: function(){ + * return true; + * } + * }); + * + * When extending a class, each re-defined method can use this._super() to call the previous + * implementation of that method. + */ +(function() { + var initializing = false, + fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; + // The web Class implementation (does nothing) + instance.web.Class = function(){}; + + /** + * Subclass an existing class + * + * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class + */ + instance.web.Class.extend = function() { var _super = this.prototype; - - // Instantiate a base class (but only create the instance, don't run the - // init constructor) - initializing = true; var prototype = new this(); initializing = false; - + // Support mixins arguments + var args = _.toArray(arguments); + args.unshift({}); + var prop = _.extend.apply(_,args); + + // Instantiate a web class (but only create the instance, + // don't run the init constructor) + initializing = true; + var prototype = new this(); + initializing = false; + // Copy the properties over onto the new prototype for (var name in prop) { - // Check if we're overwriting an existing function - prototype[name] = typeof prop[name] == "function" && - typeof _super[name] == "function" && fnTest.test(prop[name]) ? - (function(name, fn){ - return function() { - var tmp = this._super; - - // Add a new ._super() method that is the same method but on the - // super-class - this._super = _super[name]; - - // The method only need to be bound temporarily, so we remove it - // when we're done executing - var ret = fn.apply(this, arguments); this._super = tmp; - - return ret; - }; - })(name, prop[name]) : prop[name]; + // Check if we're overwriting an existing function + prototype[name] = typeof prop[name] == "function" && + typeof _super[name] == "function" && + fnTest.test(prop[name]) ? + (function(name, fn) { + return function() { + var tmp = this._super; + + // Add a new ._super() method that is the same + // method but on the super-class + this._super = _super[name]; + + // The method only need to be bound temporarily, so + // we remove it when we're done executing + var ret = fn.apply(this, arguments); + this._super = tmp; + + return ret; + }; + })(name, prop[name]) : + prop[name]; } - + // The dummy class constructor function Class() { // All construction is actually done in the init method if (!initializing && this.init) { - var ret = this.init.apply(this, arguments); if (ret) { return ret;} - } return this; + var ret = this.init.apply(this, arguments); + if (ret) { return ret; } + } + return this; } - Class.include = function (properties) { for (var name in properties) { if (typeof properties[name] !== 'function' @@ -64,138 +138,617 @@ oe_override_class = function(claz){ && prototype.hasOwnProperty(name)) { prototype[name] = (function (name, fn, previous) { return function () { - var tmp = this._super; this._super = previous; var - ret = fn.apply(this, arguments); this._super = tmp; + var tmp = this._super; + this._super = previous; + var ret = fn.apply(this, arguments); + this._super = tmp; return ret; } })(name, properties[name], prototype[name]); } else if (typeof _super[name] === 'function') { prototype[name] = (function (name, fn) { return function () { - var tmp = this._super; this._super = _super[name]; - var ret = fn.apply(this, arguments); this._super = - tmp; return ret; + var tmp = this._super; + this._super = _super[name]; + var ret = fn.apply(this, arguments); + this._super = tmp; + return ret; } })(name, properties[name]); } } }; - + // Populate our constructed prototype object Class.prototype = prototype; - + // Enforce the constructor to be what we expect - Class.prototype.constructor = Class; - + Class.constructor = Class; + // And make this class extendable Class.extend = arguments.callee; - + return Class; }; -}; -oe_override_class(nova.Class); -oe_override_class(nova.Widget); +})(); -openerp.web.Class = nova.Class; +// Mixins -openerp.web.callback = function(obj, method) { - var callback = function() { - var args = Array.prototype.slice.call(arguments); - var r; - for(var i = 0; i < callback.callback_chain.length; i++) { - var c = callback.callback_chain[i]; - if(c.unique) { - callback.callback_chain.splice(i, 1); - i -= 1; - } - var result = c.callback.apply(c.self, c.args.concat(args)); - if (c.callback === method) { - // return the result of the original method - r = result; +/** + * Mixin to structure objects' life-cycles folowing a parent-children + * relationship. Each object can a have a parent and multiple children. + * When an object is destroyed, all its children are destroyed too releasing + * any resource they could have reserved before. + */ +instance.web.ParentedMixin = { + __parentedMixin : true, + init: function() { + this.__parentedDestroyed = false; + this.__parentedChildren = []; + this.__parentedParent = null; + }, + /** + * Set the parent of the current object. When calling this method, the + * parent will also be informed and will return the current object + * when its getChildren() method is called. If the current object did + * already have a parent, it is unregistered before, which means the + * previous parent will not return the current object anymore when its + * getChildren() method is called. + */ + setParent : function(parent) { + if (this.getParent()) { + if (this.getParent().__parentedMixin) { + this.getParent().__parentedChildren = _.without(this + .getParent().getChildren(), this); } - // TODO special value to stop the chain - // openerp.web.callback_stop - } - return r; - }; - callback.callback_chain = []; - callback.add = function(f) { - if(typeof(f) == 'function') { - f = { callback: f, args: Array.prototype.slice.call(arguments, 1) }; } - f.self = f.self || null; - f.args = f.args || []; - f.unique = !!f.unique; - if(f.position == 'last') { - callback.callback_chain.push(f); - } else { - callback.callback_chain.unshift(f); + this.__parentedParent = parent; + if (parent && parent.__parentedMixin) { + parent.__parentedChildren.push(this); } - return callback; - }; - callback.add_first = function(f) { - return callback.add.apply(null,arguments); - }; - callback.add_last = function(f) { - return callback.add({ - callback: f, - args: Array.prototype.slice.call(arguments, 1), - position: "last" + }, + /** + * Return the current parent of the object (or null). + */ + getParent : function() { + return this.__parentedParent; + }, + /** + * Return a list of the children of the current object. + */ + getChildren : function() { + return _.clone(this.__parentedChildren); + }, + /** + * Returns true if destroy() was called on the current object. + */ + isDestroyed : function() { + return this.__parentedDestroyed; + }, + /** + * Inform the object it should destroy itself, releasing any + * resource it could have reserved. + */ + destroy : function() { + _.each(this.getChildren(), function(el) { + el.destroy(); }); - }; - callback.remove = function(f) { - callback.callback_chain = _.difference(callback.callback_chain, _.filter(callback.callback_chain, function(el) { - return el.callback === f; - })); - return callback; - }; - - return callback.add({ - callback: method, - self:obj, - args:Array.prototype.slice.call(arguments, 2) - }); + this.setParent(undefined); + this.__parentedDestroyed = true; + } }; /** - * web error for lookup failure + * TODO al: move into the the mixin + * + * Backbone's events + * + * (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. + * Backbone may be freely distributed under the MIT license. + * For all details and documentation: + * http://backbonejs.org + * + * This class just handle the dispatching of events, it is not meant to be extended, + * nor used directly. All integration with parenting and automatic unregistration of + * events is done in EventDispatcherMixin. * - * @class */ -openerp.web.NotFound = openerp.web.Class.extend( /** @lends openerp.web.NotFound# */ { +instance.web.Events = instance.web.Class.extend({ + + on : function(events, callback, context) { + var ev; + events = events.split(/\s+/); + var calls = this._callbacks || (this._callbacks = {}); + while (ev = events.shift()) { + var list = calls[ev] || (calls[ev] = {}); + var tail = list.tail || (list.tail = list.next = {}); + tail.callback = callback; + tail.context = context; + list.tail = tail.next = {}; + } + return this; + }, + + off : function(events, callback, context) { + var ev, calls, node; + if (!events) { + delete this._callbacks; + } else if (calls = this._callbacks) { + events = events.split(/\s+/); + while (ev = events.shift()) { + node = calls[ev]; + delete calls[ev]; + if (!callback || !node) + continue; + while ((node = node.next) && node.next) { + if (node.callback === callback + && (!context || node.context === context)) + continue; + this.on(ev, node.callback, node.context); + } + } + } + return this; + }, + + trigger : function(events) { + var event, node, calls, tail, args, all, rest; + if (!(calls = this._callbacks)) + return this; + all = calls['all']; + (events = events.split(/\s+/)).push(null); + // Save references to the current heads & tails. + while (event = events.shift()) { + if (all) + events.push({ + next : all.next, + tail : all.tail, + event : event + }); + if (!(node = calls[event])) + continue; + events.push({ + next : node.next, + tail : node.tail + }); + } + rest = Array.prototype.slice.call(arguments, 1); + while (node = events.pop()) { + tail = node.tail; + args = node.event ? [ node.event ].concat(rest) : rest; + while ((node = node.next) !== tail) { + node.callback.apply(node.context || this, args); + } + } + return this; + } +}); + +instance.web.EventDispatcherMixin = _.extend({}, instance.web.ParentedMixin, { + __eventDispatcherMixin: true, + init: function() { + instance.web.ParentedMixin.init.call(this); + this.__edispatcherEvents = new instance.web.Events(); + this.__edispatcherRegisteredEvents = []; + }, + on: function(events, dest, func) { + var self = this; + events = events.split(/\s+/); + _.each(events, function(eventName) { + self.__edispatcherEvents.on(eventName, func, dest); + if (dest && dest.__eventDispatcherMixin) { + dest.__edispatcherRegisteredEvents.push({name: eventName, func: func, source: self}); + } + }); + return this; + }, + off: function(events, dest, func) { + var self = this; + events = events.split(/\s+/); + _.each(events, function(eventName) { + self.__edispatcherEvents.off(eventName, func, dest); + if (dest && dest.__eventDispatcherMixin) { + dest.__edispatcherRegisteredEvents = _.filter(dest.__edispatcherRegisteredEvents, function(el) { + return !(el.name === eventName && el.func === func && el.source === self); + }); + } + }); + return this; + }, + trigger: function(events) { + this.__edispatcherEvents.trigger.apply(this.__edispatcherEvents, arguments); + return this; + }, + destroy: function() { + var self = this; + _.each(this.__edispatcherRegisteredEvents, function(event) { + event.source.__edispatcherEvents.off(event.name, event.func, self); + }); + this.__edispatcherRegisteredEvents = []; + if(!this.__edispatcherEvents) { + debugger; + } + this.__edispatcherEvents.off(); + instance.web.ParentedMixin.destroy.call(this); + } +}); + +instance.web.GetterSetterMixin = _.extend({}, instance.web.EventDispatcherMixin, { + init: function() { + instance.web.EventDispatcherMixin.init.call(this); + this.__getterSetterInternalMap = {}; + }, + set: function(map) { + var self = this; + var changed = false; + _.each(map, function(val, key) { + var tmp = self.__getterSetterInternalMap[key]; + if (tmp === val) + return; + changed = true; + self.__getterSetterInternalMap[key] = val; + self.trigger("change:" + key, self, { + oldValue: tmp, + newValue: val + }); + }); + if (changed) + self.trigger("change", self); + }, + get: function(key) { + return this.__getterSetterInternalMap[key]; + } +}); + +instance.web.CallbackEnabledMixin = _.extend({}, instance.web.GetterSetterMixin, { + init: function() { + instance.web.GetterSetterMixin.init.call(this); + var self = this; + var callback_maker = function(obj, name, method) { + var callback = function() { + var args = Array.prototype.slice.call(arguments); + self.trigger.apply(self, [name].concat(args)); + var r; + for(var i = 0; i < callback.callback_chain.length; i++) { + var c = callback.callback_chain[i]; + if(c.unique) { + callback.callback_chain.splice(i, 1); + i -= 1; + } + var result = c.callback.apply(c.self, c.args.concat(args)); + if (c.callback === method) { + // return the result of the original method + r = result; + } + // TODO special value to stop the chain + // instance.web.callback_stop + } + return r; + }; + callback.callback_chain = []; + callback.add = function(f) { + if(typeof(f) == 'function') { + f = { callback: f, args: Array.prototype.slice.call(arguments, 1) }; + } + f.self = f.self || null; + f.args = f.args || []; + f.unique = !!f.unique; + if(f.position == 'last') { + callback.callback_chain.push(f); + } else { + callback.callback_chain.unshift(f); + } + return callback; + }; + callback.add_first = function(f) { + return callback.add.apply(null,arguments); + }; + callback.add_last = function(f) { + return callback.add({ + callback: f, + args: Array.prototype.slice.call(arguments, 1), + position: "last" + }); + }; + callback.remove = function(f) { + callback.callback_chain = _.difference(callback.callback_chain, _.filter(callback.callback_chain, function(el) { + return el.callback === f; + })); + return callback; + }; + + return callback.add({ + callback: method, + self:obj, + args:Array.prototype.slice.call(arguments, 3) + }); + }; + // Transform on_/do_* methods into callbacks + for (var name in this) { + if(typeof(this[name]) == "function") { + this[name].debug_name = name; + if((/^on_|^do_/).test(name)) { + this[name] = callback_maker(this, name, this[name]); + } + } + } + }, + /** + * Proxies a method of the object, in order to keep the right ``this`` on + * method invocations. + * + * This method is similar to ``Function.prototype.bind`` or ``_.bind``, and + * even more so to ``jQuery.proxy`` with a fundamental difference: its + * resolution of the method being called is lazy, meaning it will use the + * method as it is when the proxy is called, not when the proxy is created. + * + * Other methods will fix the bound method to what it is when creating the + * binding/proxy, which is fine in most javascript code but problematic in + * OpenERP Web where developers may want to replace existing callbacks with + * theirs. + * + * The semantics of this precisely replace closing over the method call. + * + * @param {String} method_name name of the method to invoke + * @returns {Function} proxied method + */ + proxy: function (method_name) { + var self = this; + return function () { + return self[method_name].apply(self, arguments); + } + } }); -openerp.web.KeyNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.KeyNotFound# */ { + +instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { + /** + * Tag name when creating a default $element. + * @type string + */ + tagName: 'div', /** - * Thrown when a key could not be found in a mapping + * Constructs the widget and sets its parent if a parent is given. + * + * @constructs instance.web.Widget + * @extends instance.web.CallbackEnabled * - * @constructs openerp.web.KeyNotFound - * @extends openerp.web.NotFound - * @param {String} key the key which could not be found + * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance. + * When that widget is destroyed by calling destroy(), the current instance will be + * destroyed too. Can be null. + * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want + * to bind the current Widget to an already existing part of the DOM, which is not compatible + * with the DOM insertion methods provided by the current implementation of Widget. So + * for new components this argument should not be provided any more. */ - init: function (key) { - this.key = key; + init: function(parent) { + instance.web.CallbackEnabledMixin.init.call(this); + this.$element = $(document.createElement(this.tagName)); + this.setParent(parent); + }, + /** + * Destroys the current widget, also destroys all its children before destroying itself. + */ + destroy: function() { + _.each(this.getChildren(), function(el) { + el.destroy(); + }); + if(this.$element != null) { + this.$element.remove(); + } + instance.web.GetterSetterMixin.destroy.call(this); + }, + /** + * Renders the current widget and appends it to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + appendTo: function(target) { + var self = this; + return this.__widgetRenderAndInsert(function(t) { + self.$element.appendTo(t); + }, target); + }, + /** + * Renders the current widget and prepends it to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + prependTo: function(target) { + var self = this; + return this.__widgetRenderAndInsert(function(t) { + self.$element.prependTo(t); + }, target); + }, + /** + * Renders the current widget and inserts it after to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + insertAfter: function(target) { + var self = this; + return this.__widgetRenderAndInsert(function(t) { + self.$element.insertAfter(t); + }, target); + }, + /** + * Renders the current widget and inserts it before to the given jQuery object or Widget. + * + * @param target A jQuery object or a Widget instance. + */ + insertBefore: function(target) { + var self = this; + return this.__widgetRenderAndInsert(function(t) { + self.$element.insertBefore(t); + }, target); + }, + /** + * Renders the current widget and replaces the given jQuery object. + * + * @param target A jQuery object or a Widget instance. + */ + replace: function(target) { + return this.__widgetRenderAndInsert(_.bind(function(t) { + this.$element.replaceAll(t); + }, this), target); }, - toString: function () { - return "The key " + this.key + " was not found"; + __widgetRenderAndInsert: function(insertion, target) { + this.renderElement(); + insertion(target); + return this.start(); + }, + /** + * This is the method to implement to render the Widget. + */ + renderElement: function() { + }, + /** + * Method called after rendering. Mostly used to bind actions, perform asynchronous + * calls, etc... + * + * By convention, the method should return a promise to inform the caller when + * this widget has been initialized. + * + * @returns {jQuery.Deferred} + */ + start: function() { + } +}); + +// Classes + +instance.web.CallbackEnabled = instance.web.Class.extend(instance.web.CallbackEnabledMixin, { + init: function() { + instance.web.CallbackEnabledMixin.init.call(this); } }); -openerp.web.ObjectNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.ObjectNotFound# */ { + +/** + * Base class for all visual components. Provides a lot of functionalities helpful + * for the management of a part of the DOM. + * + * Widget handles: + * - Rendering with QWeb. + * - Life-cycle management and parenting (when a parent is destroyed, all its children are + * destroyed too). + * - Insertion in DOM. + * + * Guide to create implementations of the Widget class: + * ============================================== + * + * Here is a sample child class: + * + * MyWidget = instance.base.Widget.extend({ + * // the name of the QWeb template to use for rendering + * template: "MyQWebTemplate", + * + * init: function(parent) { + * this._super(parent); + * // stuff that you want to init before the rendering + * }, + * start: function() { + * // stuff you want to make after the rendering, `this.$element` holds a correct value + * this.$element.find(".my_button").click(/* an example of event binding * /); + * + * // if you have some asynchronous operations, it's a good idea to return + * // a promise in start() + * var promise = this.rpc(...); + * return promise; + * } + * }); + * + * Now this class can simply be used with the following syntax: + * + * var my_widget = new MyWidget(this); + * my_widget.appendTo($(".some-div")); + * + * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the + * DOM inside the ".some-div" div and its events were binded. + * + * And of course, when you don't need that widget anymore, just do: + * + * my_widget.destroy(); + * + * That will kill the widget in a clean way and erase its content from the dom. + */ +instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { /** - * Thrown when an object path does not designate a valid class or object - * in the openerp hierarchy. + * The name of the QWeb template that will be used for rendering. Must be + * redefined in subclasses or the default render() method can not be used. + * + * @type string + */ + template: null, + /** + * Constructs the widget and sets its parent if a parent is given. * - * @constructs openerp.web.ObjectNotFound - * @extends openerp.web.NotFound - * @param {String} path the invalid object path + * @constructs instance.web.Widget + * @extends instance.web.CallbackEnabled + * + * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance. + * When that widget is destroyed by calling destroy(), the current instance will be + * destroyed too. Can be null. + * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want + * to bind the current Widget to an already existing part of the DOM, which is not compatible + * with the DOM insertion methods provided by the current implementation of Widget. So + * for new components this argument should not be provided any more. + */ + init: function(parent) { + instance.web.WidgetMixin.init.call(this,parent); + this.session = instance.connection; + }, + /** + * Renders the element. The default implementation renders the widget using QWeb, + * `this.template` must be defined. The context given to QWeb contains the "widget" + * key that references `this`. */ - init: function (path) { - this.path = path; + renderElement: function() { + var rendered = null; + if (this.template) + rendered = instance.web.qweb.render(this.template, {widget: this}); + if (_.str.trim(rendered)) { + var elem = $(rendered); + this.$element.replaceWith(elem); + this.$element = elem; + } }, - toString: function () { - return "Could not find any object of path " + this.path; + /** + * Informs the action manager to do an action. This supposes that + * the action manager can be found amongst the ancestors of the current widget. + * If that's not the case this method will simply return `false`. + */ + do_action: function(action, on_finished) { + if (this.getParent()) { + return this.getParent().do_action(action, on_finished); + } + return false; + }, + do_notify: function() { + if (this.getParent()) { + return this.getParent().do_notify.apply(this,arguments); + } + return false; + }, + do_warn: function() { + if (this.getParent()) { + return this.getParent().do_warn.apply(this,arguments); + } + return false; + }, + rpc: function(url, data, success, error) { + var def = $.Deferred().then(success, error); + var self = this; + instance.connection.rpc(url, data). then(function() { + if (!self.isDestroyed()) + def.resolve.apply(def, arguments); + }, function() { + if (!self.isDestroyed()) + def.reject.apply(def, arguments); + }); + return def.promise(); } }); -openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry# */ { + +instance.web.Registry = instance.web.Class.extend({ /** * Stores a mapping of arbitrary key (strings) to object paths (as strings * as well). @@ -204,11 +757,11 @@ openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry * object, even if those objects have been overloaded/replaced after the * registry was created. * - * An object path is simply a dotted name from the openerp root to the - * object pointed to (e.g. ``"openerp.web.Connection"`` for an OpenERP + * An object path is simply a dotted name from the instance root to the + * object pointed to (e.g. ``"instance.web.Connection"`` for an OpenERP * connection object). * - * @constructs openerp.web.Registry + * @constructs instance.web.Registry * @param {Object} mapping a mapping of keys to object-paths */ init: function (mapping) { @@ -220,10 +773,7 @@ openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry * * @param {String} key the key to fetch the object for * @param {Boolean} [silent_error=false] returns undefined if the key or object is not found, rather than throwing an exception - * @returns {Class} the stored class, to initialize - * - * @throws {openerp.web.KeyNotFound} if the object was not in the mapping - * @throws {openerp.web.ObjectNotFound} if the object path was invalid + * @returns {Class} the stored class, to initialize or null if not found */ get_object: function (key, silent_error) { var path_string = this.map[key]; @@ -232,10 +782,10 @@ openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry return this.parent.get_object(key, silent_error); } if (silent_error) { return void 'nooo'; } - throw new openerp.web.KeyNotFound(key); + return null; } - var object_match = openerp; + var object_match = instance; var path = path_string.split('.'); // ignore first section for(var i=1; i<path.length; ++i) { @@ -243,7 +793,7 @@ openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry if (object_match === undefined) { if (silent_error) { return void 'noooooo'; } - throw new openerp.web.ObjectNotFound(path_string); + return null; } } return object_match; @@ -269,9 +819,6 @@ openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry * * @param {Array} keys a sequence of keys to fetch the object for * @returns {Class} the first class found matching an object - * - * @throws {openerp.web.KeyNotFound} if none of the keys was in the mapping - * @trows {openerp.web.ObjectNotFound} if a found object path was invalid */ get_any: function (keys) { for (var i=0; i<keys.length; ++i) { @@ -282,7 +829,7 @@ openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry return this.get_object(key); } - throw new openerp.web.KeyNotFound(keys.join(',')); + return null; }, /** * Adds a new key and value to the registry. @@ -291,7 +838,7 @@ openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry * * @param {String} key * @param {String} object_path fully qualified dotted object path - * @returns {openerp.web.Registry} itself + * @returns {instance.web.Registry} itself */ add: function (key, object_path) { this.map[key] = object_path; @@ -307,7 +854,7 @@ openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry * @param {Object} [mapping={}] a mapping of keys to object-paths */ extend: function (mapping) { - var child = new openerp.web.Registry(mapping); + var child = new instance.web.Registry(mapping); child.parent = this; return child; }, @@ -320,57 +867,10 @@ openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry } }); -openerp.web.CallbackEnabledMixin = { - init: function() { - // Transform on_* method into openerp.web.callbacks - for (var name in this) { - if(typeof(this[name]) == "function") { - this[name].debug_name = name; - // bind ALL function to this not only on_and _do ? - if((/^on_|^do_/).test(name)) { - this[name] = openerp.web.callback(this, this[name]); - } - } - } - }, - /** - * Proxies a method of the object, in order to keep the right ``this`` on - * method invocations. - * - * This method is similar to ``Function.prototype.bind`` or ``_.bind``, and - * even more so to ``jQuery.proxy`` with a fundamental difference: its - * resolution of the method being called is lazy, meaning it will use the - * method as it is when the proxy is called, not when the proxy is created. - * - * Other methods will fix the bound method to what it is when creating the - * binding/proxy, which is fine in most javascript code but problematic in - * OpenERP Web where developers may want to replace existing callbacks with - * theirs. - * - * The semantics of this precisely replace closing over the method call. - * - * @param {String} method_name name of the method to invoke - * @returns {Function} proxied method - */ - proxy: function (method_name) { - var self = this; - return function () { - return self[method_name].apply(self, arguments); - } - } -}; - -openerp.web.CallbackEnabled = openerp.web.Class.extend(_.extend({}, nova.GetterSetterMixin, openerp.web.CallbackEnabledMixin, { - init: function() { - nova.GetterSetterMixin.init.call(this); - openerp.web.CallbackEnabledMixin.init.call(this); - } -})); - -openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp.web.Connection# */{ +instance.web.Connection = instance.web.CallbackEnabled.extend( /** @lends instance.web.Connection# */{ /** - * @constructs openerp.web.Connection - * @extends openerp.web.CallbackEnabled + * @constructs instance.web.Connection + * @extends instance.web.CallbackEnabled * * @param {String} [server] JSON-RPC endpoint hostname * @param {String} [port] JSON-RPC endpoint port @@ -380,7 +880,7 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. this.server = null; this.debug = ($.deparam($.param.querystring()).debug != undefined); // TODO: session store in cookie should be optional - this.name = openerp._session_id; + this.name = instance._session_id; this.qweb_mutex = new $.Mutex(); }, session_bind: function(origin) { @@ -388,7 +888,7 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. this.origin = origin ? _.str.rtrim(origin,'/') : window_origin; this.prefix = this.origin; this.server = this.origin; // keep chs happy - openerp.web.qweb.default_dict['_s'] = this.origin; + instance.web.qweb.default_dict['_s'] = this.origin; this.rpc_function = (this.origin == window_origin) ? this.rpc_json : this.rpc_jsonp; this.session_id = false; this.uid = false; @@ -396,7 +896,7 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. this.user_context= {}; this.db = false; this.openerp_entreprise = false; - this.module_list = openerp._modules.slice(); + this.module_list = instance._modules.slice(); this.module_loaded = {}; _(this.module_list).each(function (mod) { self.module_loaded[mod] = true; @@ -569,7 +1069,7 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. try { var ctx = this.test_eval_contexts(source.contexts); if (!_.isEqual(ctx, expected.context)) { - openerp.webclient.notification.warn('Context mismatch, report to xmo', + instance.webclient.notification.warn('Context mismatch, report to xmo', _.str.sprintf(match_template, { source: JSON.stringify(source.contexts), local: JSON.stringify(ctx), @@ -577,17 +1077,17 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. }), true); } } catch (e) { - openerp.webclient.notification.warn('Context fail, report to xmo', + instance.webclient.notification.warn('Context fail, report to xmo', _.str.sprintf(fail_template, { error: e.message, - source: source.contexts + source: JSON.stringify(source.contexts) }), true); } try { var dom = this.test_eval_domains(source.domains, this.test_eval_get_context()); if (!_.isEqual(dom, expected.domain)) { - openerp.webclient.notification.warn('Domains mismatch, report to xmo', + instance.webclient.notification.warn('Domains mismatch, report to xmo', _.str.sprintf(match_template, { source: JSON.stringify(source.domains), local: JSON.stringify(dom), @@ -595,17 +1095,17 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. }), true); } } catch (e) { - openerp.webclient.notification.warn('Domain fail, report to xmo', + instance.webclient.notification.warn('Domain fail, report to xmo', _.str.sprintf(fail_template, { error: e.message, - source: source.domains + source: JSON.stringify(source.domains) }), true); } try { var groups = this.test_eval_groupby(source.group_by_seq); if (!_.isEqual(groups, expected.group_by)) { - openerp.webclient.notification.warn('GroupBy mismatch, report to xmo', + instance.webclient.notification.warn('GroupBy mismatch, report to xmo', _.str.sprintf(match_template, { source: JSON.stringify(source.group_by_seq), local: JSON.stringify(groups), @@ -613,43 +1113,51 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. }), true); } } catch (e) { - openerp.webclient.notification.warn('GroupBy fail, report to xmo', + instance.webclient.notification.warn('GroupBy fail, report to xmo', _.str.sprintf(fail_template, { error: e.message, - source: source.group_by_seq + source: JSON.stringify(source.group_by_seq) }), true); } }, - test_eval_contexts: function (contexts) { - var result_context = _.extend({}, this.user_context), - self = this; - _(contexts).each(function (ctx) { + test_eval_contexts: function (contexts, evaluation_context) { + evaluation_context = evaluation_context || {}; + var self = this; + return _(contexts).reduce(function (result_context, ctx) { + // __eval_context evaluations can lead to some of `contexts`'s + // values being null, skip them as well as empty contexts + if (_.isEmpty(ctx)) { return result_context; } + var evaluated = ctx; switch(ctx.__ref) { case 'context': - _.extend(result_context, py.eval(ctx.__debug)); + evaluated = py.eval(ctx.__debug, evaluation_context); break; case 'compound_context': - _.extend( - result_context, self.test_eval_contexts(ctx.__contexts)); + var eval_context = self.test_eval_contexts([ctx.__eval_context]); + evaluated = self.test_eval_contexts( + ctx.__contexts, _.extend({}, evaluation_context, eval_context)); break; - default: - _.extend(result_context, ctx); } - }); - return result_context; + // add newly evaluated context to evaluation context for following + // siblings + _.extend(evaluation_context, evaluated); + return _.extend(result_context, evaluated); + }, _.extend({}, this.user_context)); }, - test_eval_domains: function (domains, eval_context) { + test_eval_domains: function (domains, evaluation_context) { var result_domain = [], self = this; _(domains).each(function (dom) { switch(dom.__ref) { case 'domain': result_domain.push.apply( - result_domain, py.eval(dom.__debug, eval_context)); + result_domain, py.eval(dom.__debug, evaluation_context)); break; case 'compound_domain': + var eval_context = self.test_eval_contexts([dom.__eval_context]); result_domain.push.apply( result_domain, self.test_eval_domains( - dom.__domains, eval_context)); + dom.__domains, _.extend( + {}, evaluation_context, eval_context))); break; default: result_domain.push.apply( @@ -667,7 +1175,8 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. group = py.eval(ctx.__debug).group_by; break; case 'compound_context': - group = self.test_eval_contexts(ctx.__contexts).group_by; + group = self.test_eval_contexts( + ctx.__contexts, ctx.__eval_context).group_by; break; default: group = ctx.group_by @@ -691,7 +1200,7 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. * setting the correct session id and session context in the parameter * objects * - * @param {Object} url RPC endpoint + * @param {String} url RPC endpoint * @param {Object} params call parameters * @param {Function} success_callback function to execute on RPC call success * @param {Function} error_callback function to execute on RPC call failure @@ -715,7 +1224,9 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. }; var deferred = $.Deferred(); this.on_rpc_request(); - this.rpc_function(url, payload).then( + var aborter = params.aborter; + delete params.aborter; + var request = this.rpc_function(url, payload).then( function (response, textStatus, jqXHR) { self.on_rpc_response(); if (!response.error) { @@ -744,6 +1255,16 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. }; deferred.reject(error, $.Event()); }); + if (aborter) { + aborter.abort_last = function () { + if (!(request.isResolved() || request.isRejected())) { + deferred.fail(function (error, event) { + event.preventDefault(); + }); + request.abort(); + } + }; + } // Allow deferred user to disable on_rpc_error in fail deferred.fail(function() { deferred.fail(function(error, event) { @@ -848,7 +1369,7 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. // TODO: session store in cookie should be optional this.session_id = this.get_cookie('session_id'); return this.session_reload().pipe(function(result) { - var modules = openerp._modules.join(','); + var modules = instance._modules.join(','); var deferred = self.rpc('/web/webclient/qweblist', {mods: modules}).pipe(self.do_load_qweb); if(self.session_is_valid()) { return deferred.pipe(function() { return self.load_modules(); }); @@ -971,7 +1492,7 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. self.rpc('/web/webclient/csslist', {mods: to_load}, self.do_load_css), self.rpc('/web/webclient/qweblist', {mods: to_load}).pipe(self.do_load_qweb), self.rpc('/web/webclient/translations', params).pipe(function(trans) { - openerp.web._t.database.set_bundle(trans); + instance.web._t.database.set_bundle(trans); var file_list = ["/web/static/lib/datejs/globalization/" + lang.replace("_", "-") + ".js"]; return self.rpc('/web/webclient/jslist', {mods: to_load}).pipe(function(files) { return self.do_load_js(file_list.concat(files)); @@ -1033,7 +1554,7 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. self.qweb_mutex.exec(function() { return self.rpc('/web/proxy/load', {path: file}).pipe(function(xml) { if (!xml) { return; } - openerp.web.qweb.add_template(_.str.trim(xml)); + instance.web.qweb.add_template(_.str.trim(xml)); }); }); }); @@ -1044,10 +1565,10 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. var mod = this.module_list[j]; if(this.module_loaded[mod]) continue; - openerp[mod] = {}; + instance[mod] = {}; // init module mod - if(openerp._openerp[mod] != undefined) { - openerp._openerp[mod](openerp); + if(instance._openerp[mod] != undefined) { + instance._openerp[mod](instance); this.module_loaded[mod] = true; } } @@ -1167,164 +1688,10 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. } }); -/** - * Base class for all visual components. Provides a lot of functionalities helpful - * for the management of a part of the DOM. - * - * Widget handles: - * - Rendering with QWeb. - * - Life-cycle management and parenting (when a parent is destroyed, all its children are - * destroyed too). - * - Insertion in DOM. - * - * Guide to create implementations of the Widget class: - * ============================================== - * - * Here is a sample child class: - * - * MyWidget = openerp.base.Widget.extend({ - * // the name of the QWeb template to use for rendering - * template: "MyQWebTemplate", - * - * init: function(parent) { - * this._super(parent); - * // stuff that you want to init before the rendering - * }, - * start: function() { - * // stuff you want to make after the rendering, `this.$element` holds a correct value - * this.$element.find(".my_button").click(/* an example of event binding * /); - * - * // if you have some asynchronous operations, it's a good idea to return - * // a promise in start() - * var promise = this.rpc(...); - * return promise; - * } - * }); - * - * Now this class can simply be used with the following syntax: - * - * var my_widget = new MyWidget(this); - * my_widget.appendTo($(".some-div")); - * - * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the - * DOM inside the ".some-div" div and its events were binded. - * - * And of course, when you don't need that widget anymore, just do: - * - * my_widget.destroy(); - * - * That will kill the widget in a clean way and erase its content from the dom. - */ -openerp.web.Widget = nova.Widget.extend(_.extend({}, openerp.web.CallbackEnabledMixin, { - /** - * The name of the QWeb template that will be used for rendering. Must be - * redefined in subclasses or the default render() method can not be used. - * - * @type string - */ - template: null, - /** - * Constructs the widget and sets its parent if a parent is given. - * - * @constructs openerp.web.Widget - * @extends openerp.web.CallbackEnabled - * - * @param {openerp.web.Widget} parent Binds the current instance to the given Widget instance. - * When that widget is destroyed by calling destroy(), the current instance will be - * destroyed too. Can be null. - * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want - * to bind the current Widget to an already existing part of the DOM, which is not compatible - * with the DOM insertion methods provided by the current implementation of Widget. So - * for new components this argument should not be provided any more. - */ - init: function(parent) { - this._super(parent); - openerp.web.CallbackEnabledMixin.init.call(this); - this.session = openerp.connection; - }, - /** - * Renders the element. The default implementation renders the widget using QWeb, - * `this.template` must be defined. The context given to QWeb contains the "widget" - * key that references `this`. - */ - renderElement: function() { - var rendered = null; - if (this.template) - rendered = openerp.web.qweb.render(this.template, {widget: this}); - if (_.str.trim(rendered)) { - var elem = $(rendered); - this.$element.replaceWith(elem); - this.$element = elem; - } - }, - /** - * Informs the action manager to do an action. This supposes that - * the action manager can be found amongst the ancestors of the current widget. - * If that's not the case this method will simply return `false`. - */ - do_action: function(action, on_finished) { - if (this.getParent()) { - return this.getParent().do_action(action, on_finished); - } - return false; - }, - do_notify: function() { - if (this.getParent()) { - return this.getParent().do_notify.apply(this,arguments); - } - return false; - }, - do_warn: function() { - if (this.getParent()) { - return this.getParent().do_warn.apply(this,arguments); - } - return false; - }, - rpc: function(url, data, success, error) { - var def = $.Deferred().then(success, error); - var self = this; - openerp.connection.rpc(url, data). then(function() { - if (!self.isDestroyed()) - def.resolve.apply(def, arguments); - }, function() { - if (!self.isDestroyed()) - def.reject.apply(def, arguments); - }); - return def.promise(); - } -})); - -/** - * @deprecated use :class:`openerp.web.Widget` - */ -openerp.web.OldWidget = openerp.web.Widget.extend({ - init: function(parent, element_id) { - this._super(parent); - this.element_id = element_id; - this.element_id = this.element_id || _.uniqueId('widget-'); - var tmp = document.getElementById(this.element_id); - this.$element = tmp ? $(tmp) : $(document.createElement(this.tagName)); - }, - renderElement: function() { - var rendered = this.render(); - if (rendered) { - var elem = $(rendered); - this.$element.replaceWith(elem); - this.$element = elem; - } - return this; - }, - render: function (additional) { - if (this.template) - return openerp.web.qweb.render(this.template, _.extend({widget: this}, additional || {})); - return null; - } -}); - -openerp.web.TranslationDataBase = openerp.web.Class.extend(/** @lends openerp.web.TranslationDataBase# */{ +instance.web.TranslationDataBase = instance.web.Class.extend(/** @lends instance.web.TranslationDataBase# */{ /** - * @constructs openerp.web.TranslationDataBase - * @extends openerp.web.Class + * @constructs instance.web.TranslationDataBase + * @extends instance.web.Class */ init: function() { this.db = {}; @@ -1374,138 +1741,33 @@ openerp.web.TranslationDataBase = openerp.web.Class.extend(/** @lends openerp.we } }); -/** Configure blockui */ -if ($.blockUI) { - $.blockUI.defaults.baseZ = 1100; - $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber2.gif">'; -} - -/** Configure default qweb */ -openerp.web._t = new openerp.web.TranslationDataBase().build_translation_function(); -/** - * Lazy translation function, only performs the translation when actually - * printed (e.g. inserted into a template) - * - * Useful when defining translatable strings in code evaluated before the - * translation database is loaded, as class attributes or at the top-level of - * an OpenERP Web module - * - * @param {String} s string to translate - * @returns {Object} lazy translation object - */ -openerp.web._lt = function (s) { - return {toString: function () { return openerp.web._t(s); }} -}; -openerp.web.qweb = new QWeb2.Engine(); -openerp.web.qweb.debug = ($.deparam($.param.querystring()).debug != undefined); -openerp.web.qweb.default_dict = { - '_' : _, - '_t' : openerp.web._t -}; -openerp.web.qweb.preprocess_node = function() { - // Note that 'this' is the Qweb Node - switch (this.node.nodeType) { - case 3: - case 4: - // Text and CDATAs - var translation = this.node.parentNode.attributes['t-translation']; - if (translation && translation.value === 'off') { - return; - } - var ts = _.str.trim(this.node.data); - if (ts.length === 0) { - return; - } - var tr = openerp.web._t(ts); - if (tr !== ts) { - this.node.data = tr; - } - break; - case 1: - // Element - var attr, attrs = ['label', 'title', 'alt']; - while (attr = attrs.pop()) { - if (this.attributes[attr]) { - this.attributes[attr] = openerp.web._t(this.attributes[attr]); - } - } - } -}; - /** - * A small utility function to check if a class implements correctly an interface, assuming that - * interface is simply specified using a dictionary containing methods and attributes with the - * correct type. It only performs the check when in debug mode and the only effect of an invalid - * check is messages in the console. + * @deprecated use :class:`instance.web.Widget` */ -openerp.web.check_interface = function(_class, _interface) { - if (! openerp.web.check_interface.debug) - return; - for (var member in _interface) { - if ( (typeof _class.prototype[member] != typeof _interface[member]) ) { - console.error("class failed to implement interface member '" + member + "'"); +instance.web.OldWidget = instance.web.Widget.extend({ + init: function(parent, element_id) { + this._super(parent); + this.element_id = element_id; + this.element_id = this.element_id || _.uniqueId('widget-'); + var tmp = document.getElementById(this.element_id); + this.$element = tmp ? $(tmp) : $(document.createElement(this.tagName)); + }, + renderElement: function() { + var rendered = this.render(); + if (rendered) { + var elem = $(rendered); + this.$element.replaceWith(elem); + this.$element = elem; } + return this; + }, + render: function (additional) { + if (this.template) + return instance.web.qweb.render(this.template, _.extend({widget: this}, additional || {})); + return null; } -} -openerp.web.check_interface.debug = ($.deparam($.param.querystring()).debug != undefined); - -/** Jquery extentions */ -$.Mutex = (function() { - function Mutex() { - this.def = $.Deferred().resolve(); - } - Mutex.prototype.exec = function(action) { - var current = this.def; - var next = this.def = $.Deferred(); - return current.pipe(function() { - return $.when(action()).always(function() { - next.resolve(); - }); - }); - }; - return Mutex; -})(); - -/** Setup default connection */ -openerp.connection = new openerp.web.Connection(); -openerp.web.qweb.default_dict['__debug__'] = openerp.connection.debug; - - -$.async_when = function() { - var async = false; - var def = $.Deferred(); - $.when.apply($, arguments).then(function() { - var args = arguments; - var action = function() { - def.resolve.apply(def, args); - }; - if (async) - action(); - else - setTimeout(action, 0); - }, function() { - var args = arguments; - var action = function() { - def.reject.apply(def, args); - }; - if (async) - action(); - else - setTimeout(action, 0); - }); - async = true; - return def; -}; - -// special tweak for the web client -var old_async_when = $.async_when; -$.async_when = function() { - if (openerp.connection.synch) - return $.when.apply(this, arguments); - else - return old_async_when.apply(this, arguments); -}; +}); -}; +} // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js new file mode 100644 index 00000000000..86040302668 --- /dev/null +++ b/addons/web/static/src/js/coresetup.js @@ -0,0 +1,142 @@ +/*--------------------------------------------------------- + * OpenERP Web core + *--------------------------------------------------------*/ +var console; +if (!console) { + console = {log: function () {}}; +} +if (!console.debug) { + console.debug = console.log; +} + +openerp.web.coresetup = function(instance) { + +/** Configure default qweb */ +instance.web._t = new instance.web.TranslationDataBase().build_translation_function(); +/** + * Lazy translation function, only performs the translation when actually + * printed (e.g. inserted into a template) + * + * Useful when defining translatable strings in code evaluated before the + * translation database is loaded, as class attributes or at the top-level of + * an OpenERP Web module + * + * @param {String} s string to translate + * @returns {Object} lazy translation object + */ +instance.web._lt = function (s) { + return {toString: function () { return instance.web._t(s); }} +}; +instance.web.qweb = new QWeb2.Engine(); +instance.web.qweb.debug = ($.deparam($.param.querystring()).debug != undefined); +instance.web.qweb.default_dict = { + '_' : _, + '_t' : instance.web._t +}; +instance.web.qweb.preprocess_node = function() { + // Note that 'this' is the Qweb Node + switch (this.node.nodeType) { + case 3: + case 4: + // Text and CDATAs + var translation = this.node.parentNode.attributes['t-translation']; + if (translation && translation.value === 'off') { + return; + } + var ts = _.str.trim(this.node.data); + if (ts.length === 0) { + return; + } + var tr = instance.web._t(ts); + if (tr !== ts) { + this.node.data = tr; + } + break; + case 1: + // Element + var attr, attrs = ['label', 'title', 'alt']; + while (attr = attrs.pop()) { + if (this.attributes[attr]) { + this.attributes[attr] = instance.web._t(this.attributes[attr]); + } + } + } +}; + +/** Configure blockui */ +if ($.blockUI) { + $.blockUI.defaults.baseZ = 1100; + $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber2.gif">'; +} + +/** Custom jQuery plugins */ +$.fn.getAttributes = function() { + var o = {}; + if (this.length) { + for (var attr, i = 0, attrs = this[0].attributes, l = attrs.length; i < l; i++) { + attr = attrs.item(i) + o[attr.nodeName] = attr.nodeValue; + } + } + return o; +} + +/** Jquery extentions */ +$.Mutex = (function() { + function Mutex() { + this.def = $.Deferred().resolve(); + } + Mutex.prototype.exec = function(action) { + var current = this.def; + var next = this.def = $.Deferred(); + return current.pipe(function() { + return $.when(action()).always(function() { + next.resolve(); + }); + }); + }; + return Mutex; +})(); + +/** Setup default connection */ +instance.connection = new instance.web.Connection(); +instance.web.qweb.default_dict['__debug__'] = instance.connection.debug; + +$.async_when = function() { + var async = false; + var def = $.Deferred(); + $.when.apply($, arguments).then(function() { + var args = arguments; + var action = function() { + def.resolve.apply(def, args); + }; + if (async) + action(); + else + setTimeout(action, 0); + }, function() { + var args = arguments; + var action = function() { + def.reject.apply(def, args); + }; + if (async) + action(); + else + setTimeout(action, 0); + }); + async = true; + return def; +}; + +// special tweak for the web client +var old_async_when = $.async_when; +$.async_when = function() { + if (instance.connection.synch) + return $.when.apply(this, arguments); + else + return old_async_when.apply(this, arguments); +}; + +}; + +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index a29a9c5ad36..2dc66aaa135 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -1,5 +1,5 @@ -openerp.web.data = function(openerp) { +openerp.web.data = function(instance) { /** * Serializes the sort criterion array of a dataset into a form which can be @@ -8,7 +8,7 @@ openerp.web.data = function(openerp) { * @param {Array} criterion array of fields, from first to last criteria, prefixed with '-' for reverse sorting * @returns {String} SQL-like sorting string (``ORDER BY``) clause */ -openerp.web.serialize_sort = function (criterion) { +instance.web.serialize_sort = function (criterion) { return _.map(criterion, function (criteria) { if (criteria[0] === '-') { @@ -18,7 +18,7 @@ openerp.web.serialize_sort = function (criterion) { }).join(', '); }; -openerp.web.Query = openerp.web.Class.extend({ +instance.web.Query = instance.web.Class.extend({ init: function (model, fields) { this._model = model; this._fields = fields; @@ -30,7 +30,7 @@ openerp.web.Query = openerp.web.Class.extend({ }, clone: function (to_set) { to_set = to_set || {}; - var q = new openerp.web.Query(this._model, this._fields); + var q = new instance.web.Query(this._model, this._fields); q._context = this._context; q._filter = this._filter; q._limit = this._limit; @@ -41,11 +41,11 @@ openerp.web.Query = openerp.web.Class.extend({ if (!to_set.hasOwnProperty(key)) { continue; } switch(key) { case 'filter': - q._filter = new openerp.web.CompoundDomain( + q._filter = new instance.web.CompoundDomain( q._filter, to_set.filter); break; case 'context': - q._context = new openerp.web.CompoundContext( + q._context = new instance.web.CompoundContext( q._context, to_set.context); break; case 'limit': @@ -58,14 +58,14 @@ openerp.web.Query = openerp.web.Class.extend({ }, _execute: function () { var self = this; - return openerp.connection.rpc('/web/dataset/search_read', { + return instance.connection.rpc('/web/dataset/search_read', { model: this._model.name, fields: this._fields || false, domain: this._model.domain(this._filter), context: this._model.context(this._context), offset: this._offset, limit: this._limit, - sort: openerp.web.serialize_sort(this._order_by) + sort: instance.web.serialize_sort(this._order_by) }).pipe(function (results) { self._count = results.length; return results.records; @@ -127,10 +127,10 @@ openerp.web.Query = openerp.web.Class.extend({ context: this._model.context(this._context), offset: this._offset, limit: this._limit, - orderby: openerp.web.serialize_sort(this._order_by) || false + orderby: instance.web.serialize_sort(this._order_by) || false }).pipe(function (results) { return _(results).map(function (result) { - return new openerp.web.data.Group( + return new instance.web.data.Group( self._model.name, grouping[0], result); }); }); @@ -194,10 +194,10 @@ openerp.web.Query = openerp.web.Class.extend({ } }); -openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{ +instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ /** - * @constructs openerp.web.Model - * @extends openerp.web.Class + * @constructs instance.web.Model + * @extends instance.web.Class * * @param {String} model_name name of the OpenERP model this object is bound to * @param {Object} [context] @@ -233,7 +233,7 @@ openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{ kwargs = args; args = []; } - return openerp.connection.rpc('/web/dataset/call_kw', { + return instance.connection.rpc('/web/dataset/call_kw', { model: this.name, method: method, args: args, @@ -247,7 +247,7 @@ openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{ * @returns {openerp.web.Query} */ query: function (fields) { - return new openerp.web.Query(this, fields); + return new instance.web.Query(this, fields); }, /** * Executes a signal on the designated workflow, on the bound OpenERP model @@ -256,7 +256,7 @@ openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{ * @param {String} signal signal to trigger on the workflow */ exec_workflow: function (id, signal) { - return openerp.connection.rpc('/web/dataset/exec_workflow', { + return instance.connection.rpc('/web/dataset/exec_workflow', { model: this.name, id: id, signal: signal @@ -270,7 +270,7 @@ openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{ */ domain: function (domain) { if (!domain) { return this._domain; } - return new openerp.web.CompoundDomain( + return new instance.web.CompoundDomain( this._domain, domain); }, /** @@ -281,8 +281,8 @@ openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{ * @returns The union of the user's context and the model's internal context, as well as the provided context if any. In that order. */ context: function (context) { - return new openerp.web.CompoundContext( - openerp.connection.user_context, this._context, context || {}); + return new instance.web.CompoundContext( + instance.connection.user_context, this._context, context || {}); }, /** * Button action caller, needs to perform cleanup if an action is returned @@ -292,7 +292,7 @@ openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{ * FIXME: remove when evaluator integrated */ call_button: function (method, args) { - return openerp.connection.rpc('/web/dataset/call_button', { + return instance.connection.rpc('/web/dataset/call_button', { model: this.name, method: method, domain_id: null, @@ -302,12 +302,12 @@ openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{ }, }); -openerp.web.Traverser = openerp.web.Class.extend(/** @lends openerp.web.Traverser# */{ +instance.web.Traverser = instance.web.Class.extend(/** @lends openerp.web.Traverser# */{ /** - * @constructs openerp.web.Traverser - * @extends openerp.web.Class + * @constructs instance.web.Traverser + * @extends instance.web.Class * - * @param {openerp.web.Model} model instance this traverser is bound to + * @param {instance.web.Model} model instance this traverser is bound to */ init: function (model) { this._model = model; @@ -387,11 +387,11 @@ openerp.web.Traverser = openerp.web.Class.extend(/** @lends openerp.web.Traverse * * @namespace */ -openerp.web.data = { - Group: openerp.web.Class.extend(/** @lends openerp.web.data.Group# */{ +instance.web.data = { + Group: instance.web.Class.extend(/** @lends openerp.web.data.Group# */{ /** - * @constructs openerp.web.data.Group - * @extends openerp.web.Class + * @constructs instance.web.data.Group + * @extends instance.web.Class */ init: function (model, grouping_field, read_group_group) { // In cases where group_by_no_leaf and no group_by, the result of @@ -411,7 +411,7 @@ openerp.web.data = { aggregates[key] = value || 0; }); - this.model = new openerp.web.Model( + this.model = new instance.web.Model( model, fixed_group.__context, fixed_group.__domain); var group_size = fixed_group[grouping_field + '_count'] || fixed_group.__count || 0; @@ -439,7 +439,7 @@ openerp.web.data = { }) }; -openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.DataGroup# */{ +instance.web.DataGroup = instance.web.OldWidget.extend( /** @lends openerp.web.DataGroup# */{ /** * Management interface between views and grouped collections of OpenERP * records. @@ -447,13 +447,13 @@ openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.Da * The root DataGroup is instantiated with the relevant information * (a session, a model, a domain, a context and a group_by sequence), the * domain and context may be empty. It is then interacted with via - * :js:func:`~openerp.web.DataGroup.list`, which is used to read the + * :js:func:`~instance.web.DataGroup.list`, which is used to read the * content of the current grouping level. * - * @constructs openerp.web.DataGroup - * @extends openerp.web.OldWidget + * @constructs instance.web.DataGroup + * @extends instance.web.OldWidget * - * @param {openerp.web.OldWidget} parent widget + * @param {instance.web.OldWidget} parent widget * @param {String} model name of the model managed by this DataGroup * @param {Array} domain search domain for this DataGroup * @param {Object} context context of the DataGroup's searches @@ -462,7 +462,7 @@ openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.Da */ init: function(parent, model, domain, context, group_by, level) { this._super(parent, null); - this.model = new openerp.web.Model(model, context, domain); + this.model = new instance.web.Model(model, context, domain); this.group_by = group_by; this.context = context; this.domain = domain; @@ -476,7 +476,7 @@ openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.Da .group_by(this.group_by)).then(function (groups) { if (!groups) { ifRecords(_.extend( - new openerp.web.DataSetSearch( + new instance.web.DataSetSearch( self, self.model.name, self.model.context(), self.model.domain()), @@ -487,7 +487,7 @@ openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.Da var child_context = _.extend( {}, self.model.context(), group.model.context()); return _.extend( - new openerp.web.DataGroup( + new instance.web.DataGroup( self, self.model.name, group.model.domain(), child_context, group.model._context.group_by, self.level + 1), @@ -504,16 +504,16 @@ openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.Da }); } }); -openerp.web.ContainerDataGroup = openerp.web.DataGroup.extend({ }); -openerp.web.GrouplessDataGroup = openerp.web.DataGroup.extend({ }); +instance.web.ContainerDataGroup = instance.web.DataGroup.extend({ }); +instance.web.GrouplessDataGroup = instance.web.DataGroup.extend({ }); -openerp.web.StaticDataGroup = openerp.web.GrouplessDataGroup.extend( /** @lends openerp.web.StaticDataGroup# */ { +instance.web.StaticDataGroup = instance.web.GrouplessDataGroup.extend( /** @lends openerp.web.StaticDataGroup# */ { /** * A specialization of groupless data groups, relying on a single static * dataset as its records provider. * - * @constructs openerp.web.StaticDataGroup - * @extends openerp.web.GrouplessDataGroup + * @constructs instance.web.StaticDataGroup + * @extends instance.web.GrouplessDataGroup * @param {openep.web.DataSetStatic} dataset a static dataset backing the groups */ init: function (dataset) { @@ -524,13 +524,13 @@ openerp.web.StaticDataGroup = openerp.web.GrouplessDataGroup.extend( /** @lends } }); -openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.DataSet# */{ +instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.DataSet# */{ /** * DateaManagement interface between views and the collection of selected * OpenERP records (represents the view's state?) * - * @constructs openerp.web.DataSet - * @extends openerp.web.OldWidget + * @constructs instance.web.DataSet + * @extends instance.web.OldWidget * * @param {String} model the OpenERP model this dataset will manage */ @@ -540,7 +540,7 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data this.context = context || {}; this.index = null; this._sort = []; - this._model = new openerp.web.Model(model, context); + this._model = new instance.web.Model(model, context); }, previous: function () { this.index -= 1; @@ -789,7 +789,7 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data */ sort: function (field, force_reverse) { if (!field) { - return openerp.web.serialize_sort(this._sort); + return instance.web.serialize_sort(this._sort); } var reverse = force_reverse || (this._sort[0] === field); this._sort.splice.apply( @@ -806,7 +806,7 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data this.ids = n_ids; }, }); -openerp.web.DataSetStatic = openerp.web.DataSet.extend({ +instance.web.DataSetStatic = instance.web.DataSet.extend({ init: function(parent, model, context, ids) { this._super(parent, model, context); // all local records @@ -836,10 +836,10 @@ openerp.web.DataSetStatic = openerp.web.DataSet.extend({ this.set_ids(_.without.apply(null, [this.ids].concat(ids))); } }); -openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.DataSetSearch */{ +instance.web.DataSetSearch = instance.web.DataSet.extend(/** @lends openerp.web.DataSetSearch */{ /** - * @constructs openerp.web.DataSetSearch - * @extends openerp.web.DataSet + * @constructs instance.web.DataSetSearch + * @extends instance.web.DataSet * * @param {Object} parent * @param {String} model @@ -851,7 +851,7 @@ openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.D this.domain = domain || []; this._length = null; this.ids = []; - this._model = new openerp.web.Model(model, context, domain); + this._model = new instance.web.Model(model, context, domain); }, /** * Read a slice of the records represented by this DataSet, based on its @@ -906,7 +906,7 @@ openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.D return this._super(); } }); -openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({ +instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ virtual_id_prefix: "one2many_v_id_", debug_mode: true, init: function() { @@ -1071,9 +1071,9 @@ openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({ this.on_change(); }, }); -openerp.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/; +instance.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/; -openerp.web.ProxyDataSet = openerp.web.DataSetSearch.extend({ +instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({ init: function() { this._super.apply(this, arguments); this.create_function = null; @@ -1119,7 +1119,7 @@ openerp.web.ProxyDataSet = openerp.web.DataSetSearch.extend({ on_unlink: function(ids) {} }); -openerp.web.CompoundContext = openerp.web.Class.extend({ +instance.web.CompoundContext = instance.web.Class.extend({ init: function () { this.__ref = "compound_context"; this.__contexts = []; @@ -1142,7 +1142,7 @@ openerp.web.CompoundContext = openerp.web.Class.extend({ } }); -openerp.web.CompoundDomain = openerp.web.Class.extend({ +instance.web.CompoundDomain = instance.web.Class.extend({ init: function () { this.__ref = "compound_domain"; this.__domains = []; @@ -1165,10 +1165,10 @@ openerp.web.CompoundDomain = openerp.web.Class.extend({ } }); -openerp.web.DropMisordered = openerp.web.Class.extend(/** @lends openerp.web.DropMisordered# */{ +instance.web.DropMisordered = instance.web.Class.extend(/** @lends openerp.web.DropMisordered# */{ /** - * @constructs openerp.web.DropMisordered - * @extends openerp.web.Class + * @constructs instance.web.DropMisordered + * @extends instance.web.Class * * @param {Boolean} [failMisordered=false] whether mis-ordered responses should be failed or just ignored */ diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/data_export.js index 3d15a4e3607..ebe82ddb4b4 100644 --- a/addons/web/static/src/js/data_export.js +++ b/addons/web/static/src/js/data_export.js @@ -1,14 +1,14 @@ -openerp.web.data_export = function(openerp) { -var QWeb = openerp.web.qweb, - _t = openerp.web._t; -openerp.web.DataExport = openerp.web.Dialog.extend({ +openerp.web.data_export = function(instance) { +var QWeb = instance.web.qweb, + _t = instance.web._t; +instance.web.DataExport = instance.web.Dialog.extend({ template: 'ExportTreeView', dialog_title: {toString: function () { return _t("Export Data"); }}, init: function(parent, dataset) { this._super(parent); this.records = {}; this.dataset = dataset; - this.exports = new openerp.web.DataSetSearch( + this.exports = new instance.web.DataSetSearch( this, 'ir.exports', this.dataset.get_context()); }, start: function() { diff --git a/addons/web/static/src/js/data_import.js b/addons/web/static/src/js/data_import.js index 3054a7784c4..30f9323f723 100644 --- a/addons/web/static/src/js/data_import.js +++ b/addons/web/static/src/js/data_import.js @@ -1,6 +1,6 @@ -openerp.web.data_import = function(openerp) { -var QWeb = openerp.web.qweb, - _t = openerp.web._t; +openerp.web.data_import = function(instance) { +var QWeb = instance.web.qweb, + _t = instance.web._t; /** * Safari does not deal well at all with raw JSON data being returned. As a * result, we're going to cheat by using a pseudo-jsonp: instead of getting @@ -29,7 +29,7 @@ function jsonp(form, attributes, callback) { $(form).ajaxSubmit(attributes); } -openerp.web.DataImport = openerp.web.Dialog.extend({ +instance.web.DataImport = instance.web.Dialog.extend({ template: 'ImportDataView', dialog_title: {toString: function () { return _t("Import Data"); }}, init: function(parent, dataset){ @@ -79,10 +79,10 @@ openerp.web.DataImport = openerp.web.Dialog.extend({ this.$element.delegate('fieldset legend', 'click', function() { $(this).parent().toggleClass('oe-closed'); }); - this.ready.push(new openerp.web.DataSet(this, this.model).call( + this.ready.push(new instance.web.DataSet(this, this.model).call( 'fields_get', [], function (fields) { self.graft_fields(fields); - self.ready.push(new openerp.web.DataSet(self, self.model) + self.ready.push(new instance.web.DataSet(self, self.model) .default_get(_.pluck(self.fields, 'id')).then(function (fields) { _.each(fields, function(val, key) { if (val) { @@ -140,7 +140,7 @@ openerp.web.DataImport = openerp.web.Dialog.extend({ f.fields = []; // only fetch sub-fields to a depth of 2 levels if (level < 2) { - self.ready.push(new openerp.web.DataSet(self, field.relation).call( + self.ready.push(new instance.web.DataSet(self, field.relation).call( 'fields_get', [], function (fields) { self.graft_fields(fields, f, level+1); })); @@ -151,7 +151,7 @@ openerp.web.DataImport = openerp.web.Dialog.extend({ }); }, toggle_import_button: function (newstate) { - openerp.web.dialog(this.$element, 'widget') + instance.web.dialog(this.$element, 'widget') .find('.oe-dialog-import-button') .button('option', 'disabled', !newstate); }, diff --git a/addons/web/static/src/js/dates.js b/addons/web/static/src/js/dates.js index d61223113f8..3db48c8412a 100644 --- a/addons/web/static/src/js/dates.js +++ b/addons/web/static/src/js/dates.js @@ -1,5 +1,5 @@ -openerp.web.dates = function(openerp) { +openerp.web.dates = function(instance) { /** * Converts a string to a Date javascript object using OpenERP's @@ -11,7 +11,7 @@ openerp.web.dates = function(openerp) { * @param {String} str A string representing a datetime. * @returns {Date} */ -openerp.web.str_to_datetime = function(str) { +instance.web.str_to_datetime = function(str) { if(!str) { return str; } @@ -38,7 +38,7 @@ openerp.web.str_to_datetime = function(str) { * @param {String} str A string representing a date. * @returns {Date} */ -openerp.web.str_to_date = function(str) { +instance.web.str_to_date = function(str) { if(!str) { return str; } @@ -65,7 +65,7 @@ openerp.web.str_to_date = function(str) { * @param {String} str A string representing a time. * @returns {Date} */ -openerp.web.str_to_time = function(str) { +instance.web.str_to_time = function(str) { if(!str) { return str; } @@ -104,7 +104,7 @@ var zpad = function(str, size) { * @param {Date} obj * @returns {String} A string representing a datetime. */ -openerp.web.datetime_to_str = function(obj) { +instance.web.datetime_to_str = function(obj) { if (!obj) { return false; } @@ -124,7 +124,7 @@ openerp.web.datetime_to_str = function(obj) { * @param {Date} obj * @returns {String} A string representing a date. */ -openerp.web.date_to_str = function(obj) { +instance.web.date_to_str = function(obj) { if (!obj) { return false; } @@ -143,7 +143,7 @@ openerp.web.date_to_str = function(obj) { * @param {Date} obj * @returns {String} A string representing a time. */ -openerp.web.time_to_str = function(obj) { +instance.web.time_to_str = function(obj) { if (!obj) { return false; } diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js index 53863e63e97..640ba4d5a93 100644 --- a/addons/web/static/src/js/formats.js +++ b/addons/web/static/src/js/formats.js @@ -1,6 +1,6 @@ -openerp.web.formats = function(openerp) { -var _t = openerp.web._t; +openerp.web.formats = function(instance) { +var _t = instance.web._t; /** * Intersperses ``separator`` in ``str`` at the positions indicated by @@ -23,7 +23,7 @@ var _t = openerp.web._t; * @param {String} separator * @returns {String} */ -openerp.web.intersperse = function (str, indices, separator) { +instance.web.intersperse = function (str, indices, separator) { separator = separator || ''; var result = [], last = str.length; @@ -55,10 +55,10 @@ openerp.web.intersperse = function (str, indices, separator) { * @param {String} num * @returns {String} */ -openerp.web.insert_thousand_seps = function (num) { +instance.web.insert_thousand_seps = function (num) { var negative = num[0] === '-'; num = (negative ? num.slice(1) : num); - return (negative ? '-' : '') + openerp.web.intersperse( + return (negative ? '-' : '') + instance.web.intersperse( num, _t.database.parameters.grouping, _t.database.parameters.thousands_sep); }; @@ -69,7 +69,7 @@ openerp.web.insert_thousand_seps = function (num) { * * @param {String} value original format */ -openerp.web.strip_raw_chars = function (value) { +instance.web.strip_raw_chars = function (value) { var isletter = /[a-zA-Z]/, output = []; for(var index=0; index < value.length; ++index) { var character = value[index]; @@ -81,7 +81,7 @@ openerp.web.strip_raw_chars = function (value) { return output.join(''); }; var normalize_format = function (format) { - return Date.normalizeFormat(openerp.web.strip_raw_chars(format)); + return Date.normalizeFormat(instance.web.strip_raw_chars(format)); }; /** * Formats a single atomic value based on a field descriptor @@ -93,7 +93,7 @@ var normalize_format = function (format) { * @param {Object} [descriptor.digits] used for the formatting of floats * @param {String} [value_if_empty=''] returned if the ``value`` argument is considered empty */ -openerp.web.format_value = function (value, descriptor, value_if_empty) { +instance.web.format_value = function (value, descriptor, value_if_empty) { // If NaN value, display as with a `false` (empty cell) if (typeof value === 'number' && isNaN(value)) { value = false; @@ -111,16 +111,16 @@ openerp.web.format_value = function (value, descriptor, value_if_empty) { return value_if_empty === undefined ? '' : value_if_empty; } var l10n = _t.database.parameters; - switch (descriptor.widget || descriptor.type) { + switch (descriptor.type || (descriptor.field && descriptor.field.type)) { case 'id': return value.toString(); case 'integer': - return openerp.web.insert_thousand_seps( + return instance.web.insert_thousand_seps( _.str.sprintf('%d', value)); case 'float': var precision = descriptor.digits ? descriptor.digits[1] : 2; var formatted = _.str.sprintf('%.' + precision + 'f', value).split('.'); - formatted[0] = openerp.web.insert_thousand_seps(formatted[0]); + formatted[0] = instance.web.insert_thousand_seps(formatted[0]); return formatted.join(l10n.decimal_point); case 'float_time': var pattern = '%02d:%02d'; @@ -136,17 +136,17 @@ openerp.web.format_value = function (value, descriptor, value_if_empty) { return value[1]; case 'datetime': if (typeof(value) == "string") - value = openerp.web.auto_str_to_date(value); + value = instance.web.auto_str_to_date(value); return value.toString(normalize_format(l10n.date_format) + ' ' + normalize_format(l10n.time_format)); case 'date': if (typeof(value) == "string") - value = openerp.web.auto_str_to_date(value); + value = instance.web.auto_str_to_date(value); return value.toString(normalize_format(l10n.date_format)); case 'time': if (typeof(value) == "string") - value = openerp.web.auto_str_to_date(value); + value = instance.web.auto_str_to_date(value); return value.toString(normalize_format(l10n.time_format)); case 'selection': case 'statusbar': // Each choice is [value, label] @@ -163,7 +163,7 @@ openerp.web.format_value = function (value, descriptor, value_if_empty) { } }; -openerp.web.parse_value = function (value, descriptor, value_if_empty) { +instance.web.parse_value = function (value, descriptor, value_if_empty) { var date_pattern = normalize_format(_t.database.parameters.date_format), time_pattern = normalize_format(_t.database.parameters.time_format); switch (value) { @@ -171,12 +171,12 @@ openerp.web.parse_value = function (value, descriptor, value_if_empty) { case "": return value_if_empty === undefined ? false : value_if_empty; } - switch (descriptor.widget || descriptor.type) { + switch (descriptor.type || (descriptor.field && descriptor.field.type)) { case 'integer': var tmp; do { tmp = value; - value = value.replace(openerp.web._t.database.parameters.thousands_sep, ""); + value = value.replace(instance.web._t.database.parameters.thousands_sep, ""); } while(tmp !== value); tmp = Number(value); if (isNaN(tmp)) @@ -190,9 +190,9 @@ openerp.web.parse_value = function (value, descriptor, value_if_empty) { var tmp2 = value; do { tmp = tmp2; - tmp2 = tmp.replace(openerp.web._t.database.parameters.thousands_sep, ""); + tmp2 = tmp.replace(instance.web._t.database.parameters.thousands_sep, ""); } while(tmp !== tmp2); - var reformatted_value = tmp.replace(openerp.web._t.database.parameters.decimal_point, "."); + var reformatted_value = tmp.replace(instance.web._t.database.parameters.decimal_point, "."); var parsed = Number(reformatted_value); if (isNaN(parsed)) throw new Error(value + " is not a correct float"); @@ -205,62 +205,62 @@ openerp.web.parse_value = function (value, descriptor, value_if_empty) { } var float_time_pair = value.split(":"); if (float_time_pair.length != 2) - return factor * openerp.web.parse_value(value, {type: "float"}); - var hours = openerp.web.parse_value(float_time_pair[0], {type: "integer"}); - var minutes = openerp.web.parse_value(float_time_pair[1], {type: "integer"}); + return factor * instance.web.parse_value(value, {type: "float"}); + var hours = instance.web.parse_value(float_time_pair[0], {type: "integer"}); + var minutes = instance.web.parse_value(float_time_pair[1], {type: "integer"}); return factor * (hours + (minutes / 60)); case 'progressbar': - return openerp.web.parse_value(value, {type: "float"}); + return instance.web.parse_value(value, {type: "float"}); case 'datetime': var datetime = Date.parseExact( value, (date_pattern + ' ' + time_pattern)); if (datetime !== null) - return openerp.web.datetime_to_str(datetime); + return instance.web.datetime_to_str(datetime); datetime = Date.parse(value); if (datetime !== null) - return openerp.web.datetime_to_str(datetime); + return instance.web.datetime_to_str(datetime); throw new Error(value + " is not a valid datetime"); case 'date': var date = Date.parseExact(value, date_pattern); if (date !== null) - return openerp.web.date_to_str(date); + return instance.web.date_to_str(date); date = Date.parse(value); if (date !== null) - return openerp.web.date_to_str(date); + return instance.web.date_to_str(date); throw new Error(value + " is not a valid date"); case 'time': var time = Date.parseExact(value, time_pattern); if (time !== null) - return openerp.web.time_to_str(time); + return instance.web.time_to_str(time); time = Date.parse(value); if (time !== null) - return openerp.web.time_to_str(time); + return instance.web.time_to_str(time); throw new Error(value + " is not a valid time"); } return value; }; -openerp.web.auto_str_to_date = function(value, type) { +instance.web.auto_str_to_date = function(value, type) { try { - return openerp.web.str_to_datetime(value); + return instance.web.str_to_datetime(value); } catch(e) {} try { - return openerp.web.str_to_date(value); + return instance.web.str_to_date(value); } catch(e) {} try { - return openerp.web.str_to_time(value); + return instance.web.str_to_time(value); } catch(e) {} throw new Error("'" + value + "' is not a valid date, datetime nor time"); }; -openerp.web.auto_date_to_str = function(value, type) { +instance.web.auto_date_to_str = function(value, type) { switch(type) { case 'datetime': - return openerp.web.datetime_to_str(value); + return instance.web.datetime_to_str(value); case 'date': - return openerp.web.date_to_str(value); + return instance.web.date_to_str(value); case 'time': - return openerp.web.time_to_str(value); + return instance.web.time_to_str(value); default: throw new Error(type + " is not convertible to date, datetime nor time"); } @@ -290,7 +290,7 @@ openerp.web.auto_date_to_str = function(value, type) { * @param {Number} [options.id] current record's id * */ -openerp.web.format_cell = function (row_data, column, options) { +instance.web.format_cell = function (row_data, column, options) { options = options || {}; var attrs = {}; if (options.process_modifiers !== false) { @@ -303,9 +303,9 @@ openerp.web.format_cell = function (row_data, column, options) { '<img src="<%-prefix%>/web/static/src/img/icons/<%-icon%>.png" alt="<%-alt%>"/>' + '</button>', { title: column.string || '', - additional_attributes: isNaN(row_data["id"].value) && openerp.web.BufferedDataSet.virtual_id_regex.test(row_data["id"].value) ? + additional_attributes: isNaN(row_data["id"].value) && instance.web.BufferedDataSet.virtual_id_regex.test(row_data["id"].value) ? 'disabled="disabled" class="oe-listview-button-disabled"' : '', - prefix: openerp.connection.prefix, + prefix: instance.connection.prefix, icon: column.icon, alt: column.string || '' }); @@ -320,11 +320,11 @@ openerp.web.format_cell = function (row_data, column, options) { row_data[column.id].value ? 'checked="checked"' : ''); case "binary": var text = _t("Download"), - download_url = _.str.sprintf('/web/binary/saveas?session_id=%s&model=%s&field=%s&id=%d', openerp.connection.session_id, options.model, column.id, options.id); + download_url = _.str.sprintf('/web/binary/saveas?session_id=%s&model=%s&field=%s&id=%d', instance.connection.session_id, options.model, column.id, options.id); if (column.filename) { download_url += '&filename_field=' + column.filename; if (row_data[column.filename]) { - text = _.str.sprintf(_t("Download \"%s\""), openerp.web.format_value( + text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value( row_data[column.filename].value, {type: 'char'})); } } @@ -340,7 +340,7 @@ openerp.web.format_cell = function (row_data, column, options) { }); } - return _.escape(openerp.web.format_value( + return _.escape(instance.web.format_value( row_data[column.id].value, column, options.value_if_empty)); } diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 773c10382f6..c2df866b5c6 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -1,13 +1,80 @@ -openerp.web.search = function(openerp) { -var QWeb = openerp.web.qweb, - _t = openerp.web._t, - _lt = openerp.web._lt; +openerp.web.search = function(instance) { +var QWeb = instance.web.qweb, + _t = instance.web._t, + _lt = instance.web._lt; +_.mixin({ + sum: function (obj) { return _.reduce(obj, function (a, b) { return a + b; }, 0); } +}); + +// Have SearchBox optionally use callback function to produce inputs and facets +// (views) set on callbacks.make_facet and callbacks.make_input keys when +// initializing VisualSearch +var SearchBox_renderFacet = function (facet, position) { + var view = new (this.app.options.callbacks['make_facet'] || VS.ui.SearchFacet)({ + app : this.app, + model : facet, + order : position + }); -openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.SearchView# */{ - template: "EmptyComponent", + // Input first, facet second. + this.renderSearchInput(); + this.facetViews.push(view); + this.$('.VS-search-inner').children().eq(position*2).after(view.render().el); + + view.calculateSize(); + _.defer(_.bind(view.calculateSize, view)); + + return view; + }; // warning: will not match +// Ensure we're replacing the function we think +if (SearchBox_renderFacet.toString() !== VS.ui.SearchBox.prototype.renderFacet.toString().replace(/(VS\.ui\.SearchFacet)/, "(this.app.options.callbacks['make_facet'] || $1)")) { + throw new Error( + "Trying to replace wrong version of VS.ui.SearchBox#renderFacet. " + + "Please fix replacement."); +} +var SearchBox_renderSearchInput = function () { + var input = new (this.app.options.callbacks['make_input'] || VS.ui.SearchInput)({position: this.inputViews.length, app: this.app}); + this.$('.VS-search-inner').append(input.render().el); + this.inputViews.push(input); + }; +// Ensure we're replacing the function we think +if (SearchBox_renderSearchInput.toString() !== VS.ui.SearchBox.prototype.renderSearchInput.toString().replace(/(VS\.ui\.SearchInput)/, "(this.app.options.callbacks['make_input'] || $1)")) { + throw new Error( + "Trying to replace wrong version of VS.ui.SearchBox#renderSearchInput. " + + "Please fix replacement."); +} +var SearchBox_searchEvent = function (e) { + var query = null; + this.renderFacets(); + this.focusSearch(e); + this.app.options.callbacks.search(query, this.app.searchQuery); + }; +if (SearchBox_searchEvent.toString() !== VS.ui.SearchBox.prototype.searchEvent.toString().replace( + /this\.value\(\);\n[ ]{4}this\.focusSearch\(e\);\n[ ]{4}this\.value\(query\)/, + 'null;\n this.renderFacets();\n this.focusSearch(e)')) { + throw new Error( + "Trying to replace wrong version of VS.ui.SearchBox#searchEvent. " + + "Please fix replacement."); +} +_.extend(VS.ui.SearchBox.prototype, { + renderFacet: SearchBox_renderFacet, + renderSearchInput: SearchBox_renderSearchInput, + searchEvent: SearchBox_searchEvent +}); +_.extend(VS.model.SearchFacet.prototype, { + value: function () { + if (this.has('json')) { + return this.get('json'); + } + return this.get('value'); + } +}); + +instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.SearchView# */{ + template: "SearchView", /** - * @constructs openerp.web.SearchView - * @extends openerp.web.OldWidget + * @constructs instance.web.SearchView + * @extends instance.web.OldWidget * * @param parent * @param element_id @@ -25,9 +92,7 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea this.has_defaults = !_.isEmpty(this.defaults); this.inputs = []; - this.enabled_filters = []; - - this.has_focus = false; + this.controls = {}; this.hidden = !!hidden; this.headless = this.hidden && !this.has_defaults; @@ -37,20 +102,63 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea this.ready = $.Deferred(); }, start: function() { - this._super(); + var self = this; + var p = this._super(); + + this.setup_global_completion(); + this.vs = VS.init({ + container: this.$element, + query: '', + callbacks: { + make_facet: this.proxy('make_visualsearch_facet'), + make_input: this.proxy('make_visualsearch_input'), + search: function (query, searchCollection) { + self.do_search(); + }, + facetMatches: function (callback) { + }, + valueMatches : function(facet, searchTerm, callback) { + } + } + }); + + var search = function () { self.vs.searchBox.searchEvent({}); }; + // searchQuery operations + this.vs.searchQuery + .off('add').on('add', search) + .off('change').on('change', search) + .off('reset').on('reset', search) + .off('remove').on('remove', function (record, collection, options) { + if (options['trigger_search']) { + search(); + } + }); + if (this.hidden) { this.$element.hide(); } if (this.headless) { this.ready.resolve(); } else { - this.rpc("/web/searchview/load", { + var load_view = this.rpc("/web/searchview/load", { model: this.model, view_id: this.view_id, - context: this.dataset.get_context() - }, this.on_loaded); + context: this.dataset.get_context() }); + // FIXME: local eval of domain and context to get rid of special endpoint + var filters = this.rpc('/web/searchview/get_filters', { + model: this.model + }).then(function (filters) { self.custom_filters = filters; }); + + $.when(load_view, filters) + .pipe(function (load) { return load[0]; }) + .then(this.on_loaded); } - return this.ready.promise(); + + this.$element.on('click', '.oe_vs_unfold_drawer', function () { + self.$element.toggleClass('oe_searchview_open_drawer'); + }); + + return $.when(p, this.ready); }, show: function () { this.$element.show(); @@ -58,61 +166,227 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea hide: function () { this.$element.hide(); }, + + /** + * Sets up thingie where all the mess is put? + */ + setup_stuff_drawer: function () { + var self = this; + $('<div class="oe_vs_unfold_drawer">').appendTo(this.$element.find('.VS-search-box')); + var $drawer = $('<div class="oe_searchview_drawer">').appendTo(this.$element); + var $filters = $('<div class="oe_searchview_filters">').appendTo($drawer); + + var running_count = 0; + // get total filters count + var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; }; + var filters_count = _(this.controls).chain() + .flatten() + .filter(is_group) + .map(function (i) { return i.filters.length; }) + .sum() + .value(); + + var col1 = [], col2 = _(this.controls).map(function (inputs, group) { + var filters = _(inputs).filter(is_group); + return { + name: group === 'null' ? _t("Filters") : group, + filters: filters, + length: _(filters).chain().map(function (i) { + return i.filters.length; }).sum().value() + }; + }); + + while (col2.length) { + // col1 + group should be smaller than col2 + group + if ((running_count + col2[0].length) <= (filters_count - running_count)) { + running_count += col2[0].length; + col1.push(col2.shift()); + } else { + break; + } + } + + // Create a Custom Filter FilterGroup for each custom filter read from + // the db, add all of this as a group in the smallest column + [].push.call(col1.length <= col2.length ? col1 : col2, { + name: _t("Custom Filters"), + filters: _.map(this.custom_filters, function (filter) { + // FIXME: handling of ``disabled`` being set + var f = new instance.web.search.Filter({attrs: { + string: filter.name, + context: filter.context, + domain: filter.domain + }}, self); + return new instance.web.search.FilterGroup([f], self); + }), + length: 3 + }); + + return $.when( + this.render_column(col1, $('<div>').appendTo($filters)), + this.render_column(col2, $('<div>').appendTo($filters)), + (new instance.web.search.Advanced(this).appendTo($drawer))); + }, + render_column: function (column, $el) { + return $.when.apply(null, _(column).map(function (group) { + $('<h3>').text(group.name).appendTo($el); + return $.when.apply(null, + _(group.filters).invoke('appendTo', $el)); + })); + }, + /** + * Sets up search view's view-wide auto-completion widget + */ + setup_global_completion: function () { + // Prevent keydown from within a facet's input from reaching the + // auto-completion widget and opening the completion list + this.$element.on('keydown', '.search_facet input', function (e) { + e.stopImmediatePropagation(); + }); + + this.$element.autocomplete({ + source: this.proxy('complete_global_search'), + select: this.proxy('select_completion'), + focus: function (e) { e.preventDefault(); }, + html: true, + minLength: 0, + delay: 0 + }).data('autocomplete')._renderItem = function (ul, item) { + // item of completion list + var $item = $( "<li></li>" ) + .data( "item.autocomplete", item ) + .appendTo( ul ); + + if (item.value !== undefined) { + // regular completion item + return $item.append( + (item.label) + ? $('<a>').html(item.label) + : $('<a>').text(item.value)); + } + return $item.text(item.category) + .css({ + borderTop: '1px solid #cccccc', + margin: 0, + padding: 0, + zoom: 1, + 'float': 'left', + clear: 'left', + width: '100%' + }); + } + }, + /** + * Provide auto-completion result for req.term (an array to `resp`) + * + * @param {Object} req request to complete + * @param {String} req.term searched term to complete + * @param {Function} resp response callback + */ + complete_global_search: function (req, resp) { + $.when.apply(null, _(this.inputs).chain() + .invoke('complete', req.term) + .value()).then(function () { + resp(_(_(arguments).compact()).flatten(true)); + }); + }, + + /** + * Action to perform in case of selection: create a facet (model) + * and add it to the search collection + * + * @param {Object} e selection event, preventDefault to avoid setting value on object + * @param {Object} ui selection information + * @param {Object} ui.item selected completion item + */ + select_completion: function (e, ui) { + e.preventDefault(); + this.vs.searchQuery.add(new VS.model.SearchFacet(_.extend( + {app: this.vs}, ui.item))); + this.vs.searchBox.searchEvent({}); + }, + + /** + * Builds the right SearchFacet view based on the facet object to render + * (e.g. readonly facets for filters) + * + * @param {Object} options + * @param {VS.model.SearchFacet} options.model facet object to render + */ + make_visualsearch_facet: function (options) { + return new instance.web.search.FilterGroupFacet(options); + +// if (options.model.get('field') instanceof instance.web.search.FilterGroup) { +// return new instance.web.search.FilterGroupFacet(options); +// } +// return new VS.ui.SearchFacet(options); + }, + /** + * Proxies searches on a SearchInput to the search view's global completion + * + * Also disables SearchInput.autocomplete#_move so search view's + * autocomplete can get the corresponding events, or something. + * + * @param options + */ + make_visualsearch_input: function (options) { + var self = this, input = new VS.ui.SearchInput(options); + input.setupAutocomplete = function () { + _.extend(this.box.autocomplete({ + minLength: 1, + delay: 0, + search: function () { + self.$element.autocomplete('search', input.box.val()); + return false; + } + }).data('autocomplete'), { + _move: function () {}, + close: function () { self.$element.autocomplete('close'); } + }); + }; + return input; + }, + /** * Builds a list of widget rows (each row is an array of widgets) * * @param {Array} items a list of nodes to convert to widgets * @param {Object} fields a mapping of field names to (ORM) field attributes - * @returns Array + * @param {String} [group_name] name of the group to put the new controls in */ - make_widgets: function (items, fields) { - var rows = [], - row = []; - rows.push(row); + make_widgets: function (items, fields, group_name) { + group_name = group_name || null; + if (!(group_name in this.controls)) { + this.controls[group_name] = []; + } + var self = this, group = this.controls[group_name]; var filters = []; _.each(items, function (item) { if (filters.length && item.tag !== 'filter') { - row.push( - new openerp.web.search.FilterGroup( - filters, this)); + group.push(new instance.web.search.FilterGroup(filters, this)); filters = []; } - if (item.tag === 'newline') { - row = []; - rows.push(row); - } else if (item.tag === 'filter') { - if (!this.has_focus) { - item.attrs.default_focus = '1'; - this.has_focus = true; - } - filters.push( - new openerp.web.search.Filter( - item, this)); - } else if (item.tag === 'separator') { - // a separator is a no-op - } else { - if (item.tag === 'group') { - // TODO: group and field should be fetched from registries, maybe even filters - row.push( - new openerp.web.search.Group( - item, this, fields)); - } else if (item.tag === 'field') { - if (!this.has_focus) { - item.attrs.default_focus = '1'; - this.has_focus = true; - } - row.push( - this.make_field( - item, fields[item['attrs'].name])); - } + switch (item.tag) { + case 'separator': case 'newline': + break; + case 'filter': + filters.push(new instance.web.search.Filter(item, this)); + break; + case 'group': + self.make_widgets(item.children, fields, item.attrs.string); + break; + case 'field': + group.push(this.make_field(item, fields[item['attrs'].name])); + // filters + self.make_widgets(item.children, fields, group_name); + break; } }, this); + if (filters.length) { - row.push(new openerp.web.search.FilterGroup(filters, this)); + group.push(new instance.web.search.FilterGroup(filters, this)); } - - return rows; }, /** * Creates a field for the provided field descriptor item (which comes @@ -120,18 +394,13 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea * * @param {Object} item fields_view_get node for the field * @param {Object} field fields_get result for the field - * @returns openerp.web.search.Field + * @returns instance.web.search.Field */ make_field: function (item, field) { - try { - return new (openerp.web.search.fields.get_any( - [item.attrs.widget, field.type])) - (item, field, this); - } catch (e) { - if (! e instanceof openerp.web.KeyNotFound) { - throw e; - } - // KeyNotFound means unknown field type + var obj = instance.web.search.fields.get_any( [item.attrs.widget, field.type]); + if(obj) { + return new (obj) (item, field, this); + } else { console.group('Unknown field type ' + field.type); console.error('View node', item); console.info('View field', field); @@ -141,6 +410,7 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea } }, on_loaded: function(data) { + var self = this; this.fields_view = data.fields_view; if (data.fields_view.type !== 'search' || data.fields_view.arch.tag !== 'search') { @@ -148,52 +418,20 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea "Got non-search view after asking for a search view: type %s, arch root %s", data.fields_view.type, data.fields_view.arch.tag)); } - var self = this, - lines = this.make_widgets( - data.fields_view['arch'].children, - data.fields_view.fields); - - // for extended search view - var ext = new openerp.web.search.ExtendedSearch(this, this.model); - lines.push([ext]); - this.extended_search = ext; - - var render = QWeb.render("SearchView", { - 'view': data.fields_view['arch'], - 'lines': lines, - 'defaults': this.defaults - }); - this.$element.html(render); + this.make_widgets( + data.fields_view['arch'].children, + data.fields_view.fields); - var f = this.$element.find('form'); - this.$element.find('form') - .submit(this.do_search) - .bind('reset', this.do_clear); - // start() all the widgets - var widget_starts = _(lines).chain().flatten().map(function (widget) { - return widget.start(); - }).value(); - - $.when.apply(null, widget_starts).then(function () { - self.ready.resolve(); - }); - - this.reload_managed_filters(); - }, - reload_managed_filters: function() { - var self = this; - return this.rpc('/web/searchview/get_filters', { - model: this.dataset.model - }).then(function(result) { - self.managed_filters = result; - var filters = self.$element.find(".oe_search-view-filters-management"); - filters.html(QWeb.render("SearchView.managed-filters", { - filters: result, - disabled_filter_message: _t('Filter disabled due to invalid syntax') - })); - filters.change(self.on_filters_management); - }); + // load defaults + return $.when( + this.setup_stuff_drawer(), + $.when.apply(null, _(this.inputs).invoke('facet_for_defaults', this.defaults)) + .then(function () { + self.vs.searchQuery.reset(_(arguments).compact(), {silent: true}); + self.vs.searchBox.renderFacets(); + })) + .then(function () { self.ready.resolve(); }) }, /** * Handle event when the user make a selection in the filters management select box. @@ -222,11 +460,11 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea break; case 'save_filter': var data = this.build_search_data(); - var context = new openerp.web.CompoundContext(); + var context = new instance.web.CompoundContext(); _.each(data.contexts, function(x) { context.add(x); }); - var domain = new openerp.web.CompoundDomain(); + var domain = new instance.web.CompoundDomain(); _.each(data.domains, function(x) { domain.add(x); }); @@ -234,7 +472,7 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea context.add({"group_by": groupbys}); var dial_html = QWeb.render("SearchView.managed-filters.add"); var $dial = $(dial_html); - openerp.web.dialog($dial, { + instance.web.dialog($dial, { modal: true, title: _t("Filter Entry"), buttons: [ @@ -287,13 +525,13 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea on_add_to_dashboard: function() { this.$element.find(".oe_search-view-filters-management")[0].selectedIndex = 0; var self = this, - menu = openerp.webclient.menu, + menu = instance.webclient.menu, $dialog = $(QWeb.render("SearchView.add_to_dashboard", { dashboards : menu.data.data.children, selected_menu_id : menu.$element.find('a.active').data('menu') })); $dialog.find('input').val(this.fields_view.name); - openerp.web.dialog($dialog, { + instance.web.dialog($dialog, { modal: true, title: _t("Add to Dashboard"), buttons: [ @@ -305,8 +543,8 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea var menu_id = $(this).find("select").val(), title = $(this).find("input").val(), data = self.build_search_data(), - context = new openerp.web.CompoundContext(), - domain = new openerp.web.CompoundDomain(); + context = new instance.web.CompoundContext(), + domain = new instance.web.CompoundDomain(); _.each(data.contexts, function(x) { context.add(x); }); @@ -335,47 +573,33 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea * Performs the search view collection of widget data. * * If the collection went well (all fields are valid), then triggers - * :js:func:`openerp.web.SearchView.on_search`. + * :js:func:`instance.web.SearchView.on_search`. * * If at least one field failed its validation, triggers - * :js:func:`openerp.web.SearchView.on_invalid` instead. + * :js:func:`instance.web.SearchView.on_invalid` instead. * * @param e jQuery event object coming from the "Search" button */ - do_search: function (e) { - if (this.headless && !this.has_defaults) { - return this.on_search([], [], []); - } - - if (e && e.preventDefault) { e.preventDefault(); } - - var data = this.build_search_data(); + do_search: function () { + var domains = [], contexts = [], groupbys = [], errors = []; - if (data.errors.length) { - this.on_invalid(data.errors); - return; - } - - this.on_search(data.domains, data.contexts, data.groupbys); - }, - build_search_data: function() { - var domains = [], - contexts = [], - errors = []; - - _.each(this.inputs, function (input) { + this.vs.searchQuery.each(function (facet) { + var field = facet.get('field'); try { - var domain = input.get_domain(); + var domain = field.get_domain(facet); if (domain) { domains.push(domain); } - - var context = input.get_context(); + var context = field.get_context(facet); if (context) { contexts.push(context); } + var group_by = field.get_groupby(facet); + if (group_by) { + groupbys.push.apply(groupbys, group_by); + } } catch (e) { - if (e instanceof openerp.web.search.Invalid) { + if (e instanceof instance.web.search.Invalid) { errors.push(e); } else { throw e; @@ -383,23 +607,11 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea } }); - // TODO: do we need to handle *fields* with group_by in their context? - var groupbys = _(this.enabled_filters) - .chain() - .map(function (filter) { return filter.get_context();}) - .compact() - .value(); - - if (this.filter_data.contexts) { - contexts = this.filter_data.contexts.concat(contexts) - } - if (this.filter_data.domains) { - domains = this.filter_data.domains.concat(domains); - } - if (this.filter_data.groupbys) { - groupbys = this.filter_data.groupbys.concat(groupbys); + if (!_.isEmpty(errors)) { + this.on_invalid(errors); + return; } - return {domains: domains, contexts: contexts, errors: errors, groupbys: groupbys}; + return this.on_search(domains, contexts, groupbys); }, /** * Triggered after the SearchView has collected all relevant domains and @@ -432,84 +644,68 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea */ on_invalid: function (errors) { this.do_notify(_t("Invalid Search"), _t("triggered from search view")); - }, - /** - * @param {Boolean} [reload_view=true] - */ - do_clear: function (reload_view) { - this.filter_data = {}; - this.$element.find(".oe_search-view-filters-management").val(''); + } +}); + +/** @namespace */ +instance.web.search = {}; - this.$element.find('.filter_label, .filter_icon').removeClass('enabled'); - this.enabled_filters.splice(0); - var string = $('a.searchview_group_string'); - _.each(string, function(str){ - $(str).closest('div.searchview_group').removeClass("expanded").addClass('folded'); - }); - this.$element.find('table:last').hide(); +instance.web.search.FilterGroupFacet = VS.ui.SearchFacet.extend({ + events: _.extend({ + 'click': 'selectFacet' + }, VS.ui.SearchFacet.prototype.events), - $('.searchview_extended_groups_list').empty(); - return $.async_when.apply( - null, _(this.inputs).invoke('clear')).pipe( - reload_view !== false ? this.on_clear : null); + render: function () { + this.setMode('not', 'editing'); + this.setMode('not', 'selected'); + + var value = this.model.get('value'); + this.$el.html(QWeb.render('SearchView.filters.facet', { + facet: this.model + })); + // virtual input so SearchFacet code has something to play with + this.box = $('<input>').val(value); + + return this; }, - /** - * Triggered when the search view gets cleared - * - * @event - */ - on_clear: function () { - this.do_search(); + enableEdit: function () { + this.selectFacet() }, - /** - * Called by a filter propagating its state changes - * - * @param {openerp.web.search.Filter} filter a filter which got toggled - * @param {Boolean} default_enabled filter got enabled through the default values, at render time. - */ - do_toggle_filter: function (filter, default_enabled) { - if (default_enabled || filter.is_enabled()) { - this.enabled_filters.push(filter); - } else { - this.enabled_filters = _.without( - this.enabled_filters, filter); - } - - if (!default_enabled) { - // selecting a filter after initial loading automatically - // triggers refresh - this.$element.find('form').submit(); + keydown: function (e) { + var key = VS.app.hotkeys.key(e); + if (key !== 'right') { + return VS.ui.SearchFacet.prototype.keydown.call(this, e); } + e.preventDefault(); + this.deselectFacet(); + this.options.app.searchBox.focusNextFacet(this, 1); } }); - -/** @namespace */ -openerp.web.search = {}; /** - * Registry of search fields, called by :js:class:`openerp.web.SearchView` to + * Registry of search fields, called by :js:class:`instance.web.SearchView` to * find and instantiate its field widgets. */ -openerp.web.search.fields = new openerp.web.Registry({ - 'char': 'openerp.web.search.CharField', - 'text': 'openerp.web.search.CharField', - 'boolean': 'openerp.web.search.BooleanField', - 'integer': 'openerp.web.search.IntegerField', - 'id': 'openerp.web.search.IntegerField', - 'float': 'openerp.web.search.FloatField', - 'selection': 'openerp.web.search.SelectionField', - 'datetime': 'openerp.web.search.DateTimeField', - 'date': 'openerp.web.search.DateField', - 'many2one': 'openerp.web.search.ManyToOneField', - 'many2many': 'openerp.web.search.CharField', - 'one2many': 'openerp.web.search.CharField' +instance.web.search.fields = new instance.web.Registry({ + 'char': 'instance.web.search.CharField', + 'text': 'instance.web.search.CharField', + 'boolean': 'instance.web.search.BooleanField', + 'integer': 'instance.web.search.IntegerField', + 'id': 'instance.web.search.IntegerField', + 'float': 'instance.web.search.FloatField', + 'selection': 'instance.web.search.SelectionField', + 'datetime': 'instance.web.search.DateTimeField', + 'date': 'instance.web.search.DateField', + 'many2one': 'instance.web.search.ManyToOneField', + 'many2many': 'instance.web.search.CharField', + 'one2many': 'instance.web.search.CharField' }); -openerp.web.search.Invalid = openerp.web.Class.extend( /** @lends openerp.web.search.Invalid# */{ +instance.web.search.Invalid = instance.web.Class.extend( /** @lends instance.web.search.Invalid# */{ /** * Exception thrown by search widgets when they hold invalid values, * which they can not return when asked. * - * @constructs openerp.web.search.Invalid - * @extends openerp.web.Class + * @constructs instance.web.search.Invalid + * @extends instance.web.Class * * @param field the name of the field holding an invalid value * @param value the invalid value @@ -527,92 +723,42 @@ openerp.web.search.Invalid = openerp.web.Class.extend( /** @lends openerp.web.se ); } }); -openerp.web.search.Widget = openerp.web.OldWidget.extend( /** @lends openerp.web.search.Widget# */{ +instance.web.search.Widget = instance.web.OldWidget.extend( /** @lends instance.web.search.Widget# */{ template: null, /** * Root class of all search widgets * - * @constructs openerp.web.search.Widget - * @extends openerp.web.OldWidget + * @constructs instance.web.search.Widget + * @extends instance.web.OldWidget * * @param view the ancestor view of this widget */ init: function (view) { this._super(view); this.view = view; - }, - /** - * Sets and returns a globally unique identifier for the widget. - * - * If a prefix is specified, the identifier will be appended to it. - * - * @params prefix prefix sections, empty/falsy sections will be removed - */ - make_id: function () { - this.element_id = _.uniqueId( - ['search'].concat( - _.compact(_.toArray(arguments)), - ['']).join('_')); - return this.element_id; - }, - /** - * "Starts" the widgets. Called at the end of the rendering, this allows - * widgets to hook themselves to their view sections. - * - * On widgets, if they kept a reference to a view and have an element_id, - * will fetch and set their root element on $element. - */ - start: function () { - this._super(); - if (this.view && this.element_id) { - // id is unique, and no getElementById on elements - this.$element = $(document.getElementById( - this.element_id)); - } - }, - /** - * "Stops" the widgets. Called when the view destroys itself, this - * lets the widgets clean up after themselves. - */ - destroy: function () { - delete this.view; - this._super(); - }, - render: function (defaults) { - // FIXME - return this._super(_.extend(this, {defaults: defaults})); } }); -openerp.web.search.add_expand_listener = function($root) { +instance.web.search.add_expand_listener = function($root) { $root.find('a.searchview_group_string').click(function (e) { $root.toggleClass('folded expanded'); e.stopPropagation(); e.preventDefault(); }); }; -openerp.web.search.Group = openerp.web.search.Widget.extend({ +instance.web.search.Group = instance.web.search.Widget.extend({ template: 'SearchView.group', init: function (view_section, view, fields) { this._super(view); this.attrs = view_section.attrs; this.lines = view.make_widgets( view_section.children, fields); - this.make_id('group'); - }, - start: function () { - this._super(); - openerp.web.search.add_expand_listener(this.$element); - var widget_starts = _(this.lines).chain().flatten() - .map(function (widget) { return widget.start(); }) - .value(); - return $.when.apply(null, widget_starts); } }); -openerp.web.search.Input = openerp.web.search.Widget.extend( /** @lends openerp.web.search.Input# */{ +instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instance.web.search.Input# */{ /** - * @constructs openerp.web.search.Input - * @extends openerp.web.search.Widget + * @constructs instance.web.search.Input + * @extends instance.web.search.Widget * * @param view */ @@ -621,10 +767,44 @@ openerp.web.search.Input = openerp.web.search.Widget.extend( /** @lends openerp. this.view.inputs.push(this); this.style = undefined; }, + /** + * Fetch auto-completion values for the widget. + * + * The completion values should be an array of objects with keys category, + * label, value prefixed with an object with keys type=section and label + * + * @param {String} value value to complete + * @returns {jQuery.Deferred<null|Array>} + */ + complete: function (value) { + return $.when(null) + }, + /** + * Returns a VS.model.SearchFacet instance for the provided defaults if + * they apply to this widget, or null if they don't. + * + * This default implementation will try calling + * :js:func:`instance.web.search.Input#facet_for` if the widget's name + * matches the input key + * + * @param {Object} defaults + * @returns {jQuery.Deferred<null|Object>} + */ + facet_for_defaults: function (defaults) { + if (!this.attrs || + !(this.attrs.name in defaults && defaults[this.attrs.name])) { + return $.when(null); + } + return this.facet_for(defaults[this.attrs.name]); + }, get_context: function () { throw new Error( "get_context not implemented for widget " + this.attrs.type); }, + get_groupby: function () { + throw new Error( + "get_groupby not implemented for widget " + this.attrs.type); + }, get_domain: function () { throw new Error( "get_domain not implemented for widget " + this.attrs.type); @@ -638,42 +818,80 @@ openerp.web.search.Input = openerp.web.search.Widget.extend( /** @lends openerp. } } this.attrs = attrs; - }, - /** - * Specific clearing operations, if any - */ - clear: function () {} + } }); -openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends openerp.web.search.FilterGroup# */{ +instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends instance.web.search.FilterGroup# */{ template: 'SearchView.filters', /** * Inclusive group of filters, creates a continuous "button" with clickable * sections (the normal display for filters is to be a self-contained button) * - * @constructs openerp.web.search.FilterGroup - * @extends openerp.web.search.Input + * @constructs instance.web.search.FilterGroup + * @extends instance.web.search.Input * - * @param {Array<openerp.web.search.Filter>} filters elements of the group - * @param {openerp.web.SearchView} view view in which the filters are contained + * @param {Array<instance.web.search.Filter>} filters elements of the group + * @param {instance.web.SearchView} view view in which the filters are contained */ init: function (filters, view) { this._super(view); this.filters = filters; - this.length = filters.length; }, start: function () { - this._super(); - _.each(this.filters, function (filter) { - filter.start(); + this.$element.on('click', 'li', this.proxy('toggle_filter')); + return $.when(null); + }, + facet_for_defaults: function (defaults) { + var fs = _(this.filters).filter(function (f) { + return f.attrs && f.attrs.name && !!defaults[f.attrs.name]; }); + if (_.isEmpty(fs)) { return $.when(null); } + return $.when(new VS.model.SearchFacet({ + category: _t("Filter"), + value: _(fs).map(function (f) { + return f.attrs.string || f.attrs.name }).join(' | '), + json: fs, + field: this, + app: this.view.vs + })); + }, + /** + * Fetches contexts for all enabled filters in the group + * + * @param {VS.model.SearchFacet} facet + * @return {*} combined contexts of the enabled filters in this group + */ + get_context: function (facet) { + var contexts = _(facet.get('json')).chain() + .map(function (filter) { return filter.attrs.context; }) + .reject(_.isEmpty) + .value(); + + if (!contexts.length) { return; } + if (contexts.length === 1) { return contexts[0]; } + return _.extend(new instance.web.CompoundContext, { + __contexts: contexts + }); + }, + /** + * Fetches group_by sequence for all enabled filters in the group + * + * @param {VS.model.SearchFacet} facet + * @return {Array} enabled filters in this group + */ + get_groupby: function (facet) { + return _(facet.get('json')).chain() + .map(function (filter) { return filter.attrs.context; }) + .reject(_.isEmpty) + .value(); }, - get_context: function () { }, /** * Handles domains-fetching for all the filters within it: groups them. + * + * @param {VS.model.SearchFacet} facet + * @return {*} combined domains of the enabled filters in this group */ - get_domain: function () { - var domains = _(this.filters).chain() - .filter(function (filter) { return filter.is_enabled(); }) + get_domain: function (facet) { + var domains = _(facet.get('json')).chain() .map(function (filter) { return filter.attrs.domain; }) .reject(_.isEmpty) .value(); @@ -683,19 +901,62 @@ openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends open for (var i=domains.length; --i;) { domains.unshift(['|']); } - return _.extend(new openerp.web.CompoundDomain(), { + return _.extend(new instance.web.CompoundDomain(), { __domains: domains }); + }, + toggle_filter: function (e) { + this.toggle(this.filters[$(e.target).index()]); + }, + toggle: function (filter) { + // FIXME: oh god, my eyes, they hurt + var self = this, fs; + var facet = this.view.vs.searchQuery.detect(function (f) { + return f.get('field') === self; }); + if (facet) { + fs = facet.get('json'); + + if (_.include(fs, filter)) { + fs = _.without(fs, filter); + } else { + fs.push(filter); + } + if (_(fs).isEmpty()) { + this.view.vs.searchQuery.remove(facet, {trigger_search: true}); + } else { + facet.set({ + json: fs, + value: _(fs).map(function (f) { + return f.attrs.string || f.attrs.name }).join(' | ') + }); + } + return; + } else { + fs = [filter]; + } + + this.view.vs.searchQuery.add({ + category: _t("Filter"), + value: _(fs).map(function (f) { + return f.attrs.string || f.attrs.name }).join(' | '), + json: fs, + field: this, + app: this.view.vs + }); } }); -openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.web.search.Filter# */{ +instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instance.web.search.Filter# */{ template: 'SearchView.filter', /** * Implementation of the OpenERP filters (button with a context and/or * a domain sent as-is to the search view) * - * @constructs openerp.web.search.Filter - * @extends openerp.web.search.Input + * Filters are only attributes holder, the actual work (compositing + * domains and contexts, converting between facets and filters) is + * performed by the filter group. + * + * @constructs instance.web.search.Filter + * @extends instance.web.search.Input * * @param node * @param view @@ -703,56 +964,17 @@ openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.w init: function (node, view) { this._super(view); this.load_attrs(node.attrs); - this.classes = [this.attrs.string ? 'filter_label' : 'filter_icon']; - this.make_id('filter', this.attrs.name); - }, - start: function () { - this._super(); - var self = this; - this.$element.click(function (e) { - $(this).toggleClass('enabled'); - self.view.do_toggle_filter(self); - }); - }, - /** - * Returns whether the filter is currently enabled (in use) or not. - * - * @returns a boolean - */ - is_enabled:function () { - return this.$element.hasClass('enabled'); - }, - /** - * If the filter is present in the defaults (and has a truthy value), - * enable the filter. - * - * @param {Object} defaults the search view's default values - */ - render: function (defaults) { - if (this.attrs.name && defaults[this.attrs.name]) { - this.classes.push('enabled'); - this.view.do_toggle_filter(this, true); - } - return this._super(defaults); }, - get_context: function () { - if (!this.is_enabled()) { - return; - } - return this.attrs.context; - }, - /** - * Does not return anything: filter domain is handled at the FilterGroup - * level - */ - get_domain: function () { } + facet_for: function () { return $.when(null); }, + get_context: function () { }, + get_domain: function () { }, }); -openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.web.search.Field# */ { +instance.web.search.Field = instance.web.search.Input.extend( /** @lends instance.web.search.Field# */ { template: 'SearchView.field', default_operator: '=', /** - * @constructs openerp.web.search.Field - * @extends openerp.web.search.Input + * @constructs instance.web.search.Field + * @extends instance.web.search.Input * * @param view_section * @param field @@ -761,25 +983,21 @@ openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.w init: function (view_section, field, view) { this._super(view); this.load_attrs(_.extend({}, field, view_section.attrs)); - this.filters = new openerp.web.search.FilterGroup(_.compact(_.map( - view_section.children, function (filter_node) { - if (filter_node.attrs.string && - typeof console !== 'undefined' && console.debug) { - console.debug("Filter-in-field with a 'string' attribute " - + "in view", view); - } - delete filter_node.attrs.string; - return new openerp.web.search.Filter( - filter_node, view); - })), view); - this.make_id('input', field.type, this.attrs.name); }, - start: function () { - this._super(); - this.filters.start(); + facet_for: function (value) { + return $.when(new VS.model.SearchFacet({ + category: this.attrs.string || this.attrs.name, + value: String(value), + json: value, + field: this, + app: this.view.vs + })); }, - get_context: function () { - var val = this.get_value(); + get_value: function (facet) { + return facet.value(); + }, + get_context: function (facet) { + var val = this.get_value(facet); // A field needs a value to be "active", and a context to send when // active var has_value = (val !== null && val !== ''); @@ -787,10 +1005,10 @@ openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.w if (!(has_value && context)) { return; } - return _.extend( - {}, context, - {own_values: {self: val}}); + return new instance.web.CompoundContext(context) + .set_eval_context({self: val}); }, + get_groupby: function () { }, /** * Function creating the returned domain for the field, override this * methods in children if you only need to customize the field's domain @@ -802,11 +1020,11 @@ openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.w * @param {Number|String} value parsed value for the field * @returns {Array<Array>} domain to include in the resulting search */ - make_domain: function (name, operator, value) { - return [[name, operator, value]]; + make_domain: function (name, operator, facet) { + return [[name, operator, this.get_value(facet)]]; }, - get_domain: function () { - var val = this.get_value(); + get_domain: function (facet) { + var val = this.get_value(facet); if (val === null || val === '') { return; } @@ -816,9 +1034,10 @@ openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.w return this.make_domain( this.attrs.name, this.attrs.operator || this.default_operator, - val); + facet); } - return _.extend({}, domain, {own_values: {self: val}}); + return new instance.web.CompoundDomain(domain) + .set_eval_context({self: val}); } }); /** @@ -829,15 +1048,25 @@ openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.w * * The Javascript and the HTML values are identical (strings) * * @class - * @extends openerp.web.search.Field + * @extends instance.web.search.Field */ -openerp.web.search.CharField = openerp.web.search.Field.extend( /** @lends openerp.web.search.CharField# */ { +instance.web.search.CharField = instance.web.search.Field.extend( /** @lends instance.web.search.CharField# */ { default_operator: 'ilike', - get_value: function () { - return this.$element.val(); + complete: function (value) { + if (_.isEmpty(value)) { return $.when(null); } + var label = _.str.sprintf(_.str.escapeHTML( + _t("Search %(field)s for: %(value)s")), { + field: '<em>' + this.attrs.string + '</em>', + value: '<strong>' + _.str.escapeHTML(value) + '</strong>'}); + return $.when([{ + category: this.attrs.string, + label: label, + value: value, + field: this + }]); } }); -openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends openerp.web.search.NumberField# */{ +instance.web.search.NumberField = instance.web.search.Field.extend(/** @lends instance.web.search.NumberField# */{ get_value: function () { if (!this.$element.val()) { return null; @@ -846,7 +1075,7 @@ openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends open check = Number(this.$element.val()); if (isNaN(val) || val !== check) { this.$element.addClass('error'); - throw new openerp.web.search.Invalid( + throw new instance.web.search.Invalid( this.attrs.name, this.$element.val(), this.error_message); } this.$element.removeClass('error'); @@ -855,13 +1084,13 @@ openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends open }); /** * @class - * @extends openerp.web.search.NumberField + * @extends instance.web.search.NumberField */ -openerp.web.search.IntegerField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.IntegerField# */{ +instance.web.search.IntegerField = instance.web.search.NumberField.extend(/** @lends instance.web.search.IntegerField# */{ error_message: _t("not a valid integer"), parse: function (value) { try { - return openerp.web.parse_value(value, {'widget': 'integer'}); + return instance.web.parse_value(value, {'widget': 'integer'}); } catch (e) { return NaN; } @@ -869,13 +1098,13 @@ openerp.web.search.IntegerField = openerp.web.search.NumberField.extend(/** @len }); /** * @class - * @extends openerp.web.search.NumberField + * @extends instance.web.search.NumberField */ -openerp.web.search.FloatField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.FloatField# */{ +instance.web.search.FloatField = instance.web.search.NumberField.extend(/** @lends instance.web.search.FloatField# */{ error_message: _t("not a valid number"), parse: function (value) { try { - return openerp.web.parse_value(value, {'widget': 'float'}); + return instance.web.parse_value(value, {'widget': 'float'}); } catch (e) { return NaN; } @@ -883,9 +1112,9 @@ openerp.web.search.FloatField = openerp.web.search.NumberField.extend(/** @lends }); /** * @class - * @extends openerp.web.search.Field + * @extends instance.web.search.Field */ -openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends openerp.web.search.SelectionField# */{ +instance.web.search.SelectionField = instance.web.search.Field.extend(/** @lends instance.web.search.SelectionField# */{ // This implementation is a basic <select> field, but it may have to be // altered to be more in line with the GTK client, which uses a combo box // (~ jquery.autocomplete): @@ -904,48 +1133,48 @@ openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends o return !item[1]; }); }, - get_value: function () { - var index = parseInt(this.$element.val(), 10); - if (isNaN(index)) { return null; } - var value = this.attrs.selection[index][0]; - if (value === false) { return null; } - return value; - }, - /** - * The selection field needs a default ``false`` value in case none is - * provided, so that selector options with a ``false`` value (convention - * for explicitly empty options) get selected by default rather than the - * first (value-holding) option in the selection. - * - * @param {Object} defaults search default values - */ - render: function (defaults) { - if (!defaults[this.attrs.name]) { - defaults[this.attrs.name] = false; - } - return this._super(defaults); - }, - clear: function () { - var self = this, d = $.Deferred(), selection = this.attrs.selection; - for(var index=0; index<selection.length; ++index) { - var item = selection[index]; - if (!item[1]) { - setTimeout(function () { - // won't override mutable, because we immediately bail out - //noinspection JSReferencingMutableVariableFromClosure - self.$element.val(index); - d.resolve(); - }, 0); - return d.promise(); - } - } - return d.resolve().promise(); + complete: function (needle) { + var self = this; + var results = _(this.attrs.selection).chain() + .filter(function (sel) { + var value = sel[0], label = sel[1]; + if (!value) { return false; } + return label.toLowerCase().indexOf(needle.toLowerCase()) !== -1; + }) + .map(function (sel) { + return { + category: self.attrs.string, + field: self, + value: sel[1], + json: sel[0] + }; + }).value(); + if (_.isEmpty(results)) { return $.when(null); } + return $.when.apply(null, [{ + category: this.attrs.string + }].concat(results)); + }, + facet_for: function (value) { + var match = _(this.attrs.selection).detect(function (sel) { + return sel[0] === value; + }); + if (!match) { return $.when(null); } + return $.when(new VS.model.SearchFacet({ + category: this.attrs.string, + value: match[1], + json: match[0], + field: this, + app: this.view.app + })); + }, + get_value: function (facet) { + return facet.get('json'); } }); -openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @lends openerp.web.search.BooleanField# */{ +instance.web.search.BooleanField = instance.web.search.SelectionField.extend(/** @lends instance.web.search.BooleanField# */{ /** - * @constructs openerp.web.search.BooleanField - * @extends openerp.web.search.BooleanField + * @constructs instance.web.search.BooleanField + * @extends instance.web.search.BooleanField */ init: function () { this._super.apply(this, arguments); @@ -954,25 +1183,8 @@ openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @ ['false', _t("No")] ]; }, - /** - * Search defaults likely to be boolean values (for a boolean field). - * - * In the HTML, we only want/get strings, and our strings here are ``true`` - * and ``false``, so ensure we use precisely those by truth-testing the - * default value (iif there is one in the view's defaults). - * - * @param {Object} defaults default values for this search view - * @returns {String} rendered boolean field - */ - render: function (defaults) { - var name = this.attrs.name; - if (name in defaults) { - defaults[name] = defaults[name] ? "true" : "false"; - } - return this._super(defaults); - }, - get_value: function () { - switch (this._super()) { + get_value: function (facet) { + switch (this._super(facet)) { case 'false': return false; case 'true': return true; default: return null; @@ -981,26 +1193,27 @@ openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @ }); /** * @class - * @extends openerp.web.search.DateField + * @extends instance.web.search.DateField */ -openerp.web.search.DateField = openerp.web.search.Field.extend(/** @lends openerp.web.search.DateField# */{ - template: "SearchView.date", - start: function () { - this._super(); - // FIXME: this insanity puts a div inside a span - this.datewidget = new openerp.web.DateWidget(this); - this.datewidget.prependTo(this.$element); - this.datewidget.$element.find("input") - .attr("size", 15) - .attr("autofocus", this.attrs.default_focus === '1' ? 'autofocus' : null) - .removeAttr('style'); - this.datewidget.set_value(this.defaults[this.attrs.name] || false); - }, - get_value: function () { - return this.datewidget.get_value() || null; - }, - clear: function () { - this.datewidget.set_value(false); +instance.web.search.DateField = instance.web.search.Field.extend(/** @lends instance.web.search.DateField# */{ + get_value: function (facet) { + return instance.web.date_to_str(facet.get('json')); + }, + complete: function (needle) { + var d = Date.parse(needle); + if (!d) { return $.when(null); } + var value = instance.web.format_value(d, this.attrs); + var label = _.str.sprintf(_.str.escapeHTML( + _t("Search %(field)s at: %(value)s")), { + field: '<em>' + this.attrs.string + '</em>', + value: '<strong>' + value + '</strong>'}); + return $.when([{ + category: this.attrs.string, + label: label, + value: value, + json: d, + field: this + }]); } }); /** @@ -1012,205 +1225,135 @@ openerp.web.search.DateField = openerp.web.search.Field.extend(/** @lends opener * spanning the whole day selected by the date widget * * @class - * @extends openerp.web.DateField + * @extends instance.web.DateField */ -openerp.web.search.DateTimeField = openerp.web.search.DateField.extend(/** @lends openerp.web.search.DateTimeField# */{ - make_domain: function (name, operator, value) { - return ['&', [name, '>=', value + ' 00:00:00'], - [name, '<=', value + ' 23:59:59']]; +instance.web.search.DateTimeField = instance.web.search.DateField.extend(/** @lends instance.web.search.DateTimeField# */{ + get_value: function (facet) { + return instance.web.datetime_to_str(facet.get('json')); } }); -openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({ +instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ init: function (view_section, field, view) { this._super(view_section, field, view); + this.model = new instance.web.Model(this.attrs.relation); + }, + complete: function (needle) { var self = this; - this.got_name = $.Deferred().then(function () { - self.$element.val(self.name); + // TODO: context + // FIXME: "concurrent" searches (multiple requests, mis-ordered responses) + return this.model.call('name_search', [], { + name: needle, + limit: 8, + context: {} + }).pipe(function (results) { + if (_.isEmpty(results)) { return null; } + return [{category: self.attrs.string}].concat( + _(results).map(function (result) { + return { + category: self.attrs.string, + value: result[1], + json: result[0], + field: self + }; + })); }); - this.model = new openerp.web.Model(this.attrs['relation']); - - this.orderer = new openerp.web.DropMisordered(); - }, - start: function () { - this._super(); - this.setup_autocomplete(); - var started = $.Deferred(); - this.got_name.then(function () { started.resolve();}, - function () { started.resolve(); }); - return started.promise(); }, - setup_autocomplete: function () { + facet_for: function (value) { var self = this; - this.$element.autocomplete({ - source: function (req, resp) { - self.orderer.add(self.model.call('name_search', null, { - name: req.term, - args: self.attrs.domain, - limit: 8 - })).then(function (data) { - resp(_.map(data, function (result) { - return {id: result[0], label: result[1]} - })); - }); - }, - select: function (event, ui) { - self.id = ui.item.id; - self.name = ui.item.label; - }, - delay: 100 - }) - }, - on_name_get: function (name_get) { - if (!name_get.length) { - delete this.id; - this.got_name.reject(); - return; - } - this.name = name_get[0][1]; - this.got_name.resolve(); - }, - render: function (defaults) { - if (defaults[this.attrs.name]) { - this.id = defaults[this.attrs.name]; - if (this.id instanceof Array) - this.id = this.id[0]; - // TODO: maybe this should not be completely removed - delete defaults[this.attrs.name]; - this.model.call('name_get', [[this.id]]).then( - this.proxy('on_name_get')); - } else { - this.got_name.reject(); + if (value instanceof Array) { + return $.when(new VS.model.SearchFacet({ + category: this.attrs.string, + value: value[1], + json: value[0], + field: this, + app: this.view.vs + })); } - return this._super(defaults); + return this.model.call('name_get', [value], {}).pipe(function (names) { + return new VS.model.SearchFacet({ + category: self.attrs.string, + value: names[0][1], + json: names[0][0], + field: self, + app: self.view.vs + }); + }) }, - make_domain: function (name, operator, value) { - if (this.id && this.name) { - if (value === this.name) { - return [[name, '=', this.id]]; - } else { - delete this.id; - delete this.name; - } + make_domain: function (name, operator, facet) { + // ``json`` -> actual auto-completed id + if (facet.get('json')) { + return [[name, '=', facet.get('json')]]; } - return this._super(name, operator, value); + + return this._super(name, operator, facet); } }); -openerp.web.search.ExtendedSearch = openerp.web.search.Input.extend({ - template: 'SearchView.extended_search', - init: function (parent, model) { - this._super(parent); - this.model = model; - }, - add_group: function() { - var group = new openerp.web.search.ExtendedSearchGroup(this, this.fields); - group.appendTo(this.$element.find('.searchview_extended_groups_list')); - this.check_last_element(); - }, +instance.web.search.Advanced = instance.web.search.Input.extend({ + template: 'SearchView.advanced', start: function () { - this._super(); - this.$element.closest("table.oe-searchview-render-line").css("display", "none"); var self = this; - this.rpc("/web/searchview/fields_get", - {"model": this.model}, function(data) { - self.fields = data.fields; - if (!('id' in self.fields)) { - self.fields.id = { - string: 'ID', - type: 'id' - } - } - openerp.web.search.add_expand_listener(self.$element); - self.$element.find('.searchview_extended_add_group').click(function (e) { - self.add_group(); + this.$element + .on('keypress keydown keyup', function (e) { e.stopPropagation(); }) + .on('click', 'h4', function () { + self.$element.toggleClass('oe_opened'); + }).on('click', 'button.oe_add_condition', function () { + self.append_proposition(); + }).on('submit', 'form', function (e) { + e.preventDefault(); + self.commit_search(); }); + return $.when( + this._super(), + this.rpc("/web/searchview/fields_get", {model: this.view.model}, function(data) { + self.fields = _.extend({ + id: { string: 'ID', type: 'id' } + }, data.fields); + })).then(function () { + self.append_proposition(); }); }, - get_context: function() { - return null; - }, - get_domain: function() { - if (!this.$element) { - return null; // not a logical state but sometimes it happens - } - if(this.$element.closest("table.oe-searchview-render-line").css("display") == "none") { - return null; - } - return _.reduce(this.getChildren(), - function(mem, x) { return mem.concat(x.get_domain());}, []); - }, - on_activate: function() { - this.add_group(); - var table = this.$element.closest("table.oe-searchview-render-line"); - table.css("display", ""); - if(this.$element.hasClass("folded")) { - this.$element.toggleClass("folded expanded"); - } - }, - hide: function() { - var table = this.$element.closest("table.oe-searchview-render-line"); - table.css("display", "none"); - if(this.$element.hasClass("expanded")) { - this.$element.toggleClass("folded expanded"); - } - }, - check_last_element: function() { - _.each(this.getChildren(), function(x) {x.set_last_group(false);}); - if (this.getChildren().length >= 1) { - this.getChildren()[this.getChildren().length - 1].set_last_group(true); - } - } -}); - -openerp.web.search.ExtendedSearchGroup = openerp.web.OldWidget.extend({ - template: 'SearchView.extended_search.group', - init: function (parent, fields) { - this._super(parent); - this.fields = fields; - }, - add_prop: function() { - var prop = new openerp.web.search.ExtendedSearchProposition(this, this.fields); - var render = prop.render({'index': this.getChildren().length - 1}); - this.$element.find('.searchview_extended_propositions_list').append(render); - prop.start(); + append_proposition: function () { + return (new instance.web.search.ExtendedSearchProposition(this, this.fields)) + .appendTo(this.$element.find('ul')); }, - start: function () { - var _this = this; - this.add_prop(); - this.$element.find('.searchview_extended_add_proposition').click(function () { - _this.add_prop(); + commit_search: function () { + var self = this; + // Get domain sections from all propositions + var children = this.getChildren(), + domain = _.invoke(children, 'get_proposition'); + var filters = _(domain).map(function (section) { + return new instance.web.search.Filter({attrs: { + string: _.str.sprintf('%s(%s)%s', + section[0], section[1], section[2]), + domain: [section] + }}, self.view); }); - this.$element.find('.searchview_extended_delete_group').click(function () { - _this.destroy(); + // Create Filter (& FilterGroup around it) with that domain + var f = new instance.web.search.FilterGroup(filters, this.view); + // add group to query + this.view.vs.searchQuery.add({ + category: _t("Advanced"), + value: _(filters).map(function (f) { + return f.attrs.string || f.attrs.name }).join(' | '), + json: filters, + field: f, + app: this.view.vs }); - }, - get_domain: function() { - var props = _(this.getChildren()).chain().map(function(x) { - return x.get_proposition(); - }).compact().value(); - var choice = this.$element.find(".searchview_extended_group_choice").val(); - var op = choice == "all" ? "&" : "|"; - return choice == "none" ? ['!'] : [].concat( - _.map(_.range(_.max([0,props.length - 1])), function() { return op; }), - props); - }, - destroy: function() { - var parent = this.getParent(); - if (this.getParent().getChildren().length == 1) - this.getParent().hide(); - this._super(); - parent.check_last_element(); - }, - set_last_group: function(is_last) { - this.$element.toggleClass('last_group', is_last); + // remove all propositions + _.invoke(children, 'destroy'); + // add new empty proposition + this.append_proposition(); + // TODO: API on searchview + this.view.$element.removeClass('oe_searchview_open_drawer'); } }); -openerp.web.search.ExtendedSearchProposition = openerp.web.OldWidget.extend(/** @lends openerp.web.search.ExtendedSearchProposition# */{ +instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/** @lends instance.web.search.ExtendedSearchProposition# */{ template: 'SearchView.extended_search.proposition', /** - * @constructs openerp.web.search.ExtendedSearchProposition - * @extends openerp.web.OldWidget + * @constructs instance.web.search.ExtendedSearchProposition + * @extends instance.web.OldWidget * * @param parent * @param fields @@ -1225,8 +1368,6 @@ openerp.web.search.ExtendedSearchProposition = openerp.web.OldWidget.extend(/** this.value = null; }, start: function () { - this.$element = $("#" + this.element_id); - this.select_field(this.fields.length > 0 ? this.fields[0] : null); var _this = this; this.$element.find(".searchview_extended_prop_field").change(function() { _this.changed(); @@ -1234,14 +1375,7 @@ openerp.web.search.ExtendedSearchProposition = openerp.web.OldWidget.extend(/** this.$element.find('.searchview_extended_delete_prop').click(function () { _this.destroy(); }); - }, - destroy: function() { - var parent; - if (this.getParent().getChildren().length == 1) - parent = this.getParent(); - this._super(); - if (parent) - parent.destroy(); + this.changed(); }, changed: function() { var nval = this.$element.find(".searchview_extended_prop_field").val(); @@ -1267,17 +1401,11 @@ openerp.web.search.ExtendedSearchProposition = openerp.web.OldWidget.extend(/** } var type = field.type; - try { - openerp.web.search.custom_filters.get_object(type); - } catch (e) { - if (! e instanceof openerp.web.KeyNotFound) { - throw e; - } - type = "char"; - console.log('Unknow field type ' + e.key); + var obj = instance.web.search.custom_filters.get_object(type); + if(obj === null) { + obj = instance.web.search.custom_filters.get_object("char"); } - this.value = new (openerp.web.search.custom_filters.get_object(type)) - (this); + this.value = new (obj) (this); if(this.value.set_field) { this.value.set_field(field); } @@ -1301,28 +1429,24 @@ openerp.web.search.ExtendedSearchProposition = openerp.web.OldWidget.extend(/** } }); -openerp.web.search.ExtendedSearchProposition.Field = openerp.web.OldWidget.extend({ +instance.web.search.ExtendedSearchProposition.Field = instance.web.OldWidget.extend({ start: function () { this.$element = $("#" + this.element_id); } }); -openerp.web.search.ExtendedSearchProposition.Char = openerp.web.search.ExtendedSearchProposition.Field.extend({ +instance.web.search.ExtendedSearchProposition.Char = instance.web.search.ExtendedSearchProposition.Field.extend({ template: 'SearchView.extended_search.proposition.char', operators: [ {value: "ilike", text: _lt("contains")}, {value: "not ilike", text: _lt("doesn't contain")}, {value: "=", text: _lt("is equal to")}, - {value: "!=", text: _lt("is not equal to")}, - {value: ">", text: _lt("greater than")}, - {value: "<", text: _lt("less than")}, - {value: ">=", text: _lt("greater or equal than")}, - {value: "<=", text: _lt("less or equal than")} + {value: "!=", text: _lt("is not equal to")} ], get_value: function() { return this.$element.val(); } }); -openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.search.ExtendedSearchProposition.Field.extend({ +instance.web.search.ExtendedSearchProposition.DateTime = instance.web.search.ExtendedSearchProposition.Field.extend({ template: 'SearchView.extended_search.proposition.empty', operators: [ {value: "=", text: _lt("is equal to")}, @@ -1337,11 +1461,11 @@ openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.search.Exten }, start: function() { this._super(); - this.datewidget = new openerp.web.DateTimeWidget(this); + this.datewidget = new instance.web.DateTimeWidget(this); this.datewidget.prependTo(this.$element); } }); -openerp.web.search.ExtendedSearchProposition.Date = openerp.web.search.ExtendedSearchProposition.Field.extend({ +instance.web.search.ExtendedSearchProposition.Date = instance.web.search.ExtendedSearchProposition.Field.extend({ template: 'SearchView.extended_search.proposition.empty', operators: [ {value: "=", text: _lt("is equal to")}, @@ -1356,11 +1480,11 @@ openerp.web.search.ExtendedSearchProposition.Date = openerp.web.search.ExtendedS }, start: function() { this._super(); - this.datewidget = new openerp.web.DateWidget(this); + this.datewidget = new instance.web.DateWidget(this); this.datewidget.prependTo(this.$element); } }); -openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.search.ExtendedSearchProposition.Field.extend({ +instance.web.search.ExtendedSearchProposition.Integer = instance.web.search.ExtendedSearchProposition.Field.extend({ template: 'SearchView.extended_search.proposition.integer', operators: [ {value: "=", text: _lt("is equal to")}, @@ -1372,16 +1496,16 @@ openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.search.Extend ], get_value: function() { try { - return openerp.web.parse_value(this.$element.val(), {'widget': 'integer'}); + return instance.web.parse_value(this.$element.val(), {'widget': 'integer'}); } catch (e) { return ""; } } }); -openerp.web.search.ExtendedSearchProposition.Id = openerp.web.search.ExtendedSearchProposition.Integer.extend({ +instance.web.search.ExtendedSearchProposition.Id = instance.web.search.ExtendedSearchProposition.Integer.extend({ operators: [{value: "=", text: _lt("is")}] }); -openerp.web.search.ExtendedSearchProposition.Float = openerp.web.search.ExtendedSearchProposition.Field.extend({ +instance.web.search.ExtendedSearchProposition.Float = instance.web.search.ExtendedSearchProposition.Field.extend({ template: 'SearchView.extended_search.proposition.float', operators: [ {value: "=", text: _lt("is equal to")}, @@ -1393,13 +1517,13 @@ openerp.web.search.ExtendedSearchProposition.Float = openerp.web.search.Extended ], get_value: function() { try { - return openerp.web.parse_value(this.$element.val(), {'widget': 'float'}); + return instance.web.parse_value(this.$element.val(), {'widget': 'float'}); } catch (e) { return ""; } } }); -openerp.web.search.ExtendedSearchProposition.Selection = openerp.web.search.ExtendedSearchProposition.Field.extend({ +instance.web.search.ExtendedSearchProposition.Selection = instance.web.search.ExtendedSearchProposition.Field.extend({ template: 'SearchView.extended_search.proposition.selection', operators: [ {value: "=", text: _lt("is")}, @@ -1412,7 +1536,7 @@ openerp.web.search.ExtendedSearchProposition.Selection = openerp.web.search.Exte return this.$element.val(); } }); -openerp.web.search.ExtendedSearchProposition.Boolean = openerp.web.search.ExtendedSearchProposition.Field.extend({ +instance.web.search.ExtendedSearchProposition.Boolean = instance.web.search.ExtendedSearchProposition.Field.extend({ template: 'SearchView.extended_search.proposition.boolean', operators: [ {value: "=", text: _lt("is true")}, @@ -1423,21 +1547,21 @@ openerp.web.search.ExtendedSearchProposition.Boolean = openerp.web.search.Extend } }); -openerp.web.search.custom_filters = new openerp.web.Registry({ - 'char': 'openerp.web.search.ExtendedSearchProposition.Char', - 'text': 'openerp.web.search.ExtendedSearchProposition.Char', - 'one2many': 'openerp.web.search.ExtendedSearchProposition.Char', - 'many2one': 'openerp.web.search.ExtendedSearchProposition.Char', - 'many2many': 'openerp.web.search.ExtendedSearchProposition.Char', +instance.web.search.custom_filters = new instance.web.Registry({ + 'char': 'instance.web.search.ExtendedSearchProposition.Char', + 'text': 'instance.web.search.ExtendedSearchProposition.Char', + 'one2many': 'instance.web.search.ExtendedSearchProposition.Char', + 'many2one': 'instance.web.search.ExtendedSearchProposition.Char', + 'many2many': 'instance.web.search.ExtendedSearchProposition.Char', - 'datetime': 'openerp.web.search.ExtendedSearchProposition.DateTime', - 'date': 'openerp.web.search.ExtendedSearchProposition.Date', - 'integer': 'openerp.web.search.ExtendedSearchProposition.Integer', - 'float': 'openerp.web.search.ExtendedSearchProposition.Float', - 'boolean': 'openerp.web.search.ExtendedSearchProposition.Boolean', - 'selection': 'openerp.web.search.ExtendedSearchProposition.Selection', + 'datetime': 'instance.web.search.ExtendedSearchProposition.DateTime', + 'date': 'instance.web.search.ExtendedSearchProposition.Date', + 'integer': 'instance.web.search.ExtendedSearchProposition.Integer', + 'float': 'instance.web.search.ExtendedSearchProposition.Float', + 'boolean': 'instance.web.search.ExtendedSearchProposition.Boolean', + 'selection': 'instance.web.search.ExtendedSearchProposition.Selection', - 'id': 'openerp.web.search.ExtendedSearchProposition.Id' + 'id': 'instance.web.search.ExtendedSearchProposition.Id' }); }; diff --git a/addons/web/static/src/js/view_editor.js b/addons/web/static/src/js/view_editor.js index 2c0dc3d70b4..59d1fed9345 100644 --- a/addons/web/static/src/js/view_editor.js +++ b/addons/web/static/src/js/view_editor.js @@ -1,15 +1,15 @@ -openerp.web.view_editor = function(openerp) { -var _t = openerp.web._t; -var QWeb = openerp.web.qweb; -openerp.web.ViewEditor = openerp.web.OldWidget.extend({ +openerp.web.view_editor = function(instance) { +var _t = instance.web._t; +var QWeb = instance.web.qweb; +instance.web.ViewEditor = instance.web.OldWidget.extend({ init: function(parent, element_id, dataset, view, options) { this._super(parent); this.element_id = element_id; this.parent = parent; - this.dataset = new openerp.web.DataSetSearch(this, 'ir.ui.view', null, null), + this.dataset = new instance.web.DataSetSearch(this, 'ir.ui.view', null, null), this.model = dataset.model; this.xml_element_id = 0; - this.property = openerp.web.ViewEditor.property_widget; + this.property = instance.web.ViewEditor.property_widget; this.one_object = false; }, start: function() { @@ -39,7 +39,7 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ select_view_id: self.parent.fields_view.view_id } }; - this.view_edit_dialog = new openerp.web.Dialog(this, { + this.view_edit_dialog = new instance.web.Dialog(this, { title: action_title, width: 850, buttons: [ @@ -51,7 +51,7 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ }).open(); this.view_edit_dialog.on_close.add_last(function(){window.location.reload();}); this.main_view_id = this.parent.fields_view.view_id; - this.action_manager = new openerp.web.ActionManager(this); + this.action_manager = new instance.web.ActionManager(this); this.action_manager.appendTo(this.view_edit_dialog); $.when(this.action_manager.do_action(action)).then(function() { var viewmanager = self.action_manager.inner_viewmanager, @@ -67,7 +67,7 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ }, on_create_view: function() { var self = this; - this.create_view_dialog = new openerp.web.Dialog(this, { + this.create_view_dialog = new instance.web.Dialog(this, { title: _.str.sprintf(_t("Create a view (%s)"), self.model), buttons: [ {text: _t("Save"), click: function () { @@ -115,8 +115,8 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ }, do_save_view: function(values) { def = $.Deferred(); - var field_dataset = new openerp.web.DataSetSearch(this, this.model, null, null); - var model_dataset = new openerp.web.DataSetSearch(this, 'ir.model', null, null); + var field_dataset = new instance.web.DataSetSearch(this, this.model, null, null); + var model_dataset = new instance.web.DataSetSearch(this, 'ir.model', null, null); var view_string = "", field_name = false, self = this; field_dataset.call( 'fields_get', [], function(fields) { _.each(['name', 'x_name'], function(value) { @@ -360,7 +360,7 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ edit_view: function(one_object) { var self = this; this.one_object = one_object; - this.edit_xml_dialog = new openerp.web.Dialog(this, { + this.edit_xml_dialog = new instance.web.Dialog(this, { title: _.str.sprintf(_t("View Editor %d - %s"), self.main_view_id, self.model), height: '90%', buttons: [ @@ -392,7 +392,7 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ action_buttons: false } }; - var action_manager = new openerp.web.ActionManager(self); + var action_manager = new instance.web.ActionManager(self); action_manager.do_action(action); }}, {text: _t("Close"), click: function(){ @@ -517,7 +517,7 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ var self = this; var tr = self.get_object_by_id(this.one_object.clicked_tr_id, this.one_object['main_object'], [])[0].att_list[0]; var parent_tr = ($(side).prevAll("tr[level=" + String(this.one_object.clicked_tr_level - 1) + "]"))[0]; - var field_dataset = new openerp.web.DataSetSearch(this, this.model, null, null); + var field_dataset = new instance.web.DataSetSearch(this, this.model, null, null); parent_tr = $(parent_tr).find('a').text(); field_dataset.call( 'fields_get', [], function(result) { var fields = _.keys(result); @@ -811,7 +811,7 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ }, on_edit_node: function(properties){ var self = this; - this.edit_node_dialog = new openerp.web.Dialog(this,{ + this.edit_node_dialog = new instance.web.Dialog(this,{ title: _t("Properties"), width: 450, buttons: [ @@ -880,7 +880,7 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ this.edit_widget = []; self.ready = $.when(self.on_groups(properties)).then(function () { _PROPERTIES_ATTRIBUTES['groups']['selection'] = self.groups; - var values = _.keys( openerp.web.form.widgets.map); + var values = _.keys( instance.web.form.widgets.map); values.push(''); values.sort(); _PROPERTIES_ATTRIBUTES['widget']['selection'] = values; @@ -908,8 +908,8 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ def.resolve(); } var group_ids = [], group_names = {}, groups = []; - var res_groups = new openerp.web.DataSetSearch(this,'res.groups', null, null), - model_data = new openerp.web.DataSetSearch(self,'ir.model.data', null, null); + var res_groups = new instance.web.DataSetSearch(this,'res.groups', null, null), + model_data = new instance.web.DataSetSearch(self,'ir.model.data', null, null); res_groups.read_slice([], {}).done(function (res_grp) { _.each(res_grp, function (res) { var key = res.id; @@ -935,7 +935,7 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ {'name': 'field_value','selection': fields, 'value': false, 'string': '','type': 'selection'}, {'name': 'position','selection': ['After','Before','Inside'], 'value': false, 'string': 'Position','type': 'selection'}]; this.add_widget = []; - this.add_node_dialog = new openerp.web.Dialog(this,{ + this.add_node_dialog = new instance.web.Dialog(this,{ title: _t("Properties"), width: 450, buttons: [ @@ -980,7 +980,7 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ }); table_selector.find("td[id^=]").attr("width","100px"); self.add_node_dialog.$element.find('#new_field').click(function() { - model_data = new openerp.web.DataSetSearch(self,'ir.model', null, null); + model_data = new instance.web.DataSetSearch(self,'ir.model', null, null); model_data.read_slice([], {domain: [['model','=', self.model]]}).then(function(result) { self.render_new_field(result[0].id); }); @@ -998,7 +998,7 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ action_buttons: true } }; - var action_manager = new openerp.web.ActionManager(self); + var action_manager = new instance.web.ActionManager(self); $.when(action_manager.do_action(action)).then(function() { var controller = action_manager.dialog_viewmanager.views['form'].controller; controller.on_button_cancel.add_last(function(){ @@ -1015,7 +1015,7 @@ openerp.web.ViewEditor = openerp.web.OldWidget.extend({ }); } }); -openerp.web.ViewEditor.Field = openerp.web.Class.extend({ +instance.web.ViewEditor.Field = instance.web.Class.extend({ init: function(view, widget) { this.$element = view.$element; this.dirty = false; @@ -1041,7 +1041,7 @@ openerp.web.ViewEditor.Field = openerp.web.Class.extend({ validate: function() { this.is_invalid = false; try { - var value = openerp.web.parse_value(this.get_value(), this, ''); + var value = instance.web.parse_value(this.get_value(), this, ''); this.is_invalid = this.required && value === ''; } catch(e) { this.is_invalid = true; @@ -1051,7 +1051,7 @@ openerp.web.ViewEditor.Field = openerp.web.Class.extend({ return _.str.sprintf("<td id = %s>%s</td>", this.name, QWeb.render(this.template, {widget: this})) } }); -openerp.web.ViewEditor.FieldBoolean = openerp.web.ViewEditor.Field.extend({ +instance.web.ViewEditor.FieldBoolean = instance.web.ViewEditor.Field.extend({ template : "vieweditor_boolean", start: function() { var self = this; @@ -1069,7 +1069,7 @@ openerp.web.ViewEditor.FieldBoolean = openerp.web.ViewEditor.Field.extend({ return this.$element.find("input[id=" + this.name + "]").is(':checked')? "1" : null; } }); -openerp.web.ViewEditor.FieldChar = openerp.web.ViewEditor.Field.extend({ +instance.web.ViewEditor.FieldChar = instance.web.ViewEditor.Field.extend({ template : "vieweditor_char", start: function () { var self = this; @@ -1085,7 +1085,7 @@ openerp.web.ViewEditor.FieldChar = openerp.web.ViewEditor.Field.extend({ return this.$element.find("input[id=" + this.name + "]").val(); } }); -openerp.web.ViewEditor.FieldSelect = openerp.web.ViewEditor.Field.extend({ +instance.web.ViewEditor.FieldSelect = instance.web.ViewEditor.Field.extend({ template : "vieweditor_selection", start: function () { var self = this; @@ -1115,7 +1115,7 @@ openerp.web.ViewEditor.FieldSelect = openerp.web.ViewEditor.Field.extend({ return this.$element.find("select[id=" + this.name + "]").val(); } }); -openerp.web.ViewEditor.FieldSelectMulti = openerp.web.ViewEditor.FieldSelect.extend({ +instance.web.ViewEditor.FieldSelectMulti = instance.web.ViewEditor.FieldSelect.extend({ start: function () { this._super(); this.$element.find("select[id=" + this.name + "]").css('height', '100px').attr("multiple", true); @@ -1131,7 +1131,7 @@ openerp.web.ViewEditor.FieldSelectMulti = openerp.web.ViewEditor.FieldSelect.ext }); } }); -openerp.web.ViewEditor.FieldFloat = openerp.web.ViewEditor.FieldChar.extend({ +instance.web.ViewEditor.FieldFloat = instance.web.ViewEditor.FieldChar.extend({ }); var _PROPERTIES = { @@ -1196,11 +1196,11 @@ var _ICONS = ['','STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD', 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner', 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph' ]; -openerp.web.ViewEditor.property_widget = new openerp.web.Registry({ - 'boolean' : 'openerp.web.ViewEditor.FieldBoolean', - 'selection_multi' : 'openerp.web.ViewEditor.FieldSelectMulti', - 'selection' : 'openerp.web.ViewEditor.FieldSelect', - 'char' : 'openerp.web.ViewEditor.FieldChar', - 'float' : 'openerp.web.ViewEditor.FieldFloat' +instance.web.ViewEditor.property_widget = new instance.web.Registry({ + 'boolean' : 'instance.web.ViewEditor.FieldBoolean', + 'selection_multi' : 'instance.web.ViewEditor.FieldSelectMulti', + 'selection' : 'instance.web.ViewEditor.FieldSelect', + 'char' : 'instance.web.ViewEditor.FieldChar', + 'float' : 'instance.web.ViewEditor.FieldFloat' }); }; diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 50c5dcb976a..7e5b173fd37 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -1,31 +1,32 @@ -openerp.web.form = function (openerp) { +openerp.web.form = function (instance) { +var _t = instance.web._t, + _lt = instance.web._lt; +var QWeb = instance.web.qweb; -var _t = openerp.web._t, - _lt = openerp.web._lt; -var QWeb = openerp.web.qweb; +/** @namespace */ +instance.web.form = {}; -openerp.web.views.add('form', 'openerp.web.FormView'); -openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# */{ +instance.web.views.add('form', 'instance.web.FormView'); +instance.web.FormView = instance.web.View.extend({ /** * Indicates that this view is not searchable, and thus that no search * view should be displayed (if there is one active). */ searchable: false, - readonly : false, - form_template: "FormView", + template: "FormView", display_name: _lt('Form'), + view_type: "form", /** - * @constructs openerp.web.FormView - * @extends openerp.web.View + * @constructs instance.web.FormView + * @extends instance.web.View * - * @param {openerp.web.Session} session the current openerp session - * @param {openerp.web.DataSet} dataset the dataset this view will work with + * @param {instance.web.Session} session the current openerp session + * @param {instance.web.DataSet} dataset the dataset this view will work with * @param {String} view_id the identifier of the OpenERP view object * @param {Object} options - * - sidebar : [true|false] * - resize_textareas : [true|false|max_height] * - * @property {openerp.web.Registry} registry=openerp.web.form.widgets widgets registry for this form view instance + * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance */ init: function(parent, dataset, view_id, options) { this._super(parent); @@ -34,98 +35,86 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# this.model = dataset.model; this.view_id = view_id || false; this.fields_view = {}; - this.widgets = {}; - this.widgets_counter = 0; this.fields = {}; this.fields_order = []; this.datarecord = {}; this.default_focus_field = null; this.default_focus_button = null; - this.registry = openerp.web.form.widgets; + this.fields_registry = instance.web.form.widgets; + this.tags_registry = instance.web.form.tags; this.has_been_loaded = $.Deferred(); - this.$form_header = null; this.translatable_fields = []; _.defaults(this.options, { - "not_interactible_on_create": false + "not_interactible_on_create": false, + "initial_mode": "view", }); this.is_initialized = $.Deferred(); this.mutating_mutex = new $.Mutex(); this.on_change_mutex = new $.Mutex(); this.reload_mutex = new $.Mutex(); - }, - start: function() { - this._super(); - return this.init_view(); - }, - init_view: function() { - if (this.embedded_view) { - var def = $.Deferred().then(this.on_loaded); - var self = this; - $.async_when().then(function() {def.resolve(self.embedded_view);}); - return def.promise(); - } else { - var context = new openerp.web.CompoundContext(this.dataset.get_context()); - return this.rpc("/web/view/load", { - "model": this.model, - "view_id": this.view_id, - "view_type": "form", - toolbar: this.options.sidebar, - context: context - }, this.on_loaded); - } + this.mode = null; + this.rendering_engine = new instance.web.form.FormRenderingEngineReadonly(this); + this.qweb = null; // A QWeb instance will be created if the view is a QWeb template }, destroy: function() { - if (this.sidebar) { - this.sidebar.attachments.destroy(); - this.sidebar.destroy(); - } - _.each(this.widgets, function(w) { + _.each(this.get_widgets(), function(w) { w.destroy(); }); this._super(); }, - reposition: function ($e) { - this.$element = $e; - this.on_loaded(); - }, on_loaded: function(data) { var self = this; if (!data) { - throw "No data provided."; + throw new Error("No data provided."); } - if (this.root_frame) { - throw "Form view does not support multiple calls to on_loaded"; + if (this.arch) { + throw "Form view does not support multiple calls to on_loaded"; } this.fields_order = []; this.fields_view = data; - this.rendered = QWeb.render(this.form_template, {'widget': this}); - this.$element.html(this.rendered); - - this.root_frame = instanciate_widget(this.registry.get_object('frame'), this, this.fields_view.arch); - var to_append = $(".oe_form_header", this.$element); - this.root_frame.appendTo(to_append.length > 0 ? to_append : this.$element); - this.root_frame.$element.children().unwrap(); - - this.$form_header = this.$element.find('.oe_form_header:first'); - this.$form_header.find('div.oe_form_pager button[data-pager-action]').click(function() { + this.rendering_engine.set_fields_registry(this.fields_registry); + this.rendering_engine.set_tags_registry(this.tags_registry); + if (!this.extract_qweb_template(data)) { + this.rendering_engine.set_fields_view(data); + var $dest = this.$element.hasClass("oe_form_container") ? this.$element : this.$element.find('.oe_form_container'); + this.rendering_engine.render_to($dest); + } + + this.$sidebar = this.options.$sidebar || this.$element.find('.oe_form_sidebar'); + + this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self})); + if (this.options.$buttons) { + this.$buttons.appendTo(this.options.$buttons); + } else { + this.$element.find('.oe_form_buttons').replaceWith(this.$buttons); + } + this.$buttons + .on('click','button.oe_form_button_save',this.on_button_save) + .on('click','button.oe_form_button_cancel',this.on_button_cancel) + .on('click','button.oe_form_button_edit',this.on_button_edit) + .on('click','button.oe_form_button_create',this.on_button_create) + .on('click','button.oe_form_button_duplicate',this.on_button_duplicate) + .on('click','button.oe_form_button_delete',this.on_button_delete); + + this.$pager = $(QWeb.render("FormView.pager", {'widget':self})); + if (this.options.$pager) { + this.$pager.appendTo(this.options.$pager); + } else { + this.$element.find('.oe_form_pager').replaceWith(this.$pager); + } + this.$pager.on('click','a[data-pager-action]',function() { var action = $(this).data('pager-action'); self.on_pager_action(action); }); - this.$form_header.find('button.oe_form_button_save').click(this.on_button_save); - this.$form_header.find('button.oe_form_button_cancel').click(this.on_button_cancel); - - if (!this.sidebar && this.options.sidebar && this.options.sidebar_id) { - this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id); - this.sidebar.start(); - this.sidebar.do_unfold(); - this.sidebar.attachments = new openerp.web.form.SidebarAttachments(this.sidebar, this); - this.sidebar.add_toolbar(this.fields_view.toolbar); - this.set_common_sidebar_sections(this.sidebar); - - this.sidebar.add_section(_t('Customize'), 'customize'); - this.sidebar.add_items('customize', [{ + if (!this.sidebar && this.options.$sidebar) { + this.sidebar = new instance.web.Sidebar(this); + this.sidebar.appendTo(this.$sidebar); + if(this.fields_view.toolbar) { + this.sidebar.add_toolbar(this.fields_view.toolbar); + } + this.sidebar.add_items('other', [{ label: _t('Set Default'), form: this, callback: function (item) { @@ -133,9 +122,61 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } }]); } + this.on("change:mode", this, this.switch_mode); + this.set({mode: this.options.initial_mode}); this.has_been_loaded.resolve(); + return $.when(); + }, + extract_qweb_template: function(fvg) { + for (var i=0, ii=fvg.arch.children.length; i < ii; i++) { + var child = fvg.arch.children[i]; + if (child.tag === "templates") { + this.qweb = new QWeb2.Engine(); + this.qweb.add_template(instance.web.json_node_to_xml(child)); + if (!this.qweb.has_template('form')) { + throw new Error("No QWeb template found for form view"); + } + return true; + } + } + this.qweb = null; + return false; + }, + get_fvg_from_qweb: function(record) { + var view = this.qweb.render('form', this.get_qweb_context(record)); + var fvg = _.clone(this.fields_view); + fvg.arch = instance.web.xml_to_json(instance.web.str_to_xml(view).firstChild); + return fvg; + }, + get_qweb_context: function(record) { + var self = this, + new_record = {}; + _.each(record, function(value_, name) { + var r = _.clone(self.fields_view.fields[name] || {}); + if ((r.type === 'date' || r.type === 'datetime') && value_) { + r.raw_value = instance.web.auto_str_to_date(value_); + } else { + r.raw_value = value_; + } + r.value = instance.web.format_value(value_, r); + new_record[name] = r; + }); + return { + record : new_record, + new_record : !record.id + }; + }, + kill_current_form: function() { + _.each(this.getChildren(), function(el) { + el.destroy(); + }); + this.fields = {}; + this.fields_order = []; + this.default_focus_field = null; + this.default_focus_button = null; + this.translatable_fields = []; + this.$element.find('.oe_form_container').empty(); }, - do_load_state: function(state, warm) { if (state.id && this.datarecord.id != state.id) { if (!this.dataset.get_id_index(state.id)) { @@ -147,9 +188,18 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } } }, - do_show: function () { var self = this; + if (this.sidebar) { + this.sidebar.$element.show(); + } + if (this.$buttons) { + this.$buttons.show(); + this.$buttons.find('.oe_form_button_save').removeClass('oe_form_button_save_dirty'); + } + if (this.$pager) { + this.$pager.show(); + } this.$element.show().css('visibility', 'hidden'); this.$element.removeClass('oe_form_dirty'); return this.has_been_loaded.pipe(function() { @@ -165,17 +215,20 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# result.pipe(function() { self.$element.css('visibility', 'visible'); }); - if (self.sidebar) { - self.sidebar.$element.show(); - } return result; }); }, do_hide: function () { - this._super(); if (this.sidebar) { this.sidebar.$element.hide(); } + if (this.$buttons) { + this.$buttons.hide(); + } + if (this.$pager) { + this.$pager.hide(); + } + this._super(); }, on_record_loaded: function(record) { var self = this, set_values = []; @@ -185,13 +238,17 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } this.datarecord = record; + if (this.qweb) { + this.kill_current_form(); + this.rendering_engine.set_fields_view(this.get_fvg_from_qweb(record)); + var $dest = this.$element.hasClass("oe_form_container") ? this.$element : this.$element.find('.oe_form_container'); + this.rendering_engine.render_to($dest); + } + _(this.fields).each(function (field, f) { - field.reset(); + field._dirty_flag = false; var result = field.set_value(self.datarecord[f] || false); set_values.push(result); - $.when(result).then(function() { - field.validate(); - }); }); return $.when.apply(null, set_values).pipe(function() { if (!record.id) { @@ -200,7 +257,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# _.each(self.fields_order, function(field_name) { if (record[field_name] !== undefined) { var field = self.fields[field_name]; - field.dirty = true; + field._dirty_flag = true; self.do_onchange(field); } }); @@ -209,7 +266,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# self.is_initialized.resolve(); self.do_update_pager(record.id == null); if (self.sidebar) { - self.sidebar.attachments.do_update(); + self.sidebar.do_attachement_update(self.dataset, self.datarecord.id); } if (self.default_focus_field) { self.default_focus_field.focus(); @@ -218,20 +275,15 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# self.do_push_state({id:record.id}); } self.$element.removeClass('oe_form_dirty'); + self.$buttons.find('.oe_form_button_save').removeClass('oe_form_button_save_dirty'); }); }, on_form_changed: function() { - for (var w in this.widgets) { - w = this.widgets[w]; - w.process_modifiers(); - if (w.field) { - w.validate(); - } - w.update_dom(); - } + this.trigger("view_content_has_changed"); }, do_notify_change: function() { this.$element.addClass('oe_form_dirty'); + this.$buttons.find('.oe_form_button_save').addClass('oe_form_button_save_dirty'); }, on_pager_action: function(action) { if (this.can_be_discarded()) { @@ -253,11 +305,10 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } }, do_update_pager: function(hide_index) { - var $pager = this.$form_header.find('div.oe_form_pager'); var index = hide_index ? '-' : this.dataset.index + 1; - $pager.find('button').prop('disabled', this.dataset.ids.length < 2); - $pager.find('span.oe_pager_index').html(index); - $pager.find('span.oe_pager_count').html(this.dataset.ids.length); + this.$pager.find('button').prop('disabled', this.dataset.ids.length < 2).end() + .find('span.oe_pager_index').html(index).end() + .find('span.oe_pager_count').html(this.dataset.ids.length); }, parse_on_change: function (on_change, widget) { var self = this; @@ -278,7 +329,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# 'None': function () {return null;}, 'context': function (i) { context_index = i; - var ctx = new openerp.web.CompoundContext(self.dataset.get_context(), widget.build_context() ? widget.build_context() : {}); + var ctx = new instance.web.CompoundContext(self.dataset.get_context(), widget.build_context() ? widget.build_context() : {}); return ctx; } }; @@ -296,8 +347,8 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } // form field if (self.fields[field]) { - var value = self.fields[field].get_on_change_value(); - return value == null ? false : value; + var value_ = self.fields[field].get_value(); + return value_ == null ? false : value_; } // parent field var splitted = field.split('.'); @@ -360,17 +411,17 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } if (widget.field['change_default']) { - var fieldname = widget.name, value; + var fieldname = widget.name, value_; if (response.value && (fieldname in response.value)) { // Use value from onchange if onchange executed - value = response.value[fieldname]; + value_ = response.value[fieldname]; } else { // otherwise get form value for field - value = self.fields[fieldname].get_on_change_value(); + value_ = self.fields[fieldname].get_value(); } - var condition = fieldname + '=' + value; + var condition = fieldname + '=' + value_; - if (value) { + if (value_) { can_process_onchange = self.rpc({ url: '/web/dataset/call', async: false @@ -411,10 +462,10 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# var field = this.fields[f]; // If field is not defined in the view, just ignore it if (field) { - var value = result.value[f]; - if (field.get_value() != value) { - field.set_value(value); - field.dirty = true; + var value_ = result.value[f]; + if (field.get_value() != value_) { + field.set_value(value_); + field._dirty_flag = true; if (!_.contains(processed, field.name)) { this.do_onchange(field, processed); } @@ -424,7 +475,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# this.on_form_changed(); } if (!_.isEmpty(result.warning)) { - openerp.web.dialog($(QWeb.render("CrashManagerWarning", result.warning)), { + instance.web.dialog($(QWeb.render("CrashManagerWarning", result.warning)), { modal: true, buttons: [ {text: _t("Ok"), click: function() { $(this).dialog("close"); }} @@ -447,19 +498,38 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# return $.Deferred().reject(); } }, + switch_mode: function() { + var mode = this.get("mode"); + var self = this; + if(mode=="view") { + self.$buttons.find('.oe_form_buttons_edit').hide(); + self.$buttons.find('.oe_form_buttons_view').show(); + _.each(this.fields,function(field){ + field.set({"force_readonly": true}); + }); + } else { + self.$buttons.find('.oe_form_buttons_edit').show(); + self.$buttons.find('.oe_form_buttons_view').hide(); + _.each(this.fields,function(field){ + field.set({"force_readonly": false}); + }); + } + self.mode = mode; + }, on_button_save: function() { var self = this; return this.do_save().then(function(result) { - self.do_prev_view({'created': result.created, 'default': 'page'}); + self.set({mode: "view"}); }); }, on_button_cancel: function() { if (this.can_be_discarded()) { - return this.do_prev_view({'default': 'page'}); + this.set({mode: "view"}); } }, on_button_new: function() { var self = this; + this.set({mode: "edit"}); var def = $.Deferred(); $.when(this.has_been_loaded).then(function() { if (self.can_be_discarded()) { @@ -477,6 +547,44 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# }); return def.promise(); }, + on_button_edit: function() { + return this.set({mode: "edit"}); + }, + on_button_create: function() { + this.dataset.index = null; + this.do_show(); + }, + on_button_duplicate: function() { + var self = this; + var def = $.Deferred(); + $.when(this.has_been_loaded).then(function() { + self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) { + return self.on_created({ result : new_id }); + }).then(function() { + return self.set({mode: "edit"}); + }).then(function() { + def.resolve(); + }); + }); + return def.promise(); + }, + on_button_delete: function() { + var self = this; + var def = $.Deferred(); + $.when(this.has_been_loaded).then(function() { + if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) { + self.dataset.unlink([self.datarecord.id]).then(function() { + self.on_pager_action('next'); + def.resolve(); + }); + } else { + $.async_when().then(function () { + def.reject(); + }) + } + }); + return def.promise(); + }, can_be_discarded: function() { return !this.$element.is('.oe_form_dirty') || confirm(_t("Warning, the record has been modified, your changes will be discarded.")); }, @@ -499,11 +607,10 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# f = self.fields[f]; if (!f.is_valid()) { form_invalid = true; - f.update_dom(true); if (!first_invalid_field) { first_invalid_field = f; } - } else if (f.name !== 'id' && !f.readonly && (!self.datarecord.id || f.is_dirty())) { + } else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) { // Special case 'id' field, do not save this field // on 'create' : save all non readonly fields // on 'edit' : save non readonly modified fields @@ -511,20 +618,23 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } } if (form_invalid) { + self.set({'display_invalid_fields': true}); first_invalid_field.focus(); self.on_invalid(); return $.Deferred().reject(); } else { + self.set({'display_invalid_fields': false}); var save_deferral; if (!self.datarecord.id) { //console.log("FormView(", self, ") : About to create", values); save_deferral = self.dataset.create(values).pipe(function(r) { return self.on_created(r, undefined, prepend_on_create); }, null); - } else if (_.isEmpty(values)) { + } else if (_.isEmpty(values) && ! self.force_dirty) { //console.log("FormView(", self, ") : Nothing to save"); save_deferral = $.Deferred().resolve({}).promise(); } else { + self.force_dirty = false; //console.log("FormView(", self, ") : About to save", values); save_deferral = self.dataset.write(self.datarecord.id, values, {}).pipe(function(r) { return self.on_saved(r); @@ -542,7 +652,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# var msg = "<ul>"; _.each(this.fields, function(f) { if (!f.is_valid()) { - msg += "<li>" + f.string + "</li>"; + msg += "<li>" + f.node.attrs.string + "</li>"; } }); msg += "</ul>"; @@ -580,14 +690,14 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id])); this.dataset.index = this.dataset.ids.length - 1; } else { - this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids)); + this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids)); this.dataset.index = 0; } this.do_update_pager(); if (this.sidebar) { - this.sidebar.attachments.do_update(); + this.sidebar.do_attachement_update(this.dataset, this.datarecord.id); } - //openerp.log("The record has been created with id #" + this.datarecord.id); + //instance.log("The record has been created with id #" + this.datarecord.id); this.reload(); return $.when(_.extend(r, {created: true})).then(success); } @@ -598,6 +708,10 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# reload: function() { var self = this; return this.reload_mutex.exec(function() { + if (self.dataset.index == null) { + self.do_prev_view(); + return $.Deferred().reject().promise(); + } if (self.dataset.index == null || self.dataset.index < 0) { return $.when(self.on_button_new()); } else { @@ -607,15 +721,20 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } }); }, + get_widgets: function() { + return _.filter(this.getChildren(), function(obj) { + return obj instanceof instance.web.form.Widget; + }); + }, get_fields_values: function(blacklist) { blacklist = blacklist || []; var values = {}; var ids = this.get_selected_ids(); values["id"] = ids.length > 0 ? ids[0] : false; - _.each(this.fields, function(value, key) { + _.each(this.fields, function(value_, key) { if (_.include(blacklist, key)) return; - var val = value.get_value(); + var val = value_.get_value(); values[key] = val; }); return values; @@ -632,8 +751,8 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# }); }, is_dirty: function() { - return _.any(this.fields, function (value) { - return value.is_dirty(); + return _.any(this.fields, function (value_) { + return value_._dirty_flag; }); }, is_interactible_record: function() { @@ -642,7 +761,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# if (this.options.not_interactible_on_create) return false; } else if (typeof(id) === "string") { - if(openerp.web.BufferedDataSet.virtual_id_regex.test(id)) + if(instance.web.BufferedDataSet.virtual_id_regex.test(id)) return false; } return true; @@ -654,12 +773,12 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# var self = this; var fields = _.chain(this.fields) .map(function (field, name) { - var value = field.get_value(); + var value_ = field.get_value(); // ignore fields which are empty, invisible, readonly, o2m // or m2m - if (!value - || field.invisible - || field.readonly + if (!value_ + || field.get('invisible') + || field.get("readonly") || field.field.type === 'one2many' || field.field.type === 'many2many') { return false; @@ -668,33 +787,33 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# switch(field.field.type) { case 'selection': displayed = _(field.values).find(function (option) { - return option[0] === value; + return option[0] === value_; })[1]; break; case 'many2one': - displayed = field.value[1] || value; + displayed = value_; break; default: - displayed = value; + displayed = value_; } return { name: name, - string: field.string, - value: value, + string: field.node_atts.string, + value: value_, displayed: displayed, // convert undefined to false change_default: !!field.field.change_default } }) .compact() - .sortBy(function (field) { return field.string; }) + .sortBy(function (field) { return field.node_atts.string; }) .value(); var conditions = _.chain(fields) .filter(function (field) { return field.change_default; }) .value(); - var d = new openerp.web.Dialog(this, { + var d = new instance.web.Dialog(this, { title: _t("Set Default"), args: { fields: fields, @@ -706,12 +825,12 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# var $defaults = d.$element.find('#formview_default_fields'); var field_to_set = $defaults.val(); if (!field_to_set) { - $defaults.parent().addClass('invalid'); + $defaults.parent().addClass('oe_form_invalid'); return; } var condition = d.$element.find('#formview_default_conditions').val(), all_users = d.$element.find('#formview_default_all').is(':checked'); - new openerp.web.DataSet(self, 'ir.values').call( + new instance.web.DataSet(self, 'ir.values').call( 'set_default', [ self.dataset.model, field_to_set, @@ -725,9 +844,368 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# }); d.template = 'FormView.set_default'; d.open(); - } + }, + register_field: function(field, name) { + this.fields[name] = field; + this.fields_order.push(name); + if (this.get_field(name).translate) { + this.translatable_fields.push(field); + } + field.on('changed_value', this, function() { + field._dirty_flag = true; + if (field.is_syntax_valid()) { + this.do_onchange(field); + this.on_form_changed(true); + this.do_notify_change(); + } + }); + }, + get_field: function(field_name) { + return this.fields_view.fields[field_name]; + }, + is_create_mode: function() { + return !this.datarecord.id; + }, }); -openerp.web.FormDialog = openerp.web.Dialog.extend({ + +/** + * Interface to be implemented by rendering engines for the form view. + */ +instance.web.form.FormRenderingEngineInterface = { + set_fields_view: function(fields_view) {}, + set_fields_registry: function(fields_registry) {}, + render_to: function($element) {}, +}; + +/** + * Default rendering engine for the form view. + * + * It is necessary to set the view using set_view() before usage. + */ +instance.web.form.FormRenderingEngine = instance.web.Class.extend({ + init: function(view) { + this.view = view; + }, + set_fields_view: function(fvg) { + this.fvg = fvg; + }, + set_tags_registry: function(tags_registry) { + this.tags_registry = tags_registry; + }, + set_fields_registry: function(fields_registry) { + this.fields_registry = fields_registry; + }, + render_to: function($target) { + var self = this; + this.$target = $target; + + // TODO: I know this will save the world and all the kitten for a moment, + // but one day, we will have to get rid of xml2json + var xml = instance.web.json_node_to_xml(this.fvg.arch); + this.$form = $('<div class="oe_form">' + xml + '</div>'); + if (this.fvg.arch.attrs && this.fvg.arch.attrs['layout'] !== 'manual') { + this.$form.attr('layout', 'auto'); + } + + this.fields_to_init = []; + this.tags_to_init = []; + this.labels = {}; + this.process(this.$form); + + this.$form.appendTo(this.$target); + + _.each(this.fields_to_init, function($elem) { + var name = $elem.attr("name"); + if (!self.fvg.fields[name]) { + throw new Error("Field '" + name + "' specified in view could not be found."); + } + var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]); + if (!obj) { + throw new Error("Widget type '"+ key + "' is not implemented"); + } + var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); + var $label = self.labels[$elem.attr("name")]; + if ($label) { + w.set_input_id($label.attr("for")); + } + self.alter_field(w); + self.view.register_field(w, $elem.attr("name")); + w.replace($elem); + }); + _.each(this.tags_to_init, function($elem) { + var tag_name = $elem[0].tagName.toLowerCase(); + var obj = self.tags_registry.get_object(tag_name); + var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); + w.replace($elem); + }) + // TODO: return a deferred + }, + render_element: function(template, layout/* dictionaries */) { + var dicts = [].slice.call(arguments).slice(2); + dicts.unshift({ 'layout' : layout }); + var dict = _.extend.apply(_, dicts); + dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb + var alt_template = template + '.' + layout; + template = QWeb.has_template(alt_template) ? alt_template : template; + return $(QWeb.render(template, dict)); + }, + alter_field: function(field) { + }, + toggle_layout_debugging: function() { + if (!this.$target.has('.oe_layout_debug_cell:first').length) { + this.$target.find('[title]').removeAttr('title'); + this.$target.find('.oe_form_group_cell').each(function() { + var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan'); + $(this).attr('title', text); + }); + } + this.$target.toggleClass('oe_layout_debugging'); + }, + process: function($tag, layout) { + var self = this; + if ($tag.attr('layout') === 'auto') { + $tag.addClass('oe_form_autolayout'); + } + layout = $tag.attr('layout') || layout || 'auto'; + $tag.removeAttr('layout'); + var tagname = $tag[0].nodeName.toLowerCase(); + if (this.tags_registry.contains(tagname)) { + this.tags_to_init.push($tag); + return $tag; + } + var fn = self['process_' + tagname]; + if (fn) { + var args = [].slice.call(arguments); + args[0] = $tag; + args[1] = layout; + return fn.apply(self, args); + } else { + // generic tag handling, just process children + $tag.children().each(function() { + self.process($(this), layout); + }); + self.handle_common_properties($tag, $tag); + $tag.removeAttr("modifiers"); + return $tag; + } + }, + process_sheet: function() { + this.process_form.apply(this, arguments); + }, + process_form: function($form, layout) { + var $new_form = this.render_element('FormRenderingForm', layout, $form.getAttributes()); + var $dst = (layout === 'auto') ? $new_form.find('group:first') : $new_form; + $new_form.attr("modifiers", $form.attr("modifiers")); + $form.children().appendTo($dst); + if ($form[0] === this.$form[0]) { + // If root element, replace it + this.$form = $new_form; + } else { + $form.before($new_form).remove(); + } + this.process($new_form); + }, + preprocess_field: function($field) { + var self = this; + var name = $field.attr('name'), + field_colspan = parseInt($field.attr('colspan'), 10), + field_modifiers = JSON.parse($field.attr('modifiers') || '{}'); + + if ($field.attr('nolabel') === '1') + return; + $field.attr('nolabel', '1'); + var found = false; + this.$form.find('label[for="' + name + '"]').each(function(i ,el) { + $(el).parents().each(function(unused, tag) { + var name = tag.tagName.toLowerCase(); + if (name === "field" || name in self.tags_registry.map) + found = true; + }); + }); + if (found) + return; + + $label = $('<label/>').attr({ + 'for' : name, + "modifiers": JSON.stringify({invisible: field_modifiers.invisible}), + "string": $field.attr('string'), + "help": $field.attr('help'), + }); + $label.insertBefore($field); + if (field_colspan > 1) { + $field.attr('colspan', field_colspan - 1); + } + return $label; + }, + process_field: function($field, layout) { + var $label = this.preprocess_field($field); + if ($label) + this.process($label, layout); + this.fields_to_init.push($field); + return $field; + }, + process_group: function($group, layout) { + var self = this; + if ($group.parent().is('.oe_form_group_cell')) { + $group.parent().addClass('oe_form_group_nested'); + } + $group.children('field').each(function() { + self.preprocess_field($(this)); + }); + var $new_group = this.render_element('FormRenderingGroup', layout, $group.getAttributes()), + $table; + if ($new_group.is('table')) { + $table = $new_group; + } else { + $table = $new_group.find('table:first'); + } + $table.addClass('oe_form_group'); + var $tr, $td, + cols = parseInt($group.attr('col') || 4, 10), + row_cols = cols; + + var children = []; + $group.children().each(function(a,b,c) { + var $child = $(this), + colspan = parseInt($child.attr('colspan') || 1, 10), + tagName = $child[0].tagName.toLowerCase(); + if (tagName === 'newline') { + $tr = null; + return; + } + if (!$tr || row_cols < colspan) { + $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table); + row_cols = cols; + } + row_cols -= colspan; + + $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan); + // invisibility transfer + var field_modifiers = JSON.parse($child.attr('modifiers') || '{}'); + var invisible = field_modifiers.invisible; + field_modifiers.invisible = undefined; + $child.attr('modifiers', JSON.stringify(field_modifiers)); + self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible}))); + + $tr.append($td.append($child)); + children.push($child[0]); + }); + if (row_cols && $td) { + $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols); + } + $group.before($new_group).remove(); + + // Now compute width of cells + $table.find('> tbody > tr').each(function() { + var to_compute = [], + row_cols = cols, + total = 100; + $(this).children().each(function() { + var $td = $(this), + $child = $td.children(':first'); + switch ($child[0].tagName.toLowerCase()) { + case 'separator': + if ($child.attr('orientation') === 'vertical') { + $td.addClass('oe_vertical_separator').attr('width', '1'); + $td.empty(); + row_cols--; + } + break; + case 'label': + if ($child.attr('for')) { + $td.attr('width', '1%'); + row_cols--; + total--; + } + break; + default: + to_compute.push($td); + } + }); + var unit = Math.floor(total / row_cols); + _.each(to_compute, function($td, i) { + var width = parseInt($td.attr('colspan'), 10) * unit; + $td.attr('width', ((i == to_compute.length - 1) ? total : width) + '%'); + total -= width; + }); + }); + _.each(children, function(el) { + self.process($(el)); + }); + this.handle_common_properties($new_group, $group); + return $new_group; + }, + process_notebook: function($notebook, layout) { + var self = this; + var pages = []; + $notebook.find('> page').each(function() { + var $page = $(this); + var page_attrs = $page.getAttributes(); + page_attrs.id = _.uniqueId('notebook_page_'); + pages.push(page_attrs); + var $new_page = self.render_element('FormRenderingNotebookPage', layout, page_attrs); + var $dst = (layout === 'auto') ? $new_page.find('group:first') : $new_page; + $page.children().appendTo($dst); + $page.before($new_page).remove(); + self.handle_common_properties($new_page, $page); + }); + var $new_notebook = this.render_element('FormRenderingNotebook', layout, { pages : pages }); + $notebook.children().appendTo($new_notebook); + $notebook.before($new_notebook).remove(); + $new_notebook.children().each(function() { + self.process($(this)); + }); + $new_notebook.tabs(); + this.handle_common_properties($new_notebook, $notebook); + return $new_notebook; + }, + process_separator: function($separator, layout) { + var $new_separator = this.render_element('FormRenderingSeparator', layout, $separator.getAttributes()); + $separator.before($new_separator).remove(); + this.handle_common_properties($new_separator, $separator); + return $new_separator; + }, + process_label: function($label, layout) { + var name = $label.attr("for"), + field_orm = this.fvg.fields[name]; + var dict = { + string: $label.attr('string') || (field_orm || {}).string || '', + help: $label.attr('help') || (field_orm || {}).help || '', + _for: name ? _.uniqueId('oe-field-input-') : undefined, + }; + var align = parseFloat(dict.align); + if (isNaN(align) || align === 1) { + align = 'right'; + } else if (align === 0) { + align = 'left'; + } else { + align = 'center'; + } + dict.align = align; + var $new_label = this.render_element('FormRenderingLabel', layout, dict); + $label.before($new_label).remove(); + this.handle_common_properties($new_label, $label); + if (name) { + this.labels[name] = $new_label; + } + return $new_label; + }, + handle_common_properties: function($new_element, $node) { + var str_modifiers = $node.attr("modifiers") || "{}" + var modifiers = JSON.parse(str_modifiers); + if (modifiers.invisible !== undefined) + new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element); + $new_element.addClass($node.attr("class") || ""); + }, +}); + +instance.web.form.FormRenderingEngineReadonly = instance.web.form.FormRenderingEngine.extend({ + alter_field: function(field) { + field.set({"force_readonly": true}); + }, +}); + +instance.web.form.FormDialog = instance.web.Dialog.extend({ init: function(parent, options, view_id, dataset) { this._super(parent, options); this.dataset = dataset; @@ -736,8 +1214,7 @@ openerp.web.FormDialog = openerp.web.Dialog.extend({ }, start: function() { this._super(); - this.form = new openerp.web.FormView(this, this.dataset, this.view_id, { - sidebar: false, + this.form = new instance.web.FormView(this, this.dataset, this.view_id, { pager: false }); this.form.appendTo(this.$element); @@ -758,62 +1235,7 @@ openerp.web.FormDialog = openerp.web.Dialog.extend({ } }); -/** @namespace */ -openerp.web.form = {}; - -openerp.web.form.SidebarAttachments = openerp.web.OldWidget.extend({ - init: function(parent, form_view) { - var $section = parent.add_section(_t('Attachments'), 'attachments'); - this.$div = $('<div class="oe-sidebar-attachments"></div>'); - $section.append(this.$div); - - this._super(parent, $section.attr('id')); - this.view = form_view; - }, - do_update: function() { - if (!this.view.datarecord.id) { - this.on_attachments_loaded([]); - } else { - (new openerp.web.DataSetSearch( - this, 'ir.attachment', this.view.dataset.get_context(), - [ - ['res_model', '=', this.view.dataset.model], - ['res_id', '=', this.view.datarecord.id], - ['type', 'in', ['binary', 'url']] - ])).read_slice(['name', 'url', 'type'], {}).then(this.on_attachments_loaded); - } - }, - on_attachments_loaded: function(attachments) { - this.attachments = attachments; - this.$div.html(QWeb.render('FormView.sidebar.attachments', this)); - this.$element.find('.oe-binary-file').change(this.on_attachment_changed); - this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete); - }, - on_attachment_changed: function(e) { - window[this.element_id + '_iframe'] = this.do_update; - var $e = $(e.target); - if ($e.val() != '') { - this.$element.find('form.oe-binary-form').submit(); - $e.parent().find('input[type=file]').prop('disabled', true); - $e.parent().find('button').prop('disabled', true).find('img, span').toggle(); - } - }, - on_attachment_delete: function(e) { - var self = this, $e = $(e.currentTarget); - var name = _.str.trim($e.parent().find('a.oe-sidebar-attachments-link').text()); - if (confirm(_.str.sprintf(_t("Do you really want to delete the attachment %s?"), name))) { - this.rpc('/web/dataset/unlink', { - model: 'ir.attachment', - ids: [parseInt($e.attr('data-id'))] - }, function(r) { - $e.parent().remove(); - self.do_notify("Delete an attachment", "The attachment '" + name + "' has been deleted"); - }); - } - } -}); - -openerp.web.form.compute_domain = function(expr, fields) { +instance.web.form.compute_domain = function(expr, fields) { var stack = []; for (var i = expr.length - 1; i >= 0; i--) { var ex = expr[i]; @@ -842,7 +1264,7 @@ openerp.web.form.compute_domain = function(expr, fields) { _t("Unknown field %s in domain %s"), ex[0], JSON.stringify(expr))); } - var field_value = field.get_value ? fields[ex[0]].get_value() : fields[ex[0]].value; + var field_value = field.get_value ? field.get_value() : field.value; var op = ex[1]; var val = ex[2]; @@ -884,60 +1306,84 @@ openerp.web.form.compute_domain = function(expr, fields) { return _.all(stack, _.identity); }; -openerp.web.form.Widget = openerp.web.OldWidget.extend(/** @lends openerp.web.form.Widget# */{ - form_template: 'Widget', +/** + * Must be applied over an class already possessing the GetterSetterMixin. + * + * Apply the result of the "invisible" domain to this.$element. + */ +instance.web.form.InvisibilityChangerMixin = { + init: function(field_manager, invisible_domain) { + this._ic_field_manager = field_manager + this._ic_invisible_modifier = invisible_domain; + this._ic_field_manager.on("view_content_has_changed", this, function() { + var result = this._ic_invisible_modifier === undefined ? false : + instance.web.form.compute_domain(this._ic_invisible_modifier, this._ic_field_manager.fields); + this.set({"invisible": result}); + }); + this.set({invisible: this._ic_invisible_modifier === true}); + }, + start: function() { + var check_visibility = function() { + if (this.get("invisible")) { + this.$element.hide(); + } else { + this.$element.show(); + } + }; + this.on("change:invisible", this, check_visibility); + _.bind(check_visibility, this)(); + }, +}; + +instance.web.form.InvisibilityChanger = instance.web.Class.extend(_.extend({}, instance.web.GetterSetterMixin, instance.web.form.InvisibilityChangerMixin, { + init: function(parent, field_manager, invisible_domain, $element) { + this.setParent(parent); + instance.web.GetterSetterMixin.init.call(this); + instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain); + this.$element = $element; + this.start(); + }, +})); + +instance.web.form.Widget = instance.web.Widget.extend(_.extend({}, instance.web.form.InvisibilityChangerMixin, { /** - * @constructs openerp.web.form.Widget - * @extends openerp.web.OldWidget + * @constructs instance.web.form.Widget + * @extends instance.web.Widget * * @param view * @param node */ init: function(view, node) { + this._super(view); this.view = view; this.node = node; this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}'); - this.always_invisible = (this.modifiers.invisible && this.modifiers.invisible === true); - this.type = this.type || node.tag; - this.element_name = this.element_name || this.type; - - this._super(view); - - this.children = node.children; - this.colspan = parseInt(node.attrs.colspan || 1, 10); - this.decrease_max_width = 0; - - this.string = this.string || node.attrs.string; - this.help = this.help || node.attrs.help; - this.invisible = this.modifiers['invisible'] === true; - this.classname = 'oe_form_' + this.type; - - this.align = parseFloat(this.node.attrs.align); - if (isNaN(this.align) || this.align === 1) { - this.align = 'right'; - } else if (this.align === 0) { - this.align = 'left'; - } else { - this.align = 'center'; - } + instance.web.form.InvisibilityChangerMixin.init.call(this, view, this.modifiers.invisible); - this.width = this.node.attrs.width; + this.view.on("view_content_has_changed", this, this.process_modifiers); + }, + renderElement: function() { + this._super(); + this.$element.addClass(this.node.attrs["class"] || ""); + }, + start: function() { + this._super(); + instance.web.form.InvisibilityChangerMixin.start.call(this); }, destroy: function() { - this._super.apply(this, arguments); $.fn.tipsy.clear(); + this._super.apply(this, arguments); }, process_modifiers: function() { - var compute_domain = openerp.web.form.compute_domain; + var compute_domain = instance.web.form.compute_domain; + var to_set = {}; for (var a in this.modifiers) { - this[a] = compute_domain(this.modifiers[a], this.view.fields); + if (!_.include(["invisible"], a)) { + var val = compute_domain(this.modifiers[a], this.view.fields); + to_set[a] = val; + } } - }, - update_dom: function() { - this.$element.toggle(!this.invisible); - }, - renderElement: function() { - this.$element.html(QWeb.render(this.form_template, { "widget": this })); + this.set(to_set); }, do_attach_tooltip: function(widget, trigger, options) { widget = widget || this; @@ -947,12 +1393,12 @@ openerp.web.form.Widget = openerp.web.OldWidget.extend(/** @lends openerp.web.fo delayOut: 0, fade: true, title: function() { - var template = widget.form_template + '.tooltip'; + var template = widget.template + '.tooltip'; if (!QWeb.has_template(template)) { template = 'WidgetLabel.tooltip'; } return QWeb.render(template, { - debug: openerp.connection.debug, + debug: instance.connection.debug, widget: widget })}, gravity: $.fn.tipsy.autoBounds(50, 'nw'), @@ -979,7 +1425,7 @@ openerp.web.form.Widget = openerp.web.OldWidget.extend(/** @lends openerp.web.fo }, _build_eval_context: function(blacklist) { var a_dataset = this.view.dataset; - return new openerp.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values(blacklist)); + return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values(blacklist)); }, /** * Builds a new context usable for operations related to fields by merging @@ -994,7 +1440,7 @@ openerp.web.form.Widget = openerp.web.OldWidget.extend(/** @lends openerp.web.fo if (v_context.__ref || true) { //TODO: remove true var fields_values = this._build_eval_context(blacklist); - v_context = new openerp.web.CompoundContext(v_context).set_eval_context(fields_values); + v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values); } return v_context; }, @@ -1005,235 +1451,28 @@ openerp.web.form.Widget = openerp.web.OldWidget.extend(/** @lends openerp.web.fo var final_domain = n_domain !== null ? n_domain : f_domain; if (!(final_domain instanceof Array) || true) { //TODO: remove true var fields_values = this._build_eval_context(); - final_domain = new openerp.web.CompoundDomain(final_domain).set_eval_context(fields_values); + final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values); } return final_domain; } -}); +})); -openerp.web.form.WidgetFrame = openerp.web.form.Widget.extend({ - form_template: 'WidgetFrame', - init: function(view, node) { - this._super(view, node); - this.columns = parseInt(node.attrs.col || 4, 10); - this.x = 0; - this.y = 0; - this.table = []; - this.add_row(); - for (var i = 0; i < node.children.length; i++) { - var n = node.children[i]; - if (n.tag == "newline") { - this.add_row(); - } else { - this.handle_node(n); - } - } - this.set_row_cells_with(this.table[this.table.length - 1]); - }, - renderElement: function() { - this._super(); - var self = this; - _.each(this.table, function(row) { - _.each(row, function(td) { - td.$element = self.$element.find('.' + td.element_class); - td.renderElement(); - }); - }); - }, - start: function() { - _.each(this.table, function(row) { - _.each(row, function(td) { - td.start(); - }); - }); - }, - add_row: function(){ - if (this.table.length) { - this.set_row_cells_with(this.table[this.table.length - 1]); - } - var row = []; - this.table.push(row); - this.x = 0; - this.y += 1; - return row; - }, - set_row_cells_with: function(row) { - var bypass = 0, - max_width = 100, - row_length = row.length; - for (var i = 0; i < row.length; i++) { - if (row[i].always_invisible) { - row_length--; - } else { - bypass += row[i].width === undefined ? 0 : 1; - max_width -= row[i].decrease_max_width; - } - } - var size_unit = Math.round(max_width / (this.columns - bypass)), - colspan_sum = 0; - for (var i = 0; i < row.length; i++) { - var w = row[i]; - if (w.always_invisible) { - continue; - } - colspan_sum += w.colspan; - if (w.width === undefined) { - var width = (i === row_length - 1 && colspan_sum === this.columns) ? max_width : Math.round(size_unit * w.colspan); - max_width -= width; - w.width = width + '%'; - } - } - }, - handle_node: function(node) { - var type = {}; - if (node.tag == 'field') { - type = this.view.fields_view.fields[node.attrs.name] || {}; - if (node.attrs.widget == 'statusbar' && node.attrs.nolabel !== '1') { - // This way we can retain backward compatibility between addons and old clients - node.attrs.colspan = (parseInt(node.attrs.colspan, 10) || 1) + 1; - node.attrs.nolabel = '1'; - } - } - var widget = instanciate_widget(this.view.registry.get_any( - [node.attrs.widget, type.type, node.tag]), this.view, node); - if (node.tag == 'field') { - if (!this.view.default_focus_field || node.attrs.default_focus == '1') { - this.view.default_focus_field = widget; - } - if (node.attrs.nolabel != '1') { - var label = instanciate_widget(this.view.registry.get_object('label'), this.view, node); - label["for"] = widget; - this.add_widget(label, widget.colspan + 1); - } - } - this.add_widget(widget); - }, - add_widget: function(widget, colspan) { - var current_row = this.table[this.table.length - 1]; - if (!widget.always_invisible) { - colspan = colspan || widget.colspan; - if (current_row.length && (this.x + colspan) > this.columns) { - current_row = this.add_row(); - } - this.x += widget.colspan; - } - current_row.push(widget); - return widget; - } -}); - -openerp.web.form.WidgetGroup = openerp.web.form.WidgetFrame.extend({ - form_template: 'WidgetGroup' -}), - -openerp.web.form.WidgetNotebook = openerp.web.form.Widget.extend({ - form_template: 'WidgetNotebook', - init: function(view, node) { - this._super(view, node); - this.pages = []; - for (var i = 0; i < node.children.length; i++) { - var n = node.children[i]; - if (n.tag == "page") { - var page = instanciate_widget(this.view.registry.get_object('notebookpage'), - this.view, n, this, this.pages.length); - this.pages.push(page); - } - } - }, - renderElement: function() { - this._super(); - var self = this; - _.each(this.pages, function(page) { - page.$element = self.$element.find('.' + page.element_class); - page.renderElement(); - }); - }, - start: function() { - var self = this; - this._super.apply(this, arguments); - this.$element.find('> ul > li').each(function (index, tab_li) { - var page = self.pages[index], - id = _.uniqueId(self.element_name + '-'); - page.element_id = id; - $(tab_li).find('a').attr('href', '#' + id); - }); - this.$element.find('> div').each(function (index, page) { - page.id = self.pages[index].element_id; - }); - this.$element.tabs(); - this.view.on_button_new.add_first(this.do_select_first_visible_tab); - if (openerp.connection.debug) { - this.do_attach_tooltip(this, this.$element.find('ul:first'), { - gravity: 's' - }); - } - _.each(this.pages, function(page) { - page.start(); - }); - }, - do_select_first_visible_tab: function() { - for (var i = 0; i < this.pages.length; i++) { - var page = this.pages[i]; - if (page.invisible === false) { - this.$element.tabs('select', page.index); - break; - } - } - } -}); - -openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({ - form_template: 'WidgetNotebookPage', - init: function(view, node, notebook, index) { - this.notebook = notebook; - this.index = index; - this.element_name = 'page_' + index; - this._super(view, node); - }, - start: function() { - this._super.apply(this, arguments); - this.$element_tab = this.notebook.$element.find( - '> ul > li:eq(' + this.index + ')'); - }, - update_dom: function() { - if (this.invisible && this.index === this.notebook.$element.tabs('option', 'selected')) { - this.notebook.do_select_first_visible_tab(); - } - this.$element_tab.toggle(!this.invisible); - this.$element.toggle(!this.invisible); - } -}); - -openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({ - form_template: 'WidgetSeparator', - init: function(view, node) { - this._super(view, node); - this.orientation = node.attrs.orientation || 'horizontal'; - if (this.orientation === 'vertical') { - this.width = '1'; - } - this.classname += '_' + this.orientation; - } -}); - -openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ - form_template: 'WidgetButton', +instance.web.form.WidgetButton = instance.web.form.Widget.extend({ + template: 'WidgetButton', init: function(view, node) { this._super(view, node); this.force_disabled = false; - if (this.string) { - // We don't have button key bindings in the webclient - this.string = this.string.replace(/_/g, ''); - } - if (node.attrs.default_focus == '1') { + this.string = (this.node.attrs.string || '').replace(/_/g, ''); + if (this.node.attrs.default_focus == '1') { // TODO fme: provide enter key binding to widgets this.view.default_focus_button = this; } + this.view.on('view_content_has_changed', this, this.check_disable); }, start: function() { this._super.apply(this, arguments); - this.$element.find("button").click(this.on_click); - if (this.help || openerp.connection.debug) { + this.$element.click(this.on_click); + if (this.node.attrs.help || instance.connection.debug) { this.do_attach_tooltip(); } }, @@ -1251,7 +1490,7 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ var exec_action = function() { if (self.node.attrs.confirm) { var def = $.Deferred(); - var dialog = openerp.web.dialog($('<div>' + self.node.attrs.confirm + '</div>'), { + var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), { title: _t('Confirm'), modal: true, buttons: [ @@ -1275,6 +1514,7 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ } }; if (!this.node.attrs.special) { + this.view.force_dirty = true; return this.view.recursive_save().pipe(exec_action); } else { return exec_action(); @@ -1285,7 +1525,7 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ var context = this.node.attrs.context; if (context && context.__ref) { - context = new openerp.web.CompoundContext(context); + context = new instance.web.CompoundContext(context); context.set_eval_context(this._build_eval_context()); } @@ -1295,81 +1535,57 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ self.view.reload(); }); }, - update_dom: function() { - this._super.apply(this, arguments); - this.check_disable(); - }, check_disable: function() { - var disabled = (this.readonly || this.force_disabled || !this.view.is_interactible_record()); - this.$element.find('button').prop('disabled', disabled); - this.$element.find("button").css('color', disabled ? 'grey' : ''); + var disabled = (this.force_disabled || !this.view.is_interactible_record()); + this.$element.prop('disabled', disabled); + this.$element.css('color', disabled ? 'grey' : ''); } }); -openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({ - form_template: 'WidgetLabel', - init: function(view, node) { - this.element_name = 'label_' + node.attrs.name; - - this._super(view, node); - - if (this.node.tag == 'label' && !this.string && this.node.children.length) { - this.string = this.node.children[0]; - this.align = 'left'; - } - - if (this.node.tag == 'label' && (this.align === 'left' || this.node.attrs.colspan || (this.string && this.string.length > 32))) { - this.form_template = "WidgetParagraph"; - this.colspan = parseInt(this.node.attrs.colspan || 1, 10); - // Widgets default to right-aligned, but paragraph defaults to - // left-aligned - if (isNaN(parseFloat(this.node.attrs.align))) { - this.align = 'left'; - } - - this.multilines = this.string && _.str.lines(this.string).length > 1; - } else { - this.colspan = 1; - this.width = '1%'; - this.decrease_max_width = 1; - this.nowrap = true; - } - }, - renderElement: function() { - var rendered; - if (this['for'] && this.type !== 'label') { - rendered = QWeb.render(this.form_template, {widget: this['for']}); - } else { - // Actual label widgets should not have a false and have type label - rendered = QWeb.render(this.form_template, {widget: this}); - } - this.$element.html(rendered); - }, - start: function() { - this._super(); - var self = this; - if (this['for'] && (this['for'].help || openerp.connection.debug)) { - this.do_attach_tooltip(self['for']); - } - this.$element.find("label").dblclick(function() { - var widget = self['for'] || self; - openerp.log(widget.element_class , widget); - window.w = widget; - }); - } -}); +/** + * Interface implemented by the form view or any other object + * able to provide the features necessary for the fields to work. + * + * Properties: + * - display_invalid_fields : if true, all fields where is_valid() return true should + * be displayed as invalid. + * Events: + * - view_content_has_changed : when the values of the fields have changed. When + * this event is triggered all fields should reprocess their modifiers. + */ +instance.web.form.FieldManagerInterface = { + /** + * Must return the asked field as in fields_get. + */ + get_field: function(field_name) {}, + /** + * Called by the field when the translate button is clicked. + */ + open_translate_dialog: function(field) {}, + /** + * Returns true when the view is in create mode. + */ + is_create_mode: function() {}, +}; /** * Interface to be implemented by fields. * - * Novajs Attributes: - * - ... - * - * Novajs Events: - * - ... + * Properties: + * - readonly: boolean. If set to true the field should appear in readonly mode. + * - force_readonly: boolean, When it is true, the field should always appear + * in read only mode, no matter what the value of the "readonly" property can be. + * Events: + * - changed_value: triggered to inform the view to check on_changes * */ -openerp.web.form.FieldInterface = { +instance.web.form.FieldInterface = { + /** + * Constructor takes 2 arguments: + * - field_manager: Implements FieldManagerInterface + * - node: the "<field>" node in json form + */ + init: function(field_manager, node) {}, /** * Called by the form view to indicate the value of the field. * @@ -1386,7 +1602,7 @@ openerp.web.form.FieldInterface = { * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as * no information is ever given to know which format is used. */ - set_value: function(value) {}, + set_value: function(value_) {}, /** * Get the current value of the widget. * @@ -1405,221 +1621,307 @@ openerp.web.form.FieldInterface = { * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0). */ get_value: function() {}, + /** + * Inform the current object of the id it should use to match a html <label> that exists somewhere in the + * view. + */ + set_input_id: function(id) {}, + /** + * Returns true if is_syntax_valid() returns true and the value is semantically + * valid too according to the semantic restrictions applied to the field. + */ + is_valid: function() {}, + /** + * Returns true if the field holds a value which is syntaxically correct, ignoring + * the potential semantic restrictions applied to the field. + */ + is_syntax_valid: function() {}, + /** + * Must set the focus on the field. + */ + focus: function() {}, }; /** - * Abstract class for classes implementing FieldInterface. Should be renamed to AbstractField some - * day. + * Abstract class for classes implementing FieldInterface. + * + * Properties: + * - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending + * the values of the "readonly" property and the "force_readonly" property on the field manager. + * - value: useful property to hold the value of the field. By default, set_value() and get_value() + * set and retrieve the value property. Changing the value property also triggers automatically + * a 'changed_value' event that inform the view to trigger on_changes. + * */ -openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.form.Field# */{ +instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends instance.web.form.AbstractField# */{ /** - * @constructs openerp.web.form.Field - * @extends openerp.web.form.Widget + * @constructs instance.web.form.AbstractField + * @extends instance.web.form.Widget * - * @param view + * @param field_manager * @param node */ - init: function(view, node) { - this.name = node.attrs.name; - this.value = undefined; - view.fields[this.name] = this; - view.fields_order.push(this.name); - this.type = node.attrs.widget || view.fields_view.fields[node.attrs.name].type; - this.element_name = "field_" + this.name + "_" + this.type; - - this._super(view, node); - - if (node.attrs.nolabel != '1' && this.colspan > 1) { - this.colspan--; - } - this.field = view.fields_view.fields[node.attrs.name] || {}; - this.string = node.attrs.string || this.field.string; - this.help = node.attrs.help || this.field.help; - this.nolabel = (this.field.nolabel || node.attrs.nolabel) === '1'; - this.readonly = this.modifiers['readonly'] === true; - this.required = this.modifiers['required'] === true; - this.invalid = this.dirty = false; - - this.classname = 'oe_form_field_' + this.type; + init: function(field_manager, node) { + this._super(field_manager, node); + this.field_manager = field_manager; + this.name = this.node.attrs.name; + this.set({'value': false}); + this.field = this.field_manager.get_field(this.name); + this.set({required: this.modifiers['required'] === true}); + + // some events to make the property "effective_readonly" sync automatically with "readonly" and + // "force_readonly" + this.set({"readonly": this.modifiers['readonly'] === true}); + var test_effective_readonly = function() { + this.set({"effective_readonly": this.get("readonly") || !!this.get("force_readonly")}); + }; + this.on("change:readonly", this, test_effective_readonly); + this.on("change:force_readonly", this, test_effective_readonly); + _.bind(test_effective_readonly, this)(); + + this.on("change:value", this, function() { + if (! this._inhibit_on_change) + this.trigger('changed_value'); + this._check_css_flags(); + }); }, start: function() { this._super.apply(this, arguments); if (this.field.translate) { - this.view.translatable_fields.push(this); this.$element.addClass('oe_form_field_translatable'); - this.$element.find('.oe_field_translate').click(this.on_translate); + this.$element.find('.oe_field_translate').click(_.bind(function() { + this.field_manager.open_translate_dialog(this); + }, this)); } - if (this.nolabel && openerp.connection.debug) { + if (instance.connection.debug) { this.do_attach_tooltip(this, this.$element); } + if (!this.disable_utility_classes) { + var set_required = function() { + this.$element.toggleClass('oe_form_required', this.get("required")); + }; + this.on("change:required", this, set_required); + _.bind(set_required, this)(); + } }, - set_value: function(value) { - this.value = value; - this.invalid = false; - this.update_dom(); - this.on_value_changed(); - }, - set_value_from_ui: function() { - this.on_value_changed(); - }, - on_value_changed: function() { - }, - on_translate: function() { - this.view.open_translate_dialog(this); + set_value: function(value_) { + this._inhibit_on_change = true; + this.set({'value': value_}); + this._inhibit_on_change = false; }, get_value: function() { - return this.value; + return this.get('value'); }, is_valid: function() { - return !this.invalid; + return this.is_syntax_valid() && (! this.get('required') || ! this.is_false()); }, - is_dirty: function() { - return this.dirty && !this.readonly; + is_syntax_valid: function() { + return true; }, - get_on_change_value: function() { - return this.get_value(); + /** + * Method useful to implement to ease validity testing. Must return true if the current + * value is similar to false in OpenERP. + */ + is_false: function() { + return this.get('value') === false; }, - update_dom: function(show_invalid) { - this._super.apply(this, arguments); + _check_css_flags: function(show_invalid) { if (this.field.translate) { - this.$element.find('.oe_field_translate').toggle(!!this.view.datarecord.id); + this.$element.find('.oe_field_translate').toggle(!this.field_manager.is_create_mode()); } if (!this.disable_utility_classes) { - this.$element.toggleClass('disabled', this.readonly); - this.$element.toggleClass('required', this.required); - if (show_invalid) { - this.$element.toggleClass('invalid', !this.is_valid()); + if (this.field_manager.get('display_invalid_fields')) { + this.$element.toggleClass('oe_form_invalid', !this.is_valid()); } } }, - on_ui_change: function() { - this.dirty = true; - this.validate(); - if (this.is_valid()) { - this.set_value_from_ui(); - this.view.do_onchange(this); - this.view.on_form_changed(true); - this.view.do_notify_change(); - } else { - this.update_dom(true); - } - }, - validate: function() { - this.invalid = false; + focus: function() { }, - focus: function($element) { - if ($element) { - setTimeout(function() { - $element.focus(); - }, 50); - } - }, - reset: function() { - this.dirty = false; + /** + * Utility method to focus an element, but only after a small amount of time. + */ + delay_focus: function($elem) { + setTimeout(function() { + $elem.focus(); + }, 50); }, + /** + * Utility method to get the widget options defined in the field xml description. + */ get_definition_options: function() { if (!this.definition_options) { var str = this.node.attrs.options || '{}'; this.definition_options = JSON.parse(str); } return this.definition_options; - } + }, + set_input_id: function(id) { + this.id_for_label = id; + }, }); -openerp.web.form.FieldChar = openerp.web.form.Field.extend({ - form_template: 'FieldChar', - init: function (view, node) { - this._super(view, node); - this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1'; - }, +/** + * A mixin to apply on any field that has to completely re-render when its readonly state + * switch. + */ +instance.web.form.ReinitializeFieldMixin = { + /** + * Default implementation of start(), use it or call explicitly initialize_field(). + */ start: function() { - this._super.apply(this, arguments); - this.$element.find('input').change(this.on_ui_change); + this._super(); + this.initialize_field(); }, - set_value: function(value) { - this._super.apply(this, arguments); - var show_value = openerp.web.format_value(value, this, ''); - this.$element.find('input').val(show_value); - return show_value; + initialize_field: function() { + this.on("change:effective_readonly", this, function() { + this.destroy_content(); + this.renderElement(); + this.initialize_content(); + this.render_value(); + }); + this.initialize_content(); + this.render_value(); }, - update_dom: function() { - this._super.apply(this, arguments); - this.$element.find('input').prop('readonly', this.readonly); + /** + * Called to destroy anything that could have been created previously, called before a + * re-initialization. + */ + destroy_content: function() {}, + /** + * Called to initialize the content. + */ + initialize_content: function() {}, + /** + * Called to render the value. Should also be explicitly called at the end of a set_value(). + */ + render_value: function() {}, +}; + +instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { + template: 'FieldChar', + init: function (field_manager, node) { + this._super(field_manager, node); + this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1'; }, - set_value_from_ui: function() { - this.value = openerp.web.parse_value(this.$element.find('input').val(), this); - this._super(); + initialize_content: function() { + var self = this; + this.$element.find('input').change(function() { + self.set({'value': instance.web.parse_value(self.$element.find('input').val(), self)}); + }); }, - validate: function() { - this.invalid = false; - try { - var value = openerp.web.parse_value(this.$element.find('input').val(), this, ''); - this.invalid = this.required && value === ''; - } catch(e) { - this.invalid = true; + set_value: function(value_) { + this._super(value_); + this.render_value(); + }, + render_value: function() { + var show_value = instance.web.format_value(this.get('value'), this, ''); + if (!this.get("effective_readonly")) { + this.$element.find('input').val(show_value); + } else { + if (this.password) { + show_value = new Array(show_value.length + 1).join('*'); + } + this.$element.text(show_value); } }, - focus: function($element) { - this._super($element || this.$element.find('input:first')); + is_syntax_valid: function() { + if (!this.get("effective_readonly")) { + try { + var value_ = instance.web.parse_value(this.$element.find('input').val(), this, ''); + return true; + } catch(e) { + return false; + } + } + return true; + }, + is_false: function() { + return this.get('value') === ''; + }, + focus: function() { + this.delay_focus(this.$element.find('input:first')); } -}); +})); -openerp.web.form.FieldID = openerp.web.form.FieldChar.extend({ - update_dom: function() { - this._super.apply(this, arguments); - this.$element.find('input').prop('readonly', true); - } +instance.web.form.FieldID = instance.web.form.FieldChar.extend({ + }); -openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({ - form_template: 'FieldEmail', - start: function() { - this._super.apply(this, arguments); +instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({ + template: 'FieldEmail', + initialize_content: function() { + this._super(); this.$element.find('button').click(this.on_button_clicked); }, + render_value: function() { + if (!this.get("effective_readonly")) { + this._super(); + } else { + this.$element.find('a') + .attr('href', 'mailto:' + this.get('value')) + .text(this.get('value')); + } + }, on_button_clicked: function() { - if (!this.value || !this.is_valid()) { + if (!this.get('value') || !this.is_syntax_valid()) { this.do_warn("E-mail error", "Can't send email to invalid e-mail address"); } else { - location.href = 'mailto:' + this.value; + location.href = 'mailto:' + this.get('value'); } } }); -openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({ - form_template: 'FieldUrl', - start: function() { - this._super.apply(this, arguments); +instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({ + template: 'FieldUrl', + initialize_content: function() { + this._super(); this.$element.find('button').click(this.on_button_clicked); }, + render_value: function() { + if (!this.get("effective_readonly")) { + this._super(); + } else { + var tmp = this.get('value'); + var s = /(\w+):(.+)/.exec(tmp); + if (!s) { + tmp = "http://" + this.get('value'); + } + this.$element.find('a').attr('href', tmp).text(tmp); + } + }, on_button_clicked: function() { - if (!this.value) { + if (!this.get('value')) { this.do_warn("Resource error", "This resource is empty"); } else { - window.open(this.value); + var url = $.trim(this.get('value')); + if(/^www\./i.test(url)) + url = 'http://'+url; + window.open(url); } } }); -openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({ - init: function (view, node) { - this._super(view, node); - if (node.attrs.digits) { +instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({ + is_field_number: true, + init: function (field_manager, node) { + this._super(field_manager, node); + this.set({'value': 0}); + if (this.node.attrs.digits) { this.digits = py.eval(node.attrs.digits); } else { - this.digits = view.fields_view.fields[node.attrs.name].digits; + this.digits = this.field.digits; } }, - set_value: function(value) { - if (value === false || value === undefined) { + set_value: function(value_) { + if (value_ === false || value_ === undefined) { // As in GTK client, floats default to 0 - value = 0; + value_ = 0; } - this._super.apply(this, [value]); + this._super.apply(this, [value_]); } }); -openerp.web.DateTimeWidget = openerp.web.OldWidget.extend({ +instance.web.DateTimeWidget = instance.web.OldWidget.extend({ template: "web.datetimepicker", jqueryui_object: 'datetimepicker', type_of_date: "datetime", @@ -1640,46 +1942,46 @@ openerp.web.DateTimeWidget = openerp.web.OldWidget.extend({ showButtonPanel: true }); this.$element.find('img.oe_datepicker_trigger').click(function() { - if (!self.readonly && !self.picker('widget').is(':visible')) { - self.picker('setDate', self.value ? openerp.web.auto_str_to_date(self.value) : new Date()); + if (!self.get("effective_readonly") && !self.picker('widget').is(':visible')) { + self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date()); self.$input_picker.show(); self.picker('show'); self.$input_picker.hide(); } }); this.set_readonly(false); - this.value = false; + this.set({'value': false}); }, picker: function() { return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments); }, - on_picker_select: function(text, instance) { + on_picker_select: function(text, instance_) { var date = this.picker('getDate'); this.$input.val(date ? this.format_client(date) : '').change(); }, - set_value: function(value) { - this.value = value; - this.$input.val(value ? this.format_client(value) : ''); + set_value: function(value_) { + this.set({'value': value_}); + this.$input.val(value_ ? this.format_client(value_) : ''); }, get_value: function() { - return this.value; + return this.get('value'); }, - set_value_from_ui: function() { - var value = this.$input.val() || false; - this.value = this.parse_client(value); + set_value_from_ui_: function() { + var value_ = this.$input.val() || false; + this.set({'value': this.parse_client(value_)}); }, set_readonly: function(readonly) { this.readonly = readonly; this.$input.prop('readonly', this.readonly); this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly); }, - is_valid: function(required) { - var value = this.$input.val(); - if (value === "") { - return !required; + is_valid_: function() { + var value_ = this.$input.val(); + if (value_ === "") { + return true; } else { try { - this.parse_client(value); + this.parse_client(value_); return true; } catch(e) { return false; @@ -1687,99 +1989,120 @@ openerp.web.DateTimeWidget = openerp.web.OldWidget.extend({ } }, parse_client: function(v) { - return openerp.web.parse_value(v, {"widget": this.type_of_date}); + return instance.web.parse_value(v, {"widget": this.type_of_date}); }, format_client: function(v) { - return openerp.web.format_value(v, {"widget": this.type_of_date}); + return instance.web.format_value(v, {"widget": this.type_of_date}); }, on_change: function() { - if (this.is_valid()) { - this.set_value_from_ui(); + if (this.is_valid_()) { + this.set_value_from_ui_(); } } }); -openerp.web.DateWidget = openerp.web.DateTimeWidget.extend({ +instance.web.DateWidget = instance.web.DateTimeWidget.extend({ jqueryui_object: 'datepicker', type_of_date: "date" }); -openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ - form_template: "EmptyComponent", +instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { + template: "EmptyComponent", build_widget: function() { - return new openerp.web.DateTimeWidget(this); + return new instance.web.DateTimeWidget(this); }, - start: function() { - var self = this; - this._super.apply(this, arguments); - this.datewidget = this.build_widget(); - this.datewidget.on_change.add_last(this.on_ui_change); - this.datewidget.appendTo(this.$element); + destroy_content: function() { + if (this.datewidget) { + this.datewidget.destroy(); + this.datewidget = undefined; + } + }, + initialize_content: function() { + if (!this.get("effective_readonly")) { + this.datewidget = this.build_widget(); + this.datewidget.on_change.add_last(_.bind(function() { + this.set({'value': this.datewidget.get_value()}); + }, this)); + this.datewidget.appendTo(this.$element); + } }, - set_value: function(value) { - this._super(value); - this.datewidget.set_value(value); + set_value: function(value_) { + this._super(value_); + this.render_value(); }, - get_value: function() { - return this.datewidget.get_value(); + render_value: function() { + if (!this.get("effective_readonly")) { + this.datewidget.set_value(this.get('value')); + } else { + this.$element.text(instance.web.format_value(this.get('value'), this, '')); + } }, - update_dom: function() { - this._super.apply(this, arguments); - this.datewidget.set_readonly(this.readonly); + is_syntax_valid: function() { + if (!this.get("effective_readonly")) { + return this.datewidget.is_valid_(); + } + return true; }, - validate: function() { - this.invalid = !this.datewidget.is_valid(this.required); + is_false: function() { + return this.get('value') === ''; }, - focus: function($element) { - this._super($element || this.datewidget.$input); + focus: function() { + if (this.datewidget && this.datewidget.$input) + this.delay_focus(this.datewidget.$input); } -}); +})); -openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({ +instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({ build_widget: function() { - return new openerp.web.DateWidget(this); + return new instance.web.DateWidget(this); } }); -openerp.web.form.FieldText = openerp.web.form.Field.extend({ - form_template: 'FieldText', - start: function() { - this._super.apply(this, arguments); - this.$element.find('textarea').change(this.on_ui_change); +instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { + template: 'FieldText', + initialize_content: function() { + this.$textarea = this.$element.find('textarea'); this.resized = false; + if (!this.get("effective_readonly")) { + this.$textarea.change(_.bind(function() { + this.set({'value': instance.web.parse_value(this.$textarea.val(), this)}); + }, this)); + } else { + this.$textarea.attr('disabled', 'disabled'); + } }, - set_value: function(value) { + set_value: function(value_) { this._super.apply(this, arguments); - var show_value = openerp.web.format_value(value, this, ''); - this.$element.find('textarea').val(show_value); + this.render_value(); + }, + render_value: function() { + var show_value = instance.web.format_value(this.get('value'), this, ''); + this.$textarea.val(show_value); if (!this.resized && this.view.options.resize_textareas) { this.do_resize(this.view.options.resize_textareas); this.resized = true; } }, - update_dom: function() { - this._super.apply(this, arguments); - this.$element.find('textarea').prop('readonly', this.readonly); - }, - set_value_from_ui: function() { - this.value = openerp.web.parse_value(this.$element.find('textarea').val(), this); - this._super(); - }, - validate: function() { - this.invalid = false; - try { - var value = openerp.web.parse_value(this.$element.find('textarea').val(), this, ''); - this.invalid = this.required && value === ''; - } catch(e) { - this.invalid = true; + is_syntax_valid: function() { + if (!this.get("effective_readonly")) { + try { + var value_ = instance.web.parse_value(this.$textarea.val(), this, ''); + return true; + } catch(e) { + return false; + } } + return true; + }, + is_false: function() { + return this.get('value') === ''; }, focus: function($element) { - this._super($element || this.$element.find('textarea:first')); + this.delay_focus(this.$textarea); }, do_resize: function(max_height) { max_height = parseInt(max_height, 10); - var $input = this.$element.find('textarea'), + var $input = this.$textarea, $div = $('<div style="position: absolute; z-index: 1000; top: 0"/>').width($input.width()), new_height; $div.text($input.val()); @@ -1797,64 +2120,60 @@ openerp.web.form.FieldText = openerp.web.form.Field.extend({ $div.remove(); $input.height(new_height); }, - reset: function() { - this.resized = false; - } -}); +})); -openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({ - form_template: 'FieldBoolean', +instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ + template: 'FieldBoolean', start: function() { - var self = this; this._super.apply(this, arguments); - this.$element.find('input').click(self.on_ui_change); - }, - set_value: function(value) { - this._super.apply(this, arguments); - this.$element.find('input')[0].checked = value; - }, - set_value_from_ui: function() { - this.value = this.$element.find('input').is(':checked'); - this._super(); + this.$checkbox = $("input", this.$element); + this.$element.click(_.bind(function() { + this.set({'value': this.$checkbox.is(':checked')}); + }, this)); + var check_readonly = function() { + this.$checkbox.prop('disabled', this.get("effective_readonly")); + }; + this.on("change:effective_readonly", this, check_readonly); + _.bind(check_readonly, this)(); }, - update_dom: function() { + set_value: function(value_) { this._super.apply(this, arguments); - this.$element.find('input').prop('disabled', this.readonly); + this.$checkbox[0].checked = value_; }, - focus: function($element) { - this._super($element || this.$element.find('input:first')); + focus: function() { + this.delay_focus(this.$checkbox); } }); -openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({ - form_template: 'FieldProgressBar', +instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({ + template: 'FieldProgressBar', start: function() { this._super.apply(this, arguments); - this.$element.find('div').progressbar({ - value: this.value, - disabled: this.readonly + this.$element.progressbar({ + value: this.get('value'), + disabled: this.get("effective_readonly") }); }, - set_value: function(value) { + set_value: function(value_) { this._super.apply(this, arguments); - var show_value = Number(value); + var show_value = Number(value_); if (isNaN(show_value)) { show_value = 0; } - var formatted_value = openerp.web.format_value(show_value, { type : 'float' }, '0'); - this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(formatted_value + '%'); + var formatted_value = instance.web.format_value(show_value, { type : 'float' }, '0'); + this.$element.progressbar('option', 'value', show_value).find('span').html(formatted_value + '%'); } }); -openerp.web.form.FieldTextXml = openerp.web.form.Field.extend({ +instance.web.form.FieldTextXml = instance.web.form.AbstractField.extend({ // to replace view editor }); -openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ - form_template: 'FieldSelection', - init: function(view, node) { +instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { + template: 'FieldSelection', + init: function(field_manager, node) { var self = this; - this._super(view, node); + this._super(field_manager, node); this.values = _.clone(this.field.selection); _.each(this.values, function(v, i) { if (v[0] === false && v[1] === '') { @@ -1863,7 +2182,7 @@ openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ }); this.values.unshift([false, '']); }, - start: function() { + initialize_content: function() { // Flag indicating whether we're in an event chain containing a change // event on the select, in order to know what to do on keyup[RETURN]: // * If the user presses [RETURN] as part of changing the value of a @@ -1876,9 +2195,10 @@ openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ // changing the selected value), takes the action as validating the // row var ischanging = false; - this._super.apply(this, arguments); this.$element.find('select') - .change(this.on_ui_change) + .change(_.bind(function() { + this.set({'value': this.values[this.$element.find('select')[0].selectedIndex][0]}); + }, this)) .change(function () { ischanging = true; }) .click(function () { ischanging = false; }) .keyup(function (e) { @@ -1887,32 +2207,37 @@ openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ ischanging = false; }); }, - set_value: function(value) { - value = value === null ? false : value; - value = value instanceof Array ? value[0] : value; - this._super(value); - var index = 0; - for (var i = 0, ii = this.values.length; i < ii; i++) { - if (this.values[i][0] === value) index = i; - } - this.$element.find('select')[0].selectedIndex = index; - }, - set_value_from_ui: function() { - this.value = this.values[this.$element.find('select')[0].selectedIndex][0]; - this._super(); + set_value: function(value_) { + value_ = value_ === null ? false : value_; + value_ = value_ instanceof Array ? value_[0] : value_; + this._super(value_); + this.render_value(); }, - update_dom: function() { - this._super.apply(this, arguments); - this.$element.find('select').prop('disabled', this.readonly); + render_value: function() { + if (!this.get("effective_readonly")) { + var index = 0; + for (var i = 0, ii = this.values.length; i < ii; i++) { + if (this.values[i][0] === this.get('value')) index = i; + } + this.$element.find('select')[0].selectedIndex = index; + } else { + var self = this; + var option = _(this.values) + .detect(function (record) { return record[0] === self.get('value'); }); + this.$element.text(option ? option[1] : this.values[0][1]); + } }, - validate: function() { - var value = this.values[this.$element.find('select')[0].selectedIndex]; - this.invalid = !(value && !(this.required && value[0] === false)); + is_syntax_valid: function() { + if (this.get("effective_readonly")) { + return true; + } + var value_ = this.values[this.$element.find('select')[0].selectedIndex]; + return !! value_; }, - focus: function($element) { - this._super($element || this.$element.find('select:first')); + focus: function() { + this.delay_focus(this.$element.find('select:first')); } -}); +})); // jquery autocomplete tweak to allow html (function() { @@ -1921,8 +2246,8 @@ openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ function filter( array, term ) { var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); - return $.grep( array, function(value) { - return matcher.test( $( "<div>" ).html( value.label || value.value || value ).text() ); + return $.grep( array, function(value_) { + return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() ); }); } @@ -1946,114 +2271,88 @@ openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ }); })(); -openerp.web.form.dialog = function(content, options) { +instance.web.form.dialog = function(content, options) { options = _.extend({ width: '90%', height: 'auto', min_width: '800px' }, options || {}); - var dialog = new openerp.web.Dialog(null, options, content).open(); + var dialog = new instance.web.Dialog(null, options, content).open(); return dialog.$element; }; -openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ - form_template: 'FieldMany2One', - init: function(view, node) { - this._super(view, node); +instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { + template: "FieldMany2One", + init: function(field_manager, node) { + this._super(field_manager, node); this.limit = 7; - this.value = null; - this.cm_id = _.uniqueId('m2o_cm_'); + this.set({'value': false}); + this.display_value = {}; this.last_search = []; - this.tmp_value = undefined; + this.floating = false; + this.inhibit_on_change = false; this.orderer = new openerp.web.DropMisordered(); }, start: function() { this._super(); + instance.web.form.ReinitializeFieldMixin.start.call(this); + this.on("change:value", this, function() { + this.floating = false; + this.render_value(); + }); + }, + initialize_content: function() { + if (!this.get("effective_readonly")) + this.render_editable(); + this.render_value(); + }, + render_editable: function() { var self = this; this.$input = this.$element.find("input"); + + self.$input.tipsy({ + title: function() { + return "No element was selected, you should create or select one from the dropdown list."; + }, + trigger:'manual', + fade: true, + }); + this.$drop_down = this.$element.find(".oe-m2o-drop-down-button"); - this.$menu_btn = this.$element.find(".oe-m2o-cm-button"); - - // context menu - var init_context_menu_def = $.Deferred().then(function(e) { - var rdataset = new openerp.web.DataSet(self, "ir.values", self.build_context()); - rdataset.call("get", ['action', 'client_action_relate', - [[self.field.relation, false]], false, rdataset.get_context()], false, 0) - .then(function(result) { - self.related_entries = result; - - var $cmenu = $("#" + self.cm_id); - $cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self})); - var bindings = {}; - bindings[self.cm_id + "_search"] = function() { - if (self.readonly) - return; - self._search_create_popup("search"); - }; - bindings[self.cm_id + "_create"] = function() { - if (self.readonly) - return; - self._search_create_popup("form"); - }; - bindings[self.cm_id + "_open"] = function() { - if (!self.value) { - return; - } - var pop = new openerp.web.form.FormOpenPopup(self.view); - pop.show_element( - self.field.relation, - self.value[0], - self.build_context(), - { - title: _t("Open: ") + (self.string || self.name) - } - ); - pop.on_write_completed.add_last(function() { - self.set_value(self.value[0]); - }); - }; - _.each(_.range(self.related_entries.length), function(i) { - bindings[self.cm_id + "_related_" + i] = function() { - self.open_related(self.related_entries[i]); - }; - }); - var cmenu = self.$menu_btn.contextMenu(self.cm_id, {'noRightClick': true, - bindings: bindings, itemStyle: {"color": ""}, - onContextMenu: function() { - if(self.value) { - $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").removeClass("oe-m2o-disabled-cm"); - } else { - $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").addClass("oe-m2o-disabled-cm"); - } - if (!self.readonly) { - $("#" + self.cm_id + " .oe_m2o_menu_item_noreadonly").removeClass("oe-m2o-disabled-cm"); - } else { - $("#" + self.cm_id + " .oe_m2o_menu_item_noreadonly").addClass("oe-m2o-disabled-cm"); - } - return true; - }, menuStyle: {width: "200px"} - }); - $.async_when().then(function() {self.$menu_btn.trigger(e);}); + this.$follow_button = $(".oe-m2o-cm-button", this.$element); + + this.$follow_button.click(function() { + if (!self.get('value')) { + return; + } + var pop = new instance.web.form.FormOpenPopup(self.view); + pop.show_element( + self.field.relation, + self.get("value"), + self.build_context(), + { + title: _t("Open: ") + (self.string || self.name) + } + ); + pop.on_write_completed.add_last(function() { + self.display_value = {}; + self.render_value(); }); }); - var ctx_callback = function(e) {init_context_menu_def.resolve(e); e.preventDefault()}; - this.$menu_btn.click(ctx_callback); // some behavior for input this.$input.keyup(function() { if (self.$input.val() === "") { - self._change_int_value(null); - } else if (self.value === null || (self.value && self.$input.val() !== self.value[1])) { - self._change_int_value(undefined); + self.set({value: false}); + } else { + self.floating = true; } }); this.$drop_down.click(function() { - if (self.readonly) - return; if (self.$input.autocomplete("widget").is(":visible")) { self.$input.autocomplete("close"); } else { - if (self.value) { + if (self.get("value") && ! self.floating) { self.$input.autocomplete("search", ""); } else { self.$input.autocomplete("search"); @@ -2061,16 +2360,43 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ self.$input.focus(); } }); + var tip_def = $.Deferred(); + var untip_def = $.Deferred(); + var tip_delay = 200; + var tip_duration = 3000; var anyoneLoosesFocus = function() { - if (!self.$input.is(":focus") && - !self.$input.autocomplete("widget").is(":visible") && - !self.value) { - if (self.value === undefined && self.last_search.length > 0) { - self._change_int_ext_value(self.last_search[0]); + if (self.floating) { + if (self.last_search.length > 0) { + if (self.last_search[0][0] != self.get("value")) { + self.display_value = {}; + self.display_value["" + self.last_search[0][0]] = self.last_search[0][1]; + self.set({value: self.last_search[0][0]}); + } else { + self.render_value(); + } } else { - self._change_int_ext_value(null); + self.set({value: false}); } } + if (! self.get("value")) { + tip_def.reject(); + untip_def.reject(); + tip_def = $.Deferred(); + tip_def.then(function() { + self.$input.tipsy("show"); + }); + setTimeout(function() { + tip_def.resolve(); + untip_def.reject(); + untip_def = $.Deferred(); + untip_def.then(function() { + self.$input.tipsy("hide"); + }); + setTimeout(function() {untip_def.resolve();}, tip_duration); + }, tip_delay); + } else { + tip_def.reject(); + } }; this.$input.focusout(anyoneLoosesFocus); @@ -2082,9 +2408,11 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ isSelecting = true; var item = ui.item; if (item.id) { - self._change_int_value([item.id, item.name]); + self.display_value = {}; + self.display_value["" + item.id] = item.name; + self.set({value: item.id}); } else if (item.action) { - self._change_int_value(undefined); + self.floating = true; item.action(); return false; } @@ -2112,7 +2440,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ var search_val = request.term; var self = this; - var dataset = new openerp.web.DataSet(this, this.field.relation, self.build_context()); + var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context()); this.orderer.add(dataset.name_search( search_val, self.build_domain(), 'ilike', this.limit + 1)).then(function(data) { @@ -2133,7 +2461,6 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ values.push({label: _t("<em>Â Â Â Search More...</em>"), action: function() { dataset.name_search(search_val, self.build_domain(), 'ilike' , false, function(data) { - self._change_int_value(null); self._search_create_popup("search", data); }); }}); @@ -2142,7 +2469,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ var raw_result = _(data.result).map(function(x) {return x[1];}); if (search_val.length > 0 && !_.include(raw_result, search_val) && - (!self.value || search_val !== self.value[1])) { + (!self.get("value") || self.floating)) { values.push({label: _.str.sprintf(_t('<em>Â Â Â Create "<strong>%s</strong>"</em>'), $('<span />').text(search_val).html()), action: function() { self._quick_create(search_val); @@ -2150,7 +2477,6 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ } // create... values.push({label: _t("<em>Â Â Â Create and Edit...</em>"), action: function() { - self._change_int_value(null); self._search_create_popup("form", undefined, {"default_name": search_val}); }}); @@ -2160,13 +2486,14 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ _quick_create: function(name) { var self = this; var slow_create = function () { - self._change_int_value(null); self._search_create_popup("form", undefined, {"default_name": name}); }; if (self.get_definition_options().quick_create === undefined || self.get_definition_options().quick_create) { - new openerp.web.DataSet(this, this.field.relation, self.build_context()) + new instance.web.DataSet(this, this.field.relation, self.build_context()) .name_create(name, function(data) { - self._change_int_ext_value(data); + self.display_value = {}; + self.display_value["" + data[0]] = data[1]; + self.set({value: data[0]}); }).fail(function(error, event) { event.preventDefault(); slow_create(); @@ -2177,7 +2504,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ // all search/create popup handling _search_create_popup: function(view, ids, context) { var self = this; - var pop = new openerp.web.form.SelectCreatePopup(this); + var pop = new instance.web.form.SelectCreatePopup(this); pop.select_element( self.field.relation, { @@ -2187,100 +2514,71 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ disable_multiple_selection: true }, self.build_domain(), - new openerp.web.CompoundContext(self.build_context(), context || {}) + new instance.web.CompoundContext(self.build_context(), context || {}) ); pop.on_select_elements.add(function(element_ids) { - new openerp.web.DataSet(self, self.field.relation, self.build_context()) - .name_get([element_ids[0]], function(data) { - self._change_int_ext_value(data[0]); - }); + self.set({value: element_ids[0]}); }); }, - _change_int_ext_value: function(value) { - this._change_int_value(value); - this.$input.val(this.value ? this.value[1] : ""); - }, - _change_int_value: function(value) { - this.value = value; - var back_orig_value = this.original_value; - if (this.value === null || this.value) { - this.original_value = this.value; + render_value: function(no_recurse) { + var self = this; + if (! this.get("value")) { + this.display_string(""); + return; } - if (back_orig_value === undefined) { // first use after a set_value() + var display = this.display_value["" + this.get("value")]; + if (display) { + this.display_string(display); return; } - if (this.value !== undefined && ((back_orig_value ? back_orig_value[0] : null) - !== (this.value ? this.value[0] : null))) { - this.on_ui_change(); + if (! no_recurse) { + var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); + dataset.name_get([self.get("value")], function(data) { + self.display_value["" + self.get("value")] = data[0][1]; + self.render_value(true); + }); } }, - set_value: function(value) { - value = value || null; - this.invalid = false; + display_string: function(str) { var self = this; - this.tmp_value = value; - self.update_dom(); - self.on_value_changed(); - var real_set_value = function(rval) { - self.tmp_value = undefined; - self.value = rval; - self.original_value = undefined; - self._change_int_ext_value(rval); - }; - if (value && !(value instanceof Array)) { - // name_get in a m2o does not use the context of the field - new openerp.web.DataSet(this, this.field.relation, self.view.dataset.get_context()) - .name_get([value], function(data) { - real_set_value(data[0]); - }).fail(function() {self.tmp_value = undefined;}); + if (!this.get("effective_readonly")) { + this.$input.val(str); } else { - $.async_when().then(function() {real_set_value(value);}); - } - }, - get_value: function() { - if (this.tmp_value !== undefined) { - if (this.tmp_value instanceof Array) { - return this.tmp_value[0]; - } - return this.tmp_value ? this.tmp_value : false; - } - if (this.value === undefined) - return this.original_value ? this.original_value[0] : false; - return this.value ? this.value[0] : false; - }, - validate: function() { - this.invalid = false; - var val = this.tmp_value !== undefined ? this.tmp_value : this.value; - if (val === null) { - this.invalid = this.required; + this.$element.find('a') + .unbind('click') + .text(str) + .click(function () { + self.do_action({ + type: 'ir.actions.act_window', + res_model: self.field.relation, + res_id: self.get("value"), + context: self.build_context(), + views: [[false, 'form']], + target: 'current' + }); + return false; + }); } }, - open_related: function(related) { + set_value: function(value_) { var self = this; - if (!self.value) - return; - var additional_context = { - active_id: self.value[0], - active_ids: [self.value[0]], - active_model: self.field.relation - }; - self.rpc("/web/action/load", { - action_id: related[2].id, - context: additional_context - }, function(result) { - result.result.context = _.extend(result.result.context || {}, additional_context); - self.do_action(result.result); - }); + if (value_ instanceof Array) { + this.display_value = {}; + this.display_value["" + value_[0]] = value_[1]; + value_ = value_[0]; + } + value_ = value_ || false; + this.inhibit_on_change = true; + this._super(value_); + this.inhibit_on_change = false; }, - focus: function ($element) { - this._super($element || this.$input); + is_false: function() { + return ! this.get("value"); }, - update_dom: function() { - this._super.apply(this, arguments); - this.$input.prop('readonly', this.readonly); + focus: function () { + this.delay_focus(this.$input); } -}); -openerp.web.check_interface(openerp.web.form.FieldMany2One, openerp.web.form.FieldInterface); +})); /* # Values: (0, 0, { fields }) create @@ -2327,24 +2625,24 @@ var commands = { return [6, false, ids]; } }; -openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ - form_template: 'FieldOne2Many', +instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ multi_selection: false, - init: function(view, node) { - this._super(view, node); + disable_utility_classes: true, + init: function(field_manager, node) { + this._super(field_manager, node); + lazy_build_o2m_kanban_view(); this.is_loaded = $.Deferred(); this.initial_is_loaded = this.is_loaded; this.is_setted = $.Deferred(); this.form_last_update = $.Deferred(); this.init_form_last_update = this.form_last_update; - this.disable_utility_classes = true; }, start: function() { this._super.apply(this, arguments); var self = this; - this.dataset = new openerp.web.form.One2ManyDataSet(this, this.field.relation); + this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation); this.dataset.o2m = this; this.dataset.parent_view = this.view; this.dataset.child_name = this.name; @@ -2356,16 +2654,23 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ this.is_setted.then(function() { self.load_views(); }); + this.is_loaded.then(function() { + self.on("change:effective_readonly", self, function() { + self.is_loaded = self.is_loaded.pipe(function() { + self.viewmanager.destroy(); + return $.when(self.load_views()).then(function() { + self.reload_current_view(); + }); + }); + }); + }); }, trigger_on_change: function() { var tmp = this.doing_on_change; this.doing_on_change = true; - this.on_ui_change(); + this.trigger('changed_value'); this.doing_on_change = tmp; }, - is_readonly: function() { - return this.readonly || this.force_readonly; - }, load_views: function() { var self = this; @@ -2376,21 +2681,21 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ var view = { view_id: false, view_type: mode == "tree" ? "list" : mode, - options: { sidebar : false } + options: {} }; if (self.field.views && self.field.views[mode]) { view.embedded_view = self.field.views[mode]; } if(view.view_type === "list") { view.options.selectable = self.multi_selection; - if (self.is_readonly()) { + if (self.get("effective_readonly")) { view.options.addable = null; view.options.deletable = null; view.options.isClarkGable = false; } } else if (view.view_type === "form") { - if (self.is_readonly()) { - view.view_type = 'page'; + if (self.get("effective_readonly")) { + view.view_type = 'form'; } view.options.not_interactible_on_create = true; } @@ -2398,12 +2703,14 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ }); this.views = views; - this.viewmanager = new openerp.web.ViewManager(this, this.dataset, views, {}); + this.viewmanager = new instance.web.ViewManager(this, this.dataset, views, { + $sidebar: false, + }); this.viewmanager.template = 'One2Many.viewmanager'; - this.viewmanager.registry = openerp.web.views.extend({ - list: 'openerp.web.form.One2ManyListView', - form: 'openerp.web.form.One2ManyFormView', - page: 'openerp.web.PageView' + this.viewmanager.registry = instance.web.views.extend({ + list: 'instance.web.form.One2ManyListView', + form: 'instance.web.form.One2ManyFormView', + kanban: 'instance.web.form.One2ManyKanbanView', }); var once = $.Deferred().then(function() { self.init_form_last_update.resolve(); @@ -2412,12 +2719,12 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ self.initial_is_loaded.resolve(); }); this.viewmanager.on_controller_inited.add_last(function(view_type, controller) { + controller.o2m = self; if (view_type == "list") { - controller.o2m = self; - if (self.is_readonly()) + if (self.get("effective_readonly")) controller.set_editable(false); - } else if (view_type == "form" || view_type == 'page') { - if (view_type == 'page' || self.is_readonly()) { + } else if (view_type === "form") { + if (self.get("effective_readonly")) { $(".oe_form_buttons", controller.$element).children().remove(); } controller.on_record_loaded.add_last(function() { @@ -2451,7 +2758,7 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ var view = self.viewmanager.views[active_view].controller; if(active_view === "list") { return view.reload_content(); - } else if (active_view === "form" || active_view === 'page') { + } else if (active_view === "form") { if (self.dataset.index === null && self.dataset.ids.length >= 1) { self.dataset.index = 0; } @@ -2460,18 +2767,18 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ }; self.form_last_update = self.form_last_update.pipe(act, act); return self.form_last_update; - } else if (active_view === "graph") { + } else if (view.do_search) { return view.do_search(self.build_domain(), self.dataset.get_context(), []); } }, undefined); }, - set_value: function(value) { - value = value || []; + set_value: function(value_) { + value_ = value_ || []; var self = this; this.dataset.reset_ids([]); - if(value.length >= 1 && value[0] instanceof Array) { + if(value_.length >= 1 && value_[0] instanceof Array) { var ids = []; - _.each(value, function(command) { + _.each(value_, function(command) { var obj = {values: command[2]}; switch (command[0]) { case commands.CREATE: @@ -2500,10 +2807,10 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ }); this._super(ids); this.dataset.set_ids(ids); - } else if (value.length >= 1 && typeof(value[0]) === "object") { + } else if (value_.length >= 1 && typeof(value_[0]) === "object") { var ids = []; this.dataset.delete_all = true; - _.each(value, function(command) { + _.each(value_, function(command) { var obj = {values: command}; obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix); obj.defaults = {}; @@ -2514,8 +2821,8 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ this._super(ids); this.dataset.set_ids(ids); } else { - this._super(value); - this.dataset.reset_ids(value); + this._super(value_); + this.dataset.reset_ids(value_); } if (this.dataset.index === null && this.dataset.ids.length > 0) { this.dataset.index = 0; @@ -2572,61 +2879,37 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ return false; }, this)); }, - is_valid: function() { - this.validate(); - return this._super(); - }, - validate: function() { - this.invalid = false; + is_syntax_valid: function() { if (!this.viewmanager.views[this.viewmanager.active_view]) - return; + return true; var view = this.viewmanager.views[this.viewmanager.active_view].controller; if (this.viewmanager.active_view === "form") { for (var f in view.fields) { f = view.fields[f]; if (!f.is_valid()) { - this.invalid = true; - return; + return false; } } } + return true; }, - is_dirty: function() { - this.save_any_view(); - return this._super(); - }, - update_dom: function() { - this._super.apply(this, arguments); - var self = this; - if (this.previous_readonly !== this.readonly) { - this.previous_readonly = this.readonly; - if (this.viewmanager) { - this.is_loaded = this.is_loaded.pipe(function() { - self.viewmanager.destroy(); - return $.when(self.load_views()).then(function() { - self.reload_current_view(); - }); - }); - } - } - } }); -openerp.web.form.One2ManyDataSet = openerp.web.BufferedDataSet.extend({ +instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({ get_context: function() { this.context = this.o2m.build_context([this.o2m.name]); return this.context; } }); -openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ +instance.web.form.One2ManyListView = instance.web.ListView.extend({ _template: 'One2Many.listview', do_add_record: function () { if (this.options.editable) { this._super.apply(this, arguments); } else { var self = this; - var pop = new openerp.web.form.SelectCreatePopup(this); + var pop = new instance.web.form.SelectCreatePopup(this); pop.on_default_get.add(self.dataset.on_default_get); pop.select_element( self.o2m.field.relation, @@ -2657,7 +2940,7 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ }, do_activate_record: function(index, id) { var self = this; - var pop = new openerp.web.form.FormOpenPopup(self.o2m.view); + var pop = new instance.web.form.FormOpenPopup(self.o2m.view); pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), { title: _t("Open: ") + self.name, auto_write: false, @@ -2668,7 +2951,7 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); }, form_view_options: {'not_interactible_on_create':true}, - readonly: self.o2m.is_readonly() + readonly: self.o2m.get("effective_readonly") }); pop.on_write.add(function(id, data) { self.o2m.dataset.write(id, data, {}, function(r) { @@ -2683,12 +2966,12 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ } }); -openerp.web.form.One2ManyFormView = openerp.web.FormView.extend({ +instance.web.form.One2ManyFormView = instance.web.FormView.extend({ form_template: 'One2Many.formview', on_loaded: function(data) { this._super(data); var self = this; - this.$form_header.find('button.oe_form_button_create').click(function() { + this.$buttons.find('button.oe_form_button_create').click(function() { self.do_save().then(self.on_button_new); }); }, @@ -2701,12 +2984,43 @@ openerp.web.form.One2ManyFormView = openerp.web.FormView.extend({ } }); -openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({ - form_template: 'FieldMany2Many', +var lazy_build_o2m_kanban_view = function() { +if (! instance.web_kanban || instance.web.form.One2ManyKanbanView) + return; +instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({ + open_record: function(id) { + var self = this; + var pop = new instance.web.form.FormOpenPopup(self.o2m.view); + pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), { + title: _t("Open: ") + self.name, + auto_write: false, + alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, + parent_view: self.o2m.view, + child_name: self.o2m.name, + read_function: function() { + return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); + }, + form_view_options: {'not_interactible_on_create':true}, + readonly: self.o2m.get("effective_readonly"), + }); + pop.on_write.add(function(id, data) { + self.o2m.dataset.write(id, data, {}, function(r) { + self.o2m.reload_current_view(); + }); + }); + + }, +}); +} + +/* + * TODO niv: clean those deferred stuff, it could be better + */ +instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ multi_selection: false, - init: function(view, node) { - this._super(view, node); - this.list_id = _.uniqueId("many2many"); + disable_utility_classes: true, + init: function(field_manager, node) { + this._super(field_manager, node); this.is_loaded = $.Deferred(); this.initial_is_loaded = this.is_loaded; this.is_setted = $.Deferred(); @@ -2716,43 +3030,44 @@ openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({ var self = this; - this.dataset = new openerp.web.form.Many2ManyDataSet(this, this.field.relation); + this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation); this.dataset.m2m = this; this.dataset.on_unlink.add_last(function(ids) { - self.on_ui_change(); + self.dataset_changed(); }); this.is_setted.then(function() { self.load_view(); }); + this.is_loaded.then(function() { + self.on("change:effective_readonly", self, function() { + self.is_loaded = self.is_loaded.pipe(function() { + self.list_view.destroy(); + return $.when(self.load_view()).then(function() { + self.reload_content(); + }); + }); + }); + }) }, - set_value: function(value) { - value = value || []; - if (value.length >= 1 && value[0] instanceof Array) { - value = value[0][2]; + set_value: function(value_) { + value_ = value_ || []; + if (value_.length >= 1 && value_[0] instanceof Array) { + value_ = value_[0][2]; } - this._super(value); - this.dataset.set_ids(value); + this._super(value_); + this.dataset.set_ids(value_); var self = this; self.reload_content(); this.is_setted.resolve(); }, - get_value: function() { - return [commands.replace_with(this.dataset.ids)]; - }, - validate: function() { - this.invalid = false; - }, - is_readonly: function() { - return this.readonly || this.force_readonly; - }, load_view: function() { var self = this; - this.list_view = new openerp.web.form.Many2ManyListView(this, this.dataset, false, { - 'addable': self.is_readonly() ? null : _t("Add"), - 'deletable': self.is_readonly() ? false : true, + this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, { + 'addable': self.get("effective_readonly") ? null : _t("Add"), + 'deletable': self.get("effective_readonly") ? false : true, 'selectable': self.multi_selection, - 'isClarkGable': self.is_readonly() ? false : true + 'isClarkGable': self.get("effective_readonly") ? false : true }); var embedded = (this.field.views || {}).tree; if (embedded) { @@ -2765,7 +3080,7 @@ openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({ loaded.resolve(); }); $.async_when().then(function () { - self.list_view.appendTo($("#" + self.list_id)); + self.list_view.appendTo(self.$element); }); return loaded; }, @@ -2775,24 +3090,12 @@ openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({ return self.list_view.reload_content(); }); }, - update_dom: function() { - this._super.apply(this, arguments); - var self = this; - if (this.previous_readonly !== this.readonly) { - this.previous_readonly = this.readonly; - if (this.list_view) { - this.is_loaded = this.is_loaded.pipe(function() { - self.list_view.destroy(); - return $.when(self.load_view()).then(function() { - self.reload_content(); - }); - }); - } - } - } + dataset_changed: function() { + this.set({'value': [commands.replace_with(this.dataset.ids)]}); + }, }); -openerp.web.form.Many2ManyDataSet = openerp.web.DataSetStatic.extend({ +instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({ get_context: function() { this.context = this.m2m.build_context(); return this.context; @@ -2801,25 +3104,25 @@ openerp.web.form.Many2ManyDataSet = openerp.web.DataSetStatic.extend({ /** * @class - * @extends openerp.web.ListView + * @extends instance.web.ListView */ -openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends openerp.web.form.Many2ManyListView# */{ +instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{ do_add_record: function () { - var pop = new openerp.web.form.SelectCreatePopup(this); + var pop = new instance.web.form.SelectCreatePopup(this); pop.select_element( this.model, { title: _t("Add: ") + this.name }, - new openerp.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]), + new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]), this.m2m_field.build_context() ); var self = this; pop.on_select_elements.add(function(element_ids) { - _.each(element_ids, function(element_id) { - if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) { - self.dataset.set_ids([].concat(self.dataset.ids, [element_id])); - self.m2m_field.on_ui_change(); + _.each(element_ids, function(one_id) { + if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) { + self.dataset.set_ids([].concat(self.dataset.ids, [one_id])); + self.m2m_field.dataset_changed(); self.reload_content(); } }); @@ -2827,10 +3130,10 @@ openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends open }, do_activate_record: function(index, id) { var self = this; - var pop = new openerp.web.form.FormOpenPopup(this); + var pop = new instance.web.form.FormOpenPopup(this); pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), { title: _t("Open: ") + this.name, - readonly: this.getParent().is_readonly() + readonly: this.getParent().get("effective_readonly") }); pop.on_write_completed.add_last(function() { self.reload_content(); @@ -2840,9 +3143,9 @@ openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends open /** * @class - * @extends openerp.web.OldWidget + * @extends instance.web.OldWidget */ -openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.SelectCreatePopup# */{ +instance.web.form.SelectCreatePopup = instance.web.OldWidget.extend(/** @lends instance.web.form.SelectCreatePopup# */{ template: "SelectCreatePopup", /** * options: @@ -2868,7 +3171,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope this.initial_ids = this.options.initial_ids; this.created_elements = []; this.renderElement(); - openerp.web.form.dialog(this.$element, { + instance.web.form.dialog(this.$element, { close: function() { self.check_exit(); }, @@ -2879,7 +3182,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope start: function() { this._super(); var self = this; - this.dataset = new openerp.web.ProxyDataSet(this, this.model, + this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context); this.dataset.create_function = function() { return self.options.create_function.apply(null, arguments).then(function(r) { @@ -2899,10 +3202,10 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope contexts: [this.context] }, function (results) { var search_defaults = {}; - _.each(results.context, function (value, key) { + _.each(results.context, function (value_, key) { var match = /^search_default_(.*)$/.exec(key); if (match) { - search_defaults[match[1]] = value; + search_defaults[match[1]] = value_; } }); self.setup_search_view(search_defaults); @@ -2920,7 +3223,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope if (this.searchview) { this.searchview.destroy(); } - this.searchview = new openerp.web.SearchView(this, + this.searchview = new instance.web.SearchView(this, this.dataset, false, search_defaults); this.searchview.on_search.add(function(domains, contexts, groupbys) { if (self.initial_ids) { @@ -2932,13 +3235,13 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope } }); this.searchview.on_loaded.add_last(function () { - self.view_list = new openerp.web.form.SelectCreateListView(self, + self.view_list = new instance.web.form.SelectCreateListView(self, self.dataset, false, _.extend({'deletable': false, 'selectable': !self.options.disable_multiple_selection }, self.options.list_view_options || {})); self.view_list.popup = self; - self.view_list.appendTo($("#" + self.element_id + "_view_list")).pipe(function() { + self.view_list.appendTo($(".oe-select-create-popup-view-list", self.$element)).pipe(function() { self.view_list.do_show(); }).pipe(function() { self.searchview.do_search(); @@ -2960,7 +3263,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope }); }); }); - this.searchview.appendTo($("#" + this.element_id + "_search")); + this.searchview.appendTo($(".oe-select-create-popup-view-list", self.$element)); }, do_search: function(domains, contexts, groupbys) { var self = this; @@ -2974,14 +3277,14 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope }, create_row: function() { var self = this; - var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain); + var wdataset = new instance.web.DataSetSearch(this, this.model, this.context, this.domain); wdataset.parent_view = this.options.parent_view; wdataset.child_name = this.options.child_name; return wdataset.create.apply(wdataset, arguments); }, write_row: function() { var self = this; - var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain); + var wdataset = new instance.web.DataSetSearch(this, this.model, this.context, this.domain); wdataset.parent_view = this.options.parent_view; wdataset.child_name = this.options.child_name; return wdataset.write.apply(wdataset, arguments); @@ -3005,11 +3308,11 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope this.view_list.$element.hide(); } this.dataset.index = null; - this.view_form = new openerp.web.FormView(this, this.dataset, false, self.options.form_view_options); + this.view_form = new instance.web.FormView(this, this.dataset, false, self.options.form_view_options); if (this.options.alternative_form_view) { this.view_form.set_embedded_view(this.options.alternative_form_view); } - this.view_form.appendTo(this.$element.find("#" + this.element_id + "_view_form")); + this.view_form.appendTo(this.$element.find(".oe-select-create-popup-view-form")); this.view_form.on_loaded.add_last(function() { var $buttons = self.view_form.$element.find(".oe_form_buttons"); $buttons.html(QWeb.render("SelectCreatePopup.form.buttons", {widget:self})); @@ -3045,7 +3348,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope on_default_get: function(res) {} }); -openerp.web.form.SelectCreateListView = openerp.web.ListView.extend({ +instance.web.form.SelectCreateListView = instance.web.ListView.extend({ do_add_record: function () { this.popup.new_object(); }, @@ -3061,9 +3364,9 @@ openerp.web.form.SelectCreateListView = openerp.web.ListView.extend({ /** * @class - * @extends openerp.web.OldWidget + * @extends instance.web.OldWidget */ -openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.FormOpenPopup# */{ +instance.web.form.FormOpenPopup = instance.web.OldWidget.extend(/** @lends instance.web.form.FormOpenPopup# */{ template: "FormOpenPopup", /** * options: @@ -3081,7 +3384,7 @@ openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp this.context = context || {}; this.options = _.defaults(options || {}, {"auto_write": true}); this.renderElement(); - openerp.web.dialog(this.$element, { + instance.web.dialog(this.$element, { title: options.title || '', modal: true, width: 960, @@ -3091,7 +3394,7 @@ openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp }, start: function() { this._super(); - this.dataset = new openerp.web.form.FormOpenDataset(this, this.model, this.context); + this.dataset = new instance.web.form.FormOpenDataset(this, this.model, this.context); this.dataset.fop = this; this.dataset.ids = [this.row_id]; this.dataset.index = 0; @@ -3103,7 +3406,7 @@ openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp if (!this.options.auto_write) return; var self = this; - var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain); + var wdataset = new instance.web.DataSetSearch(this, this.model, this.context, this.domain); wdataset.parent_view = this.options.parent_view; wdataset.child_name = this.options.child_name; wdataset.write(id, data, {}, function(r) { @@ -3113,14 +3416,14 @@ openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp on_write_completed: function() {}, setup_form_view: function() { var self = this; - var FormClass = this.options.readonly - ? openerp.web.views.get_object('page') - : openerp.web.views.get_object('form'); - this.view_form = new FormClass(this, this.dataset, false, self.options.form_view_options); + var FormClass = instance.web.views.get_object('form'); + var options = _.clone(self.options.form_view_options) || {}; + options.initial_mode = this.options.readonly ? "view" : "edit"; + this.view_form = new FormClass(this, this.dataset, false, options); if (this.options.alternative_form_view) { this.view_form.set_embedded_view(this.options.alternative_form_view); } - this.view_form.appendTo(this.$element.find("#" + this.element_id + "_view_form")); + this.view_form.appendTo(this.$element.find(".oe-form-open-popup-form-view")); this.view_form.on_loaded.add_last(function() { var $buttons = self.view_form.$element.find(".oe_form_buttons"); $buttons.html(QWeb.render("FormOpenPopup.form.buttons")); @@ -3144,7 +3447,7 @@ openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp } }); -openerp.web.form.FormOpenDataset = openerp.web.ProxyDataSet.extend({ +instance.web.form.FormOpenDataset = instance.web.ProxyDataSet.extend({ read_ids: function() { if (this.fop.options.read_function) { return this.fop.options.read_function.apply(null, arguments); @@ -3154,40 +3457,28 @@ openerp.web.form.FormOpenDataset = openerp.web.ProxyDataSet.extend({ } }); -openerp.web.form.FieldReference = openerp.web.form.Field.extend({ - form_template: 'FieldReference', - init: function(view, node) { - this._super(view, node); +instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { + template: 'FieldReference', + init: function(field_manager, node) { + this._super(field_manager, node); this.fields_view = { fields: { selection: { - selection: view.fields_view.fields[this.name].selection + selection: this.view.fields_view.fields[this.name].selection }, m2o: { relation: null } } }; - this.get_fields_values = view.get_fields_values; - this.get_selected_ids = view.get_selected_ids; + this.get_fields_values = this.view.get_fields_values; + this.get_selected_ids = this.view.get_selected_ids; this.do_onchange = this.on_form_changed = this.do_notify_change = this.on_nop; this.dataset = this.view.dataset; - this.widgets_counter = 0; this.view_id = 'reference_' + _.uniqueId(); - this.widgets = {}; this.fields = {}; this.fields_order = []; - this.selection = new openerp.web.form.FieldSelection(this, { attrs: { - name: 'selection', - widget: 'selection' - }}); this.reference_ready = true; - this.selection.on_value_changed.add_last(this.on_selection_changed); - this.m2o = new openerp.web.form.FieldMany2One(this, { attrs: { - name: 'm2o', - widget: 'many2one' - }}); - this.m2o.on_ui_change.add_last(this.on_ui_change); }, on_nop: function() { }, @@ -3195,59 +3486,82 @@ openerp.web.form.FieldReference = openerp.web.form.Field.extend({ if (this.reference_ready) { var sel = this.selection.get_value(); this.m2o.field.relation = sel; - this.m2o.set_value(null); + this.m2o.set_value(false); this.m2o.$element.toggle(sel !== false); } }, - start: function() { - this._super(); + destroy_content: function() { + if (this.selection) { + this.selection.destroy(); + this.selection = undefined; + } + if (this.m2o) { + this.m2o.destroy(); + this.m2o = undefined; + } + }, + initialize_content: function() { + this.selection = new instance.web.form.FieldSelection(this, { attrs: { + name: 'selection', + widget: 'selection' + }}); + this.selection.set({readonly: this.get('effective_readonly')}); + this.selection.on("change:value", this, this.on_selection_changed); this.selection.$element = $(".oe_form_view_reference_selection", this.$element); this.selection.renderElement(); this.selection.start(); + + this.m2o = new instance.web.form.FieldMany2One(this, { attrs: { + name: 'm2o', + widget: 'many2one' + }}); + this.m2o.set({"readonly": this.get("effective_readonly")}); + this.m2o.on("change:value", this, this.data_changed); this.m2o.$element = $(".oe_form_view_reference_m2o", this.$element); this.m2o.renderElement(); this.m2o.start(); }, - is_valid: function() { - return this.required === false || typeof(this.get_value()) === 'string'; + is_false: function() { + return typeof(this.get_value()) !== 'string'; }, - is_dirty: function() { - return this.selection.is_dirty() || this.m2o.is_dirty(); + set_value: function(value_) { + this._super(value_); + this.render_value(); }, - set_value: function(value) { - this._super(value); + render_value: function() { this.reference_ready = false; var vals = [], sel_val, m2o_val; - if (typeof(value) === 'string') { - vals = value.split(','); + if (typeof(this.get('value')) === 'string') { + vals = this.get('value').split(','); } sel_val = vals[0] || false; m2o_val = vals[1] ? parseInt(vals[1], 10) : false; - this.selection.set_value(sel_val); + if (!this.get("effective_readonly")) { + this.selection.set_value(sel_val); + } this.m2o.field.relation = sel_val; this.m2o.set_value(m2o_val); this.m2o.$element.toggle(sel_val !== false); this.reference_ready = true; }, - get_value: function() { + data_changed: function() { var model = this.selection.get_value(), id = this.m2o.get_value(); if (typeof(model) === 'string' && typeof(id) === 'number') { - return model + ',' + id; + this.set({'value': model + ',' + id}); } else { - return false; + this.set({'value': false}); } - } -}); + }, +})); -openerp.web.form.FieldBinary = openerp.web.form.Field.extend({ - init: function(view, node) { - this._super(view, node); +instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { + init: function(field_manager, node) { + this._super(field_manager, node); this.iframe = this.element_id + '_iframe'; this.binary_value = false; }, - start: function() { - this._super.apply(this, arguments); + initialize_content: function() { this.$element.find('input.oe-binary-file').change(this.on_file_change); this.$element.find('button.oe-binary-file-save').click(this.on_save_as); this.$element.find('.oe-binary-file-clear').click(this.on_clear); @@ -3281,7 +3595,6 @@ openerp.web.form.FieldBinary = openerp.web.form.Field.extend({ console.warn("Error while uploading file : ", name); } else { this.on_file_uploaded_and_valid.apply(this, arguments); - this.on_ui_change(); } this.$element.find('.oe-binary-progress').hide(); this.$element.find('.oe-binary').show(); @@ -3300,48 +3613,64 @@ openerp.web.form.FieldBinary = openerp.web.form.Field.extend({ context: this.view.dataset.get_context() })}, complete: $.unblockUI, - error: openerp.webclient.crashmanager.on_rpc_error + error: instance.webclient.crashmanager.on_rpc_error }); }, on_clear: function() { - if (this.value !== false) { - this.value = false; + if (this.get('value') !== false) { this.binary_value = false; - this.on_ui_change(); + this.set({'value': false}); } return false; } -}); +})); -openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({ - form_template: 'FieldBinaryFile', - update_dom: function() { - this._super.apply(this, arguments); - this.$element.find('.oe-binary-file-set, .oe-binary-file-clear').toggle(!this.readonly); - this.$element.find('input[type=text]').prop('readonly', this.readonly); +instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ + template: 'FieldBinaryFile', + initialize_content: function() { + this._super(); + if (this.get("effective_readonly")) { + var self = this; + this.$element.find('a').click(function() { + if (self.get('value')) { + self.on_save_as(); + } + return false; + }); + } }, - set_value: function(value) { + set_value: function(value_) { this._super.apply(this, arguments); - var show_value; - if (this.node.attrs.filename) { - show_value = this.view.datarecord[this.node.attrs.filename] || ''; + this.render_value(); + }, + render_value: function() { + if (!this.get("effective_readonly")) { + var show_value; + if (this.node.attrs.filename) { + show_value = this.view.datarecord[this.node.attrs.filename] || ''; + } else { + show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : ''; + } + this.$element.find('input').eq(0).val(show_value); } else { - show_value = (value != null && value !== false) ? value : ''; + this.$element.find('a').show(!!this.get('value')); + if (this.get('value')) { + var show_value = _t("Download") + " " + (this.view.datarecord[this.node.attrs.filename] || ''); + this.$element.find('a').text(show_value); + } } - this.$element.find('input').eq(0).val(show_value); }, on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { - this.value = file_base64; this.binary_value = true; + this.set({'value': file_base64}); var show_value = name + " (" + this.human_filesize(size) + ")"; this.$element.find('input').eq(0).val(show_value); this.set_filename(name); }, - set_filename: function(value) { + set_filename: function(value_) { var filename = this.node.attrs.filename; if (this.view.fields[filename]) { - this.view.fields[filename].set_value(value); - this.view.fields[filename].on_ui_change(); + this.view.fields[filename].set({value: value_}); } }, on_clear: function() { @@ -3351,58 +3680,59 @@ openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({ } }); -openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({ - form_template: 'FieldBinaryImage', - start: function() { - this._super.apply(this, arguments); - this.$image = this.$element.find('img.oe-binary-image'); +instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ + template: 'FieldBinaryImage', + initialize_content: function() { + this._super(); + this.$placeholder = $(".oe_form_field-binary-image-placeholder", this.$element); + if (!this.get("effective_readonly")) + this.$element.find('.oe-binary').show(); + else + this.$element.find('.oe-binary').hide(); }, - update_dom: function() { + set_value: function(value_) { this._super.apply(this, arguments); - this.$element.find('.oe-binary').toggle(!this.readonly); + this.render_value(); }, - set_value: function(value) { - this._super.apply(this, arguments); - this.set_image_maxwidth(); - + render_value: function() { var url; - if (value && value.substr(0, 10).indexOf(' ') == -1) { - url = 'data:image/png;base64,' + this.value; - } else { + if (this.get('value') && this.get('value').substr(0, 10).indexOf(' ') == -1) { + url = 'data:image/png;base64,' + this.get('value'); + } else if (this.get('value')) { url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' + this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime()); + } else { + url = "/web/static/src/img/placeholder.png"; } - this.$image.attr('src', url); - }, - set_image_maxwidth: function() { - this.$image.css('max-width', this.$element.width()); + var rendered = QWeb.render("FieldBinaryImage-img", {widget: this, url: url});; + this.$placeholder.html(rendered); }, on_file_change: function() { - this.set_image_maxwidth(); + this.render_value(); this._super.apply(this, arguments); }, on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { - this.value = file_base64; + this.set({'value': file_base64}); this.binary_value = true; - this.$image.attr('src', 'data:' + (content_type || 'image/png') + ';base64,' + file_base64); + this.render_value(); }, on_clear: function() { this._super.apply(this, arguments); - this.$image.attr('src', '/web/static/src/img/placeholder.png'); + this.render_value(); } }); -openerp.web.form.FieldStatus = openerp.web.form.Field.extend({ - form_template: "EmptyComponent", +instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ + template: "EmptyComponent", start: function() { this._super(); this.selected_value = null; this.render_list(); }, - set_value: function(value) { - this._super(value); - this.selected_value = value; + set_value: function(value_) { + this._super(value_); + this.selected_value = value_; this.render_list(); }, @@ -3420,7 +3750,7 @@ openerp.web.form.FieldStatus = openerp.web.form.Field.extend({ }); } - var content = openerp.web.qweb.render("FieldStatus.content", {widget: this, _:_}); + var content = instance.web.qweb.render("FieldStatus.content", {widget: this, _:_}); this.$element.html(content); var colors = JSON.parse((this.node.attrs || {}).statusbar_colors || "{}"); @@ -3457,77 +3787,36 @@ openerp.web.form.FieldStatus = openerp.web.form.Field.extend({ } }); -openerp.web.form.WidgetHtml = openerp.web.form.Widget.extend({ - renderElement: function () { - var $root = $('<div class="oe_form_html_view">'); - this.render_children(this, $root); - var rendered = $root.html(); - this.$element.html(rendered); - }, - render_children: function (object, $into) { - var self = this, - fields = this.view.fields_view.fields; - _(object.children).each(function (child) { - if (typeof child === 'string') { - $into.text(child); - } else if (child.tag === 'field') { - var widget = instanciate_widget(self.view.registry.get_object('frame'), - self.view, {tag: 'ueule', attrs: {}, children: [child] }); - widget.appendTo($into); - } else { - var $child = $(document.createElement(child.tag)) - .attr(child.attrs) - .appendTo($into); - self.render_children(child, $child); - } - }); - } -}); - - /** - * Registry of form widgets, called by :js:`openerp.web.FormView` + * Registry of form widgets, called by :js:`instance.web.FormView` */ -openerp.web.form.widgets = new openerp.web.Registry({ - 'frame' : 'openerp.web.form.WidgetFrame', - 'group' : 'openerp.web.form.WidgetGroup', - 'notebook' : 'openerp.web.form.WidgetNotebook', - 'notebookpage' : 'openerp.web.form.WidgetNotebookPage', - 'separator' : 'openerp.web.form.WidgetSeparator', - 'label' : 'openerp.web.form.WidgetLabel', - 'button' : 'openerp.web.form.WidgetButton', - 'char' : 'openerp.web.form.FieldChar', - 'id' : 'openerp.web.form.FieldID', - 'email' : 'openerp.web.form.FieldEmail', - 'url' : 'openerp.web.form.FieldUrl', - 'text' : 'openerp.web.form.FieldText', - 'date' : 'openerp.web.form.FieldDate', - 'datetime' : 'openerp.web.form.FieldDatetime', - 'selection' : 'openerp.web.form.FieldSelection', - 'many2one' : 'openerp.web.form.FieldMany2One', - 'many2many' : 'openerp.web.form.FieldMany2Many', - 'one2many' : 'openerp.web.form.FieldOne2Many', - 'one2many_list' : 'openerp.web.form.FieldOne2Many', - 'reference' : 'openerp.web.form.FieldReference', - 'boolean' : 'openerp.web.form.FieldBoolean', - 'float' : 'openerp.web.form.FieldFloat', - 'integer': 'openerp.web.form.FieldFloat', - 'float_time': 'openerp.web.form.FieldFloat', - 'progressbar': 'openerp.web.form.FieldProgressBar', - 'image': 'openerp.web.form.FieldBinaryImage', - 'binary': 'openerp.web.form.FieldBinaryFile', - 'statusbar': 'openerp.web.form.FieldStatus', - 'html': 'openerp.web.form.WidgetHtml' +instance.web.form.widgets = new instance.web.Registry({ + 'char' : 'instance.web.form.FieldChar', + 'id' : 'instance.web.form.FieldID', + 'email' : 'instance.web.form.FieldEmail', + 'url' : 'instance.web.form.FieldUrl', + 'text' : 'instance.web.form.FieldText', + 'date' : 'instance.web.form.FieldDate', + 'datetime' : 'instance.web.form.FieldDatetime', + 'selection' : 'instance.web.form.FieldSelection', + 'many2one' : 'instance.web.form.FieldMany2One', + 'many2many' : 'instance.web.form.FieldMany2Many', + 'one2many' : 'instance.web.form.FieldOne2Many', + 'one2many_list' : 'instance.web.form.FieldOne2Many', + 'reference' : 'instance.web.form.FieldReference', + 'boolean' : 'instance.web.form.FieldBoolean', + 'float' : 'instance.web.form.FieldFloat', + 'integer': 'instance.web.form.FieldFloat', + 'float_time': 'instance.web.form.FieldFloat', + 'progressbar': 'instance.web.form.FieldProgressBar', + 'image': 'instance.web.form.FieldBinaryImage', + 'binary': 'instance.web.form.FieldBinaryFile', + 'statusbar': 'instance.web.form.FieldStatus' }); -var instanciate_widget = function(claz, view, node, o1, o2) { - var widget = new (claz)(view, node, o1, o2); - widget.element_class = (['formview', view.view_id, widget.element_name, - view.widgets_counter++].join("_")).replace(/[^\r\n\f0-9A-Za-z_-]/g, "_"); - view.widgets[widget.element_class] = widget; - return widget; -} - +instance.web.form.tags = new instance.web.Registry({ + 'button' : 'instance.web.form.WidgetButton', +}); }; diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index e8fff450c6e..c29957cac2d 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -1,9 +1,9 @@ -openerp.web.list = function (openerp) { -var _t = openerp.web._t, - _lt = openerp.web._lt; -var QWeb = openerp.web.qweb; -openerp.web.views.add('list', 'openerp.web.ListView'); -openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# */ { +openerp.web.list = function (instance) { +var _t = instance.web._t, + _lt = instance.web._lt; +var QWeb = instance.web.qweb; +instance.web.views.add('list', 'instance.web.ListView'); +instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListView# */ { _template: 'ListView', display_name: _lt('List'), defaults: { @@ -33,11 +33,11 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * See constructor parameters and method documentations for information on * the default behaviors and possible options for the list view. * - * @constructs openerp.web.ListView - * @extends openerp.web.View + * @constructs instance.web.ListView + * @extends instance.web.View * * @param parent parent object - * @param {openerp.web.DataSet} dataset the dataset the view should work with + * @param {instance.web.DataSet} dataset the dataset the view should work with * @param {String} view_id the listview's identifier, if any * @param {Object} options A set of options used to configure the view * @param {Boolean} [options.selectable=true] determines whether view rows are selectable (e.g. via a checkbox) @@ -61,12 +61,12 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# this.records = new Collection(); - this.set_groups(new openerp.web.ListView.Groups(this)); + this.set_groups(new instance.web.ListView.Groups(this)); - if (this.dataset instanceof openerp.web.DataSetStatic) { - this.groups.datagroup = new openerp.web.StaticDataGroup(this.dataset); + if (this.dataset instanceof instance.web.DataSetStatic) { + this.groups.datagroup = new instance.web.StaticDataGroup(this.dataset); } else { - this.groups.datagroup = new openerp.web.DataGroup( + this.groups.datagroup = new instance.web.DataGroup( this, this.model, dataset.get_domain(), dataset.get_context(), @@ -104,7 +104,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# /** * Set a custom Group construct as the root of the List View. * - * @param {openerp.web.ListView.Groups} groups + * @param {instance.web.ListView.Groups} groups */ set_groups: function (groups) { var self = this; @@ -136,7 +136,6 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * @returns {$.Deferred} loading promise */ start: function() { - this._super(); this.$element.addClass('oe-listview'); return this.reload_view(null, null, true); }, @@ -208,6 +207,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# this.$element.html(QWeb.render(this._template, this)); // Head hook + // Selecting records this.$element.find('.all-record-selector').click(function(){ self.$element.find('.oe-record-selector input').prop('checked', self.$element.find('.all-record-selector').prop('checked') || false); @@ -216,70 +216,95 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# 'selected', [selection.ids, selection.records]); }); - this.$element.find('.oe-list-add') - .click(this.proxy('do_add_record')) - .attr('disabled', grouped && this.options.editable); - this.$element.find('.oe-list-delete') - .attr('disabled', true) - .click(this.proxy('do_delete_selected')); + // Sorting columns this.$element.find('thead').delegate('th.oe-sortable[data-id]', 'click', function (e) { e.stopPropagation(); - var $this = $(this); self.dataset.sort($this.data('id')); if ($this.find('span').length) { - $this.find('span').toggleClass( - 'ui-icon-triangle-1-s ui-icon-triangle-1-n'); + $this.find('span').toggleClass( 'ui-icon-triangle-1-s ui-icon-triangle-1-n'); } else { - $this.append('<span class="ui-icon ui-icon-triangle-1-n">') - .siblings('.oe-sortable').find('span').remove(); + $this.append('<span class="ui-icon ui-icon-triangle-1-n">') .siblings('.oe-sortable').find('span').remove(); } self.reload_content(); }); - this.$element.find('.oe-list-pager') - .delegate('button', 'click', function () { - var $this = $(this); - switch ($this.data('pager-action')) { - case 'first': - self.page = 0; break; - case 'last': - self.page = Math.floor( - self.dataset.size() / self.limit()); - break; - case 'next': - self.page += 1; break; - case 'previous': - self.page -= 1; break; - } - self.reload_content(); - }).find('.oe-pager-state') - .click(function (e) { - e.stopPropagation(); + // Add and delete + if (!this.$buttons) { + this.$buttons = $(QWeb.render("ListView.buttons", {'widget':self})); + if (this.options.$buttons) { + this.$buttons.appendTo(this.options.$buttons); + } else { + this.$element.find('.oe_list_buttons').replaceWith(this.$buttons); + } + this.$buttons.find('.oe_list_add') + .click(this.proxy('do_add_record')) + .prop('disabled', grouped && this.options.editable) + .end() + .find('.oe_list_delete') + .click(this.proxy('do_delete_selected')) + .prop('disabled', true); + } + + // Pager + if (!this.$pager) { + this.$pager = $(QWeb.render("ListView.pager", {'widget':self})); + if (this.options.$buttons) { + this.$pager.appendTo(this.options.$pager); + } else { + this.$element.find('.oe_list_pager').replaceWith(this.$pager); + } + + this.$pager + .on('click', 'a[data-pager-action]', function () { var $this = $(this); + var max_page = Math.floor(self.dataset.size() / self.limit()); + switch ($this.data('pager-action')) { + case 'first': + self.page = 0; break; + case 'last': + self.page = max_page - 1; + break; + case 'next': + self.page += 1; break; + case 'previous': + self.page -= 1; break; + } + if (self.page < 0) { + self.page = max_page; + } else if (self.page > max_page) { + self.page = 0; + } + self.reload_content(); + }).find('.oe-pager-state') + .click(function (e) { + e.stopPropagation(); + var $this = $(this); + + var $select = $('<select>') + .appendTo($this.empty()) + .click(function (e) {e.stopPropagation();}) + .append('<option value="80">80</option>' + + '<option value="100">100</option>' + + '<option value="200">200</option>' + + '<option value="500">500</option>' + + '<option value="NaN">' + _t("Unlimited") + '</option>') + .change(function () { + var val = parseInt($select.val(), 10); + self._limit = (isNaN(val) ? null : val); + self.page = 0; + self.reload_content(); + }) + .val(self._limit || 'NaN'); + }); + } - var $select = $('<select>') - .appendTo($this.empty()) - .click(function (e) {e.stopPropagation();}) - .append('<option value="80">80</option>' + - '<option value="100">100</option>' + - '<option value="200">200</option>' + - '<option value="500">500</option>' + - '<option value="NaN">' + _t("Unlimited") + '</option>') - .change(function () { - var val = parseInt($select.val(), 10); - self._limit = (isNaN(val) ? null : val); - self.page = 0; - self.reload_content(); - }) - .val(self._limit || 'NaN'); - }); - if (!this.sidebar && this.options.sidebar && this.options.sidebar_id) { - this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id); - this.sidebar.start(); + // Sidebar + if (!this.sidebar && this.options.$sidebar) { + this.sidebar = new instance.web.Sidebar(this); + this.sidebar.appendTo(this.options.$sidebar); this.sidebar.add_toolbar(this.fields_view.toolbar); - this.set_common_sidebar_sections(this.sidebar); } }, /** @@ -287,7 +312,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * * Horrifying side-effect: sets the dataset's data on this.dataset? * - * @param {openerp.web.DataSet} dataset + * @param {instance.web.DataSet} dataset */ configure_pager: function (dataset) { this.dataset.ids = dataset.ids; @@ -296,28 +321,11 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# this.dataset._length = dataset._length; } - var limit = this.limit(), - total = dataset.size(), - first = (this.page * limit), - last; - if (!limit || (total - first) < limit) { - last = total; - } else { - last = first + limit; - } - this.$element.find('span.oe-pager-state').empty().text(_.str.sprintf( - _t("[%(first_record)d to %(last_record)d] of %(records_count)d"), { - first_record: first + 1, - last_record: last, - records_count: total - })); + var page = this.page + 1, + total = Math.floor(dataset.size() / this.limit()) + 1; - this.$element - .find('button[data-pager-action=first], button[data-pager-action=previous]') - .attr('disabled', this.page === 0) - .end() - .find('button[data-pager-action=last], button[data-pager-action=next]') - .attr('disabled', last === total); + this.$pager.find('.oe-pager-state').text(isNaN(total) + ? '-' : _.str.sprintf('%d / %d', page, total)); }, /** * Sets up the listview's columns: merges view and fields data, move @@ -328,7 +336,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed */ setup_columns: function (fields, grouped) { - var domain_computer = openerp.web.form.compute_domain; + var domain_computer = instance.web.form.compute_domain; var noop = function () { return {}; }; var field_to_column = function (field) { @@ -397,7 +405,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * * The default implementation asks the list view's view manager to switch * to a different view (by calling - * :js:func:`~openerp.web.ViewManager.on_mode_switch`), using the + * :js:func:`~instance.web.ViewManager.on_mode_switch`), using the * provided record index (within the current list view's dataset). * * If the index is null, ``switch_to_record`` asks for the creation of a @@ -407,7 +415,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * @param {String} [view="page"] the view type to switch to */ select_record:function (index, view) { - view = view || index == null ? 'form' : 'page'; + view = view || index == null ? 'form' : 'form'; this.dataset.index = index; _.delay(_.bind(function () { this.do_switch_view(view); @@ -418,12 +426,24 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# if (this.sidebar) { this.sidebar.$element.show(); } + if (this.$buttons) { + this.$buttons.show(); + } + if (this.$pager) { + this.$pager.show(); + } }, do_hide: function () { - this._super(); if (this.sidebar) { this.sidebar.$element.hide(); } + if (this.$buttons) { + this.$buttons.hide(); + } + if (this.$pager) { + this.$pager.hide(); + } + this._super(); }, /** * Reloads the list view based on the current settings (dataset & al) @@ -444,7 +464,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# view_id: this.view_id, view_type: "tree", context: this.dataset.get_context(context), - toolbar: this.options.sidebar + toolbar: !!this.options.$sidebar }, callback); } }, @@ -479,7 +499,6 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# reload: function () { return this.reload_content(); }, - do_load_state: function(state, warm) { var reload = false; if (state.page && this.page !== state.page) { @@ -507,7 +526,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# */ do_search: function (domain, context, group_by) { this.page = 0; - this.groups.datagroup = new openerp.web.DataGroup( + this.groups.datagroup = new instance.web.DataGroup( this, this.model, domain, context, group_by); this.groups.datagroup.sort = this.dataset._sort; @@ -544,16 +563,20 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * @param {Array} records selected record values */ do_select: function (ids, records) { - this.$element.find('.oe-list-delete').attr('disabled', !ids.length); + this.$buttons.find('.oe_list_delete').attr('disabled', !ids.length); if (!ids.length) { this.dataset.index = 0; - if (this.sidebar) { this.sidebar.do_fold(); } + if (this.sidebar) { + this.sidebar.$element.hide(); + } this.compute_aggregates(); return; } this.dataset.index = _(this.dataset.ids).indexOf(ids[0]); - if (this.sidebar) { this.sidebar.do_unfold(); } + if (this.sidebar) { + this.sidebar.$element.show(); + } this.compute_aggregates(_(records).map(function (record) { return {count: 1, values: record}; @@ -575,7 +598,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# return; } - var c = new openerp.web.CompoundContext(); + var c = new instance.web.CompoundContext(); c.set_eval_context(_.extend({ active_id: id, active_ids: [id], @@ -592,7 +615,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * * @param {Number} index index of the record in the dataset * @param {Object} id identifier of the activated record - * @param {openerp.web.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups) + * @param {instance.web.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups) */ do_activate_record: function (index, id, dataset, view) { this.dataset.ids = dataset.ids; @@ -616,11 +639,11 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# /** * Computes the aggregates for the current list view, either on the * records provided or on the records of the internal - * :js:class:`~openerp.web.ListView.Group`, by calling - * :js:func:`~openerp.web.ListView.group.get_records`. + * :js:class:`~instance.web.ListView.Group`, by calling + * :js:func:`~instance.web.ListView.group.get_records`. * * Then displays the aggregates in the table through - * :js:method:`~openerp.web.ListView.display_aggregates`. + * :js:method:`~instance.web.ListView.display_aggregates`. * * @param {Array} [records] */ @@ -696,7 +719,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# } $footer_cells.filter(_.str.sprintf('[data-field=%s]', column.id)) - .html(openerp.web.format_cell(aggregation, column, { + .html(instance.web.format_cell(aggregation, column, { process_modifiers: false })); }); @@ -747,9 +770,20 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# .attr('colspan', this.previous_colspan); this.previous_colspan = null; } + }, + no_result: function () { + if (this.groups.group_by + || !this.options.action + || !this.options.action.help) { + return; + } + this.$element.children('table').replaceWith( + $('<div class="oe_listview_nocontent">') + .append($('<img>', { src: '/web/static/src/img/list_empty_arrow.png' })) + .append($('<div>').html(this.options.action.help))); } }); -openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.ListView.List# */{ +instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.ListView.List# */{ /** * List display for the ListView, handles basic DOM events and transforms * them in the relevant higher-level events, to which the list view (or @@ -774,10 +808,10 @@ openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.Lis * Triggered when a row of the table is clicked, provides the index (in * the rows array) and id of the selected record to the handle function. * - * @constructs openerp.web.ListView.List - * @extends openerp.web.Class + * @constructs instance.web.ListView.List + * @extends instance.web.Class * - * @param {Object} opts display options, identical to those of :js:class:`openerp.web.ListView` + * @param {Object} opts display options, identical to those of :js:class:`instance.web.ListView` */ init: function (group, opts) { var self = this; @@ -824,7 +858,7 @@ openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.Lis this.records.bind(event, callback); }, this); - this.$_element = $('<tbody class="ui-widget-content">') + this.$_element = $('<tbody>') .appendTo(document.body) .delegate('th.oe-record-selector', 'click', function (e) { e.stopPropagation(); @@ -892,7 +926,7 @@ openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.Lis // to get a correctly displayable value in the field var model = ref_match[1], id = parseInt(ref_match[2], 10); - new openerp.web.DataSet(this.view, model).name_get([id], function(names) { + new instance.web.DataSet(this.view, model).name_get([id], function(names) { if (!names.length) { return; } record.set(column.id, names[0][1]); }); @@ -907,14 +941,14 @@ openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.Lis // fetch the name, set it on the record (in the right field) // and let the various registered events handle refreshing the // row - new openerp.web.DataSet(this.view, column.relation) + new instance.web.DataSet(this.view, column.relation) .name_get([value], function (names) { if (!names.length) { return; } record.set(column.id, names[0]); }); } } - return openerp.web.format_cell(record.toForm().data, column, { + return instance.web.format_cell(record.toForm().data, column, { model: this.dataset.model, id: record.get('id') }); @@ -1068,19 +1102,19 @@ openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.Lis }); } }); -openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.ListView.Groups# */{ +instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.web.ListView.Groups# */{ passtrough_events: 'action deleted row_link', /** * Grouped display for the ListView. Handles basic DOM events and interacts - * with the :js:class:`~openerp.web.DataGroup` bound to it. + * with the :js:class:`~instance.web.DataGroup` bound to it. * * Provides events similar to those of - * :js:class:`~openerp.web.ListView.List` + * :js:class:`~instance.web.ListView.List` * - * @constructs openerp.web.ListView.Groups - * @extends openerp.web.Class + * @constructs instance.web.ListView.Groups + * @extends instance.web.Class * - * @param {openerp.web.ListView} view + * @param {instance.web.ListView} view * @param {Object} [options] * @param {Collection} [options.records] * @param {Object} [options.options] @@ -1191,7 +1225,7 @@ openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.L self.records.proxy(group.value).reset(); delete self.children[group.value]; } - var child = self.children[group.value] = new openerp.web.ListView.Groups(self.view, { + var child = self.children[group.value] = new instance.web.ListView.Groups(self.view, { records: self.records.proxy(group.value), options: self.options, columns: self.columns @@ -1227,7 +1261,7 @@ openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.L var group_column = _(self.columns).detect(function (column) { return column.id === group.grouped_on; }); try { - $group_column.html(openerp.web.format_cell( + $group_column.html(instance.web.format_cell( row_data, group_column, { value_if_empty: _t("Undefined"), process_modifiers: false @@ -1264,7 +1298,7 @@ openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.L var r = {}; r[column.id] = {value: group.aggregates[column.id]}; $('<td class="oe-number">') - .html(openerp.web.format_cell( + .html(instance.web.format_cell( r, column, {process_modifiers: false})) .appendTo($row); } else { @@ -1297,7 +1331,7 @@ openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.L }, render_dataset: function (dataset) { var self = this, - list = new openerp.web.ListView.List(this, { + list = new instance.web.ListView.List(this, { options: this.options, columns: this.columns, dataset: dataset, @@ -1344,6 +1378,9 @@ openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.L self.records.add(records, {silent: true}); list.render(); d.resolve(list); + if (_.isEmpty(records)) { + view.no_result(); + } });}); return d.promise(); }, @@ -1510,10 +1547,10 @@ var Events = /** @lends Events# */{ return this; } }; -var Record = openerp.web.Class.extend(/** @lends Record# */{ +var Record = instance.web.Class.extend(/** @lends Record# */{ /** * @constructs Record - * @extends openerp.web.Class + * @extends instance.web.Class * * @mixes Events * @param {Object} [data] @@ -1591,7 +1628,7 @@ var Record = openerp.web.Class.extend(/** @lends Record# */{ } }); Record.include(Events); -var Collection = openerp.web.Class.extend(/** @lends Collection# */{ +var Collection = instance.web.Class.extend(/** @lends Collection# */{ /** * Smarter collections, with events, very strongly inspired by Backbone's. * @@ -1599,7 +1636,7 @@ var Collection = openerp.web.Class.extend(/** @lends Collection# */{ * various serious * * @constructs Collection - * @extends openerp.web.Class + * @extends instance.web.Class * * @mixes Events * @param {Array} [records] records to initialize the collection with @@ -1631,19 +1668,19 @@ var Collection = openerp.web.Class.extend(/** @lends Collection# */{ var records = record instanceof Array ? record : [record]; for(var i=0, length=records.length; i<length; ++i) { - var instance = (records[i] instanceof Record) ? records[i] : new Record(records[i]); - instance.bind(null, this._onRecordEvent); - this._byId[instance.get('id')] = instance; + var instance_ = (records[i] instanceof Record) ? records[i] : new Record(records[i]); + instance_.bind(null, this._onRecordEvent); + this._byId[instance_.get('id')] = instance_; if (options.at == undefined) { - this.records.push(instance); + this.records.push(instance_); if (!options.silent) { - this.trigger('add', this, instance, this.records.length-1); + this.trigger('add', this, instance_, this.records.length-1); } } else { var insertion_index = options.at + i; - this.records.splice(insertion_index, 0, instance); + this.records.splice(insertion_index, 0, instance_); if (!options.silent) { - this.trigger('add', this, instance, insertion_index); + this.trigger('add', this, instance_, insertion_index); } } this.length++; @@ -1774,7 +1811,7 @@ var Collection = openerp.web.Class.extend(/** @lends Collection# */{ } }); Collection.include(Events); -openerp.web.list = { +instance.web.list = { Events: Events, Record: Record, Collection: Collection diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index b8699bcc006..ac19e3f341d 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -2,16 +2,16 @@ * handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out * @namespace */ -openerp.web.list_editable = function (openerp) { +openerp.web.list_editable = function (instance) { var KEY_RETURN = 13, KEY_ESCAPE = 27; - var QWeb = openerp.web.qweb; + var QWeb = instance.web.qweb; // editability status of list rows - openerp.web.ListView.prototype.defaults.editable = null; + instance.web.ListView.prototype.defaults.editable = null; // TODO: not sure second @lends on existing item is correct, to check - openerp.web.ListView.include(/** @lends openerp.web.ListView# */{ + instance.web.ListView.include(/** @lends instance.web.ListView# */{ init: function () { var self = this; this._super.apply(this, arguments); @@ -37,7 +37,7 @@ openerp.web.list_editable = function (openerp) { * * @param {Number} index index of the record in the dataset * @param {Object} id identifier of the record being edited - * @param {openerp.web.DataSet} dataset dataset in which the record is available + * @param {instance.web.DataSet} dataset dataset in which the record is available */ do_edit: function (index, id, dataset) { _.extend(this.dataset, dataset); @@ -93,8 +93,8 @@ openerp.web.list_editable = function (openerp) { } }); - openerp.web.ListView.Groups.include(/** @lends openerp.web.ListView.Groups# */{ - passtrough_events: openerp.web.ListView.Groups.prototype.passtrough_events + " edit saved", + instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{ + passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved", new_record: function () { // TODO: handle multiple children this.children[null].new_record(); @@ -113,7 +113,7 @@ openerp.web.list_editable = function (openerp) { } }); - openerp.web.ListView.List.include(/** @lends openerp.web.ListView.List# */{ + instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{ row_clicked: function (event) { if (!this.options.editable) { return this._super.apply(this, arguments); @@ -235,7 +235,8 @@ openerp.web.list_editable = function (openerp) { } self.edition = true; self.edition_id = record_id; - self.edition_form = new openerp.web.ListEditableFormView(self.view, self.dataset, false); + $new_row.addClass("oe_form_container"); + self.edition_form = new instance.web.ListEditableFormView(self.view, self.dataset, false); self.edition_form.$element = $new_row; self.edition_form.editable_list = self; // HO HO @@ -273,7 +274,7 @@ openerp.web.list_editable = function (openerp) { // insert after the source record var index = self.records.indexOf( self.records.get(source_record_id)) + 1; - record = new openerp.web.list.Record({id: id}); + record = new instance.web.list.Record({id: id}); self.records.add(record, {at: index}); self.dataset.ids.splice(index, 0, id); } @@ -366,52 +367,57 @@ openerp.web.list_editable = function (openerp) { }); } }); - if (!openerp.web.list) { - openerp.web.list = {}; - } - if (!openerp.web.list.form) { - openerp.web.list.form = {}; - } - openerp.web.list.form.WidgetFrame = openerp.web.form.WidgetFrame.extend({ - form_template: 'ListView.row.frame' - }); - var form_widgets = openerp.web.form.widgets; - openerp.web.list.form.widgets = form_widgets.extend({ - 'frame': 'openerp.web.list.form.WidgetFrame' - }); - // All form widgets inherit a problematic behavior from - // openerp.web.form.WidgetFrame: the cell itself is removed when invisible - // whether it's @invisible or @attrs[invisible]. In list view, only the - // former should completely remove the cell. We need to override update_dom - // on all widgets since we can't just hit on widget itself (I think) - var list_form_widgets = openerp.web.list.form.widgets; - _(form_widgets.map).each(function (widget_path, key) { - if (key === 'frame') { return; } - var new_path = 'openerp.web.list.form.' + key; - - openerp.web.list.form[key] = (form_widgets.get_object(key)).extend({ - update_dom: function () { - this.$element.children().css('visibility', ''); - if (this.modifiers.tree_invisible) { - var old_invisible = this.invisible; - this.invisible = true; - this._super(); - this.invisible = old_invisible; - } else if (this.invisible) { - this.$element.children().css('visibility', 'hidden'); - } else { - this._super(); - } - } - }); - list_form_widgets.add(key, new_path); - }); - openerp.web.ListEditableFormView = openerp.web.FormView.extend({ - form_template: 'ListView.row.form', + instance.web.ListEditableFormView = instance.web.FormView.extend({ init: function() { - this._super.apply(this, arguments); - this.registry = openerp.web.list.form.widgets; + this._super.apply(this, arguments); + this.rendering_engine = new instance.web.ListEditableRenderingEngine(this); + }, + renderElement: function() {} + }); + + instance.web.ListEditableRenderingEngine = instance.web.Class.extend({ + init: function(view) { + this.view = view; + }, + set_fields_view: function(fields_view) { + this.fvg = fields_view; + }, + set_tags_registry: function(tags_registry) { + this.tags_registry = tags_registry; + }, + set_fields_registry: function(fields_registry) { + this.fields_registry = fields_registry; + }, + render_to: function($element) { + var self = this; + + var xml = instance.web.json_node_to_xml(this.fvg.arch); + var $xml = $(xml); + + if (this.view.editable_list.options.selectable) + $("<td>").appendTo($element); + if (this.view.editable_list.options.isClarkGable) + $("<td>").appendTo($element); + + $xml.children().each(function(i, el) { + var modifiers = JSON.parse($(el).attr("modifiers") || "{}"); + var $td = $("<td>"); + if (modifiers.tree_invisible === true) + $td.hide(); + var tag_name = el.tagName.toLowerCase(); + var key = tag_name; + if (tag_name === "field") { + var name = $(el).attr("name"); + key = $(el).attr('widget') || self.fvg.fields[name].type; + } + var obj = self.view.fields_registry.get_object(key); + var w = new (obj)(self.view, instance.web.xml_to_json(el)); + self.view.register_field(w, $(el).attr("name")); + w.appendTo($td); + $td.appendTo($element); + }); + $("<td><button class='oe-edit-row-save' type='button'></button></td>").appendTo($element); }, }); }; diff --git a/addons/web/static/src/js/view_page.js b/addons/web/static/src/js/view_page.js deleted file mode 100644 index 8949169838a..00000000000 --- a/addons/web/static/src/js/view_page.js +++ /dev/null @@ -1,288 +0,0 @@ -openerp.web.page = function (openerp) { - var _t = openerp.web._t, - _lt = openerp.web._lt; - - openerp.web.views.add('page', 'openerp.web.PageView'); - openerp.web.PageView = openerp.web.FormView.extend({ - form_template: "PageView", - display_name: _lt('Page'), - init: function () { - this._super.apply(this, arguments); - this.registry = openerp.web.page.readonly; - }, - reload: function () { - if (this.dataset.index == null) { - this.do_prev_view(); - return $.Deferred().reject().promise(); - } - return this._super(); - }, - on_loaded: function(data) { - this._super(data); - this.$form_header.find('button.oe_form_button_edit').click(this.on_button_edit); - this.$form_header.find('button.oe_form_button_create').click(this.on_button_create); - this.$form_header.find('button.oe_form_button_duplicate').click(this.on_button_duplicate); - this.$form_header.find('button.oe_form_button_delete').click(this.on_button_delete); - }, - on_button_edit: function() { - return this.do_switch_view('form'); - }, - on_button_create: function() { - this.dataset.index = null; - return this.do_switch_view('form'); - }, - on_button_duplicate: function() { - var self = this; - var def = $.Deferred(); - $.when(this.has_been_loaded).then(function() { - self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) { - return self.on_created({ result : new_id }); - }).then(function() { - return self.do_switch_view('form'); - }).then(function() { - def.resolve(); - }); - }); - return def.promise(); - }, - on_button_delete: function() { - var self = this; - var def = $.Deferred(); - $.when(this.has_been_loaded).then(function() { - if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) { - self.dataset.unlink([self.datarecord.id]).then(function() { - self.on_pager_action('next'); - def.resolve(); - }); - } else { - $.async_when().then(function () { - def.reject(); - }) - } - }); - return def.promise(); - } - }); - - /** @namespace */ - openerp.web.page = {}; - - openerp.web.page.WidgetFrameReadonly = openerp.web.form.WidgetFrame.extend({ - form_template: 'WidgetFrame.readonly' - }); - openerp.web.page.FieldReadonly = openerp.web.form.Field.extend({ - - }); - openerp.web.page.FieldCharReadonly = openerp.web.page.FieldReadonly.extend({ - form_template: 'FieldChar.readonly', - init: function(view, node) { - this._super(view, node); - this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1'; - }, - set_value: function (value) { - this._super.apply(this, arguments); - var show_value = openerp.web.format_value(value, this, ''); - if (this.password) { - show_value = new Array(show_value.length + 1).join('*'); - } - this.$element.find('div').text(show_value); - return show_value; - } - }); - openerp.web.page.FieldFloatReadonly = openerp.web.page.FieldCharReadonly.extend({ - init: function (view, node) { - this._super(view, node); - if (node.attrs.digits) { - this.digits = py.eval(node.attrs.digits); - } else { - this.digits = view.fields_view.fields[node.attrs.name].digits; - } - } - }); - openerp.web.page.FieldURIReadonly = openerp.web.page.FieldCharReadonly.extend({ - form_template: 'FieldURI.readonly', - scheme: null, - format_value: function (value) { - return value; - }, - set_value: function (value) { - if (!value) { - this.$element.find('a').text('').attr('href', '#'); - return; - } - this.$element.find('a') - .attr('href', this.scheme + ':' + value) - .text(this.format_value(value)); - } - }); - openerp.web.page.FieldEmailReadonly = openerp.web.page.FieldURIReadonly.extend({ - scheme: 'mailto' - }); - openerp.web.page.FieldUrlReadonly = openerp.web.page.FieldURIReadonly.extend({ - set_value: function (value) { - if (!value) { - this.$element.find('a').text('').attr('href', '#'); - return; - } - var s = /(\w+):(.+)/.exec(value); - if (!s) { - value = "http://" + value; - } - this.$element.find('a').attr('href', value).text(value); - } - }); - openerp.web.page.FieldBooleanReadonly = openerp.web.form.FieldBoolean.extend({ - update_dom: function() { - this._super.apply(this, arguments); - this.$element.find('input').prop('disabled', true); - } - }); - openerp.web.page.FieldSelectionReadonly = openerp.web.page.FieldReadonly.extend({ - form_template: 'FieldChar.readonly', - init: function(view, node) { - // lifted straight from r/w version - var self = this; - this._super(view, node); - this.values = _.clone(this.field.selection); - _.each(this.values, function(v, i) { - if (v[0] === false && v[1] === '') { - self.values.splice(i, 1); - } - }); - this.values.unshift([false, '']); - }, - set_value: function (value) { - value = value === null ? false : value; - value = value instanceof Array ? value[0] : value; - var option = _(this.values) - .detect(function (record) { return record[0] === value; }); - this._super(value); - this.$element.find('div').text(option ? option[1] : this.values[0][1]); - } - }); - openerp.web.page.FieldMany2OneReadonly = openerp.web.page.FieldURIReadonly.extend({ - set_value: function (value) { - value = value || null; - this.invalid = false; - var self = this; - this.value = value; - self.update_dom(); - self.on_value_changed(); - var real_set_value = function(rval) { - self.value = rval; - self.$element.find('a') - .unbind('click') - .text(rval ? rval[1] : '') - .click(function () { - self.do_action({ - type: 'ir.actions.act_window', - res_model: self.field.relation, - res_id: self.value[0], - context: self.build_context(), - views: [[false, 'page'], [false, 'form']], - target: 'current' - }); - return false; - }); - }; - if (value && !(value instanceof Array)) { - new openerp.web.DataSet( - this, this.field.relation, self.build_context()) - .name_get([value], function(data) { - real_set_value(data[0]); - }); - } else { - $.async_when().then(function() {real_set_value(value);}); - } - }, - get_value: function() { - if (!this.value) { - return false; - } else if (this.value instanceof Array) { - return this.value[0]; - } else { - return this.value; - } - } - }); - openerp.web.page.FieldReferenceReadonly = openerp.web.page.FieldMany2OneReadonly.extend({ - set_value: function (value) { - if (!value) { - return this._super(null); - } - var reference = value.split(','); - this.field.relation = reference[0]; - var id = parseInt(reference[1], 10); - return this._super(id); - }, - get_value: function () { - if (!this.value) { - return null; - } - var id; - if (typeof this.value === 'number') { - // name_get has not run yet - id = this.value; - } else { - id = this.value[0]; - } - return _.str.sprintf('%s,%d', this.field.relation, id); - } - }); - - openerp.web.page.FieldMany2ManyReadonly = openerp.web.form.FieldMany2Many.extend({ - force_readonly: true - }); - openerp.web.page.FieldOne2ManyReadonly = openerp.web.form.FieldOne2Many.extend({ - force_readonly: true - }); - openerp.web.page.FieldBinaryImageReaonly = openerp.web.form.FieldBinaryImage.extend({ - update_dom: function() { - this._super.apply(this, arguments); - this.$element.find('.oe-binary').hide(); - } - }); - openerp.web.page.FieldBinaryFileReadonly = openerp.web.form.FieldBinary.extend({ - form_template: 'FieldURI.readonly', - start: function() { - this._super.apply(this, arguments); - var self = this; - this.$element.find('a').click(function() { - if (self.value) { - self.on_save_as(); - } - return false; - }); - }, - set_value: function(value) { - this._super.apply(this, arguments); - this.$element.find('a').show(!!value); - if (value) { - var show_value = _t("Download") + " " + (this.view.datarecord[this.node.attrs.filename] || ''); - this.$element.find('a').text(show_value); - } - } - }); - openerp.web.page.readonly = openerp.web.form.widgets.extend({ - 'frame': 'openerp.web.page.WidgetFrameReadonly', - 'char': 'openerp.web.page.FieldCharReadonly', - 'id': 'openerp.web.page.FieldCharReadonly', - 'email': 'openerp.web.page.FieldEmailReadonly', - 'url': 'openerp.web.page.FieldUrlReadonly', - 'text': 'openerp.web.page.FieldCharReadonly', - 'date': 'openerp.web.page.FieldCharReadonly', - 'datetime': 'openerp.web.page.FieldCharReadonly', - 'selection' : 'openerp.web.page.FieldSelectionReadonly', - 'many2one': 'openerp.web.page.FieldMany2OneReadonly', - 'many2many' : 'openerp.web.page.FieldMany2ManyReadonly', - 'one2many' : 'openerp.web.page.FieldOne2ManyReadonly', - 'one2many_list' : 'openerp.web.page.FieldOne2ManyReadonly', - 'reference': 'openerp.web.page.FieldReferenceReadonly', - 'boolean': 'openerp.web.page.FieldBooleanReadonly', - 'float': 'openerp.web.page.FieldFloatReadonly', - 'integer': 'openerp.web.page.FieldCharReadonly', - 'float_time': 'openerp.web.page.FieldCharReadonly', - 'binary': 'openerp.web.page.FieldBinaryFileReadonly', - 'image': 'openerp.web.page.FieldBinaryImageReaonly' - }); -}; diff --git a/addons/web/static/src/js/view_tree.js b/addons/web/static/src/js/view_tree.js index 6039e4a0822..9e938fe293a 100644 --- a/addons/web/static/src/js/view_tree.js +++ b/addons/web/static/src/js/view_tree.js @@ -2,12 +2,12 @@ * OpenERP web library *---------------------------------------------------------*/ -openerp.web.view_tree = function(openerp) { -var QWeb = openerp.web.qweb, - _lt = openerp.web._lt; +openerp.web.view_tree = function(instance) { +var QWeb = instance.web.qweb, + _lt = instance.web._lt; -openerp.web.views.add('tree', 'openerp.web.TreeView'); -openerp.web.TreeView = openerp.web.View.extend(/** @lends openerp.web.TreeView# */{ +instance.web.views.add('tree', 'instance.web.TreeView'); +instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeView# */{ display_name: _lt('Tree'), /** * Indicates that this view is not searchable, and thus that no search @@ -17,8 +17,8 @@ openerp.web.TreeView = openerp.web.View.extend(/** @lends openerp.web.TreeView# /** * Genuine tree view (the one displayed as a tree, not the list) * - * @constructs openerp.web.TreeView - * @extends openerp.web.View + * @constructs instance.web.TreeView + * @extends instance.web.View * * @param parent * @param dataset @@ -39,7 +39,6 @@ openerp.web.TreeView = openerp.web.View.extend(/** @lends openerp.web.TreeView# }, start: function () { - this._super(); return this.rpc("/web/treeview/load", { model: this.model, view_id: this.view_id, @@ -204,7 +203,7 @@ openerp.web.TreeView = openerp.web.View.extend(/** @lends openerp.web.TreeView# 'fields_view': self.fields_view.arch.children, 'fields': self.fields, 'level': $curr_node.data('level') || 0, - 'render': openerp.web.format_value, + 'render': instance.web.format_value, 'color_for': self.color_for }); @@ -227,12 +226,12 @@ openerp.web.TreeView = openerp.web.View.extend(/** @lends openerp.web.TreeView# return this.rpc('/web/treeview/action', { id: id, model: this.dataset.model, - context: new openerp.web.CompoundContext( + context: new instance.web.CompoundContext( this.dataset.get_context(), local_context) }).pipe(function (actions) { if (!actions.length) { return; } var action = actions[0][2]; - var c = new openerp.web.CompoundContext(local_context); + var c = new instance.web.CompoundContext(local_context); if (action.context) { c.add(action.context); } diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index 43ecc8444fc..36e3c2b802b 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -2,21 +2,11 @@ * OpenERP web library *---------------------------------------------------------*/ -openerp.web.views = function(session) { -var QWeb = session.web.qweb, - _t = session.web._t; +openerp.web.views = function(instance) { +var QWeb = instance.web.qweb, + _t = instance.web._t; -/** - * Registry for all the client actions key: tag value: widget - */ -session.web.client_actions = new session.web.Registry(); - -/** - * Registry for all the main views - */ -session.web.views = new session.web.Registry(); - -session.web.ActionManager = session.web.OldWidget.extend({ +instance.web.ActionManager = instance.web.Widget.extend({ init: function(parent) { this._super(parent); this.inner_action = null; @@ -25,9 +15,6 @@ session.web.ActionManager = session.web.OldWidget.extend({ this.dialog_viewmanager = null; this.client_widget = null; }, - render: function() { - return '<div id="' + this.element_id + '" style="height: 100%;"></div>'; - }, dialog_stop: function () { if (this.dialog) { this.dialog_viewmanager.destroy(); @@ -65,8 +52,8 @@ session.web.ActionManager = session.web.OldWidget.extend({ if (run_action) { this.null_action(); action_loaded = this.do_action(state.action_id); - session.webclient.menu.has_been_loaded.then(function() { - session.webclient.menu.open_action(state.action_id); + instance.webclient.menu.has_been_loaded.then(function() { + instance.webclient.menu.open_action(state.action_id); }); } } else if (state.model && state.id) { @@ -76,7 +63,7 @@ session.web.ActionManager = session.web.OldWidget.extend({ res_model: state.model, res_id: state.id, type: 'ir.actions.act_window', - views: [[false, 'page'], [false, 'form']] + views: [[false, 'form']] }; action_loaded = this.do_action(action); } else if (state.sa) { @@ -112,12 +99,13 @@ session.web.ActionManager = session.web.OldWidget.extend({ } var type = action.type.replace(/\./g,'_'); var popup = action.target === 'new'; + var inline = action.target === 'inline'; action.flags = _.extend({ - views_switcher : !popup, - search_view : !popup, - action_buttons : !popup, - sidebar : !popup, - pager : !popup, + views_switcher : !popup && !inline, + search_view : !popup && !inline, + action_buttons : !popup && !inline, + sidebar : !popup && !inline, + pager : !popup && !inline, display_title : !popup }, action.flags || {}); if (!(type in this)) { @@ -136,31 +124,31 @@ session.web.ActionManager = session.web.OldWidget.extend({ .contains(action.res_model)) { var old_close = on_close; on_close = function () { - session.webclient.do_reload().then(old_close); + instance.webclient.do_reload().then(old_close); }; } if (action.target === 'new') { if (this.dialog == null) { - this.dialog = new session.web.Dialog(this, { width: '80%' }); + this.dialog = new instance.web.Dialog(this, { width: '80%' }); if(on_close) this.dialog.on_close.add(on_close); } else { this.dialog_viewmanager.destroy(); } this.dialog.dialog_title = action.name; - this.dialog_viewmanager = new session.web.ViewManagerAction(this, action); + this.dialog_viewmanager = new instance.web.ViewManagerAction(this, action); this.dialog_viewmanager.appendTo(this.dialog.$element); this.dialog.open(); } else { if(action.menu_id) { return this.getParent().do_action(action, function () { - session.webclient.menu.open_menu(action.menu_id); + instance.webclient.menu.open_menu(action.menu_id); }); } this.dialog_stop(); this.content_stop(); this.inner_action = action; - this.inner_viewmanager = new session.web.ViewManagerAction(this, action); + this.inner_viewmanager = new instance.web.ViewManagerAction(this, action); this.inner_viewmanager.appendTo(this.$element); } }, @@ -182,7 +170,7 @@ session.web.ActionManager = session.web.OldWidget.extend({ ir_actions_client: function (action) { this.content_stop(); this.dialog_stop(); - var ClientWidget = session.web.client_actions.get_object(action.tag); + var ClientWidget = instance.web.client_actions.get_object(action.tag); (this.client_widget = new ClientWidget(this, action.params)).appendTo(this.$element); }, ir_actions_report_xml: function(action, on_closed) { @@ -204,7 +192,7 @@ session.web.ActionManager = session.web.OldWidget.extend({ } self.dialog_stop(); }, - error: session.webclient.crashmanager.on_rpc_error + error: instance.webclient.crashmanager.on_rpc_error }) }); }, @@ -216,16 +204,8 @@ session.web.ActionManager = session.web.OldWidget.extend({ } }); -session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.ViewManager# */{ +instance.web.ViewManager = instance.web.Widget.extend({ template: "ViewManager", - /** - * @constructs session.web.ViewManager - * @extends session.web.OldWidget - * - * @param parent - * @param dataset - * @param views - */ init: function(parent, dataset, views, flags) { this._super(parent); this.model = dataset ? dataset.model : undefined; @@ -234,7 +214,7 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V this.active_view = null; this.views_src = _.map(views, function(x) { if (x instanceof Array) { - var View = session.web.views.get_object(x[1], true); + var View = instance.web.views.get_object(x[1], true); return { view_id: x[0], view_type: x[1], @@ -246,22 +226,16 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V }); this.views = {}; this.flags = flags || {}; - this.registry = session.web.views; + this.registry = instance.web.views; this.views_history = []; }, - render: function() { - return session.web.qweb.render(this.template, { - self: this, - prefix: this.element_id, - views: this.views_src}); - }, /** * @returns {jQuery.Deferred} initial view loading promise */ start: function() { this._super(); var self = this; - this.$element.find('.oe_vm_switch button').click(function() { + this.$element.find('.oe_view_manager_switch a').click(function() { self.on_mode_switch($(this).data('view-type')); }); var views_ids = {}; @@ -270,7 +244,9 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V deferred : $.Deferred(), controller : null, options : _.extend({ - sidebar_id : self.element_id + '_sidebar_' + view.view_type, + $buttons : self.$element.find('.oe_view_manager_buttons'), + $sidebar : self.$element.find('.oe_view_manager_sidebar'), + $pager : self.$element.find('.oe_view_manager_pager'), action : self.action, action_views_ids : views_ids }, self.flags, self.flags[view.view_type] || {}, view.options || {}) @@ -278,7 +254,7 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V views_ids[view.view_type] = view.view_id; }); if (this.flags.views_switcher === false) { - this.$element.find('.oe_vm_switch').hide(); + this.$element.find('.oe_view_manager_switch').hide(); } // If no default view defined, switch to the first one in sequence var default_view = this.flags.default_view || this.views_src[0].view_type; @@ -292,9 +268,9 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V * @returns {jQuery.Deferred} new view loading promise */ on_mode_switch: function(view_type, no_store) { - var self = this, - view = this.views[view_type], - view_promise; + var self = this; + var view = this.views[view_type]; + var view_promise; if(!view) return $.Deferred().reject(); @@ -306,13 +282,22 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V if (!view.controller) { // Lazy loading of views var controllerclass = this.registry.get_object(view_type); - var controller = new controllerclass(this, this.dataset, view.view_id, view.options); + var options = _.clone(view.options); + if (view_type === "form" && this.action) { + switch (this.action.target) { + case 'new': + case 'inline': + options.initial_mode = 'edit'; + break; + } + } + var controller = new controllerclass(this, this.dataset, view.view_id, options); if (view.embedded_view) { controller.set_embedded_view(view.embedded_view); } controller.do_switch_view.add_last(this.on_mode_switch); controller.do_prev_view.add_last(this.on_prev_view); - var container = $("#" + this.element_id + '_view_' + view_type); + var container = this.$element.find(".oe_view_manager_view_" + view_type); view_promise = controller.appendTo(container); this.views[view_type].controller = controller; this.views[view_type].deferred.resolve(view_type); @@ -335,9 +320,10 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V } this.$element - .find('.oe_vm_switch button').removeAttr('disabled') - .filter('[data-view-type="' + view_type + '"]') - .attr('disabled', true); + .find('.oe_view_manager_switch a').parent().removeClass('active') + this.$element + .find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]') + .parent().addClass('active'); $.when(view_promise).then(function () { _.each(_.keys(self.views), function(view_name) { @@ -367,15 +353,16 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V * @returns {$.Deferred} switching end signal */ on_prev_view: function (options) { + options = options || {}; var current_view = this.views_history.pop(); var previous_view = this.views_history[this.views_history.length - 1] || options['default']; if (options.created && current_view === 'form' && previous_view === 'list') { // APR special case: "If creation mode from list (and only from a list), // after saving, go to page view (don't come back in list)" - return this.on_mode_switch('page'); + return this.on_mode_switch('form'); } else if (options.created && !previous_view && this.action && this.action.flags.default_view === 'form') { // APR special case: "If creation from dashboard, we have no previous view - return this.on_mode_switch('page'); + return this.on_mode_switch('form'); } return this.on_mode_switch(previous_view, true); }, @@ -390,12 +377,10 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V if (this.searchview) { this.searchview.destroy(); } - this.searchview = new session.web.SearchView( - this, this.dataset, - view_id, search_defaults, this.flags.search_view === false); + this.searchview = new instance.web.SearchView(this, this.dataset, view_id, search_defaults, this.flags.search_view === false); this.searchview.on_search.add(this.do_searchview_search); - return this.searchview.appendTo($("#" + this.element_id + "_search")); + return this.searchview.appendTo(this.$element.find(".oe_view_manager_view_search")); }, do_searchview_search: function(domains, contexts, groupbys) { var self = this, @@ -451,13 +436,13 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V } }); -session.web.ViewManagerAction = session.web.ViewManager.extend(/** @lends oepnerp.web.ViewManagerAction# */{ +instance.web.ViewManagerAction = instance.web.ViewManager.extend({ template:"ViewManagerAction", /** - * @constructs session.web.ViewManagerAction - * @extends session.web.ViewManager + * @constructs instance.web.ViewManagerAction + * @extends instance.web.ViewManager * - * @param {session.web.ActionManager} parent parent object/widget + * @param {instance.web.ActionManager} parent parent object/widget * @param {Object} action descriptor for the action this viewmanager needs to manage its views. */ init: function(parent, action) { @@ -482,7 +467,7 @@ session.web.ViewManagerAction = session.web.ViewManager.extend(/** @lends oepner this._super(parent, null, action.views, flags); this.session = parent.session; this.action = action; - var dataset = new session.web.DataSetSearch(this, action.res_model, action.context, action.domain); + var dataset = new instance.web.DataSetSearch(this, action.res_model, action.context, action.domain); if (action.res_id) { dataset.ids.push(action.res_id); dataset.index = 0; @@ -523,7 +508,7 @@ session.web.ViewManagerAction = session.web.ViewManager.extend(/** @lends oepner this.$element.find('.oe_debug_view').change(this.on_debug_changed); if (this.action.help && !this.flags.low_profile) { - var Users = new session.web.DataSet(self, 'res.users'), + var Users = new instance.web.DataSet(self, 'res.users'), $tips = this.$element.find('.oe_view_manager_menu_tips'); $tips.delegate('blockquote button', 'click', function() { var $this = $(this); @@ -547,17 +532,6 @@ session.web.ViewManagerAction = session.web.ViewManager.extend(/** @lends oepner } } - var $res_logs = this.$element.find('.oe-view-manager-logs:first'); - $res_logs.delegate('a.oe-more-logs', 'click', function () { - $res_logs.removeClass('oe-folded'); - return false; - }).delegate('a.oe-remove-everything', 'click', function () { - $res_logs.removeClass('oe-has-more').find('ul').empty(); - $res_logs.css('display','none'); - return false; - }); - $res_logs.css('display','none'); - return manager_ready; }, on_debug_changed: function (evt) { @@ -568,23 +542,26 @@ session.web.ViewManagerAction = session.web.ViewManager.extend(/** @lends oepner current_view = this.views[this.active_view].controller; switch (val) { case 'fvg': - var dialog = new session.web.Dialog(this, { title: _t("Fields View Get"), width: '95%' }).open(); - $('<pre>').text(session.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$element); + var dialog = new instance.web.Dialog(this, { title: _t("Fields View Get"), width: '95%' }).open(); + $('<pre>').text(instance.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$element); break; case 'perm_read': var ids = current_view.get_selected_ids(); if (ids.length === 1) { this.dataset.call('perm_read', [ids]).then(function(result) { - var dialog = new session.web.Dialog(this, { + var dialog = new instance.web.Dialog(this, { title: _.str.sprintf(_t("View Log (%s)"), self.dataset.model), width: 400 }, QWeb.render('ViewManagerDebugViewLog', { perm : result[0], - format : session.web.format_value + format : instance.web.format_value })).open(); }); } break; + case 'toggle_layout_outline': + current_view.rendering_engine.toggle_layout_debugging(); + break; case 'fields': this.dataset.call_and_eval( 'fields_get', [false, {}], null, 1).then(function (fields) { @@ -602,7 +579,7 @@ session.web.ViewManagerAction = session.web.ViewManager.extend(/** @lends oepner .append($('<dd style="white-space: pre-wrap;">').text(def)); }); }); - new session.web.Dialog(self, { + new instance.web.Dialog(self, { title: _.str.sprintf(_t("Model %s fields"), self.dataset.model), width: '95%'}, $root).open(); @@ -610,7 +587,7 @@ session.web.ViewManagerAction = session.web.ViewManager.extend(/** @lends oepner break; case 'manage_views': if (current_view.fields_view && current_view.fields_view.arch) { - var view_editor = new session.web.ViewEditor(current_view, current_view.$element, this.dataset, current_view.fields_view.arch); + var view_editor = new instance.web.ViewEditor(current_view, current_view.$element, this.dataset, current_view.fields_view.arch); view_editor.start(); } else { this.do_warn(_t("Manage Views"), @@ -661,8 +638,6 @@ session.web.ViewManagerAction = session.web.ViewManager.extend(/** @lends oepner return $.when(this._super(view_type, no_store)).then(function () { self.shortcut_check(self.views[view_type]); - self.$element.find('.oe-view-manager-logs:first').addClass('oe-folded').removeClass('oe-has-more').css('display','none').find('ul').empty(); - var controller = self.views[self.active_view].controller, fvg = controller.fields_view, view_id = (fvg && fvg.view_id) || '--'; @@ -744,127 +719,88 @@ session.web.ViewManagerAction = session.web.ViewManager.extend(/** @lends oepner } }); }, - /** - * Intercept do_action resolution from children views - */ - on_action_executed: function () { - return new session.web.DataSet(this, 'res.log') - .call('get', [], this.do_display_log); - }, - /** - * @param {Array<Object>} log_records - */ - do_display_log: function (log_records) { - var self = this; - var cutoff = 3; - var $logs = this.$element.find('.oe-view-manager-logs:first').addClass('oe-folded').css('display', 'block'); - var $logs_list = $logs.find('ul').empty(); - $logs.toggleClass('oe-has-more', log_records.length > cutoff); - _(log_records.reverse()).each(function (record) { - var context = {}; - if (record.context) { - try { context = py.eval(record.context); } - catch (e) { /* TODO: what do I do now? */ } - } - $(_.str.sprintf('<li><a href="#">%s</a></li>', record.name)) - .appendTo($logs_list) - .delegate('a', 'click', function () { - self.do_action({ - type: 'ir.actions.act_window', - res_model: record.res_model, - res_id: record.res_id, - // TODO: need to have an evaluated context here somehow - context: context, - views: [[context.view_id || false, 'form']] - }); - return false; - }); - }); - }, display_title: function () { return this.action.name; } }); -session.web.Sidebar = session.web.OldWidget.extend({ - init: function(parent, element_id) { - this._super(parent, element_id); - this.items = {}; - this.sections = {}; +instance.web.Sidebar = instance.web.Widget.extend({ + init: function(parent) { + this._super(parent); + var view = this.getParent(); + this.sections = [ + { 'name' : 'print', 'label' : _t('Print'), }, + { 'name' : 'files', 'label' : _t('Attachement'), }, + { 'name' : 'other', 'label' : _t('More'), } + ]; + this.items = { + 'print' : [], + 'files' : [], + 'other' : [ + { label: _t("Import"), callback: view.on_sidebar_import }, + { label: _t("Export"), callback: view.on_sidebar_export } + ] + } + if (this.session.uid === 1) { + var item = { label: _t("Translate"), callback: view.on_sidebar_translate, title: _t("Technical translation") }; + this.items.other.push(item); + } }, start: function() { - this._super(this); var self = this; - this.$element.html(session.web.qweb.render('Sidebar')); - this.$element.find(".toggle-sidebar").click(function(e) { - self.do_toggle(); + this._super(this); + this.redraw(); + this.$element.on('click','.oe_dropdown_toggle',function(event) { + $(this).parent().find('ul').toggle(); + return false; }); - }, - add_default_sections: function() { - var self = this, - view = this.getParent(), - view_manager = view.getParent(), - action = view_manager.action; - if (this.session.uid === 1) { - this.add_section(_t('Customize'), 'customize'); - this.add_items('customize', [{ - label: _t("Translate"), - callback: view.on_sidebar_translate, - title: _t("Technical translation") - }]); - } - - this.add_section(_t('Other Options'), 'other'); - this.add_items('other', [ - { - label: _t("Import"), - callback: view.on_sidebar_import - }, { - label: _t("Export"), - callback: view.on_sidebar_export + this.$element.on('click','.oe_dropdown_menu li a', function(event) { + var section = $(this).data('section'); + var index = $(this).data('index'); + $(this).closest('ul').hide(); + var item = self.items[section][index]; + if (item.callback) { + item.callback.apply(self, [item]); + } else if (item.action) { + self.on_item_action_clicked(item); + } else if (item.url) { + return true; } - ]); + return false; + }); + //this.$div.html(QWeb.render('FormView.sidebar.attachments', this)); + //this.$element.find('.oe-binary-file').change(this.on_attachment_changed); + //this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete); + }, + redraw: function() { + var self = this; + self.$element.html(QWeb.render('Sidebar', {widget: self})); + this.$element.find('ul').hide(); + }, + add_section: function() { + var self = this; }, - add_toolbar: function(toolbar) { var self = this; - _.each([['print', _t("Reports")], ['action', _t("Actions")], ['relate', _t("Links")]], function(type) { - var items = toolbar[type[0]]; - if (items.length) { + _.each(['print','action','relate'], function(type) { + var items = toolbar[type]; + if (items) { for (var i = 0; i < items.length; i++) { items[i] = { label: items[i]['name'], action: items[i], - classname: 'oe_sidebar_' + type[0] + classname: 'oe_sidebar_' + type } } - self.add_section(type[1], type[0]); - self.add_items(type[0], items); + self.add_items(type=='print' ? 'print' : 'other', items); } }); }, - - add_section: function(name, code) { - if(!code) code = _.str.underscored(name); - var $section = this.sections[code]; - - if(!$section) { - var section_id = _.uniqueId(this.element_id + '_section_' + code + '_'); - $section = $(session.web.qweb.render("Sidebar.section", { - section_id: section_id, - name: name, - classname: 'oe_sidebar_' + code - })); - $section.appendTo(this.$element.find('div.sidebar-actions')); - this.sections[code] = $section; - } - return $section; - }, /** * For each item added to the section: * * ``label`` - * will be used as the item's name in the sidebar + * will be used as the item's name in the sidebar, can be html * * ``action`` * descriptor for the action which will be executed, ``action`` and @@ -886,34 +822,10 @@ session.web.Sidebar = session.web.OldWidget.extend({ * @param {Array<{label, action | callback[, classname][, title]}>} items */ add_items: function(section_code, items) { - var self = this, - $section = this.add_section(_.str.titleize(section_code.replace('_', ' ')), section_code), - section_id = $section.attr('id'); - + var self = this; if (items) { - for (var i = 0; i < items.length; i++) { - items[i].element_id = _.uniqueId(section_id + '_item_'); - this.items[items[i].element_id] = items[i]; - } - - var $items = $(session.web.qweb.render("Sidebar.section.items", {items: items})); - - $items.find('a.oe_sidebar_action_a').click(function() { - var item = self.items[$(this).attr('id')]; - if (item.callback) { - item.callback.apply(self, [item]); - } - if (item.action) { - self.on_item_action_clicked(item); - } - return false; - }); - - var $ul = $section.find('ul'); - if(!$ul.length) { - $ul = $('<ul/>').appendTo($section); - } - $items.appendTo($ul); + this.items[section_code].push.apply(this.items[section_code],items); + this.redraw(); } }, on_item_action_clicked: function(item) { @@ -921,11 +833,7 @@ session.web.Sidebar = session.web.OldWidget.extend({ self.getParent().sidebar_context().then(function (context) { var ids = self.getParent().get_selected_ids(); if (ids.length == 0) { - //TODO: make prettier warning? - openerp.web.dialog($("<div />").text(_t("You must choose at least one record.")), { - title: _t("Warning"), - modal: true - }); + instance.web.dialog($("<div />").text(_t("You must choose at least one record.")), { title: _t("Warning"), modal: true }); return false; } var additional_context = _.extend({ @@ -948,18 +856,60 @@ session.web.Sidebar = session.web.OldWidget.extend({ }); }); }, - do_fold: function() { - this.$element.addClass('closed-sidebar').removeClass('open-sidebar'); + do_attachement_update: function(dataset, model_id) { + if (!model_id) { + this.on_attachments_loaded([]); + } else { + var dom = [ ['res_model', '=', dataset.model], ['res_id', '=', model_id], ['type', 'in', ['binary', 'url']] ]; + var ds = new instance.web.DataSetSearch(this, 'ir.attachment', dataset.get_context(), dom); + ds.read_slice(['name', 'url', 'type'], {}).then(this.on_attachments_loaded); + } }, - do_unfold: function() { - this.$element.addClass('open-sidebar').removeClass('closed-sidebar'); + on_attachments_loaded: function(attachments) { + var self = this; + var items = []; + // TODO: preprend: _s + + var prefix = '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=name&id='; + _.each(attachments,function(a) { + a.label = a.name; + if(a.type === "binary") { + a.url = prefix + a.id + '&t=' + (new Date().getTime()); + } + }); + attachments.push( { label: _t("Add..."), callback: self.on_attachment_add } ); + self.items['files'] = attachments; + self.redraw(); + }, + on_attachment_add: function(e) { + this.$element.find('.oe_sidebar_add').show(); + }, + on_attachment_changed: function(e) { + return; + window[this.element_id + '_iframe'] = this.do_update; + var $e = $(e.target); + if ($e.val() != '') { + this.$element.find('form.oe-binary-form').submit(); + $e.parent().find('input[type=file]').prop('disabled', true); + $e.parent().find('button').prop('disabled', true).find('img, span').toggle(); + } }, - do_toggle: function() { - this.$element.toggleClass('open-sidebar closed-sidebar'); + on_attachment_delete: function(e) { + return; + var self = this, $e = $(e.currentTarget); + var name = _.str.trim($e.parent().find('a.oe-sidebar-attachments-link').text()); + if (confirm(_.str.sprintf(_t("Do you really want to delete the attachment %s?"), name))) { + this.rpc('/web/dataset/unlink', { + model: 'ir.attachment', + ids: [parseInt($e.attr('data-id'))] + }, function(r) { + $e.parent().remove(); + self.do_notify("Delete an attachment", "The attachment '" + name + "' has been deleted"); + }); + } } }); -session.web.TranslateDialog = session.web.Dialog.extend({ +instance.web.TranslateDialog = instance.web.Dialog.extend({ dialog_title: {toString: function () { return _t("Translations"); }}, init: function(view) { // TODO fme: should add the language to fields_view_get because between the fields view get @@ -979,14 +929,14 @@ session.web.TranslateDialog = session.web.Dialog.extend({ this.translatable_fields_keys = _.map(this.view.translatable_fields || [], function(i) { return i.name }); this.languages = null; this.languages_loaded = $.Deferred(); - (new session.web.DataSetSearch(this, 'res.lang', this.view.dataset.get_context(), + (new instance.web.DataSetSearch(this, 'res.lang', this.view.dataset.get_context(), [['translatable', '=', '1']])).read_slice(['code', 'name'], { sort: 'id' }).then(this.on_languages_loaded); }, start: function() { var self = this; this._super(); $.when(this.languages_loaded).then(function() { - self.$element.html(session.web.qweb.render('TranslateDialog', { widget: self })); + self.$element.html(instance.web.qweb.render('TranslateDialog', { widget: self })); self.$fields_form = self.$element.find('.oe_translation_form'); self.$fields_form.find('.oe_trad_field').change(function() { $(this).toggleClass('touched', ($(this).val() != $(this).attr('data-value'))); @@ -1075,29 +1025,61 @@ session.web.TranslateDialog = session.web.Dialog.extend({ } }); -session.web.View = session.web.Widget.extend(/** @lends session.web.View# */{ +instance.web.View = instance.web.Widget.extend({ template: "EmptyComponent", // name displayed in view switchers display_name: '', + /** + * Define a view type for each view to allow automatic call to fields_view_get. + */ + view_type: undefined, init: function(parent, dataset, view_id, options) { this._super(parent); this.dataset = dataset; this.view_id = view_id; this.set_default_options(options); }, + start: function() { + return this.load_view(); + }, + load_view: function() { + if (this.embedded_view) { + var def = $.Deferred(); + var self = this; + $.async_when().then(function() {def.resolve(self.embedded_view);}); + return def.pipe(this.on_loaded); + } else { + var context = new instance.web.CompoundContext(this.dataset.get_context()); + if (! this.view_type) + console.warn("view_type is not defined", this); + return this.rpc("/web/view/load", { + "model": this.dataset.model, + "view_id": this.view_id, + "view_type": this.view_type, + toolbar: !!this.options.$sidebar, + context: context + }).pipe(this.on_loaded); + } + }, + /** + * Called after a successful call to fields_view_get. + * Must return a promise. + */ + on_loaded: function(fields_view_get) { + }, set_default_options: function(options) { this.options = options || {}; _.defaults(this.options, { // All possible views options should be defaulted here + $sidebar: null, sidebar_id: null, - sidebar: true, action: null, action_views_ids: {} }); }, open_translate_dialog: function(field) { if (!this.translate_dialog) { - this.translate_dialog = new session.web.TranslateDialog(this).start(); + this.translate_dialog = new instance.web.TranslateDialog(this).start(); } this.translate_dialog.open(field); }, @@ -1109,7 +1091,7 @@ session.web.View = session.web.Widget.extend(/** @lends session.web.View# */{ * @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``) * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'`` * @param {Object} [action_data.context=null] additional action context, to add to the current context - * @param {session.web.DataSet} dataset a dataset object used to communicate with the server + * @param {instance.web.DataSet} dataset a dataset object used to communicate with the server * @param {Object} [record_id] the identifier of the object on which the action is to be applied * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action) */ @@ -1121,12 +1103,12 @@ session.web.View = session.web.Widget.extend(/** @lends session.web.View# */{ return self.getParent().on_action_executed.apply(null, arguments); } }; - var context = new session.web.CompoundContext(dataset.get_context(), action_data.context || {}); + var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {}); var handler = function (r) { var action = r.result; if (action && action.constructor == Object) { - var ncontext = new session.web.CompoundContext(context); + var ncontext = new instance.web.CompoundContext(context); if (record_id) { ncontext.add({ active_id: record_id, @@ -1175,13 +1157,12 @@ session.web.View = session.web.Widget.extend(/** @lends session.web.View# */{ /** * Directly set a view to use instead of calling fields_view_get. This method must * be called before start(). When an embedded view is set, underlying implementations - * of session.web.View must use the provided view instead of any other one. + * of instance.web.View must use the provided view instead of any other one. * * @param embedded_view A view. */ set_embedded_view: function(embedded_view) { this.embedded_view = embedded_view; - this.options.sidebar = false; }, do_show: function () { this.$element.show(); @@ -1214,15 +1195,12 @@ session.web.View = session.web.Widget.extend(/** @lends session.web.View# */{ }, do_search: function(view) { }, - set_common_sidebar_sections: function(sidebar) { - sidebar.add_default_sections(); - }, on_sidebar_import: function() { - var import_view = new session.web.DataImport(this, this.dataset); + var import_view = new instance.web.DataImport(this, this.dataset); import_view.start(); }, on_sidebar_export: function() { - var export_view = new session.web.DataExport(this, this.dataset); + var export_view = new instance.web.DataExport(this, this.dataset); export_view.start(); }, on_sidebar_translate: function() { @@ -1247,7 +1225,29 @@ session.web.View = session.web.Widget.extend(/** @lends session.web.View# */{ } }); -session.web.json_node_to_xml = function(node, human_readable, indent) { +instance.web.xml_to_json = function(node) { + switch (node.nodeType) { + case 3: + case 4: + return node.data; + break; + case 1: + var attrs = $(node).getAttributes(); + _.each(['domain', 'filter_domain', 'context', 'default_get'], function(key) { + if (attrs[key]) { + try { + attrs[key] = JSON.parse(attrs[key]); + } catch(e) { } + } + }); + return { + tag: node.tagName.toLowerCase(), + attrs: attrs, + children: _.map(node.childNodes, instance.web.xml_to_json) + } + } +} +instance.web.json_node_to_xml = function(node, human_readable, indent) { // For debugging purpose, this function will convert a json node back to xml // Maybe useful for xml view editor indent = indent || 0; @@ -1276,7 +1276,7 @@ session.web.json_node_to_xml = function(node, human_readable, indent) { r += '>' + cr; var childs = []; for (var i = 0, ii = node.children.length; i < ii; i++) { - childs.push(session.web.json_node_to_xml(node.children[i], human_readable, indent + 1)); + childs.push(instance.web.json_node_to_xml(node.children[i], human_readable, indent + 1)); } r += childs.join(cr); r += cr + sindent + '</' + node.tag + '>'; @@ -1285,6 +1285,43 @@ session.web.json_node_to_xml = function(node, human_readable, indent) { return r + '/>'; } } +instance.web.xml_to_str = function(node) { + if (window.ActiveXObject) { + return node.xml; + } else { + return (new XMLSerializer()).serializeToString(node); + } +} +instance.web.str_to_xml = function(s) { + if (window.DOMParser) { + var dp = new DOMParser(); + var r = dp.parseFromString(s, "text/xml"); + if (r.body && r.body.firstChild && r.body.firstChild.nodeName == 'parsererror') { + throw new Error("Could not parse string to xml"); + } + return r; + } + var xDoc; + try { + xDoc = new ActiveXObject("MSXML2.DOMDocument"); + } catch (e) { + throw new Error("Could not find a DOM Parser: " + e.message); + } + xDoc.async = false; + xDoc.preserveWhiteSpace = true; + xDoc.loadXML(s); + return xDoc; +} + +/** + * Registry for all the client actions key: tag value: widget + */ +instance.web.client_actions = new instance.web.Registry(); + +/** + * Registry for all the main views + */ +instance.web.views = new instance.web.Registry(); }; |
