diff options
| author | niv-openerp <nicolas.vanhoren@openerp.com> | 2012-04-16 12:25:03 +0200 |
|---|---|---|
| committer | niv-openerp <nicolas.vanhoren@openerp.com> | 2012-04-16 12:25:03 +0200 |
| commit | f7418cc61ac3f5445f50bae2c60aab38fdca2a2a (patch) | |
| tree | 10b7efd371bd89139352aca3695aca0f0ec4df86 /addons/web/static/src/js | |
| parent | b7fa2c39b4f16c1d47762d993fbc5fcf122da436 (diff) | |
| parent | 3687148b501d1d2c114188c797c81054725d944b (diff) | |
[merge] lot and lot of improvements, mainly css + new form views
bzr revid: nicolas.vanhoren@openerp.com-20120416102503-latyewf3ksvc6mvv
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 | 393 | ||||
| -rw-r--r-- | addons/web/static/src/js/corelib.js (renamed from addons/web/static/src/js/core.js) | 1191 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 142 | ||||
| -rw-r--r-- | addons/web/static/src/js/search.js | 1281 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_form.js | 1981 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 180 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list_editable.js | 91 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_page.js | 288 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 286 |
10 files changed, 3125 insertions, 2729 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..892c86f2a92 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -5,14 +5,13 @@ openerp.web.chrome = function(openerp) { var QWeb = openerp.web.qweb, _t = openerp.web._t; -openerp.web.Notification = openerp.web.OldWidget.extend(/** @lends openerp.web.Notification# */{ +openerp.web.Notification = openerp.web.Widget.extend({ template: 'Notification', - init: function() { this._super.apply(this, arguments); + // move to openerp.web.notification openerp.notification = this; }, - start: function() { this._super.apply(this, arguments); this.$element.notify({ @@ -42,7 +41,6 @@ openerp.web.Notification = openerp.web.OldWidget.extend(/** @lends openerp.web. text: text }, opts); } - }); openerp.web.dialog = function(element) { @@ -51,15 +49,8 @@ openerp.web.dialog = function(element) { return result; } -openerp.web.Dialog = openerp.web.OldWidget.extend(/** @lends openerp.web.Dialog# */{ +openerp.web.Dialog = openerp.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); @@ -128,7 +119,7 @@ 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'); @@ -149,7 +140,6 @@ 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(); @@ -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>'), { + openerp.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"); }} @@ -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: openerp.connection, error: error})); } }); -openerp.web.Loading = openerp.web.OldWidget.extend(/** @lends openerp.web.Loading# */{ +openerp.web.Loading = openerp.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); +openerp.web.DatabaseManager = openerp.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. @@ -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: openerp.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, - +openerp.web.Login = openerp.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 openerp.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(openerp.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 - */ +openerp.web.Menu = openerp.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# */{ +openerp.web.UserMenu = openerp.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(); @@ -910,6 +812,7 @@ openerp.web.UserMenu = openerp.web.Widget.extend(/** @lends openerp.web.UserMen 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: { @@ -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; @@ -968,11 +871,7 @@ 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 - */ +openerp.web.WebClient = openerp.web.Widget.extend({ init: function(parent) { var self = this; this._super(parent); @@ -982,7 +881,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie }, 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) { @@ -1126,16 +1025,15 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie } }); -openerp.web.EmbeddedClient = openerp.web.OldWidget.extend({ +openerp.web.EmbeddedClient = openerp.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); }, - start: function() { var self = this; this.am.appendTo(this.$element.addClass('openerp')); @@ -1152,7 +1050,6 @@ openerp.web.EmbeddedClient = openerp.web.OldWidget.extend({ self.am.do_action(action); }); } - }); openerp.web.embed = function (origin, dbname, login, key, action, options) { @@ -1168,7 +1065,7 @@ openerp.web.embed = function (origin, dbname, login, key, action, options) { } 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); + var client = new openerp.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 ffdabb75c90..2da804c3d2c 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(openerp) { -// a function to override the "extend()" method of JR's inheritance, allowing -// the usage of "include()" -var 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 = openerp.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) + openerp.web.Class = function(){}; + + /** + * Subclass an existing class + * + * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class + */ + openerp.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 @@ var 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. + */ +openerp.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) }; + this.__parentedParent = parent; + if (parent && parent.__parentedMixin) { + parent.__parentedChildren.push(this); } - 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" + }, + /** + * 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# */ { +openerp.web.Events = openerp.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; + } +}); + +openerp.web.EventDispatcherMixin = _.extend({}, openerp.web.ParentedMixin, { + __eventDispatcherMixin: true, + init: function() { + openerp.web.ParentedMixin.init.call(this); + this.__edispatcherEvents = new openerp.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(); + openerp.web.ParentedMixin.destroy.call(this); + } +}); + +openerp.web.GetterSetterMixin = _.extend({}, openerp.web.EventDispatcherMixin, { + init: function() { + openerp.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]; + } +}); + +openerp.web.CallbackEnabledMixin = _.extend({}, openerp.web.GetterSetterMixin, { + init: function() { + openerp.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 + // 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); + } + 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# */ { + +openerp.web.WidgetMixin = _.extend({},openerp.web.CallbackEnabledMixin, { + /** + * Tag name when creating a default $element. + * @type string + */ + tagName: 'div', + /** + * 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) { + openerp.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(); + } + openerp.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); + }, /** - * Thrown when a key could not be found in a mapping + * Renders the current widget and replaces the given jQuery object. * - * @constructs openerp.web.KeyNotFound - * @extends openerp.web.NotFound - * @param {String} key the key which could not be found + * @param target A jQuery object or a Widget instance. */ - init: function (key) { - this.key = key; + replace: function(target) { + return this.__widgetRenderAndInsert(_.bind(function(t) { + this.$element.replaceAll(t); + }, this), target); + }, + __widgetRenderAndInsert: function(insertion, target) { + this.renderElement(); + insertion(target); + return this.start(); }, - toString: function () { - return "The key " + this.key + " was not found"; + /** + * 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() { } }); -openerp.web.ObjectNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.ObjectNotFound# */ { + +// Classes + +openerp.web.CallbackEnabled = openerp.web.Class.extend(openerp.web.CallbackEnabledMixin, { + init: function() { + openerp.web.CallbackEnabledMixin.init.call(this); + } +}); + +/** + * 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 = openerp.web.Class.extend(openerp.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. * - * @constructs openerp.web.ObjectNotFound - * @extends openerp.web.NotFound - * @param {String} path the invalid object path + * @type string */ - init: function (path) { - this.path = path; + 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) { + openerp.web.WidgetMixin.init.call(this,parent); + this.session = openerp.connection; }, - toString: function () { - return "Could not find any object of path " + this.path; + /** + * 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(); } }); -openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry# */ { + +openerp.web.Registry = openerp.web.Class.extend({ /** * Stores a mapping of arbitrary key (strings) to object paths (as strings * as well). @@ -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,7 +782,7 @@ 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; @@ -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. @@ -320,53 +867,6 @@ 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# */{ /** * @constructs openerp.web.Connection @@ -1187,160 +1687,6 @@ 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# */{ /** * @constructs openerp.web.TranslationDataBase @@ -1394,138 +1740,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:`openerp.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 + "'"); +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.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..a68a7bb3d7e --- /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(openerp) { + +/** 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]); + } + } + } +}; + +/** 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 */ +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/search.js b/addons/web/static/src/js/search.js index 0136d41ada0..6e1026b48a1 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -2,9 +2,76 @@ openerp.web.search = function(openerp) { var QWeb = openerp.web.qweb, _t = openerp.web._t, _lt = openerp.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'); + } +}); + +openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.SearchView# */{ + template: "SearchView", /** * @constructs openerp.web.SearchView * @extends openerp.web.OldWidget @@ -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 openerp.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 openerp.web.search.Filter({attrs: { + string: filter.name, + context: filter.context, + domain: filter.domain + }}, self); + return new openerp.web.search.FilterGroup([f], self); + }), + length: 3 + }); + + return $.when( + this.render_column(col1, $('<div>').appendTo($filters)), + this.render_column(col2, $('<div>').appendTo($filters)), + (new openerp.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 openerp.web.search.FilterGroupFacet(options); + +// if (options.model.get('field') instanceof openerp.web.search.FilterGroup) { +// return new openerp.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 openerp.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 openerp.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 openerp.web.search.FilterGroup(filters, this)); } - - return rows; }, /** * Creates a field for the provided field descriptor item (which comes @@ -123,15 +397,10 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea * @returns openerp.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 = openerp.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); - 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(); + this.make_widgets( + data.fields_view['arch'].children, + data.fields_view.fields); - $.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. @@ -342,38 +580,24 @@ openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.Sea * * @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(); } + do_search: function () { + var domains = [], contexts = [], groupbys = [], errors = []; - var data = this.build_search_data(); - - 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) { errors.push(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,59 +644,43 @@ 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 */ +openerp.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(); +openerp.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 * find and instantiate its field widgets. @@ -540,47 +736,6 @@ openerp.web.search.Widget = openerp.web.OldWidget.extend( /** @lends openerp.web 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) { @@ -597,15 +752,6 @@ openerp.web.search.Group = openerp.web.search.Widget.extend({ 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); } }); @@ -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:`openerp.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,11 +818,7 @@ 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# */{ template: 'SearchView.filters', @@ -659,21 +835,63 @@ openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends open 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 openerp.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(); @@ -686,6 +904,45 @@ openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends open return _.extend(new openerp.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# */{ @@ -694,6 +951,10 @@ openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.w * Implementation of the OpenERP filters (button with a context and/or * a domain sent as-is to the search view) * + * Filters are only attributes holder, the actual work (compositing + * domains and contexts, converting between facets and filters) is + * performed by the filter group. + * * @constructs openerp.web.search.Filter * @extends openerp.web.search.Input * @@ -703,49 +964,10 @@ 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# */ { template: 'SearchView.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 !== ''); @@ -790,6 +1008,7 @@ openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.w return new openerp.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 @@ -801,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; } @@ -815,7 +1034,7 @@ 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 new openerp.web.CompoundDomain(domain) .set_eval_context({self: val}); @@ -833,8 +1052,18 @@ openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.w */ openerp.web.search.CharField = openerp.web.search.Field.extend( /** @lends openerp.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# */{ @@ -904,42 +1133,42 @@ 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# */{ @@ -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; @@ -984,23 +1196,24 @@ openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @ * @extends openerp.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); + get_value: function (facet) { + return openerp.web.date_to_str(facet.get('json')); + }, + complete: function (needle) { + var d = Date.parse(needle); + if (!d) { return $.when(null); } + var value = openerp.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 + }]); } }); /** @@ -1015,194 +1228,124 @@ openerp.web.search.DateField = openerp.web.search.Field.extend(/** @lends opener * @extends openerp.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']]; + get_value: function (facet) { + return openerp.web.datetime_to_str(facet.get('json')); } }); openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({ init: function (view_section, field, view) { this._super(view_section, field, view); + this.model = new openerp.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.dataset = new openerp.web.DataSet( - this.view, this.attrs['relation']); }, - 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) { - if (self.abort_last) { - self.abort_last(); - delete self.abort_last; - } - self.dataset.name_search( - req.term, self.attrs.domain, 'ilike', 8, function (data) { - resp(_.map(data, function (result) { - return {id: result[0], label: result[1]} - })); - }); - self.abort_last = self.dataset.abort_last; - }, - select: function (event, ui) { - self.id = ui.item.id; - self.name = ui.item.label; - }, - delay: 0 - }) - }, - 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.dataset.name_get([this.id], $.proxy(this, '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(); - }, +openerp.web.search.Advanced = openerp.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 openerp.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 openerp.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 openerp.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'); } }); @@ -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,12 @@ 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"; + var obj = openerp.web.search.custom_filters.get_object(type); + if(obj === null) { console.log('Unknow field type ' + e.key); + obj = openerp.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); } @@ -1312,11 +1441,7 @@ openerp.web.search.ExtendedSearchProposition.Char = openerp.web.search.ExtendedS {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(); diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 9abf8fdfbf9..e5824d64f59 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -1,18 +1,19 @@ openerp.web.form = function (openerp) { - var _t = openerp.web._t, _lt = openerp.web._lt; var QWeb = openerp.web.qweb; +/** @namespace */ +openerp.web.form = {}; + openerp.web.views.add('form', 'openerp.web.FormView'); -openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# */{ +openerp.web.FormView = openerp.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", /** @@ -35,77 +36,82 @@ 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 = openerp.web.form.widgets; + this.tags_registry = openerp.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(); + this.mode = null; + this.rendering_engine = new openerp.web.form.FormRenderingEngineReadonly(this); }, 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_view(data); + this.rendering_engine.set_fields_registry(this.fields_registry); + this.rendering_engine.set_tags_registry(this.tags_registry); + 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); + if (!this.sidebar && this.options.sidebar) { + this.sidebar = new openerp.web.Sidebar(this); + this.sidebar.appendTo(this.$sidebar); + //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', [{ + this.sidebar.add_items('other', [{ label: _t('Set Default'), form: this, callback: function (item) { @@ -113,10 +119,11 @@ 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(); }, - do_load_state: function(state, warm) { if (state.id && this.datarecord.id != state.id) { if (!this.dataset.get_id_index(state.id)) { @@ -128,9 +135,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() { @@ -146,17 +162,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 = []; @@ -190,7 +209,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.attachments.do_update(); } if (self.default_focus_field) { self.default_focus_field.focus(); @@ -199,20 +218,21 @@ 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(); + this.trigger("view_content_has_changed"); + _.each(this.get_widgets(), function(w) { if (w.field) { w.validate(); } w.update_dom(); - } + }); }, 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()) { @@ -234,11 +254,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; @@ -277,7 +296,7 @@ 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(); + var value = self.fields[field].get_value(); return value == null ? false : value; } // parent field @@ -347,7 +366,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# 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; @@ -428,19 +447,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()) { @@ -458,6 +496,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.")); }, @@ -484,7 +560,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# 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.is_dirty())) { // Special case 'id' field, do not save this field // on 'create' : save all non readonly fields // on 'edit' : save non readonly modified fields @@ -523,7 +599,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>"; @@ -566,7 +642,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } this.do_update_pager(); if (this.sidebar) { - this.sidebar.attachments.do_update(); + // this.sidebar.attachments.do_update(); } //openerp.log("The record has been created with id #" + this.datarecord.id); this.reload(); @@ -579,6 +655,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 { @@ -588,6 +668,11 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } }); }, + get_widgets: function() { + return _.filter(this.getChildren(), function(obj) { + return obj instanceof openerp.web.form.Widget; + }); + }, get_fields_values: function(blacklist) { blacklist = blacklist || []; var values = {}; @@ -639,8 +724,8 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# // ignore fields which are empty, invisible, readonly, o2m // or m2m if (!value - || field.invisible - || field.readonly + || field.get('invisible') + || field.get("readonly") || field.field.type === 'one2many' || field.field.type === 'many2many') { return false; @@ -661,7 +746,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# return { name: name, - string: field.string, + string: field.node_atts.string, value: value, displayed: displayed, // convert undefined to false @@ -669,7 +754,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } }) .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; }) @@ -687,7 +772,7 @@ 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(), @@ -708,7 +793,343 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# d.open(); } }); -openerp.web.FormDialog = openerp.web.Dialog.extend({ + +/** + * Interface to be implemented by rendering engines for the form view. + */ +openerp.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. + */ +openerp.web.form.FormRenderingEngine = openerp.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 = openerp.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, openerp.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); + 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, openerp.web.xml_to_json($elem[0])); + w.replace($elem); + }) + }, + 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('.oe_form_group_cell').each(function() { + var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan'), + $span = $('<span class="oe_layout_debug_cell"/>').text(text); + $span.prependTo($(this)); + }); + } + 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 openerp.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element); + $new_element.addClass($node.attr("class") || ""); + }, +}); + +openerp.web.form.FormRenderingEngineReadonly = openerp.web.form.FormRenderingEngine.extend({ + alter_field: function(field) { + field.set({"force_readonly": true}); + }, +}); + +openerp.web.form.FormDialog = openerp.web.Dialog.extend({ init: function(parent, options, view_id, dataset) { this._super(parent, options); this.dataset = dataset; @@ -739,19 +1160,17 @@ openerp.web.FormDialog = openerp.web.Dialog.extend({ } }); -/** @namespace */ -openerp.web.form = {}; -openerp.web.form.SidebarAttachments = openerp.web.OldWidget.extend({ +openerp.web.form.SidebarAttachments = openerp.web.Widget.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')); + //var $section = parent.add_section(_t('Attachments'), 'attachments'); + //this.$div = $('<div class="oe-sidebar-attachments"></div>'); + //$section.append(this.$div); + this._super(parent); this.view = form_view; }, do_update: function() { + return; if (!this.view.datarecord.id) { this.on_attachments_loaded([]); } else { @@ -765,12 +1184,14 @@ openerp.web.form.SidebarAttachments = openerp.web.OldWidget.extend({ } }, on_attachments_loaded: function(attachments) { + return; 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) { + return; window[this.element_id + '_iframe'] = this.do_update; var $e = $(e.target); if ($e.val() != '') { @@ -780,6 +1201,7 @@ openerp.web.form.SidebarAttachments = openerp.web.OldWidget.extend({ } }, 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))) { @@ -865,60 +1287,86 @@ 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. + */ +openerp.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 : + openerp.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)(); + }, +}; + +openerp.web.form.InvisibilityChanger = openerp.web.Class.extend(_.extend({}, openerp.web.GetterSetterMixin, openerp.web.form.InvisibilityChangerMixin, { + init: function(parent, field_manager, invisible_domain, $element) { + this.setParent(parent); + openerp.web.GetterSetterMixin.init.call(this); + openerp.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain); + this.$element = $element; + this.start(); + }, +})); + +openerp.web.form.Widget = openerp.web.Widget.extend(_.extend({}, openerp.web.form.InvisibilityChangerMixin, { /** * @constructs openerp.web.form.Widget - * @extends openerp.web.OldWidget + * @extends openerp.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; + openerp.web.form.InvisibilityChangerMixin.init.call(this, view, this.modifiers.invisible); - 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'; - } - - 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(); + openerp.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 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; + } } + this.set(to_set); }, update_dom: function() { - this.$element.toggle(!this.invisible); - }, - renderElement: function() { - this.$element.html(QWeb.render(this.form_template, { "widget": this })); }, do_attach_tooltip: function(widget, trigger, options) { widget = widget || this; @@ -928,7 +1376,7 @@ 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'; } @@ -990,231 +1438,23 @@ openerp.web.form.Widget = openerp.web.OldWidget.extend(/** @lends openerp.web.fo } 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', + 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; } }, 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 || openerp.connection.debug) { this.do_attach_tooltip(); } }, @@ -1232,7 +1472,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 = openerp.web.dialog($('<div/>').text(self.node.attrs.confirm), { title: _t('Confirm'), modal: true, buttons: [ @@ -1281,77 +1521,45 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ 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'; - } +/** + * Interface implemented by the form view or any other object + * able to provide the features necessary for the fields to work. + * + * Properties: + * - ... + * Events: + * - view_content_has_changed : when the values of the fields have changed. When + * this event is triggered all fields should reprocess their modifiers. + */ +openerp.web.form.FieldManagerInterface = { - 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 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: * - ... * */ openerp.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. * * set_value() may return an object that can be passed to $.when() that represents the moment when @@ -1386,42 +1594,48 @@ 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) {} }; /** - * 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. + * */ -openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.form.Field# */{ +openerp.web.form.AbstractField = openerp.web.form.Widget.extend(/** @lends openerp.web.form.AbstractField# */{ /** - * @constructs openerp.web.form.Field + * @constructs openerp.web.form.AbstractField * @extends openerp.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; + init: function(field_manager, node) { + this._super(field_manager, node); + this.name = this.node.attrs.name; + this.value = false; + this.view.fields[this.name] = this; + this.view.fields_order.push(this.name); + this.field = this.view.fields_view.fields[this.name] || {}; + this.set({required: this.modifiers['required'] === true}); this.invalid = this.dirty = false; - - this.classname = 'oe_form_field_' + this.type; + + // 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)(); }, start: function() { this._super.apply(this, arguments); @@ -1430,8 +1644,15 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f this.$element.addClass('oe_form_field_translatable'); this.$element.find('.oe_field_translate').click(this.on_translate); } - if (this.nolabel && openerp.connection.debug) { - this.do_attach_tooltip(this, this.$element); + if (this.node.attrs.nolabel && openerp.connection.debug) { + this.do_attach_tooltip(this, this.$label || 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) { @@ -1455,10 +1676,7 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f return !this.invalid; }, is_dirty: function() { - return this.dirty && !this.readonly; - }, - get_on_change_value: function() { - return this.get_value(); + return this.dirty && !this.get("effective_readonly"); }, update_dom: function(show_invalid) { this._super.apply(this, arguments); @@ -1466,10 +1684,8 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f this.$element.find('.oe_field_translate').toggle(!!this.view.datarecord.id); } 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()); + this.$element.toggleClass('oe_form_invalid', !this.is_valid()); } } }, @@ -1504,28 +1720,72 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f 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', +/** + * A mixin to apply on any field that has to completely re-render when its readonly state + * switch. + */ +openerp.web.form.ReinitializeFieldMixin = { + /** + * Default implementation of start(), use it or call explicitly initialize_field(). + */ + start: function() { + this._super(); + this.initialize_field(); + }, + 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(); + }, + /** + * 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() {}, +}; + +openerp.web.form.FieldChar = openerp.web.form.AbstractField.extend(_.extend({}, openerp.web.form.ReinitializeFieldMixin, { + template: 'FieldChar', init: function (view, node) { this._super(view, node); this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1'; }, - start: function() { - this._super.apply(this, arguments); + initialize_content: function() { this.$element.find('input').change(this.on_ui_change); }, 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; + this.render_value(); }, - update_dom: function() { - this._super.apply(this, arguments); - this.$element.find('input').prop('readonly', this.readonly); + render_value: function() { + var show_value = openerp.web.format_value(this.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); + } }, set_value_from_ui: function() { this.value = openerp.web.parse_value(this.$element.find('input').val(), this); @@ -1533,31 +1793,39 @@ openerp.web.form.FieldChar = openerp.web.form.Field.extend({ }, 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; + if (!this.get("effective_readonly")) { + try { + var value = openerp.web.parse_value(this.$element.find('input').val(), this, ''); + this.invalid = this.get("required") && value === ''; + } catch(e) { + this.invalid = true; + } } }, focus: function($element) { this._super($element || 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); - } + }); openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({ - form_template: 'FieldEmail', - start: function() { - this._super.apply(this, arguments); + 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.value) + .text(this.value); + } + }, on_button_clicked: function() { if (!this.value || !this.is_valid()) { this.do_warn("E-mail error", "Can't send email to invalid e-mail address"); @@ -1568,11 +1836,23 @@ openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({ }); openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({ - form_template: 'FieldUrl', - start: function() { - this._super.apply(this, arguments); + 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.value; + var s = /(\w+):(.+)/.exec(tmp); + if (!s) { + tmp = "http://" + this.value; + } + this.$element.find('a').attr('href', tmp).text(tmp); + } + }, on_button_clicked: function() { if (!this.value) { this.do_warn("Resource error", "This resource is empty"); @@ -1583,12 +1863,14 @@ openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({ }); openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({ + is_field_number: true, init: function (view, node) { this._super(view, node); - if (node.attrs.digits) { + this.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) { @@ -1621,7 +1903,7 @@ 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')) { + if (!self.get("effective_readonly") && !self.picker('widget').is(':visible')) { self.picker('setDate', self.value ? openerp.web.auto_str_to_date(self.value) : new Date()); self.$input_picker.show(); self.picker('show'); @@ -1654,10 +1936,10 @@ openerp.web.DateTimeWidget = openerp.web.OldWidget.extend({ this.$input.prop('readonly', this.readonly); this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly); }, - is_valid: function(required) { + is_valid: function() { var value = this.$input.val(); if (value === "") { - return !required; + return true; } else { try { this.parse_client(value); @@ -1685,36 +1967,52 @@ openerp.web.DateWidget = openerp.web.DateTimeWidget.extend({ type_of_date: "date" }); -openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ - form_template: "EmptyComponent", +openerp.web.form.FieldDatetime = openerp.web.form.AbstractField.extend(_.extend({}, openerp.web.form.ReinitializeFieldMixin, { + template: "EmptyComponent", build_widget: function() { return new openerp.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(this.on_ui_change); + this.datewidget.appendTo(this.$element); + } }, set_value: function(value) { this._super(value); - this.datewidget.set_value(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.value); + } else { + this.$element.text(openerp.web.format_value(this.value, this, '')); + } }, - update_dom: function() { - this._super.apply(this, arguments); - this.datewidget.set_readonly(this.readonly); + get_value: function() { + if (!this.get("effective_readonly")) { + return this.datewidget.get_value(); + } else { + return this.value; + } }, validate: function() { - this.invalid = !this.datewidget.is_valid(this.required); + this.invalid = false; + if (!this.get("effective_readonly")) { + this.invalid = !this.datewidget.is_valid() || (this.get("required") && !this.datewidget.get_value()); + } }, focus: function($element) { - this._super($element || this.datewidget.$input); + this._super($element || (this.datewidget && this.datewidget.$input)); } -}); +})); openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({ build_widget: function() { @@ -1722,45 +2020,52 @@ openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({ } }); -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); - this.resized = false; +openerp.web.form.FieldText = openerp.web.form.AbstractField.extend(_.extend({}, openerp.web.form.ReinitializeFieldMixin, { + template: 'FieldText', + initialize_content: function() { + this.$textarea = undefined; + if (!this.get("effective_readonly")) { + this.$textarea = this.$element.find('textarea').change(this.on_ui_change); + this.resized = false; + } }, 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); - if (!this.resized && this.view.options.resize_textareas) { - this.do_resize(this.view.options.resize_textareas); - this.resized = true; - } + this.render_value(); }, - update_dom: function() { - this._super.apply(this, arguments); - this.$element.find('textarea').prop('readonly', this.readonly); + render_value: function() { + var show_value = openerp.web.format_value(this.value, this, ''); + if (!this.get("effective_readonly")) { + this.$textarea.val(show_value); + if (!this.resized && this.view.options.resize_textareas) { + this.do_resize(this.view.options.resize_textareas); + this.resized = true; + } + } else { + this.$element.text(show_value); + } }, set_value_from_ui: function() { - this.value = openerp.web.parse_value(this.$element.find('textarea').val(), this); + this.value = openerp.web.parse_value(this.$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; + if (!this.get("effective_readonly")) { + try { + var value = openerp.web.parse_value(this.$textarea.val(), this, ''); + this.invalid = this.get("required") && value === ''; + } catch(e) { + this.invalid = true; + } } }, focus: function($element) { - this._super($element || this.$element.find('textarea:first')); + this._super($element || 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()); @@ -1781,39 +2086,40 @@ openerp.web.form.FieldText = openerp.web.form.Field.extend({ reset: function() { this.resized = false; } -}); +})); -openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({ - form_template: 'FieldBoolean', +openerp.web.form.FieldBoolean = openerp.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); + this.$checkbox = $("input", this.$element); + this.$element.click(this.on_ui_change); + var check_readonly = function() { + this.$checkbox.prop('disabled', this.get("effective_readonly")); + }; + this.on("change:effective_readonly", this, check_readonly); + _.bind(check_readonly, this)(); }, set_value: function(value) { this._super.apply(this, arguments); - this.$element.find('input')[0].checked = value; + this.$checkbox[0].checked = value; }, set_value_from_ui: function() { - this.value = this.$element.find('input').is(':checked'); - this._super(); - }, - update_dom: function() { + this.value = this.$checkbox.is(':checked'); this._super.apply(this, arguments); - this.$element.find('input').prop('disabled', this.readonly); }, focus: function($element) { - this._super($element || this.$element.find('input:first')); + this._super($element || this.$checkbox); } }); -openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({ - form_template: 'FieldProgressBar', +openerp.web.form.FieldProgressBar = openerp.web.form.AbstractField.extend({ + template: 'FieldProgressBar', start: function() { this._super.apply(this, arguments); - this.$element.find('div').progressbar({ + this.$element.progressbar({ value: this.value, - disabled: this.readonly + disabled: this.get("effective_readonly") }); }, set_value: function(value) { @@ -1823,16 +2129,16 @@ openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({ 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 + '%'); + this.$element.progressbar('option', 'value', show_value).find('span').html(formatted_value + '%'); } }); -openerp.web.form.FieldTextXml = openerp.web.form.Field.extend({ +openerp.web.form.FieldTextXml = openerp.web.form.AbstractField.extend({ // to replace view editor }); -openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ - form_template: 'FieldSelection', +openerp.web.form.FieldSelection = openerp.web.form.AbstractField.extend(_.extend({}, openerp.web.form.ReinitializeFieldMixin, { + template: 'FieldSelection', init: function(view, node) { var self = this; this._super(view, node); @@ -1844,7 +2150,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 @@ -1857,7 +2163,6 @@ 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(function () { ischanging = true; }) @@ -1872,28 +2177,38 @@ openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ 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.render_value(); + }, + 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.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.value; }); + this.$element.text(option ? option[1] : this.values[0][1]); } - 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(); }, - update_dom: function() { - this._super.apply(this, arguments); - this.$element.find('select').prop('disabled', this.readonly); - }, validate: function() { + if (this.get("effective_readonly")) { + this.invalid = false; + return; + } var value = this.values[this.$element.find('select')[0].selectedIndex]; - this.invalid = !(value && !(this.required && value[0] === false)); + this.invalid = !(value && !(this.get("required") && value[0] === false)); }, focus: function($element) { this._super($element || this.$element.find('select:first')); } -}); +})); // jquery autocomplete tweak to allow html (function() { @@ -1937,103 +2252,80 @@ openerp.web.form.dialog = function(content, options) { return dialog.$element; }; -openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ - form_template: 'FieldMany2One', +openerp.web.form.FieldMany2One = openerp.web.form.AbstractField.extend(_.extend({}, openerp.web.form.ReinitializeFieldMixin, { + template: "FieldMany2One", init: function(view, node) { this._super(view, 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; }, start: function() { this._super(); + openerp.web.form.ReinitializeFieldMixin.start.call(this); + this.on("change:value", this, function() { + this.floating = false; + this.render_value(); + if (! this.inhibit_on_change) { + this.on_ui_change(); + } + }); + }, + 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.DataSetStatic(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 openerp.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"); @@ -2041,16 +2333,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); @@ -2062,9 +2381,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; } @@ -2117,7 +2438,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); }); }}); @@ -2126,7 +2446,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); @@ -2134,7 +2454,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}); }}); @@ -2145,13 +2464,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) { var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context()); dataset.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(); @@ -2175,97 +2495,72 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ new openerp.web.CompoundContext(self.build_context(), context || {}) ); pop.on_select_elements.add(function(element_ids) { - var dataset = new openerp.web.DataSetStatic(self, self.field.relation, self.build_context()); - dataset.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 openerp.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 - var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); - dataset.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);}); + 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; + }); } }, + set_value: function(value) { + var self = this; + 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.set({value: value}); + this.inhibit_on_change = false; + this._super(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; + return this.get("value"); }, validate: function() { - this.invalid = false; - var val = this.tmp_value !== undefined ? this.tmp_value : this.value; - if (val === null) { - this.invalid = this.required; - } - }, - open_related: function(related) { - 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); - }); + this.invalid = this.get("required") && ! this.get("value"); }, focus: function ($element) { this._super($element || this.$input); - }, - update_dom: function() { - this._super.apply(this, arguments); - this.$input.prop('readonly', this.readonly); } -}); -openerp.web.check_interface(openerp.web.form.FieldMany2One, openerp.web.form.FieldInterface); +})); /* # Values: (0, 0, { fields }) create @@ -2312,9 +2607,9 @@ var commands = { return [6, false, ids]; } }; -openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ - form_template: 'FieldOne2Many', +openerp.web.form.FieldOne2Many = openerp.web.form.AbstractField.extend({ multi_selection: false, + disable_utility_classes: true, init: function(view, node) { this._super(view, node); lazy_build_o2m_kanban_view(); @@ -2323,7 +2618,6 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ 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); @@ -2342,6 +2636,16 @@ 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; @@ -2349,9 +2653,6 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ this.on_ui_change(); this.doing_on_change = tmp; }, - is_readonly: function() { - return this.readonly || this.force_readonly; - }, load_views: function() { var self = this; @@ -2369,14 +2670,14 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ } 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; } @@ -2389,7 +2690,6 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ this.viewmanager.registry = openerp.web.views.extend({ list: 'openerp.web.form.One2ManyListView', form: 'openerp.web.form.One2ManyFormView', - page: 'openerp.web.PageView', kanban: 'openerp.web.form.One2ManyKanbanView', }); var once = $.Deferred().then(function() { @@ -2401,10 +2701,10 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ this.viewmanager.on_controller_inited.add_last(function(view_type, controller) { controller.o2m = self; if (view_type == "list") { - 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() { @@ -2438,7 +2738,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; } @@ -2581,21 +2881,6 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ 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(); - }); - }); - } - } } }); @@ -2655,7 +2940,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) { @@ -2705,7 +2990,7 @@ openerp.web.form.One2ManyKanbanView = openerp.web_kanban.KanbanView.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) { @@ -2717,12 +3002,14 @@ openerp.web.form.One2ManyKanbanView = openerp.web_kanban.KanbanView.extend({ }); } -openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({ - form_template: 'FieldMany2Many', +/* + * TODO niv: clean those deferred stuff, it could be better + */ +openerp.web.form.FieldMany2Many = openerp.web.form.AbstractField.extend({ multi_selection: false, + disable_utility_classes: true, init: function(view, node) { this._super(view, node); - this.list_id = _.uniqueId("many2many"); this.is_loaded = $.Deferred(); this.initial_is_loaded = this.is_loaded; this.is_setted = $.Deferred(); @@ -2741,6 +3028,16 @@ openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({ 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 || []; @@ -2759,16 +3056,13 @@ openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({ 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, + '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) { @@ -2781,7 +3075,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; }, @@ -2791,21 +3085,6 @@ 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(); - }); - }); - } - } - } }); openerp.web.form.Many2ManyDataSet = openerp.web.DataSetStatic.extend({ @@ -2832,9 +3111,9 @@ openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends open ); 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])); + _.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.on_ui_change(); self.reload_content(); } @@ -2846,7 +3125,7 @@ openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends open var pop = new openerp.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(); @@ -2954,7 +3233,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope '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(); @@ -2976,7 +3255,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; @@ -3025,7 +3304,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope 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})); @@ -3129,14 +3408,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 = openerp.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")); @@ -3170,8 +3449,8 @@ openerp.web.form.FormOpenDataset = openerp.web.ProxyDataSet.extend({ } }); -openerp.web.form.FieldReference = openerp.web.form.Field.extend({ - form_template: 'FieldReference', +openerp.web.form.FieldReference = openerp.web.form.AbstractField.extend(_.extend({}, openerp.web.form.ReinitializeFieldMixin, { + template: 'FieldReference', init: function(view, node) { this._super(view, node); this.fields_view = { @@ -3188,22 +3467,10 @@ openerp.web.form.FieldReference = openerp.web.form.Field.extend({ this.get_selected_ids = 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() { }, @@ -3215,31 +3482,58 @@ openerp.web.form.FieldReference = openerp.web.form.Field.extend({ 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 openerp.web.form.FieldSelection(this, { attrs: { + name: 'selection', + widget: 'selection' + }}); + this.selection.set({readonly: this.get('effective_readonly')}); + this.selection.on_value_changed.add_last(this.on_selection_changed); this.selection.$element = $(".oe_form_view_reference_selection", this.$element); this.selection.renderElement(); this.selection.start(); + + this.m2o = new openerp.web.form.FieldMany2One(this, { attrs: { + name: 'm2o', + widget: 'many2one' + }}); + this.m2o.set({"readonly": this.get("effective_readonly")}); + this.m2o.on_ui_change.add_last(this.on_ui_change); 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'; + return this.get("required") === false || 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(); + }, + render_value: function() { this.reference_ready = false; var vals = [], sel_val, m2o_val; - if (typeof(value) === 'string') { - vals = value.split(','); + if (typeof(this.value) === 'string') { + vals = this.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); @@ -3254,16 +3548,15 @@ openerp.web.form.FieldReference = openerp.web.form.Field.extend({ return false; } } -}); +})); -openerp.web.form.FieldBinary = openerp.web.form.Field.extend({ +openerp.web.form.FieldBinary = openerp.web.form.AbstractField.extend(_.extend({}, openerp.web.form.ReinitializeFieldMixin, { init: function(view, node) { this._super(view, 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); @@ -3327,24 +3620,42 @@ openerp.web.form.FieldBinary = openerp.web.form.Field.extend({ } 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); + template: 'FieldBinaryFile', + initialize_content: function() { + this._super(); + if (this.get("effective_readonly")) { + 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); - 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.value != null && this.value !== false) ? this.value : ''; + } + this.$element.find('input').eq(0).val(show_value); } else { - show_value = (value != null && value !== false) ? value : ''; + this.$element.find('a').show(!!this.value); + if (this.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; @@ -3368,48 +3679,49 @@ 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'); - }, - update_dom: function() { - this._super.apply(this, arguments); - this.$element.find('.oe-binary').toggle(!this.readonly); + 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(); }, set_value: function(value) { this._super.apply(this, arguments); - this.set_image_maxwidth(); - + this.render_value(); + }, + render_value: function() { var url; - if (value && value.substr(0, 10).indexOf(' ') == -1) { + if (this.value && this.value.substr(0, 10).indexOf(' ') == -1) { url = 'data:image/png;base64,' + this.value; - } else { + } else if (this.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.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", +openerp.web.form.FieldStatus = openerp.web.form.AbstractField.extend({ + template: "EmptyComponent", start: function() { this._super(); this.selected_value = null; @@ -3473,45 +3785,10 @@ 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` */ 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', @@ -3532,18 +3809,12 @@ openerp.web.form.widgets = new openerp.web.Registry({ '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' + 'statusbar': 'openerp.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; -} - +openerp.web.form.tags = new openerp.web.Registry({ + 'button' : 'openerp.web.form.WidgetButton', +}); }; diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index 06e1e23e1db..f137bd6c211 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -207,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); @@ -215,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.options.$sidebar) { + this.sidebar = new openerp.web.Sidebar(this); + this.sidebar.appendTo(this.options.$sidebar); this.sidebar.add_toolbar(this.fields_view.toolbar); - this.set_common_sidebar_sections(this.sidebar); } }, /** @@ -295,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 @@ -406,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); @@ -417,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) @@ -478,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) { @@ -543,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}; @@ -823,7 +847,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(); diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index b8699bcc006..aea7f6837c1 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -235,6 +235,7 @@ openerp.web.list_editable = function (openerp) { } self.edition = true; self.edition_id = record_id; + $new_row.addClass("oe_form_container"); self.edition_form = new openerp.web.ListEditableFormView(self.view, self.dataset, false); self.edition_form.$element = $new_row; self.edition_form.editable_list = self; @@ -366,52 +367,56 @@ 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', init: function() { - this._super.apply(this, arguments); - this.registry = openerp.web.list.form.widgets; + this._super.apply(this, arguments); + this.rendering_engine = new openerp.web.ListEditableRenderingEngine(this); + }, + renderElement: function() {} + }); + + openerp.web.ListEditableRenderingEngine = openerp.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 = openerp.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, openerp.web.xml_to_json(el)); + 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 1e7c570abdf..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.DataSetStatic( - 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/views.js b/addons/web/static/src/js/views.js index 052998f4264..de9ff55758c 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -6,17 +6,7 @@ openerp.web.views = function(session) { var QWeb = session.web.qweb, _t = session.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({ +session.web.ActionManager = session.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(); @@ -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) { @@ -217,16 +204,8 @@ session.web.ActionManager = session.web.OldWidget.extend({ } }); -session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.ViewManager# */{ +session.web.ViewManager = session.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; @@ -250,19 +229,13 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V this.registry = session.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 = {}; @@ -271,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 || {}) @@ -279,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; @@ -293,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(); @@ -307,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") { + 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); @@ -336,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) { @@ -373,10 +358,10 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V 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); }, @@ -391,12 +376,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 session.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, @@ -452,7 +435,7 @@ session.web.ViewManager = session.web.OldWidget.extend(/** @lends session.web.V } }); -session.web.ViewManagerAction = session.web.ViewManager.extend(/** @lends oepnerp.web.ViewManagerAction# */{ +session.web.ViewManagerAction = session.web.ViewManager.extend({ template:"ViewManagerAction", /** * @constructs session.web.ViewManagerAction @@ -575,6 +558,9 @@ session.web.ViewManagerAction = session.web.ViewManager.extend(/** @lends oepner }); } 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) { @@ -737,85 +723,85 @@ session.web.ViewManagerAction = session.web.ViewManager.extend(/** @lends oepner } }); -session.web.Sidebar = session.web.OldWidget.extend({ - init: function(parent, element_id) { - this._super(parent, element_id); - this.items = {}; - this.sections = {}; +session.web.Sidebar = session.web.Widget.extend({ + init: function(parent) { + this._super(parent); + var view = this.getParent(); + var view_manager = view.getParent(); + var action = view_manager.action; + 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; + }); + 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(); + console.log('click item',section,index); + var item = self.items[section][index]; + if (item.callback) { + item.callback.apply(self, [item]); + } + if (item.action) { + self.on_item_action_clicked(item); + } + return false; }); }, + redraw: function() { + var self = this; + self.$element.html(QWeb.render('Sidebar', {widget: self})); + this.$element.find('ul').hide(); + }, 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 - } - ]); + var self = this; + }, + 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 @@ -837,34 +823,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) { @@ -872,11 +834,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 - }); + session.web.dialog($("<div />").text(_t("You must choose at least one record.")), { title: _t("Warning"), modal: true }); return false; } var additional_context = _.extend({ @@ -899,15 +857,6 @@ session.web.Sidebar = session.web.OldWidget.extend({ }); }); }, - do_fold: function() { - this.$element.addClass('closed-sidebar').removeClass('open-sidebar'); - }, - do_unfold: function() { - this.$element.addClass('open-sidebar').removeClass('closed-sidebar'); - }, - do_toggle: function() { - this.$element.toggleClass('open-sidebar closed-sidebar'); - } }); session.web.TranslateDialog = session.web.Dialog.extend({ @@ -1026,7 +975,7 @@ session.web.TranslateDialog = session.web.Dialog.extend({ } }); -session.web.View = session.web.Widget.extend(/** @lends session.web.View# */{ +session.web.View = session.web.Widget.extend({ template: "EmptyComponent", // name displayed in view switchers display_name: '', @@ -1066,11 +1015,13 @@ session.web.View = session.web.Widget.extend(/** @lends session.web.View# */{ * Called after a successful call to fields_view_get. * Must return a promise. */ - on_loaded: function(fields_view_get) {}, + 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, @@ -1229,6 +1180,28 @@ session.web.View = session.web.Widget.extend(/** @lends session.web.View# */{ } }); +session.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, session.web.xml_to_json) + } + } +} session.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 @@ -1267,6 +1240,23 @@ session.web.json_node_to_xml = function(node, human_readable, indent) { return r + '/>'; } } +session.web.xml_to_str = function(node) { + if (window.ActiveXObject) { + return node.xml; + } else { + return (new XMLSerializer()).serializeToString(node); + } +} + +/** + * 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(); }; |
