From 500c33b18406c2663366ce6eefc5d8e258d6f681 Mon Sep 17 00:00:00 2001
From: Christophe Matthieu
Date: Wed, 8 Jul 2015 10:29:42 +0200
Subject: [PATCH 01/26] [IMP] web: split model.js into model.js and
data_model.js. Web js modules must call web.DataModel.
---
.../static/src/js/move_line_quickadd.js | 2 +-
.../barcodes/static/src/js/barcode_parser.js | 2 +-
addons/base_import/static/src/js/import.js | 2 +-
addons/board/static/src/js/dashboard.js | 2 +-
.../calendar/static/src/js/base_calendar.js | 2 +-
.../static/src/js/gamification.js | 2 +-
addons/google_drive/static/src/js/gdrive.js | 2 +-
.../hr_attendance/static/src/js/attendance.js | 2 +-
.../static/src/js/timesheet.js | 2 +-
.../im_chat/static/src/js/im_chat_backend.js | 2 +-
addons/mail/static/src/js/announcement.js | 2 +-
.../static/src/js/many2many_tags_email.js | 2 +-
addons/point_of_sale/static/src/js/gui.js | 2 +-
addons/point_of_sale/static/src/js/models.js | 2 +-
addons/point_of_sale/static/src/js/screens.js | 2 +-
addons/pos_restaurant/static/src/js/floors.js | 2 +-
.../static/src/js/web_addons.js | 2 +-
addons/web/static/src/js/compatibility.js | 2 +-
.../web/static/src/js/framework/data_model.js | 80 +++++++++++++++++++
addons/web/static/src/js/framework/model.js | 80 -------------------
addons/web/static/src/js/view_manager.js | 2 +-
.../src/js/views/form_relational_widgets.js | 2 +-
addons/web/static/src/js/views/form_view.js | 2 +-
.../web/static/src/js/views/form_widgets.js | 2 +-
addons/web/static/src/js/views/graph_view.js | 2 +-
.../web/static/src/js/views/graph_widget.js | 2 +-
addons/web/static/src/js/views/list_view.js | 2 +-
addons/web/static/src/js/views/pivot_view.js | 2 +-
.../web/static/src/js/views/search_inputs.js | 2 +-
.../web/static/src/js/views/search_menus.js | 2 +-
addons/web/static/src/js/views/search_view.js | 2 +-
addons/web/static/src/js/web_client.js | 2 +-
addons/web/static/src/js/widgets/user_menu.js | 2 +-
addons/web/static/test/data.js | 2 +-
addons/web/static/test/evals.js | 2 +-
addons/web/views/webclient_templates.xml | 1 +
.../static/src/js/web_calendar.js | 2 +-
addons/web_gantt/static/src/js/gantt.js | 2 +-
.../web_kanban/static/src/js/kanban_view.js | 2 +-
addons/web_tests_demo/static/test/demo.js | 2 +-
addons/web_tip/static/src/js/tip.js | 2 +-
openerp/addons/base/static/src/js/apps.js | 2 +-
42 files changed, 120 insertions(+), 119 deletions(-)
create mode 100644 addons/web/static/src/js/framework/data_model.js
diff --git a/addons/account/static/src/js/move_line_quickadd.js b/addons/account/static/src/js/move_line_quickadd.js
index 6d437826a6a..e77477a1c79 100644
--- a/addons/account/static/src/js/move_line_quickadd.js
+++ b/addons/account/static/src/js/move_line_quickadd.js
@@ -4,7 +4,7 @@ odoo.define('account.move_line_quickadd', function (require) {
var core = require('web.core');
var data = require('web.data');
var ListView = require('web.ListView');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var QWeb = core.qweb;
diff --git a/addons/barcodes/static/src/js/barcode_parser.js b/addons/barcodes/static/src/js/barcode_parser.js
index 4ad34c6543b..00462f36a51 100644
--- a/addons/barcodes/static/src/js/barcode_parser.js
+++ b/addons/barcodes/static/src/js/barcode_parser.js
@@ -2,7 +2,7 @@ odoo.define('barcodes.BarcodeParser', function (require) {
"use strict";
var Class = require('web.Class');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
// The BarcodeParser is used to detect what is the category
// of a barcode (product, partner, ...) and extract an encoded value
diff --git a/addons/base_import/static/src/js/import.js b/addons/base_import/static/src/js/import.js
index 991dbee7462..779655bbef9 100644
--- a/addons/base_import/static/src/js/import.js
+++ b/addons/base_import/static/src/js/import.js
@@ -3,7 +3,7 @@ odoo.define('base_import.import', function (require) {
var core = require('web.core');
var ListView = require('web.ListView');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var session = require('web.session');
var Widget = require('web.Widget');
diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js
index 568a6e2babd..5320893b15e 100644
--- a/addons/board/static/src/js/dashboard.js
+++ b/addons/board/static/src/js/dashboard.js
@@ -7,7 +7,7 @@ var data = require('web.data');
var Dialog = require('web.Dialog');
var FavoriteMenu = require('web.FavoriteMenu');
var form_common = require('web.form_common');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var pyeval = require('web.pyeval');
var ViewManager = require('web.ViewManager');
diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js
index 0bfcc0311db..0b10febc743 100644
--- a/addons/calendar/static/src/js/base_calendar.js
+++ b/addons/calendar/static/src/js/base_calendar.js
@@ -4,7 +4,7 @@ odoo.define('base_calendar.base_calendar', function (require) {
var core = require('web.core');
var data = require('web.data');
var form_common = require('web.form_common');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var WebClient = require('web.WebClient');
var CalendarView = require('web_calendar.CalendarView');
var widgets = require('web_calendar.widgets');
diff --git a/addons/gamification/static/src/js/gamification.js b/addons/gamification/static/src/js/gamification.js
index 37ba574ff19..649df68ecdc 100644
--- a/addons/gamification/static/src/js/gamification.js
+++ b/addons/gamification/static/src/js/gamification.js
@@ -4,7 +4,7 @@ odoo.define('gamification.gamification', function(require) {
var mail = require('mail.mail');
var core = require('web.core');
var form_common = require('web.form_common');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var session = require('web.session');
var Widget = require('web.Widget');
diff --git a/addons/google_drive/static/src/js/gdrive.js b/addons/google_drive/static/src/js/gdrive.js
index f7d33a53979..bc7449c5507 100644
--- a/addons/google_drive/static/src/js/gdrive.js
+++ b/addons/google_drive/static/src/js/gdrive.js
@@ -2,7 +2,7 @@ odoo.define('google_drive.google_drive', function (require) {
"use strict";
var data = require('web.data');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var Sidebar = require('web.Sidebar');
Sidebar.include({
diff --git a/addons/hr_attendance/static/src/js/attendance.js b/addons/hr_attendance/static/src/js/attendance.js
index 80859b76bd5..c1545d10470 100644
--- a/addons/hr_attendance/static/src/js/attendance.js
+++ b/addons/hr_attendance/static/src/js/attendance.js
@@ -4,7 +4,7 @@ odoo.define('hr_attendance.hr_attendance', function(require) {
var core = require('web.core');
var data = require('web.data');
var formats = require('web.formats');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var SystrayMenu = require('web.SystrayMenu');
var time = require('web.time');
var Widget = require('web.Widget');
diff --git a/addons/hr_timesheet_sheet/static/src/js/timesheet.js b/addons/hr_timesheet_sheet/static/src/js/timesheet.js
index 819556b0a59..59e99cf130e 100644
--- a/addons/hr_timesheet_sheet/static/src/js/timesheet.js
+++ b/addons/hr_timesheet_sheet/static/src/js/timesheet.js
@@ -5,7 +5,7 @@ var core = require('web.core');
var data = require('web.data');
var form_common = require('web.form_common');
var formats = require('web.formats');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var time = require('web.time');
var utils = require('web.utils');
diff --git a/addons/im_chat/static/src/js/im_chat_backend.js b/addons/im_chat/static/src/js/im_chat_backend.js
index fe2d5ae97b9..08eb06ab2e1 100644
--- a/addons/im_chat/static/src/js/im_chat_backend.js
+++ b/addons/im_chat/static/src/js/im_chat_backend.js
@@ -6,7 +6,7 @@ odoo.define('im_chat.im_chat', function (require) {
var bus = require('bus.bus');
var core = require('web.core');
var data = require('web.data');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
var utils = require('web.utils');
diff --git a/addons/mail/static/src/js/announcement.js b/addons/mail/static/src/js/announcement.js
index 71e74bedab7..61e6ac1159d 100644
--- a/addons/mail/static/src/js/announcement.js
+++ b/addons/mail/static/src/js/announcement.js
@@ -2,7 +2,7 @@ odoo.define('mail.announcement', function (require) {
"use strict";
var core = require('web.core');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var session = require('web.session');
var WebClient = require('web.WebClient');
diff --git a/addons/mail/static/src/js/many2many_tags_email.js b/addons/mail/static/src/js/many2many_tags_email.js
index 2250935e1da..a9fd41a3cd7 100644
--- a/addons/mail/static/src/js/many2many_tags_email.js
+++ b/addons/mail/static/src/js/many2many_tags_email.js
@@ -4,7 +4,7 @@ odoo.define('mail.many2manytags', function (require) {
var core = require('web.core');
var form_common = require('web.form_common');
var form_relational = require('web.form_relational');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var _t = core._t;
diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js
index 91da131e88a..ec64b5991a8 100644
--- a/addons/point_of_sale/static/src/js/gui.js
+++ b/addons/point_of_sale/static/src/js/gui.js
@@ -7,7 +7,7 @@ odoo.define('point_of_sale.gui', function (require) {
// it is available to all pos objects trough the '.gui' field.
var core = require('web.core');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var _t = core._t;
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 6823197df5c..45b7db18481 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -5,7 +5,7 @@ var BarcodeParser = require('barcodes.BarcodeParser');
var PosDB = require('point_of_sale.DB');
var devices = require('point_of_sale.devices');
var core = require('web.core');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var session = require('web.session');
var time = require('web.time');
var utils = require('web.utils');
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 10e9ef66c1e..192d70cbeba 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -31,7 +31,7 @@ var PosBaseWidget = require('point_of_sale.BaseWidget');
var gui = require('point_of_sale.gui');
var models = require('point_of_sale.models');
var core = require('web.core');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var utils = require('web.utils');
var formats = require('web.formats');
diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js
index 9009c89f69e..732db3cc9d8 100644
--- a/addons/pos_restaurant/static/src/js/floors.js
+++ b/addons/pos_restaurant/static/src/js/floors.js
@@ -7,7 +7,7 @@ var gui = require('point_of_sale.gui');
var models = require('point_of_sale.models');
var screens = require('point_of_sale.screens');
var core = require('web.core');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var QWeb = core.qweb;
var _t = core._t;
diff --git a/addons/purchase_requisition/static/src/js/web_addons.js b/addons/purchase_requisition/static/src/js/web_addons.js
index 9e913df53ad..d67c8ec146d 100644
--- a/addons/purchase_requisition/static/src/js/web_addons.js
+++ b/addons/purchase_requisition/static/src/js/web_addons.js
@@ -2,7 +2,7 @@ odoo.define('purchase_requisition.purchase_requisition', function (require) {
"use strict";
var core = require('web.core');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var ListView = require('web.ListView');
var QWeb = core.qweb;
diff --git a/addons/web/static/src/js/compatibility.js b/addons/web/static/src/js/compatibility.js
index cc80915735a..09a01241f33 100644
--- a/addons/web/static/src/js/compatibility.js
+++ b/addons/web/static/src/js/compatibility.js
@@ -23,7 +23,7 @@ var form_widgets = require('web.form_widgets'); // necessary
var framework = require('web.framework');
var ListView = require('web.ListView');
var Menu = require('web.Menu');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var pyeval = require('web.pyeval');
var Registry = require('web.Registry');
var SearchView = require('web.SearchView');
diff --git a/addons/web/static/src/js/framework/data_model.js b/addons/web/static/src/js/framework/data_model.js
new file mode 100644
index 00000000000..f722339167f
--- /dev/null
+++ b/addons/web/static/src/js/framework/data_model.js
@@ -0,0 +1,80 @@
+odoo.define('web.DataModel', function (require) {
+"use strict";
+
+var data = require('web.data');
+var Model = require('web.Model');
+var pyeval = require('web.pyeval');
+var session = require('web.session');
+
+Model.include({
+ /**
+ * Fetches a Query instance bound to this model, for searching
+ *
+ * @param {Array} [fields] fields to ultimately fetch during the search
+ * @returns {instance.web.Query}
+ */
+ query: function (fields) {
+ return new data.Query(this, fields);
+ },
+ /**
+ * Fetches the model's domain, combined with the provided domain if any
+ *
+ * @param {Array} [domain] to combine with the model's internal domain
+ * @returns {instance.web.CompoundDomain} The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain
+ */
+ domain: function (domain) {
+ if (!domain) { return this._domain; }
+ return new data.CompoundDomain(this._domain, domain);
+ },
+ /**
+ * Fetches the combination of the user's context and the domain context,
+ * combined with the provided context if any
+ *
+ * @param {Object} [context] to combine with the model's internal context
+ * @returns {instance.web.CompoundContext} The union of the user's context and the model's internal context, as well as the provided context if any. In that order.
+ */
+ context: function (context) {
+ return new data.CompoundContext(session.user_context, this._context, context || {});
+ },
+ /**
+ * Call a method (over RPC) on the bound OpenERP model.
+ *
+ * @param {String} method name of the method to call
+ * @param {Array} [args] positional arguments
+ * @param {Object} [kwargs] keyword arguments
+ * @param {Object} [options] additional options for the rpc() method
+ * @returns {jQuery.Deferred<>} call result
+ */
+ call: function (method, args, kwargs, options) {
+ args = args || [];
+ kwargs = kwargs || {};
+ if (!_.isArray(args)) {
+ // call(method, kwargs)
+ kwargs = args;
+ args = [];
+ }
+ pyeval.ensure_evaluated(args, kwargs);
+ var call_kw = '/web/dataset/call_kw/' + this.name + '/' + method;
+ return session.rpc(call_kw, {
+ model: this.name,
+ method: method,
+ args: args,
+ kwargs: kwargs
+ }, options);
+ },
+ call_button: function (method, args) {
+ pyeval.ensure_evaluated(args, {});
+ return session.rpc('/web/dataset/call_button', {
+ model: this.name,
+ method: method,
+ // Should not be necessary anymore. Integrate remote in this?
+ domain_id: null,
+ context_id: args.length - 1,
+ args: args || []
+ });
+ },
+
+});
+
+return Model;
+});
diff --git a/addons/web/static/src/js/framework/model.js b/addons/web/static/src/js/framework/model.js
index ca4c29452a3..2621ca490d4 100644
--- a/addons/web/static/src/js/framework/model.js
+++ b/addons/web/static/src/js/framework/model.js
@@ -109,83 +109,3 @@ var Model = Class.extend({
return Model;
});
-
-odoo.define('web.DataModel', function (require) {
-"use strict";
-
-var data = require('web.data');
-var Model = require('web.Model');
-var pyeval = require('web.pyeval');
-var session = require('web.session');
-
-Model.include({
- /**
- * Fetches a Query instance bound to this model, for searching
- *
- * @param {Array} [fields] fields to ultimately fetch during the search
- * @returns {instance.web.Query}
- */
- query: function (fields) {
- return new data.Query(this, fields);
- },
- /**
- * Fetches the model's domain, combined with the provided domain if any
- *
- * @param {Array} [domain] to combine with the model's internal domain
- * @returns {instance.web.CompoundDomain} The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain
- */
- domain: function (domain) {
- if (!domain) { return this._domain; }
- return new data.CompoundDomain(this._domain, domain);
- },
- /**
- * Fetches the combination of the user's context and the domain context,
- * combined with the provided context if any
- *
- * @param {Object} [context] to combine with the model's internal context
- * @returns {instance.web.CompoundContext} The union of the user's context and the model's internal context, as well as the provided context if any. In that order.
- */
- context: function (context) {
- return new data.CompoundContext(session.user_context, this._context, context || {});
- },
- /**
- * Call a method (over RPC) on the bound OpenERP model.
- *
- * @param {String} method name of the method to call
- * @param {Array} [args] positional arguments
- * @param {Object} [kwargs] keyword arguments
- * @param {Object} [options] additional options for the rpc() method
- * @returns {jQuery.Deferred<>} call result
- */
- call: function (method, args, kwargs, options) {
- args = args || [];
- kwargs = kwargs || {};
- if (!_.isArray(args)) {
- // call(method, kwargs)
- kwargs = args;
- args = [];
- }
- pyeval.ensure_evaluated(args, kwargs);
- var call_kw = '/web/dataset/call_kw/' + this.name + '/' + method;
- return session.rpc(call_kw, {
- model: this.name,
- method: method,
- args: args,
- kwargs: kwargs
- }, options);
- },
- call_button: function (method, args) {
- pyeval.ensure_evaluated(args, {});
- return session.rpc('/web/dataset/call_button', {
- model: this.name,
- method: method,
- // Should not be necessary anymore. Integrate remote in this?
- domain_id: null,
- context_id: args.length - 1,
- args: args || []
- });
- },
-
-});
-
-});
diff --git a/addons/web/static/src/js/view_manager.js b/addons/web/static/src/js/view_manager.js
index d1722fcce32..9321b010dd1 100644
--- a/addons/web/static/src/js/view_manager.js
+++ b/addons/web/static/src/js/view_manager.js
@@ -4,7 +4,7 @@ odoo.define('web.ViewManager', function (require) {
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
var data = require('web.data');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var pyeval = require('web.pyeval');
var SearchView = require('web.SearchView');
var Widget = require('web.Widget');
diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js
index 4513b206f33..3d53dd00675 100644
--- a/addons/web/static/src/js/views/form_relational_widgets.js
+++ b/addons/web/static/src/js/views/form_relational_widgets.js
@@ -8,7 +8,7 @@ var Dialog = require('web.Dialog');
var common = require('web.form_common');
var FormView = require('web.FormView');
var ListView = require('web.ListView');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var session = require('web.session');
var utils = require('web.utils');
var ViewManager = require('web.ViewManager');
diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js
index 488ddae9e61..fd024ae0f75 100644
--- a/addons/web/static/src/js/views/form_view.js
+++ b/addons/web/static/src/js/views/form_view.js
@@ -6,7 +6,7 @@ var crash_manager = require('web.crash_manager');
var data = require('web.data');
var Dialog = require('web.Dialog');
var common = require('web.form_common');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var Sidebar = require('web.Sidebar');
var utils = require('web.utils');
var View = require('web.View');
diff --git a/addons/web/static/src/js/views/form_widgets.js b/addons/web/static/src/js/views/form_widgets.js
index 558a47d4e98..cedc2a8b14a 100644
--- a/addons/web/static/src/js/views/form_widgets.js
+++ b/addons/web/static/src/js/views/form_widgets.js
@@ -10,7 +10,7 @@ var Dialog = require('web.Dialog');
var common = require('web.form_common');
var formats = require('web.formats');
var framework = require('web.framework');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var pyeval = require('web.pyeval');
var session = require('web.session');
var utils = require('web.utils');
diff --git a/addons/web/static/src/js/views/graph_view.js b/addons/web/static/src/js/views/graph_view.js
index 3d3e2e649d6..7f6f38ab05b 100644
--- a/addons/web/static/src/js/views/graph_view.js
+++ b/addons/web/static/src/js/views/graph_view.js
@@ -6,7 +6,7 @@ odoo.define('web.GraphView', function (require) {
var core = require('web.core');
var GraphWidget = require('web.GraphWidget');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var View = require('web.View');
var _lt = core._lt;
diff --git a/addons/web/static/src/js/views/graph_widget.js b/addons/web/static/src/js/views/graph_widget.js
index 3f8a7566d97..e0f425ca4e9 100644
--- a/addons/web/static/src/js/views/graph_widget.js
+++ b/addons/web/static/src/js/views/graph_widget.js
@@ -2,7 +2,7 @@ odoo.define('web.GraphWidget', function (require) {
"use strict";
var core = require('web.core');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var Widget = require('web.Widget');
var _t = core._t;
diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js
index c5af8488cec..8b25776c9dd 100644
--- a/addons/web/static/src/js/views/list_view.js
+++ b/addons/web/static/src/js/views/list_view.js
@@ -7,7 +7,7 @@ var data = require('web.data');
var DataExport = require('web.DataExport');
var formats = require('web.formats');
var common = require('web.list_common');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var pyeval = require('web.pyeval');
var session = require('web.session');
var Sidebar = require('web.Sidebar');
diff --git a/addons/web/static/src/js/views/pivot_view.js b/addons/web/static/src/js/views/pivot_view.js
index 22b74ecbf15..40b3f59dcb8 100644
--- a/addons/web/static/src/js/views/pivot_view.js
+++ b/addons/web/static/src/js/views/pivot_view.js
@@ -8,7 +8,7 @@ var core = require('web.core');
var crash_manager = require('web.crash_manager');
var formats = require('web.formats');
var framework = require('web.framework');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var session = require('web.session');
var Sidebar = require('web.Sidebar');
var utils = require('web.utils');
diff --git a/addons/web/static/src/js/views/search_inputs.js b/addons/web/static/src/js/views/search_inputs.js
index 4127b020970..ce05c99eca2 100644
--- a/addons/web/static/src/js/views/search_inputs.js
+++ b/addons/web/static/src/js/views/search_inputs.js
@@ -4,7 +4,7 @@ odoo.define('web.search_inputs', function (require) {
var core = require('web.core');
var data = require('web.data');
var formats = require('web.formats');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var pyeval = require('web.pyeval');
var time = require('web.time');
var utils = require('web.utils');
diff --git a/addons/web/static/src/js/views/search_menus.js b/addons/web/static/src/js/views/search_menus.js
index 48a8cf65fe7..a2f6c8970ef 100644
--- a/addons/web/static/src/js/views/search_menus.js
+++ b/addons/web/static/src/js/views/search_menus.js
@@ -2,7 +2,7 @@ odoo.define('web.FavoriteMenu', function (require) {
"use strict";
var core = require('web.core');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var pyeval = require('web.pyeval');
var session = require('web.session');
var Widget = require('web.Widget');
diff --git a/addons/web/static/src/js/views/search_view.js b/addons/web/static/src/js/views/search_view.js
index 3511bbca01c..1a44c6af953 100644
--- a/addons/web/static/src/js/views/search_view.js
+++ b/addons/web/static/src/js/views/search_view.js
@@ -6,7 +6,7 @@ var core = require('web.core');
var FavoriteMenu = require('web.FavoriteMenu');
var FilterMenu = require('web.FilterMenu');
var GroupByMenu = require('web.GroupByMenu');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var pyeval = require('web.pyeval');
var search_inputs = require('web.search_inputs');
var utils = require('web.utils');
diff --git a/addons/web/static/src/js/web_client.js b/addons/web/static/src/js/web_client.js
index 496f83c7d3a..d3b27ae48dc 100644
--- a/addons/web/static/src/js/web_client.js
+++ b/addons/web/static/src/js/web_client.js
@@ -8,7 +8,7 @@ var data = require('web.data');
var framework = require('web.framework');
var Loading = require('web.Loading');
var Menu = require('web.Menu');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var Notification = require('web.Notification');
var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
diff --git a/addons/web/static/src/js/widgets/user_menu.js b/addons/web/static/src/js/widgets/user_menu.js
index ef2927d668f..94a47cfa4b0 100644
--- a/addons/web/static/src/js/widgets/user_menu.js
+++ b/addons/web/static/src/js/widgets/user_menu.js
@@ -4,7 +4,7 @@ odoo.define('web.UserMenu', function (require) {
var core = require('web.core');
var Dialog = require('web.Dialog');
var framework = require('web.framework');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var session = require('web.session');
var Widget = require('web.Widget');
diff --git a/addons/web/static/test/data.js b/addons/web/static/test/data.js
index 413ccdaf353..968a787a65c 100644
--- a/addons/web/static/test/data.js
+++ b/addons/web/static/test/data.js
@@ -27,7 +27,7 @@ odoo.define_section('web.dataset', ['web.data'], function (test, mock) {
});
});
-odoo.define_section('data.model.group_by', ['web.Model'], function (test, mock) {
+odoo.define_section('data.model.group_by', ['web.DataModel'], function (test, mock) {
var group_result = [
{ bar: 3, bar_count: 5, __context: {}, __domain: [['bar', '=', 3]], },
diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js
index 27763ebd9a8..15cdf221a6a 100644
--- a/addons/web/static/test/evals.js
+++ b/addons/web/static/test/evals.js
@@ -385,7 +385,7 @@ odoo.define_section('eval.edc', ['web.pyeval', 'web.session'], function (test, m
});
});
- test('empty, context altered', ['web.Model'], function (assert, pyeval, session, Model) {
+ test('empty, context altered', ['web.DataModel'], function (assert, pyeval, session, Model) {
assert.expect(3);
var lang = new Model('res.lang');
var users = new Model('res.users');
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index df140e8ad1d..0d28c646c58 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -123,6 +123,7 @@
+
diff --git a/addons/web_calendar/static/src/js/web_calendar.js b/addons/web_calendar/static/src/js/web_calendar.js
index d2a57bab06a..b2909f23dc6 100644
--- a/addons/web_calendar/static/src/js/web_calendar.js
+++ b/addons/web_calendar/static/src/js/web_calendar.js
@@ -7,7 +7,7 @@ odoo.define('web_calendar.CalendarView', function (require) {
var core = require('web.core');
var data = require('web.data');
var form_common = require('web.form_common');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var time = require('web.time');
var View = require('web.View');
var widgets = require('web_calendar.widgets');
diff --git a/addons/web_gantt/static/src/js/gantt.js b/addons/web_gantt/static/src/js/gantt.js
index 556167f7229..1f4eb415ebb 100644
--- a/addons/web_gantt/static/src/js/gantt.js
+++ b/addons/web_gantt/static/src/js/gantt.js
@@ -7,7 +7,7 @@ odoo.define('web_gantt.GanttView', function (require) {
var core = require('web.core');
var form_common = require('web.form_common');
var formats = require('web.formats');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var time = require('web.time');
var View = require('web.View');
diff --git a/addons/web_kanban/static/src/js/kanban_view.js b/addons/web_kanban/static/src/js/kanban_view.js
index 44e50935348..0f93413a699 100644
--- a/addons/web_kanban/static/src/js/kanban_view.js
+++ b/addons/web_kanban/static/src/js/kanban_view.js
@@ -3,7 +3,7 @@ odoo.define('web_kanban.KanbanView', function (require) {
var core = require('web.core');
var data = require('web.data');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var Dialog = require('web.Dialog');
var form_common = require('web.form_common');
var Pager = require('web.Pager');
diff --git a/addons/web_tests_demo/static/test/demo.js b/addons/web_tests_demo/static/test/demo.js
index 5c5c90f08e4..ae0dcbfaafb 100644
--- a/addons/web_tests_demo/static/test/demo.js
+++ b/addons/web_tests_demo/static/test/demo.js
@@ -44,7 +44,7 @@ odoo.define_section('basic section', [], function (test, mock) {
});
- test('XML-RPC', ['web.Model'], function (assert, Model) {
+ test('XML-RPC', ['web.DataModel'], function (assert, Model) {
assert.expect(3);
mock.add('people.famous:name_search', function (args, kwargs) {
diff --git a/addons/web_tip/static/src/js/tip.js b/addons/web_tip/static/src/js/tip.js
index 8f8e7f15ff1..b5ed982aabe 100644
--- a/addons/web_tip/static/src/js/tip.js
+++ b/addons/web_tip/static/src/js/tip.js
@@ -3,7 +3,7 @@ odoo.define('web_tip.web_tip', function (require) {
var core = require('web.core');
var form_widgets = require('web.form_widgets');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var utils = require('web.utils');
var WebClient = require('web.WebClient');
diff --git a/openerp/addons/base/static/src/js/apps.js b/openerp/addons/base/static/src/js/apps.js
index b73c06aa576..156fe9e10b7 100644
--- a/openerp/addons/base/static/src/js/apps.js
+++ b/openerp/addons/base/static/src/js/apps.js
@@ -3,7 +3,7 @@ odoo.define('base.apps', function (require) {
var core = require('web.core');
var framework = require('web.framework');
-var Model = require('web.Model');
+var Model = require('web.DataModel');
var session = require('web.session');
var web_client = require('web.web_client');
var Widget = require('web.Widget');
From a616c57c609da0e5e56835dcb45e1c05590d25b8 Mon Sep 17 00:00:00 2001
From: Christophe Matthieu
Date: Thu, 25 Jun 2015 14:56:03 +0200
Subject: [PATCH 02/26] [IMP] web: create a template with bootstrap scripts
---
addons/web/views/webclient_templates.xml | 28 +++++++++++--------
addons/website/views/website_templates.xml | 28 +++----------------
.../view/website_slides_embed.xml | 2 +-
3 files changed, 21 insertions(+), 37 deletions(-)
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 0d28c646c58..6082bca1363 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -49,6 +49,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index 8c2a6c3c9fb..309c7009fad 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -80,18 +80,9 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -871,18 +862,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
From 8c559044a5e1be111dcb577ad7e5ab462c1002a0 Mon Sep 17 00:00:00 2001
From: Christophe Matthieu
Date: Tue, 7 Jul 2015 16:02:53 +0200
Subject: [PATCH 03/26] [IMP] web_editor: split website into web_editor (to use
editor in backend for html field) and website
---
addons/web/static/src/css/base.css | 2 +-
addons/web/static/src/js/framework/ajax.js | 34 +
addons/web/static/src/js/tour.js | 24 +-
addons/web/static/src/js/views/form_common.js | 6 +
addons/web/static/src/js/views/form_view.js | 130 +-
addons/web/views/webclient_templates.xml | 22 +-
addons/web_editor/__init__.py | 4 +
addons/web_editor/__openerp__.py | 21 +
addons/web_editor/controllers/__init__.py | 1 +
addons/web_editor/controllers/main.py | 276 ++
addons/web_editor/models/__init__.py | 7 +
addons/web_editor/models/ir_attachment.py | 149 +
addons/web_editor/models/ir_http.py | 16 +
addons/web_editor/models/ir_qweb.py | 566 ++++
addons/web_editor/models/ir_ui_view.py | 123 +
.../models/test_models.py | 7 +-
.../web_editor/security/ir.model.access.csv | 3 +
addons/web_editor/static/src/css/backend.css | 73 +
addons/web_editor/static/src/css/common.sass | 4 +
addons/web_editor/static/src/css/inline.css | 64 +
addons/web_editor/static/src/css/snippets.css | 490 ++++
.../web_editor/static/src/css/snippets.sass | 376 +++
.../web_editor/static/src/css/web_editor.css | 266 ++
.../web_editor/static/src/css/web_editor.sass | 195 ++
.../static/src/img/drag_here.png | Bin
.../static/src/img/edit_here.png | Bin
addons/web_editor/static/src/js/backend.js | 322 +++
addons/web_editor/static/src/js/base.js | 85 +
addons/web_editor/static/src/js/editor.js | 164 ++
addons/web_editor/static/src/js/iframe.js | 78 +
addons/web_editor/static/src/js/inline.js | 77 +
addons/web_editor/static/src/js/rte.js | 597 ++++
.../static/src/js/rte.summernote.js | 781 ++++++
.../static/src/js/snippets.animation.js | 82 +
.../static/src/js/snippets.editor.js | 1137 ++++++++
.../static/src/js/snippets.options.js | 534 ++++
.../static/src/js/summernote.js | 112 +-
.../static/src/js/tour.rte.js} | 222 +-
addons/web_editor/static/src/js/transcoder.js | 172 ++
addons/web_editor/static/src/js/translator.js | 214 ++
addons/web_editor/static/src/js/widgets.js | 1069 +++++++
addons/web_editor/static/src/xml/backend.xml | 59 +
addons/web_editor/static/src/xml/editor.xml | 372 +++
.../static/src/xml/snippets.xml} | 12 +-
.../web_editor/static/src/xml/translator.xml | 14 +
addons/web_editor/tests/__init__.py | 3 +
addons/web_editor/tests/test_converter.py | 243 ++
addons/web_editor/tests/test_ui.py | 9 +
addons/web_editor/views/editor.xml | 153 +
addons/web_editor/views/iframe.xml | 62 +
addons/web_editor/views/snippets.xml | 56 +
addons/web_tests/static/src/js/web.tour.js | 6 +-
addons/website/__openerp__.py | 2 +-
addons/website/controllers/main.py | 194 +-
addons/website/models/__init__.py | 2 -
addons/website/models/ir_http.py | 3 +-
addons/website/models/ir_qweb.py | 558 +---
addons/website/models/ir_ui_view.py | 148 +-
addons/website/models/website.py | 186 +-
addons/website/security/ir.model.access.csv | 2 -
.../static/lib/summernote/src/js/Renderer.js | 2 +-
addons/website/static/src/css/snippets.css | 722 -----
addons/website/static/src/css/snippets.sass | 579 ----
.../static/src/css/website.backend.css | 31 -
addons/website/static/src/css/website.css | 249 +-
.../website/static/src/css/website.editor.css | 263 ++
.../static/src/css/website.editor.sass | 234 ++
addons/website/static/src/css/website.sass | 181 +-
.../website/static/src/js/openerp.define.js | 107 -
.../static/src/js/website.backend.iframe.js | 75 -
.../website/static/src/js/website.backend.js | 153 -
.../website/static/src/js/website.editor.js | 2463 +----------------
addons/website/static/src/js/website.js | 268 +-
.../src/js/website.snippets.animation.js | 72 -
.../static/src/js/website.snippets.editor.js | 1667 +----------
addons/website/static/src/js/website.theme.js | 2 +-
.../static/src/js/website.translator.js | 258 +-
.../static/src/xml/website.backend.xml | 6 -
.../static/src/xml/website.contentMenu.xml | 6 +-
.../website/static/src/xml/website.editor.xml | 609 +---
.../static/src/xml/website.gallery.xml | 2 +-
.../static/src/xml/website.translator.xml | 8 +-
addons/website/tests/test_converter.py | 240 +-
addons/website/tests/test_ui.py | 9 +-
addons/website/views/snippets.xml | 17 +-
addons/website/views/website.backend.xml | 65 -
.../website/views/website_backend_navbar.xml | 6 +-
addons/website/views/website_templates.xml | 131 +-
.../static/src/xml/website.gengo.xml | 4 +-
addons/website_version/models/ir_ui_view.py | 2 +-
openerp/addons/base/ir/ir_qweb.py | 5 +-
openerp/addons/base/ir/ir_ui_view.py | 2 +-
.../addons/base/tests/test_mail_examples.py | 2 +-
openerp/tools/translate.py | 2 +-
94 files changed, 10096 insertions(+), 8935 deletions(-)
create mode 100644 addons/web_editor/__init__.py
create mode 100644 addons/web_editor/__openerp__.py
create mode 100644 addons/web_editor/controllers/__init__.py
create mode 100644 addons/web_editor/controllers/main.py
create mode 100644 addons/web_editor/models/__init__.py
create mode 100644 addons/web_editor/models/ir_attachment.py
create mode 100644 addons/web_editor/models/ir_http.py
create mode 100644 addons/web_editor/models/ir_qweb.py
create mode 100644 addons/web_editor/models/ir_ui_view.py
rename addons/{website => web_editor}/models/test_models.py (88%)
create mode 100644 addons/web_editor/security/ir.model.access.csv
create mode 100644 addons/web_editor/static/src/css/backend.css
create mode 100644 addons/web_editor/static/src/css/common.sass
create mode 100644 addons/web_editor/static/src/css/inline.css
create mode 100644 addons/web_editor/static/src/css/snippets.css
create mode 100644 addons/web_editor/static/src/css/snippets.sass
create mode 100644 addons/web_editor/static/src/css/web_editor.css
create mode 100644 addons/web_editor/static/src/css/web_editor.sass
rename addons/{website => web_editor}/static/src/img/drag_here.png (100%)
rename addons/{website => web_editor}/static/src/img/edit_here.png (100%)
create mode 100644 addons/web_editor/static/src/js/backend.js
create mode 100644 addons/web_editor/static/src/js/base.js
create mode 100644 addons/web_editor/static/src/js/editor.js
create mode 100644 addons/web_editor/static/src/js/iframe.js
create mode 100644 addons/web_editor/static/src/js/inline.js
create mode 100644 addons/web_editor/static/src/js/rte.js
create mode 100644 addons/web_editor/static/src/js/rte.summernote.js
create mode 100644 addons/web_editor/static/src/js/snippets.animation.js
create mode 100644 addons/web_editor/static/src/js/snippets.editor.js
create mode 100644 addons/web_editor/static/src/js/snippets.options.js
rename addons/{website => web_editor}/static/src/js/summernote.js (95%)
rename addons/{website/static/src/js/website.tour.rte.js => web_editor/static/src/js/tour.rte.js} (56%)
create mode 100644 addons/web_editor/static/src/js/transcoder.js
create mode 100644 addons/web_editor/static/src/js/translator.js
create mode 100644 addons/web_editor/static/src/js/widgets.js
create mode 100644 addons/web_editor/static/src/xml/backend.xml
create mode 100644 addons/web_editor/static/src/xml/editor.xml
rename addons/{website/static/src/xml/website.snippets.xml => web_editor/static/src/xml/snippets.xml} (92%)
create mode 100644 addons/web_editor/static/src/xml/translator.xml
create mode 100644 addons/web_editor/tests/__init__.py
create mode 100644 addons/web_editor/tests/test_converter.py
create mode 100644 addons/web_editor/tests/test_ui.py
create mode 100644 addons/web_editor/views/editor.xml
create mode 100644 addons/web_editor/views/iframe.xml
create mode 100644 addons/web_editor/views/snippets.xml
create mode 100644 addons/website/static/src/css/website.editor.css
create mode 100644 addons/website/static/src/css/website.editor.sass
delete mode 100644 addons/website/static/src/js/openerp.define.js
delete mode 100644 addons/website/static/src/js/website.backend.iframe.js
diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css
index 2fce98dd145..37e68889f01 100644
--- a/addons/web/static/src/css/base.css
+++ b/addons/web/static/src/css/base.css
@@ -1,4 +1,4 @@
-@charset "UTF-8";
+@charset "utf-8";
@font-face {
font-family: "mnmliconsRegular";
src: url("/web/static/src/font/mnmliconsv21-webfont.eot") format("eot");
diff --git a/addons/web/static/src/js/framework/ajax.js b/addons/web/static/src/js/framework/ajax.js
index dd1eee906c1..ce61f225524 100644
--- a/addons/web/static/src/js/framework/ajax.js
+++ b/addons/web/static/src/js/framework/ajax.js
@@ -275,11 +275,45 @@ function get_file(options) {
};
+var loadXML = (function () {
+ var loading = false;
+ var urls = [];
+ var qwebs = [];
+ var templates_def = $.Deferred();
+
+ var load = function loadXML(url, qweb) {
+ if (url) {
+ urls.push(url);
+ qwebs.push(qweb);
+ }
+
+ if (!loading && urls.length) {
+ if (templates_def.state() === "resolved") {
+ templates_def = $.Deferred();
+ }
+
+ loading = true;
+ qwebs.shift().add_template(urls.shift(), function () {
+ loading = false;
+ if (!urls.length) {
+ templates_def.resolve();
+ }
+ load(null);
+ });
+ }
+
+ return templates_def;
+ };
+
+ return load;
+})();
+
return {
jsonRpc: jsonRpc,
jsonpRpc: jsonpRpc,
loadCSS: loadCSS,
loadJS: loadJS,
+ loadXML: loadXML,
get_file: get_file,
};
diff --git a/addons/web/static/src/js/tour.js b/addons/web/static/src/js/tour.js
index 234b681f7d2..585626ab4a2 100644
--- a/addons/web/static/src/js/tour.js
+++ b/addons/web/static/src/js/tour.js
@@ -6,25 +6,6 @@ var core = require('web.core');
var _t = core._t;
var qweb = core.qweb;
-// make this work
-// raise an error in test mode if openerp don't exist
-// if (typeof openerp === "undefined") {
-// var error = "openerp is undefined"
-// + "\nhref: " + window.location.href
-// + "\nreferrer: " + document.referrer
-// + "\nlocalStorage: " + window.localStorage.getItem("tour");
-// if (typeof $ !== "undefined") {
-// error += '\n\n' + $("body").html();
-// }
-// throw new Error(error);
-// }
-
-
-// // don't rewrite T in test mode
-// if (typeof openerp.Tour !== "undefined") {
-// return;
-// }
-
/////////////////////////////////////////////////
@@ -78,7 +59,7 @@ var Tour = {
defaultDelay: 50,
autoRunning: true,
retryRunningDelay: 1000,
- errorDelay: 5000,
+ errorDelay: 10000,
state: null,
$element: null,
timer: null,
@@ -700,7 +681,7 @@ var Tour = {
},
scrollIntoView: function ($element){
// if element is not on window (screen) then it scroll down to get popup visible
- if (!$element.is('body')){
+ if (!$element.is('body') && !$element.parents().filter(function () { return $(this).css("position") === "fixed";}).length){
var element_top = $element.offset().top;
var visible = ((element_top > $(window).scrollTop()) && (element_top < ($(window).scrollTop() + $(window).height())));
if (!visible) {
@@ -709,7 +690,6 @@ var Tour = {
}
}
};
-// openerp.Tour = Tour;
/////////////////////////////////////////////////
diff --git a/addons/web/static/src/js/views/form_common.js b/addons/web/static/src/js/views/form_common.js
index 19737746b4a..4d8a302882d 100644
--- a/addons/web/static/src/js/views/form_common.js
+++ b/addons/web/static/src/js/views/form_common.js
@@ -630,6 +630,12 @@ var FieldInterface = {
the field to save its value before reading it using get_value(). Must return a promise.
*/
commit_value: function() {},
+ /*
+ The form view call before_save before save data and if before_save return a deferred,
+ the form view wait that all deferred are resolve or fail.
+ If the deferred is rejected, the field is invalidate
+ */
+ before_save: function() {},
};
/**
diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js
index fd024ae0f75..3286f34eff4 100644
--- a/addons/web/static/src/js/views/form_view.js
+++ b/addons/web/static/src/js/views/form_view.js
@@ -837,68 +837,92 @@ var FormView = View.extend(common.FieldManagerMixin, {
_process_save: function(save_obj) {
var self = this;
var prepend_on_create = save_obj.prepend_on_create;
+ var def_process_save = $.Deferred();
try {
var form_invalid = false,
values = {},
first_invalid_field = null,
- readonly_values = {};
- for (var f in self.fields) {
- if (!self.fields.hasOwnProperty(f)) { continue; }
- f = self.fields[f];
- if (!f.is_valid()) {
- form_invalid = true;
- if (!first_invalid_field) {
- first_invalid_field = f;
- }
- } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
- // Special case 'id' field, do not save this field
- // on 'create' : save all non readonly fields
- // on 'edit' : save non readonly modified fields
- if (!f.get("readonly")) {
- values[f.name] = f.get_value(true);
- } else {
- readonly_values[f.name] = f.get_value(true);
- }
+ readonly_values = {},
+ deferred = [];
+
+ _.each(self.fields, function (f) {
+ var res = f.before_save();
+ if (res) {
+ deferred.push(res);
+ res.fail(function () {
+ form_invalid = true;
+ if (!first_invalid_field) {
+ first_invalid_field = f;
+ }
+ });
}
- }
- // Heuristic to assign a proper sequence number for new records that
- // are added in a dataset containing other lines with existing sequence numbers
- if (!self.datarecord.id && self.fields.sequence &&
- !_.has(values, 'sequence') && !_.isEmpty(self.dataset.cache)) {
- // Find current max or min sequence (editable top/bottom)
- var current = _[prepend_on_create ? "min" : "max"](
- _.map(self.dataset.cache, function(o){return o.values.sequence})
- );
- values['sequence'] = prepend_on_create ? current - 1 : current + 1;
- }
- if (form_invalid) {
- self.set({'display_invalid_fields': true});
- first_invalid_field.focus();
- self.on_invalid();
- return $.Deferred().reject();
- } else {
- self.set({'display_invalid_fields': false});
- var save_deferral;
- if (!self.datarecord.id) {
- // Creation save
- save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
- return self.record_created(r, prepend_on_create);
- }, null);
- } else if (_.isEmpty(values)) {
- // Not dirty, noop save
- save_deferral = $.Deferred().resolve({}).promise();
+ });
+
+ $.when.apply($, deferred).always(function () {
+
+ _.each(self.fields, function (f) {
+ if (!f.is_valid()) {
+ form_invalid = true;
+ if (!first_invalid_field) {
+ first_invalid_field = f;
+ }
+ } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
+ // Special case 'id' field, do not save this field
+ // on 'create' : save all non readonly fields
+ // on 'edit' : save non readonly modified fields
+ if (!f.get("readonly")) {
+ values[f.name] = f.get_value(true);
+ } else {
+ readonly_values[f.name] = f.get_value(true);
+ }
+ }
+
+ });
+
+ // Heuristic to assign a proper sequence number for new records that
+ // are added in a dataset containing other lines with existing sequence numbers
+ if (!self.datarecord.id && self.fields.sequence &&
+ !_.has(values, 'sequence') && !_.isEmpty(self.dataset.cache)) {
+ // Find current max or min sequence (editable top/bottom)
+ var current = _[prepend_on_create ? "min" : "max"](
+ _.map(self.dataset.cache, function(o){return o.values.sequence})
+ );
+ values['sequence'] = prepend_on_create ? current - 1 : current + 1;
+ }
+ if (form_invalid) {
+ self.set({'display_invalid_fields': true});
+ first_invalid_field.focus();
+ self.on_invalid();
+ def_process_save.reject();
} else {
- // Write save
- save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
- return self.record_saved(r);
- }, null);
+ self.set({'display_invalid_fields': false});
+ var save_deferral;
+ if (!self.datarecord.id) {
+ // Creation save
+ save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
+ return self.record_created(r, prepend_on_create);
+ }, null);
+ } else if (_.isEmpty(values)) {
+ // Not dirty, noop save
+ save_deferral = $.Deferred().resolve({}).promise();
+ } else {
+ // Write save
+ save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
+ return self.record_saved(r);
+ }, null);
+ }
+ save_deferral.then(function(result) {
+ def_process_save.resolve(result);
+ }).fail(function() {
+ def_process_save.reject();
+ });
}
- return save_deferral;
- }
+ });
} catch (e) {
console.error(e);
- return $.Deferred().reject();
+ return def_process_save.reject();
}
+ return def_process_save;
},
on_invalid: function() {
var warnings = _(this.fields).chain()
@@ -1266,7 +1290,7 @@ var FormRenderingEngine = FormRenderingEngineInterface.extend({
}
var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
if (!obj) {
- throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
+ throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget') || self.fvg.fields[name].type ));
}
var w = new (obj)(self.view, utils.xml_to_json($elem[0]));
var $label = self.labels[$elem.attr("name")];
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 6082bca1363..94553687043 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -10,6 +10,8 @@
+
+
@@ -64,19 +66,6 @@
-
-
-
-
-
-
-
@@ -91,13 +80,6 @@
-
-
-
-
-
-
-
diff --git a/addons/web_editor/__init__.py b/addons/web_editor/__init__.py
new file mode 100644
index 00000000000..03ab5da2fb6
--- /dev/null
+++ b/addons/web_editor/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+import controllers
+import models
+import tests
diff --git a/addons/web_editor/__openerp__.py b/addons/web_editor/__openerp__.py
new file mode 100644
index 00000000000..66fdff08ce1
--- /dev/null
+++ b/addons/web_editor/__openerp__.py
@@ -0,0 +1,21 @@
+{
+ 'name': 'Web Editor',
+ 'category': 'Hidden',
+ 'description': """
+Odoo Web Editor widget.
+==========================
+
+""",
+ 'author': 'Odoo S.A.',
+ 'depends': ['web'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/editor.xml',
+ 'views/iframe.xml',
+ 'views/snippets.xml',
+ ],
+ 'qweb': [
+ 'static/src/xml/*.xml',
+ ],
+ 'auto_install': True
+}
diff --git a/addons/web_editor/controllers/__init__.py b/addons/web_editor/controllers/__init__.py
new file mode 100644
index 00000000000..8ee9bae18d9
--- /dev/null
+++ b/addons/web_editor/controllers/__init__.py
@@ -0,0 +1 @@
+import main
diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py
new file mode 100644
index 00000000000..7d9b6206ae7
--- /dev/null
+++ b/addons/web_editor/controllers/main.py
@@ -0,0 +1,276 @@
+# -*- coding: utf-8 -*-
+from openerp.http import request, STATIC_CACHE
+from openerp.addons.web import http
+import json
+import io
+from PIL import Image, ImageFont, ImageDraw
+from openerp import tools
+import cStringIO
+import werkzeug.wrappers
+import time
+import logging
+logger = logging.getLogger(__name__)
+
+
+class Web_Editor(http.Controller):
+ #------------------------------------------------------
+ # Backend snippet
+ #------------------------------------------------------
+ @http.route('/web_editor/snippets', type='json', auth="user")
+ def snippets(self, **kwargs):
+ return request.registry["ir.ui.view"].render(request.cr, request.uid, 'web_editor.snippets', None, context=request.context)
+
+ #------------------------------------------------------
+ # Backend html field
+ #------------------------------------------------------
+ @http.route('/web_editor/field/html', type='http', auth="user")
+ def FieldTextHtml(self, model=None, res_id=None, field=None, callback=None, **kwargs):
+ cr, uid, context = request.cr, request.uid, request.context
+
+ kwargs.update(
+ model=model,
+ res_id=res_id,
+ field=field,
+ datarecord=json.loads(kwargs['datarecord']),
+ debug='debug' in kwargs)
+
+ for k in kwargs:
+ if isinstance(kwargs[k], basestring) and kwargs[k].isdigit():
+ kwargs[k] = int(kwargs[k])
+
+ trans = dict(
+ lang=kwargs.get('lang', context.get('lang')),
+ translatable=kwargs.get('translatable'),
+ edit_translations=kwargs.get('edit_translations'),
+ editable=kwargs.get('enable_editor'))
+
+ context.update(trans)
+ kwargs.update(trans)
+
+ record = None
+ if model and kwargs.get('res_id'):
+ record = request.registry[model].browse(cr, uid, kwargs.get('res_id'), context)
+
+ kwargs.update(content=record and getattr(record, field) or "")
+
+ return request.render(kwargs.get("template") or "web_editor.FieldTextHtml", kwargs, uid=request.uid)
+
+ #------------------------------------------------------
+ # Backend html field in inline mode
+ #------------------------------------------------------
+ @http.route('/web_editor/field/html/inline', type='http', auth="user")
+ def FieldTextHtmlInline(self, model=None, res_id=None, field=None, callback=None, **kwargs):
+ kwargs['inline_mode'] = True
+ kwargs['dont_load_assets'] = not kwargs.get('enable_editor') and not kwargs.get('edit_translations')
+ return self.FieldTextHtml(model, res_id, field, callback, **kwargs)
+
+ #------------------------------------------------------
+ # convert font into picture
+ #------------------------------------------------------
+ @http.route([
+ '/web_editor/font_to_img/',
+ '/web_editor/font_to_img//',
+ '/web_editor/font_to_img///',
+ '/web_editor/font_to_img////',
+ ], type='http', auth="none")
+ def export_icon_to_png(self, icon, color='#000', size=100, alpha=255, font='/web/static/lib/fontawesome/fonts/fontawesome-webfont.ttf'):
+ """ This method converts FontAwesom pictograms to Images and is Used only
+ for mass mailing becuase custom fonts are not supported in mail.
+ :param icon : character from FontAwesom cheatsheet
+ :param color : RGB code of the color
+ :param size : Pixels in integer
+ :param alpha : transparency of the image from 0 to 255
+ :param font : font path
+
+ :returns PNG image converted from given font
+ """
+ # Initialize font
+ addons_path = http.addons_manifest['web']['addons_path']
+ font_obj = ImageFont.truetype(addons_path + font, size)
+
+ # Determine the dimensions of the icon
+ image = Image.new("RGBA", (size, size), color=(0, 0, 0, 0))
+ draw = ImageDraw.Draw(image)
+
+ boxw, boxh = draw.textsize(icon, font=font_obj)
+ draw.text((0, 0), icon, font=font_obj)
+ left, top, right, bottom = image.getbbox()
+
+ # Create an alpha mask
+ imagemask = Image.new("L", (boxw, boxh), 0)
+ drawmask = ImageDraw.Draw(imagemask)
+ drawmask.text((-left, -top), icon, font=font_obj, fill=alpha)
+
+ # Create a solid color image and apply the mask
+ if color.startswith('rgba'):
+ color = color.replace('rgba', 'rgb')
+ color = ','.join(color.split(',')[:-1])+')'
+ iconimage = Image.new("RGBA", (boxw, boxh), color)
+ iconimage.putalpha(imagemask)
+
+ # Create output image
+ outimage = Image.new("RGBA", (boxw, size), (0, 0, 0, 0))
+ outimage.paste(iconimage, (left, top))
+
+ # output image
+ output = io.BytesIO()
+ outimage.save(output, format="PNG")
+ response = werkzeug.wrappers.Response()
+ response.mimetype = 'image/png'
+ response.data = output.getvalue()
+ response.headers['Cache-Control'] = 'public, max-age=604800'
+ response.headers['Access-Control-Allow-Origin'] = '*'
+ response.headers['Access-Control-Allow-Methods'] = 'GET, POST'
+ response.headers['Connection'] = 'close'
+ response.headers['Date'] = time.strftime("%a, %d-%b-%Y %T GMT", time.gmtime())
+ response.headers['Expires'] = time.strftime("%a, %d-%b-%Y %T GMT", time.gmtime(time.time()+604800*60))
+
+ return response
+
+ #------------------------------------------------------
+ # image route for browse record
+ #------------------------------------------------------
+ def placeholder(self, response):
+ return request.registry['ir.attachment']._image_placeholder(response)
+
+ @http.route([
+ '/web_editor/image',
+ '/web_editor/image/',
+ '/web_editor/image//x',
+ '/web_editor/image//',
+ '/web_editor/image///x',
+ '/web_editor/image///',
+ '/web_editor/image////x'
+ ], type='http', auth="public")
+ def image(self, model=None, id=None, field=None, xmlid=None, max_width=None, max_height=None):
+ """ Fetches the requested field and ensures it does not go above
+ (max_width, max_height), resizing it if necessary.
+
+ If the record is not found or does not have the requested field,
+ returns a placeholder image via :meth:`~.placeholder`.
+
+ Sets and checks conditional response parameters:
+ * :mailheader:`ETag` is always set (and checked)
+ * :mailheader:`Last-Modified is set iif the record has a concurrency
+ field (``__last_update``)
+
+ The requested field is assumed to be base64-encoded image data in
+ all cases.
+
+ xmlid can be used to load the image. But the field image must by base64-encoded
+ """
+ if xmlid and "." in xmlid:
+ try:
+ record = request.env.ref(xmlid)
+ model, id = record._name, record.id
+ except:
+ raise werkzeug.exceptions.NotFound()
+ if model == 'ir.attachment' and not field:
+ if record.sudo().type == "url":
+ field = "url"
+ else:
+ field = "datas"
+
+ if not model or not id or not field:
+ raise werkzeug.exceptions.NotFound()
+
+ try:
+ idsha = str(id).split('_')
+ id = idsha[0]
+ response = werkzeug.wrappers.Response()
+ return request.registry['ir.attachment']._image(
+ request.cr, request.uid, model, id, field, response, max_width, max_height,
+ cache=STATIC_CACHE if len(idsha) > 1 else None)
+ except Exception:
+ logger.exception("Cannot render image field %r of record %s[%s] at size(%s,%s)",
+ field, model, id, max_width, max_height)
+ response = werkzeug.wrappers.Response()
+ return self.placeholder(response)
+
+ #------------------------------------------------------
+ # add attachment (images or link)
+ #------------------------------------------------------
+ @http.route('/web_editor/attachment/add', type='http', auth='user', methods=['POST'])
+ def attach(self, func, upload=None, url=None, disable_optimization=None, **kwargs):
+ # the upload argument doesn't allow us to access the files if more than
+ # one file is uploaded, as upload references the first file
+ # therefore we have to recover the files from the request object
+ Attachments = request.registry['ir.attachment'] # registry for the attachment table
+
+ uploads = []
+ message = None
+ if not upload: # no image provided, storing the link and the image name
+ uploads.append({'website_url': url})
+ name = url.split("/").pop() # recover filename
+ attachment_id = Attachments.create(request.cr, request.uid, {
+ 'name': name,
+ 'type': 'url',
+ 'url': url,
+ 'res_model': 'ir.ui.view',
+ }, request.context)
+ else: # images provided
+ try:
+ attachment_ids = []
+ for c_file in request.httprequest.files.getlist('upload'):
+ image_data = c_file.read()
+ image = Image.open(cStringIO.StringIO(image_data))
+ w, h = image.size
+ if w*h > 42e6: # Nokia Lumia 1020 photo resolution
+ raise ValueError(
+ u"Image size excessive, uploaded images must be smaller "
+ u"than 42 million pixel")
+
+ if not disable_optimization and image.format in ('PNG', 'JPEG'):
+ image_data = tools.image_save_for_web(image)
+
+ attachment_id = Attachments.create(request.cr, request.uid, {
+ 'name': c_file.filename,
+ 'datas': image_data.encode('base64'),
+ 'datas_fname': c_file.filename,
+ 'res_model': 'ir.ui.view',
+ }, request.context)
+ attachment_ids.append(attachment_id)
+
+ uploads = Attachments.read(
+ request.cr, request.uid, attachment_ids, ['website_url'],
+ context=request.context)
+ except Exception, e:
+ logger.exception("Failed to upload image to attachment")
+ message = unicode(e)
+
+ return """""" % (func, json.dumps(uploads), json.dumps(message))
+
+ #------------------------------------------------------
+ # remove attachment (images or link)
+ #------------------------------------------------------
+ @http.route('/web_editor/attachment/remove', type='json', auth='user')
+ def remove(self, ids, **kwargs):
+ """ Removes a web-based image attachment if it is used by no view (template)
+
+ Returns a dict mapping attachments which would not be removed (if any)
+ mapped to the views preventing their removal
+ """
+ cr, uid, context = request.cr, request.uid, request.context
+ Attachment = request.registry['ir.attachment']
+ Views = request.registry['ir.ui.view']
+
+ attachments_to_remove = []
+ # views blocking removal of the attachment
+ removal_blocked_by = {}
+
+ for attachment in Attachment.browse(cr, uid, ids, context=context):
+ # in-document URLs are html-escaped, a straight search will not
+ # find them
+ url = tools.html_escape(attachment.local_url)
+ ids = Views.search(cr, uid, ["|", ('arch_db', 'like', '"%s"' % url), ('arch_db', 'like', "'%s'" % url)], context=context)
+
+ if ids:
+ removal_blocked_by[attachment.id] = Views.read(
+ cr, uid, ids, ['name'], context=context)
+ else:
+ attachments_to_remove.append(attachment.id)
+ if attachments_to_remove:
+ Attachment.unlink(cr, uid, attachments_to_remove, context=context)
+ return removal_blocked_by
diff --git a/addons/web_editor/models/__init__.py b/addons/web_editor/models/__init__.py
new file mode 100644
index 00000000000..43522002bfc
--- /dev/null
+++ b/addons/web_editor/models/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+import ir_attachment
+import ir_qweb
+import ir_ui_view
+import ir_http
+
+import test_models
diff --git a/addons/web_editor/models/ir_attachment.py b/addons/web_editor/models/ir_attachment.py
new file mode 100644
index 00000000000..3068807b2ab
--- /dev/null
+++ b/addons/web_editor/models/ir_attachment.py
@@ -0,0 +1,149 @@
+# -*- coding: utf-8 -*-
+import openerp
+from openerp.osv import osv, fields
+
+import contextlib
+from sys import maxint
+from openerp.addons.web.http import request
+import datetime
+import hashlib
+import time
+import os
+
+from openerp.tools import html_escape as escape, ustr, image_resize_and_sharpen, image_save_for_web
+from PIL import Image
+import cStringIO
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class ir_attachment(osv.osv):
+
+ _inherit = "ir.attachment"
+
+ def _local_url_get(self, cr, uid, ids, name, arg, context=None):
+ result = {}
+ for attach in self.browse(cr, uid, ids, context=context):
+ if attach.url:
+ result[attach.id] = attach.url
+ else:
+ result[attach.id] = self.image_url(cr, uid, attach, 'datas')
+ return result
+
+ _columns = {
+ 'local_url': fields.function(_local_url_get, string="Attachment URL", type='char'),
+ }
+
+ def _image_placeholder(self, response):
+ # file_open may return a StringIO. StringIO can be closed but are
+ # not context managers in Python 2 though that is fixed in 3
+ with contextlib.closing(openerp.tools.misc.file_open(
+ os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
+ mode='rb')) as f:
+ response.data = f.read()
+ return response.make_conditional(request.httprequest)
+
+ def _image(self, cr, uid, model, id_or_ids, field, response, max_width=maxint, max_height=maxint, cache=None, context=None):
+ """ Fetches the requested field and ensures it does not go above
+ (max_width, max_height), resizing it if necessary.
+
+ Resizing is bypassed if the object provides a $field_big, which will
+ be interpreted as a pre-resized version of the base field.
+
+ If the record is not found or does not have the requested field,
+ returns a placeholder image via :meth:`~._image_placeholder`.
+
+ Sets and checks conditional response parameters:
+ * :mailheader:`ETag` is always set (and checked)
+ * :mailheader:`Last-Modified is set iif the record has a concurrency
+ field (``__last_update``)
+
+ The requested field is assumed to be base64-encoded image data in
+ all cases.
+ """
+ Model = self.pool[model]
+ ids = isinstance(id_or_ids, (list, tuple)) and id_or_ids or [int(id_or_ids)]
+ ids = Model.search(cr, uid, [('id', 'in', ids)], context=context)
+
+ if not ids:
+ return self._image_placeholder(response)
+
+ concurrency = '__last_update'
+ [record] = Model.read(cr, openerp.SUPERUSER_ID, ids, [concurrency, field], context=context)
+
+ if concurrency in record:
+ server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
+ try:
+ response.last_modified = datetime.datetime.strptime(
+ record[concurrency], server_format + '.%f')
+ except ValueError:
+ # just in case we have a timestamp without microseconds
+ response.last_modified = datetime.datetime.strptime(
+ record[concurrency], server_format)
+
+ # Field does not exist on model or field set to False
+ if not record.get(field):
+ # FIXME: maybe a field which does not exist should be a 404?
+ return self._image_placeholder(response)
+
+ response.set_etag(hashlib.sha1(record[field]).hexdigest())
+ response.make_conditional(request.httprequest)
+
+ if cache:
+ response.cache_control.max_age = cache
+ response.expires = int(time.time() + cache)
+
+ # conditional request match
+ if response.status_code == 304:
+ return response
+
+ if model == 'ir.attachment' and field == 'url' and field in record:
+ path = record[field].strip('/')
+
+ # Check that we serve a file from within the module
+ if os.path.normpath(path).startswith('..'):
+ return self._image_placeholder(response)
+
+ # Check that the file actually exists
+ path = path.split('/')
+ resource = openerp.modules.get_module_resource(*path)
+ if not resource:
+ return self._image_placeholder(response)
+
+ data = open(resource, 'rb').read()
+ else:
+ data = record[field].decode('base64')
+ image = Image.open(cStringIO.StringIO(data))
+ response.mimetype = Image.MIME[image.format]
+
+ filename = '%s_%s.%s' % (model.replace('.', '_'), ids[0], str(image.format).lower())
+ response.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
+
+ if (not max_width) and (not max_height):
+ response.data = data
+ return response
+
+ w, h = image.size
+ max_w = int(max_width) if max_width else maxint
+ max_h = int(max_height) if max_height else maxint
+
+ if w < max_w and h < max_h:
+ response.data = data
+ else:
+ size = (max_w, max_h)
+ img = image_resize_and_sharpen(image, size, preserve_aspect_ratio=True)
+ image_save_for_web(img, response.stream, format=image.format)
+ # invalidate content-length computed by make_conditional as
+ # writing to response.stream does not do it (as of werkzeug 0.9.3)
+ del response.headers['Content-Length']
+
+ return response
+
+ def image_url(self, cr, uid, record, field, size=None, context=None):
+ """Returns a local url that points to the image field of a given browse record."""
+ model = record._name
+ sudo_record = record.sudo()
+ id = '%s_%s' % (record.id, hashlib.sha1(sudo_record.write_date or sudo_record.create_date or '').hexdigest()[0:7])
+ size = '' if size is None else '/%s' % size
+ return '/web_editor/image/%s/%s/%s%s' % (model, id, field, size)
diff --git a/addons/web_editor/models/ir_http.py b/addons/web_editor/models/ir_http.py
new file mode 100644
index 00000000000..c72fee55e83
--- /dev/null
+++ b/addons/web_editor/models/ir_http.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+from openerp.osv import orm
+from openerp.http import request
+
+
+class ir_http(orm.AbstractModel):
+ _inherit = 'ir.http'
+
+ def _dispatch(self):
+ if 'editable' in request.httprequest.args and 'editable' not in request.context:
+ request.context['editable'] = True
+ if 'edit_translations' in request.httprequest.args and 'edit_translations' not in request.context:
+ request.context['edit_translations'] = True
+ if request.context.get('lang') != "en_US" and 'translatable' not in request.context:
+ request.context['translatable'] = True
+ return super(ir_http, self)._dispatch()
diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py
new file mode 100644
index 00000000000..9642a3c2881
--- /dev/null
+++ b/addons/web_editor/models/ir_qweb.py
@@ -0,0 +1,566 @@
+# -*- coding: utf-8 -*-
+"""
+Web_editor-context rendering needs to add some metadata to rendered and allow to edit fields,
+as well as render a few fields differently.
+
+Also, adds methods to convert values back to openerp models.
+"""
+
+import cStringIO
+import datetime
+import itertools
+import logging
+import os
+import urllib2
+import urlparse
+import re
+import simplejson
+
+import pytz
+from dateutil import parser
+from lxml import etree, html
+from PIL import Image as I
+import openerp.modules
+
+import openerp
+from openerp.osv import orm, fields
+from openerp.tools import ustr, DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
+from openerp.tools import html_escape as escape
+from openerp.addons.base.ir import ir_qweb
+
+REMOTE_CONNECTION_TIMEOUT = 2.5
+
+logger = logging.getLogger(__name__)
+
+
+class QWeb(orm.AbstractModel):
+ """ QWeb object for rendering editor stuff
+ """
+ _name = 'ir.qweb'
+ _inherit = 'ir.qweb'
+
+ re_remove_spaces = re.compile('\s+')
+
+ def render_tag_snippet(self, element, template_attributes, generated_attributes, qwebcontext):
+ cr = qwebcontext['request'].cr
+ uid = qwebcontext['request'].uid
+ view_reg = self.pool['ir.ui.view']
+ page_xmlid = self.eval_format(template_attributes["snippet"], qwebcontext)
+ view = view_reg.browse(cr, uid, view_reg.search(cr, uid, [('key', '=', page_xmlid)]))
+ generated_attributes += " " + "".join([
+ 'data-oe-name="%s"' % view.name,
+ 'data-oe-type="snippet"',
+ 'data-oe-thumbnail="%s"' % template_attributes.get('thumbnail', "oe-thumbnail")])
+
+ template_attributes['call'] = template_attributes['snippet']
+ return self.render_tag_call(element, template_attributes, generated_attributes, qwebcontext)
+
+ def get_converter_for(self, field_type):
+ return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
+
+
+#------------------------------------------------------
+# QWeb fields
+#------------------------------------------------------
+
+
+class Field(orm.AbstractModel):
+ _name = 'ir.qweb.field'
+ _inherit = 'ir.qweb.field'
+
+ def attributes(self, cr, uid, field_name, record, options,
+ source_element, g_att, t_att, qweb_context, context=None):
+ if options is None:
+ options = {}
+ field = record._model._fields[field_name]
+ attrs = []
+
+ placeholder = options.get('placeholder') \
+ or source_element.get('placeholder') \
+ or getattr(field, 'placeholder', None)
+ if placeholder:
+ attrs.append(('placeholder', placeholder))
+
+ return itertools.chain(
+ super(Field, self).attributes(cr, uid, field_name, record, options,
+ source_element, g_att, t_att,
+ qweb_context, context=context),
+ attrs
+ )
+
+ def value_from_string(self, value):
+ return value
+
+ def from_html(self, cr, uid, model, field, element, context=None):
+ return self.value_from_string(element.text_content().strip())
+
+ def qweb_object(self):
+ return self.pool['ir.qweb']
+
+
+class Integer(orm.AbstractModel):
+ _name = 'ir.qweb.field.integer'
+ _inherit = ['ir.qweb.field']
+
+ value_from_string = int
+
+
+class Float(orm.AbstractModel):
+ _name = 'ir.qweb.field.float'
+ _inherit = ['ir.qweb.field', 'ir.qweb.field.float']
+
+ def from_html(self, cr, uid, model, field, element, context=None):
+ lang = self.user_lang(cr, uid, context=context)
+
+ value = element.text_content().strip()
+
+ return float(value.replace(lang.thousands_sep, '')
+ .replace(lang.decimal_point, '.'))
+
+
+class ManyToOne(orm.AbstractModel):
+ _name = 'ir.qweb.field.many2one'
+ _inherit = ['ir.qweb.field.many2one']
+
+ def attributes(self, cr, uid, field_name, record, options,
+ source_element, g_att, t_att, qweb_context,
+ context=None):
+ attrs = super(ManyToOne, self).attributes(
+ cr, uid, field_name, record, options, source_element, g_att, t_att,
+ qweb_context, context=context)
+ many2one = getattr(record, field_name)
+ if many2one:
+ data = [('data-oe-many2one-id', many2one.id),
+ ('data-oe-many2one-model', many2one._name)]
+ return itertools.chain(attrs, data)
+ else:
+ return attrs
+
+ def from_html(self, cr, uid, model, field, element, context=None):
+ Model = self.pool[element.get('data-oe-model')]
+ M2O = self.pool[field.comodel_name]
+ field_name = element.get('data-oe-field')
+ id = int(element.get('data-oe-id'))
+ many2one_id = int(element.get('data-oe-many2one-id'))
+
+ if many2one_id and M2O.exists(cr, uid, [many2one_id]):
+ # save the new id of the many2one
+ Model.write(cr, uid, [id], {
+ field_name: many2one_id
+ }, context=context)
+
+ # not necessary, but might as well be explicit about it
+ return None
+
+
+class Contact(orm.AbstractModel):
+ _name = 'ir.qweb.field.contact'
+ _inherit = ['ir.qweb.field.contact', 'ir.qweb.field.many2one']
+
+ def attributes(self, cr, uid, field_name, record, options,
+ source_element, g_att, t_att, qweb_context,
+ context=None):
+ attrs = super(Contact, self).attributes(
+ cr, uid, field_name, record, options, source_element, g_att, t_att,
+ qweb_context, context=context)
+ if getattr(record, field_name):
+ return itertools.chain(attrs, [('data-oe-contact-options', simplejson.dumps(options))])
+ else:
+ return attrs
+
+ # helper to call the rendering of contact field
+ def get_record_to_html(self, cr, uid, ids, options=None, context=None):
+ node = self.record_to_html(cr, uid, 'record', {
+ 'record': self.pool['res.partner'].browse(cr, uid, ids[0], context=context)},
+ options=options, context=context)
+ return node and node.__html__()
+
+
+def parse_fuzzy(in_format, value):
+ day_first = in_format.find('%d') < in_format.find('%m')
+
+ if '%y' in in_format:
+ year_first = in_format.find('%y') < in_format.find('%d')
+ else:
+ year_first = in_format.find('%Y') < in_format.find('%d')
+
+ return parser.parse(value, dayfirst=day_first, yearfirst=year_first)
+
+
+class Date(orm.AbstractModel):
+ _name = 'ir.qweb.field.date'
+ _inherit = ['ir.qweb.field', 'ir.qweb.field.date']
+
+ def attributes(self, cr, uid, field_name, record, options,
+ source_element, g_att, t_att, qweb_context,
+ context=None):
+ attrs = super(Date, self).attributes(
+ cr, uid, field_name, record, options, source_element, g_att, t_att,
+ qweb_context, context=None)
+ return itertools.chain(attrs, [('data-oe-original', record[field_name])])
+
+ def from_html(self, cr, uid, model, field, element, context=None):
+ value = element.text_content().strip()
+ if not value:
+ return False
+
+ datetime.datetime.strptime(value, DEFAULT_SERVER_DATE_FORMAT)
+ return value
+
+
+class DateTime(orm.AbstractModel):
+ _name = 'ir.qweb.field.datetime'
+ _inherit = ['ir.qweb.field', 'ir.qweb.field.datetime']
+
+ def attributes(self, cr, uid, field_name, record, options,
+ source_element, g_att, t_att, qweb_context,
+ context=None):
+ value = record[field_name]
+ if isinstance(value, basestring):
+ value = datetime.datetime.strptime(
+ value, DEFAULT_SERVER_DATETIME_FORMAT)
+ if value:
+ # convert from UTC (server timezone) to user timezone
+ value = fields.datetime.context_timestamp(
+ cr, uid, timestamp=value, context=context)
+ value = value.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+
+ attrs = super(DateTime, self).attributes(
+ cr, uid, field_name, record, options, source_element, g_att, t_att,
+ qweb_context, context=None)
+ return itertools.chain(attrs, [
+ ('data-oe-original', value)
+ ])
+
+ def from_html(self, cr, uid, model, field, element, context=None):
+ if context is None:
+ context = {}
+ value = element.text_content().strip()
+ if not value:
+ return False
+
+ # parse from string to datetime
+ dt = datetime.datetime.strptime(value, DEFAULT_SERVER_DATETIME_FORMAT)
+
+ # convert back from user's timezone to UTC
+ tz_name = context.get('tz') \
+ or self.pool['res.users'].read(cr, openerp.SUPERUSER_ID, uid, ['tz'], context=context)['tz']
+ if tz_name:
+ try:
+ user_tz = pytz.timezone(tz_name)
+ utc = pytz.utc
+
+ dt = user_tz.localize(dt).astimezone(utc)
+ except Exception:
+ logger.warn(
+ "Failed to convert the value for a field of the model"
+ " %s back from the user's timezone (%s) to UTC",
+ model, tz_name,
+ exc_info=True)
+
+ # format back to string
+ return dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+
+
+class Text(orm.AbstractModel):
+ _name = 'ir.qweb.field.text'
+ _inherit = ['ir.qweb.field', 'ir.qweb.field.text']
+
+ def from_html(self, cr, uid, model, field, element, context=None):
+ return html_to_text(element)
+
+
+class Selection(orm.AbstractModel):
+ _name = 'ir.qweb.field.selection'
+ _inherit = ['ir.qweb.field', 'ir.qweb.field.selection']
+
+ def from_html(self, cr, uid, model, field, element, context=None):
+ record = self.browse(cr, uid, [], context=context)
+ value = element.text_content().strip()
+ selection = field.get_description(record.env)['selection']
+ for k, v in selection:
+ if isinstance(v, str):
+ v = ustr(v)
+ if value == v:
+ return k
+
+ raise ValueError(u"No value found for label %s in selection %s" % (
+ value, selection))
+
+
+class HTML(orm.AbstractModel):
+ _name = 'ir.qweb.field.html'
+ _inherit = ['ir.qweb.field', 'ir.qweb.field.html']
+
+ def from_html(self, cr, uid, model, field, element, context=None):
+ content = []
+ if element.text:
+ content.append(element.text)
+ content.extend(html.tostring(child)
+ for child in element.iterchildren(tag=etree.Element))
+ return '\n'.join(content)
+
+
+class Image(orm.AbstractModel):
+ """
+ Widget options:
+
+ ``class``
+ set as attribute on the generated
tag
+ """
+ _name = 'ir.qweb.field.image'
+ _inherit = ['ir.qweb.field', 'ir.qweb.field.image']
+
+ def to_html(self, cr, uid, field_name, record, options,
+ source_element, t_att, g_att, qweb_context, context=None):
+ assert source_element.tag != 'img',\
+ "Oddly enough, the root tag of an image field can not be img. " \
+ "That is because the image goes into the tag, or it gets the " \
+ "hose again."
+
+ return super(Image, self).to_html(
+ cr, uid, field_name, record, options,
+ source_element, t_att, g_att, qweb_context, context=context)
+
+ def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
+ if options is None:
+ options = {}
+ aclasses = ['img', 'img-responsive'] + options.get('class', '').split()
+ classes = ' '.join(itertools.imap(escape, aclasses))
+
+ max_size = None
+ max_width, max_height = options.get('max_width', 0), options.get('max_height', 0)
+ if max_width or max_height:
+ max_size = '%sx%s' % (max_width, max_height)
+
+ src = self.pool['ir.attachment'].image_url(cr, uid, record, field_name, max_size)
+ if options.get('resize'):
+ src = "%s/%s" % (src, options.get('resize'))
+
+ img = '
' % (classes, src, options.get('style', ''))
+ return ir_qweb.HTMLSafe(img)
+
+ local_url_re = re.compile(r'^/(?P[^]]+)/static/(?P.+)$')
+
+ def from_html(self, cr, uid, model, field, element, context=None):
+ url = element.find('img').get('src')
+
+ url_object = urlparse.urlsplit(url)
+ if url_object.path.startswith('/web_editor/image'):
+ # url might be /web_editor/image//[_]/[/x]
+ fragments = url_object.path.split('/')
+ query = dict(urlparse.parse_qsl(url_object.query))
+ model = query.get('model', fragments[3])
+ oid = query.get('id', fragments[4].split('_')[0])
+ field = query.get('field', fragments[5])
+ item = self.pool[model].browse(cr, uid, int(oid), context=context)
+ return item[field]
+
+ if self.local_url_re.match(url_object.path):
+ return self.load_local_url(url)
+
+ return self.load_remote_url(url)
+
+ def load_local_url(self, url):
+ match = self.local_url_re.match(urlparse.urlsplit(url).path)
+
+ rest = match.group('rest')
+ for sep in os.sep, os.altsep:
+ if sep and sep != '/':
+ rest.replace(sep, '/')
+
+ path = openerp.modules.get_module_resource(
+ match.group('module'), 'static', *(rest.split('/')))
+
+ if not path:
+ return None
+
+ try:
+ with open(path, 'rb') as f:
+ # force complete image load to ensure it's valid image data
+ image = I.open(f)
+ image.load()
+ f.seek(0)
+ return f.read().encode('base64')
+ except Exception:
+ logger.exception("Failed to load local image %r", url)
+ return None
+
+ def load_remote_url(self, url):
+ try:
+ # should probably remove remote URLs entirely:
+ # * in fields, downloading them without blowing up the server is a
+ # challenge
+ # * in views, may trigger mixed content warnings if HTTPS CMS
+ # linking to HTTP images
+ # implement drag & drop image upload to mitigate?
+
+ req = urllib2.urlopen(url, timeout=REMOTE_CONNECTION_TIMEOUT)
+ # PIL needs a seekable file-like image, urllib result is not seekable
+ image = I.open(cStringIO.StringIO(req.read()))
+ # force a complete load of the image data to validate it
+ image.load()
+ except Exception:
+ logger.exception("Failed to load remote image %r", url)
+ return None
+
+ # don't use original data in case weird stuff was smuggled in, with
+ # luck PIL will remove some of it?
+ out = cStringIO.StringIO()
+ image.save(out, image.format)
+ return out.getvalue().encode('base64')
+
+
+class Monetary(orm.AbstractModel):
+ _name = 'ir.qweb.field.monetary'
+ _inherit = ['ir.qweb.field', 'ir.qweb.field.monetary']
+
+ def from_html(self, cr, uid, model, field, element, context=None):
+ lang = self.user_lang(cr, uid, context=context)
+
+ value = element.find('span').text.strip()
+
+ return float(value.replace(lang.thousands_sep, '')
+ .replace(lang.decimal_point, '.'))
+
+
+class Duration(orm.AbstractModel):
+ _name = 'ir.qweb.field.duration'
+ _inherit = [
+ 'ir.qweb.field.duration',
+ 'ir.qweb.field.float',
+ ]
+
+ def attributes(self, cr, uid, field_name, record, options,
+ source_element, g_att, t_att, qweb_context,
+ context=None):
+ attrs = super(Duration, self).attributes(
+ cr, uid, field_name, record, options, source_element, g_att, t_att,
+ qweb_context, context=None)
+ return itertools.chain(attrs, [('data-oe-original', record[field_name])])
+
+ def from_html(self, cr, uid, model, field, element, context=None):
+ value = element.text_content().strip()
+
+ # non-localized value
+ return float(value)
+
+
+class RelativeDatetime(orm.AbstractModel):
+ _name = 'ir.qweb.field.relative'
+ _inherit = [
+ 'ir.qweb.field.relative',
+ 'ir.qweb.field.datetime',
+ ]
+
+ # get formatting from ir.qweb.field.relative but edition/save from datetime
+
+
+class QwebView(orm.AbstractModel):
+ _name = 'ir.qweb.field.qweb'
+ _inherit = ['ir.qweb.field.qweb']
+
+
+def html_to_text(element):
+ """ Converts HTML content with HTML-specified line breaks (br, p, div, ...)
+ in roughly equivalent textual content.
+
+ Used to replace and fixup the roundtripping of text and m2o: when using
+ libxml 2.8.0 (but not 2.9.1) and parsing HTML with lxml.html.fromstring
+ whitespace text nodes (text nodes composed *solely* of whitespace) are
+ stripped out with no recourse, and fundamentally relying on newlines
+ being in the text (e.g. inserted during user edition) is probably poor form
+ anyway.
+
+ -> this utility function collapses whitespace sequences and replaces
+ nodes by roughly corresponding linebreaks
+ * p are pre-and post-fixed by 2 newlines
+ * br are replaced by a single newline
+ * block-level elements not already mentioned are pre- and post-fixed by
+ a single newline
+
+ ought be somewhat similar (but much less high-tech) to aaronsw's html2text.
+ the latter produces full-blown markdown, our text -> html converter only
+ replaces newlines by
elements at this point so we're reverting that,
+ and a few more newline-ish elements in case the user tried to add
+ newlines/paragraphs into the text field
+
+ :param element: lxml.html content
+ :returns: corresponding pure-text output
+ """
+
+ # output is a list of str | int. Integers are padding requests (in minimum
+ # number of newlines). When multiple padding requests, fold them into the
+ # biggest one
+ output = []
+ _wrap(element, output)
+
+ # remove any leading or tailing whitespace, replace sequences of
+ # (whitespace)\n(whitespace) by a single newline, where (whitespace) is a
+ # non-newline whitespace in this case
+ return re.sub(
+ r'[ \t\r\f]*\n[ \t\r\f]*',
+ '\n',
+ ''.join(_realize_padding(output)).strip())
+
+_PADDED_BLOCK = set('p h1 h2 h3 h4 h5 h6'.split())
+# https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements minus p
+_MISC_BLOCK = set((
+ 'address article aside audio blockquote canvas dd dl div figcaption figure'
+ ' footer form header hgroup hr ol output pre section tfoot ul video'
+).split())
+
+
+def _collapse_whitespace(text):
+ """ Collapses sequences of whitespace characters in ``text`` to a single
+ space
+ """
+ return re.sub('\s+', ' ', text)
+
+
+def _realize_padding(it):
+ """ Fold and convert padding requests: integers in the output sequence are
+ requests for at least n newlines of padding. Runs thereof can be collapsed
+ into the largest requests and converted to newlines.
+ """
+ padding = None
+ for item in it:
+ if isinstance(item, int):
+ padding = max(padding, item)
+ continue
+
+ if padding:
+ yield '\n' * padding
+ padding = None
+
+ yield item
+ # leftover padding irrelevant as the output will be stripped
+
+
+def _wrap(element, output, wrapper=u''):
+ """ Recursively extracts text from ``element`` (via _element_to_text), and
+ wraps it all in ``wrapper``. Extracted text is added to ``output``
+
+ :type wrapper: basestring | int
+ """
+ output.append(wrapper)
+ if element.text:
+ output.append(_collapse_whitespace(element.text))
+ for child in element:
+ _element_to_text(child, output)
+ output.append(wrapper)
+
+
+def _element_to_text(e, output):
+ if e.tag == 'br':
+ output.append(u'\n')
+ elif e.tag in _PADDED_BLOCK:
+ _wrap(e, output, 2)
+ elif e.tag in _MISC_BLOCK:
+ _wrap(e, output, 1)
+ else:
+ # inline
+ _wrap(e, output)
+
+ if e.tail:
+ output.append(_collapse_whitespace(e.tail))
diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py
new file mode 100644
index 00000000000..5946e7128bb
--- /dev/null
+++ b/addons/web_editor/models/ir_ui_view.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+import copy
+import openerp
+from openerp.exceptions import AccessError
+from openerp.osv import osv
+from lxml import etree, html
+
+
+class view(osv.osv):
+ _inherit = 'ir.ui.view'
+
+ def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
+ if not values:
+ values = {}
+ if values.get('editable'):
+ try:
+ if not isinstance(id_or_xml_id, (int, long)):
+ if '.' not in id_or_xml_id:
+ raise ValueError('Invalid template id: %r' % (id_or_xml_id,))
+ id_or_xml_id = self.get_view_id(cr, uid, id_or_xml_id, context=context)
+
+ self.check_access_rule(cr, uid, [id_or_xml_id], 'write', context=context)
+
+ except AccessError:
+ values['editable'] = False
+
+ return super(view, self).render(cr, uid, id_or_xml_id, values=values, engine=engine, context=context)
+
+ #------------------------------------------------------
+ # Save from html
+ #------------------------------------------------------
+
+ def extract_embedded_fields(self, cr, uid, arch, context=None):
+ return arch.xpath('//*[@data-oe-model != "ir.ui.view"]')
+
+ def save_embedded_field(self, cr, uid, el, context=None):
+ Model = self.pool[el.get('data-oe-model')]
+ field = el.get('data-oe-field')
+
+ converter = self.pool['ir.qweb'].get_converter_for(el.get('data-oe-type'))
+ value = converter.from_html(cr, uid, Model, Model._fields[field], el)
+
+ if value is not None:
+ # TODO: batch writes?
+ Model.write(cr, uid, [int(el.get('data-oe-id'))], {
+ field: value
+ }, context=context)
+
+ def _pretty_arch(self, arch):
+ # remove_blank_string does not seem to work on HTMLParser, and
+ # pretty-printing with lxml more or less requires stripping
+ # whitespace: http://lxml.de/FAQ.html#why-doesn-t-the-pretty-print-option-reformat-my-xml-output
+ # so serialize to XML, parse as XML (remove whitespace) then serialize
+ # as XML (pretty print)
+ arch_no_whitespace = etree.fromstring(
+ etree.tostring(arch, encoding='utf-8'),
+ parser=etree.XMLParser(encoding='utf-8', remove_blank_text=True))
+ return etree.tostring(
+ arch_no_whitespace, encoding='unicode', pretty_print=True)
+
+ def replace_arch_section(self, cr, uid, view_id, section_xpath, replacement, context=None):
+ # the root of the arch section shouldn't actually be replaced as it's
+ # not really editable itself, only the content truly is editable.
+
+ [view] = self.browse(cr, uid, [view_id], context=context)
+ arch = etree.fromstring(view.arch.encode('utf-8'))
+ # => get the replacement root
+ if not section_xpath:
+ root = arch
+ else:
+ # ensure there's only one match
+ [root] = arch.xpath(section_xpath)
+
+ root.text = replacement.text
+ root.tail = replacement.tail
+ # replace all children
+ del root[:]
+ for child in replacement:
+ root.append(copy.deepcopy(child))
+
+ return arch
+
+ def to_field_ref(self, cr, uid, el, context=None):
+ # filter out meta-information inserted in the document
+ attributes = dict((k, v) for k, v in el.items()
+ if not k.startswith('data-oe-'))
+ attributes['t-field'] = el.get('data-oe-expression')
+
+ out = html.html_parser.makeelement(el.tag, attrib=attributes)
+ out.tail = el.tail
+ return out
+
+ def save(self, cr, uid, res_id, value, xpath=None, context=None):
+ """ Update a view section. The view section may embed fields to write
+
+ :param str model:
+ :param int res_id:
+ :param str xpath: valid xpath to the tag to replace
+ """
+ res_id = int(res_id)
+
+ arch_section = html.fromstring(
+ value, parser=html.HTMLParser(encoding='utf-8'))
+
+ if xpath is None:
+ # value is an embedded field on its own, not a view section
+ self.save_embedded_field(cr, uid, arch_section, context=context)
+ return
+
+ for el in self.extract_embedded_fields(cr, uid, arch_section, context=context):
+ self.save_embedded_field(cr, uid, el, context=context)
+
+ # transform embedded field back to t-field
+ el.getparent().replace(el, self.to_field_ref(cr, uid, el, context=context))
+
+ arch = self.replace_arch_section(cr, uid, res_id, xpath, arch_section, context=context)
+ self.write(cr, uid, res_id, {
+ 'arch': self._pretty_arch(arch)
+ }, context=context)
+
+ view = self.browse(cr, openerp.SUPERUSER_ID, res_id, context=context)
+ if view.model_data_id:
+ view.model_data_id.write({'noupdate': True})
diff --git a/addons/website/models/test_models.py b/addons/web_editor/models/test_models.py
similarity index 88%
rename from addons/website/models/test_models.py
rename to addons/web_editor/models/test_models.py
index 52e614ade4a..100b49b6448 100644
--- a/addons/website/models/test_models.py
+++ b/addons/web_editor/models/test_models.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
from openerp.osv import orm, fields
+
class test_converter(orm.Model):
- _name = 'website.converter.test'
+ _name = 'web_editor.converter.test'
# disable translation export for those brilliant field labels and values
_translate = False
@@ -12,7 +13,7 @@ class test_converter(orm.Model):
'integer': fields.integer(),
'float': fields.float(),
'numeric': fields.float(digits=(16, 2)),
- 'many2one': fields.many2one('website.converter.test.sub'),
+ 'many2one': fields.many2one('web_editor.converter.test.sub'),
'binary': fields.binary(),
'date': fields.date(),
'datetime': fields.datetime(),
@@ -35,7 +36,7 @@ class test_converter(orm.Model):
class test_converter_sub(orm.Model):
- _name = 'website.converter.test.sub'
+ _name = 'web_editor.converter.test.sub'
_columns = {
'name': fields.char(),
diff --git a/addons/web_editor/security/ir.model.access.csv b/addons/web_editor/security/ir.model.access.csv
new file mode 100644
index 00000000000..a02dd5f1957
--- /dev/null
+++ b/addons/web_editor/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_web_editor_converter_test,access_web_editor_converter_test,model_web_editor_converter_test,,1,1,1,1
+access_web_editor_converter_test_sub,access_web_editor_converter_test_sub,model_web_editor_converter_test_sub,,1,1,1,1
diff --git a/addons/web_editor/static/src/css/backend.css b/addons/web_editor/static/src/css/backend.css
new file mode 100644
index 00000000000..decaed2d593
--- /dev/null
+++ b/addons/web_editor/static/src/css/backend.css
@@ -0,0 +1,73 @@
+.oe_form_field_html_text .note-editable {
+ min-height: 180px;
+ font: initial !important;
+ line-height: initial !important;
+ color: initial !important;
+}
+.oe_form_field_html_text .note-editable p {
+ margin: initial;
+}
+.oe_form_field_html_text .note-editable a, .oe_form_field_html_text .note-editable a:hover {
+ color: initial;
+}
+
+.oe_form_field_html iframe {
+ display: block;
+ width: 100%;
+ border: 0;
+ padding: 0;
+ margin: 0;
+ min-height: 300px;
+ min-height: -webkit-calc(100vh - 170px);
+ min-height: calc(100vh - 170px);
+}
+.o_form_FieldTextHtml_fullscreen .oe_form_field_html.o_form_fullscreen_ancestor iframe {
+ position: absolute !important;
+ left: 0 !important;
+ right: 0 !important;
+ top: 0 !important;
+ bottom: 0 !important;
+ width: 100% !important;
+ min-height: 100% !important;
+ z-index: 1001 !important;
+ border: 0;
+}
+
+.o_form_FieldTextHtml_fullscreen * {
+ display: none;
+}
+.o_form_FieldTextHtml_fullscreen .o_form_fullscreen_ancestor {
+ display: block !important;
+ position: static !important;
+ top: 0 !important;
+ left: 0 !important;
+ overflow: visible !important;
+ width: auto !important;
+ overflow: hidden !important;
+}
+
+
+.openerp .oe_form .oe_form_field_html_translate textarea {
+ min-width: 60px;
+ width: 100%;
+ color: #4c4c4c;
+ font: normal 13px "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
+}
+.openerp .oe_form_readonly .oe_form .oe_form_field_html_translate textarea {
+ border:0;
+ background: #fff;
+}
+.openerp .oe_form_editable .oe_form .oe_form_field_html_translate > button {
+ float: right;
+ margin: -6px -3px 0 0;
+}
+.openerp .oe_form_editable .oe_form .oe_form_field_html_translate > .note-editable {
+ border: 1px solid #ccc;
+ min-height: 22px;
+ border-radius: 3px;
+}
+.openerp .oe_form_editable .oe_form .oe_form_field_html_translate .note-editable [contentEditable='true']:hover {
+ -moz-box-shadow: #DDCC33 0 0 5px 2px inset;
+ -webkit-box-shadow: #DDCC33 0 0 5px 2px inset;
+ box-shadow: #DDCC33 0 0 5px 2px inset;
+}
\ No newline at end of file
diff --git a/addons/web_editor/static/src/css/common.sass b/addons/web_editor/static/src/css/common.sass
new file mode 100644
index 00000000000..264f0166c49
--- /dev/null
+++ b/addons/web_editor/static/src/css/common.sass
@@ -0,0 +1,4 @@
+@import "compass/css3"
+@import "compass/css3/user-interface"
+@import "compass/css3/transition"
+@import "compass/support"
diff --git a/addons/web_editor/static/src/css/inline.css b/addons/web_editor/static/src/css/inline.css
new file mode 100644
index 00000000000..61e995fa3fa
--- /dev/null
+++ b/addons/web_editor/static/src/css/inline.css
@@ -0,0 +1,64 @@
+body {
+ background-color: #fff;
+}
+#editable_area p {
+ margin: initial;
+}
+#editable_area a {
+ color: inherit;
+}
+#editable_area a:hover {
+ color: inherit;
+}
+#editable_area {
+ margin-top: 0 !important;
+ font: inherit !important;
+ line-height: inherit !important;
+ color: inherit !important;
+}
+#oe_snippets ~ #wrapwrap #editable_area {
+ margin-bottom: 100px;
+}
+
+#wrapwrap > main > div:first-child {
+ max-width: 600px;
+ margin-left: auto;
+ margin-right: auto;
+ position: relative;
+}
+
+.editor_enable #wrapwrap > main > div:first-child {
+ border-top: 1px dashed #888;
+ margin-top: 50px;
+}
+
+.editor_enable #wrapwrap > main > div:first-child:before, #wrapwrap > main > div:first-child:after {
+ content: " ";
+ display: block;
+ height: 100%;
+ vertical-align: top;
+ position: fixed;
+ top: -4px;
+ overflow: hidden;
+ z-index: -1;
+ margin-top: 54px;
+}
+.editor_enable #wrapwrap > main > div:first-child[data-oe-placeholder]:empty:before {
+ content: " \00a0 " attr(data-oe-placeholder);
+}
+
+.editor_enable #wrapwrap > main > div:first-child:before {
+ border-left: 1px dashed #000;
+ opacity: 0.4;
+ margin-left: -5px;
+}
+.editor_enable #wrapwrap > main > div:first-child:after {
+ border-left: 1px dashed #000;
+ opacity: 0.4;
+ margin-left: 605px;
+ width: 0;
+}
+
+.note-color-palette .colorpicker {
+ display: none;
+}
diff --git a/addons/web_editor/static/src/css/snippets.css b/addons/web_editor/static/src/css/snippets.css
new file mode 100644
index 00000000000..ca2ba0c6deb
--- /dev/null
+++ b/addons/web_editor/static/src/css/snippets.css
@@ -0,0 +1,490 @@
+/* ---- SNIPPET EDITOR ---- {{{ */
+#oe_snippets #o_left_bar, #oe_snippets #o_arrow {
+ position: fixed;
+ top: 34px;
+ left: 0px;
+ bottom: 0;
+ background: #282828;
+ -moz-box-shadow: 0px 10px 10px -10px black inset;
+ -webkit-box-shadow: 0px 10px 10px -10px black inset;
+ box-shadow: 0px 10px 10px -10px black inset;
+}
+#oe_snippets #o_arrow {
+ z-index: 1009;
+ width: 24px;
+ opacity: 0.75;
+}
+#oe_snippets #o_arrow > * {
+ position: absolute;
+ color: white;
+}
+#oe_snippets #o_arrow div {
+ -moz-transform: translateX(-36%) rotate(-90deg);
+ -ms-transform: translateX(-36%) rotate(-90deg);
+ -webkit-transform: translateX(-36%) rotate(-90deg);
+ transform: translateX(-36%) rotate(-90deg);
+ top: 50%;
+ white-space: nowrap;
+}
+#oe_snippets #o_arrow .fa {
+ width: 100%;
+ left: 40%;
+}
+#oe_snippets #o_left_bar {
+ left: -228px;
+ z-index: 1010;
+ width: 208px;
+ -moz-transition: all 500ms ease;
+ -o-transition: all 500ms ease;
+ -webkit-transition: all 500ms ease;
+ transition: all 500ms ease;
+}
+#oe_snippets #o_left_bar ul {
+ text-align: center;
+ margin: 0;
+ padding: 6px 0;
+ list-style-type: none;
+}
+#oe_snippets #o_left_bar ul li {
+ display: inline-block;
+ padding: 0 3px;
+}
+#oe_snippets #o_left_bar ul li a {
+ color: #aaa;
+}
+#oe_snippets #o_left_bar ul li .fa-stack {
+ overflow: hidden;
+}
+#oe_snippets #o_left_bar ul li.active a.fa, #oe_snippets #o_left_bar ul li.active .fa-stack-1x {
+ color: #428bca;
+}
+#oe_snippets.o_open #o_left_bar {
+ left: 0;
+}
+#oe_snippets .o_panel {
+ margin: 0;
+ border: 0;
+ width: 208px;
+}
+#oe_snippets .o_panel_header {
+ background: #dff0d8;
+ padding: 1px 8px;
+ color: #414141;
+ text-align: center;
+}
+#oe_snippets .o_panel_header .fa-stack {
+ overflow: hidden;
+ font-size: 1em;
+}
+#oe_snippets .o_panel_body {
+ white-space: normal;
+ background: black;
+ padding: 7px 3px;
+}
+#oe_snippets .o_panel_body > div {
+ background: black;
+}
+#oe_snippets .o_panel_body > div label {
+ width: 44px;
+ color: #fff;
+ padding-left: 10px;
+}
+#oe_snippets .o_panel_body > div label div {
+ width: 100px;
+ text-align: center;
+ -moz-transform: translate(-39px, 44px);
+ -webkit-transform: translate(-39px, 44px);
+ transform: translate(-39px, 44px);
+ -moz-transform-origin: 50% 50%;
+ -ms-transform-origin: 50% 50%;
+ -webkit-transform-origin: 50% 50%;
+ transform-origin: 50% 50%;
+}
+
+.oe_snippet {
+ display: inline-block;
+ vertical-align: top;
+ width: 93px;
+ margin-left: 3px;
+ margin-right: 3px;
+ margin-top: 1px;
+ position: relative;
+ overflow: hidden;
+ -moz-user-select: -moz-none;
+ -ms-user-select: none;
+ -webkit-user-select: none;
+ user-select: none;
+ cursor: move;
+}
+.oe_snippet .oe_snippet_thumbnail {
+ text-align: center;
+ height: 100%;
+ background: transparent;
+ color: white;
+ position: relative;
+}
+.oe_snippet .oe_snippet_thumbnail:hover .oe_snippet_thumbnail_img {
+ -moz-transform: scale(0.95, 0.95);
+ -ms-transform: scale(0.95, 0.95);
+ -webkit-transform: scale(0.95, 0.95);
+ transform: scale(0.95, 0.95);
+}
+.oe_snippet .oe_snippet_thumbnail .oe_snippet_thumbnail_title {
+ font-size: 12px;
+ display: block;
+ text-shadow: 0 0 2px black;
+}
+.oe_snippet .oe_snippet_thumbnail .oe_snippet_thumbnail_img {
+ height: 74px;
+ width: 92px;
+ background-size: cover;
+ -moz-transform: scale(1, 1);
+ -ms-transform: scale(1, 1);
+ -webkit-transform: scale(1, 1);
+ transform: scale(1, 1);
+}
+.oe_snippet .oe_snippet_thumbnail span, .oe_snippet .oe_snippet_thumbnail div {
+ line-height: 18px;
+}
+.oe_snippet > :not(.oe_snippet_thumbnail) {
+ display: none !important;
+}
+.oe_snippet.disable .oe_snippet_thumbnail {
+ background-color: rgba(255, 0, 0, 0.75);
+}
+.oe_snippet.disable .oe_snippet_thumbnail_img {
+ opacity: 0.8;
+}
+
+#oe_snippets .oe_snippet_thumbnail {
+ background: #747474, url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjUwJSIgY3k9IjUwJSIgcj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwMDAwMCIgc3RvcC1vcGFjaXR5PSIwLjI1Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDAwMDAwIiBzdG9wLW9wYWNpdHk9IjAuNCIvPjwvcmFkaWFsR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
+ background: #747474, -moz-radial-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.4));
+ background: #747474, -webkit-radial-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.4));
+ background: #747474, radial-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.4));
+}
+
+/* ---- SNIPPETS DROP ZONES ---- {{{ */
+.oe_drop_zone.oe_insert {
+ display: block;
+ height: 48px;
+ margin: 0px;
+ margin-top: -4px;
+ margin-bottom: -44px;
+ -moz-transition: margin 250ms linear;
+ -o-transition: margin 250ms linear;
+ -webkit-transition: margin 250ms linear;
+ transition: margin 250ms linear;
+ width: 100%;
+ position: absolute;
+ z-index: 1000;
+}
+.oe_drop_zone.oe_insert:not(.oe_vertical):before {
+ content: "";
+ display: block;
+ border-top: dashed 2px rgba(209, 178, 255, 0.72);
+ position: relative;
+ top: 0px;
+}
+.oe_drop_zone.oe_insert.oe_hover:before {
+ border-top: dashed 2px rgba(116, 255, 161, 0.72);
+}
+.oe_drop_zone.oe_insert.oe_vertical {
+ width: 48px;
+ float: left;
+ position: relative;
+ margin: 0px -24px !important;
+}
+.oe_drop_zone.oe_insert.oe_vertical_text {
+ width: 48px;
+ float: none;
+ display: inline-block;
+ position: relative;
+ margin: 0px -24px !important;
+}
+.oe_drop_zone.oe_insert.oe_overlay {
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+ background: rgba(153, 0, 255, 0.5);
+}
+
+.oe_drop_zone, .oe_drop_zone_style {
+ border: none;
+ background: rgba(153, 0, 255, 0.3);
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+}
+.oe_drop_zone.oe_hover, .oe_drop_zone_style.oe_hover {
+ background: rgba(0, 255, 133, 0.3);
+ z-index: 1001;
+}
+
+.oe_drop_zone_style {
+ color: white;
+ height: 48px;
+ margin-bottom: 32px;
+ padding-top: 12px;
+}
+
+/* ---- SNIPPET MANIPULATOR ---- {{{ */
+.resize_editor_busy {
+ background-color: rgba(0, 0, 0, 0.3);
+}
+
+.oe_overlay {
+ display: none;
+ height: 0;
+ position: absolute;
+ background: transparent;
+ z-index: 1001;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+ -moz-transition: opacity 100ms linear;
+ -o-transition: opacity 100ms linear;
+ -webkit-transition: opacity 100ms linear;
+ transition: opacity 100ms linear;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oe_overlay.oe_active {
+ display: block;
+}
+.oe_overlay .oe_handle {
+ display: block !important;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 16px;
+ height: 16px;
+ margin: -2px;
+}
+.oe_overlay .oe_handle > div {
+ z-index: 1;
+ position: absolute;
+ border-style: dashed;
+ border-width: 1px;
+ border-color: #666666;
+ -moz-box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.5), 0px 0px 0px 1px rgba(255, 255, 255, 0.5) inset;
+ -webkit-box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.5), 0px 0px 0px 1px rgba(255, 255, 255, 0.5) inset;
+ box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.5), 0px 0px 0px 1px rgba(255, 255, 255, 0.5) inset;
+}
+.oe_overlay .oe_handle.e:before, .oe_overlay .oe_handle.w:before, .oe_overlay .oe_handle.s:before, .oe_overlay .oe_handle.n:before, .oe_overlay .oe_handle.size .oe_handle_button {
+ z-index: 2;
+ position: relative;
+ top: 50%;
+ left: 50%;
+ display: block;
+ background: white;
+ border: solid 1px rgba(0, 0, 0, 0.2);
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ border-radius: 5px;
+ width: 18px;
+ height: 18px;
+ margin: -9px;
+ padding-left: 1px;
+ font-size: 14px;
+ line-height: 14px;
+ font-family: FontAwesome;
+ color: rgba(0, 0, 0, 0.5);
+ -moz-transition: background 100ms linear;
+ -o-transition: background 100ms linear;
+ -webkit-transition: background 100ms linear;
+ transition: background 100ms linear;
+}
+.oe_overlay .oe_handle.e:hover:before, .oe_overlay .oe_handle.w:hover:before, .oe_overlay .oe_handle.s:hover:before, .oe_overlay .oe_handle.n:hover:before {
+ background: rgba(0, 0, 0, 0.5);
+ color: #fff;
+ -moz-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
+ -webkit-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
+ box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
+}
+.oe_overlay .oe_handle.e, .oe_overlay .oe_handle.w {
+ top: 4px;
+ height: 100%;
+}
+.oe_overlay .oe_handle.e:before, .oe_overlay .oe_handle.w:before {
+ content: "";
+ line-height: 16px;
+}
+.oe_overlay .oe_handle.e > div, .oe_overlay .oe_handle.w > div {
+ width: 0;
+ height: 100%;
+ top: 2px;
+ left: 8px;
+}
+.oe_overlay .oe_handle.e {
+ left: auto;
+ right: -6px;
+ cursor: w-resize;
+}
+.oe_overlay .oe_handle.w {
+ left: -6px;
+ cursor: e-resize;
+}
+.oe_overlay .oe_handle.s, .oe_overlay .oe_handle.n {
+ left: 2px;
+ width: 100%;
+}
+.oe_overlay .oe_handle.s:before, .oe_overlay .oe_handle.n:before {
+ content: "";
+ text-align: center;
+ padding: 1px;
+}
+.oe_overlay .oe_handle.s > div, .oe_overlay .oe_handle.n > div {
+ width: 100%;
+ height: 0;
+ top: 7px;
+ left: 1px;
+}
+.oe_overlay .oe_handle.s {
+ top: auto;
+ cursor: n-resize;
+}
+.oe_overlay .oe_handle.n {
+ cursor: s-resize;
+}
+.oe_overlay .oe_handle.n > div {
+ top: 5px;
+}
+.oe_overlay .oe_handle.size {
+ z-index: 3;
+ top: auto;
+ left: 50%;
+ bottom: -6px;
+ margin-left: -50px;
+}
+.oe_overlay .oe_handle.size .oe_handle_button {
+ position: relative;
+ z-index: 3;
+ text-align: center;
+ margin-left: -52px;
+ margin-top: -10px;
+ left: 0px;
+}
+.oe_overlay .oe_handle.size .oe_handle_button:hover {
+ background: rgba(30, 30, 30, 0.8);
+ color: #fff;
+ -moz-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
+ -webkit-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
+ box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
+}
+.oe_overlay .oe_handle.size .size {
+ position: absolute;
+ width: 64px;
+ cursor: row-resize;
+ top: 9px;
+ margin-left: 52px;
+ padding: 0 5px;
+}
+.oe_overlay .oe_handle.size .auto_size {
+ position: absolute;
+ width: 100px;
+ top: 9px;
+ cursor: pointer;
+}
+.oe_overlay .oe_handle.readonly {
+ cursor: auto !important;
+}
+.oe_overlay .oe_handle.readonly:before, .oe_overlay .oe_handle.readonly.size {
+ display: none !important;
+}
+.oe_overlay .icon.btn {
+ display: inline-block;
+}
+.oe_overlay .oe_overlay_options {
+ position: absolute;
+ left: 50% !important;
+ text-align: center;
+ top: -20px;
+ z-index: 1002;
+}
+.oe_overlay .oe_overlay_options > .btn-group {
+ left: -50%;
+ white-space: nowrap;
+}
+.oe_overlay .oe_overlay_options > .btn-group > a {
+ cursor: pointer;
+ display: inline-block;
+ float: none;
+ margin: 0 -3px;
+}
+.oe_overlay .oe_overlay_options > .btn-group > div {
+ float: left;
+}
+.oe_overlay .oe_overlay_options .btn, .oe_overlay .oe_overlay_options a {
+ cursor: pointer;
+}
+.oe_overlay .oe_overlay_options .dropdown {
+ display: inline-block;
+}
+.oe_overlay .oe_overlay_options .dropdown-menu {
+ text-align: left;
+ min-width: 180px;
+}
+.oe_overlay .oe_overlay_options .dropdown-menu select, .oe_overlay .oe_overlay_options .dropdown-menu input {
+ display: block;
+}
+.oe_overlay.block-w-left .w:before {
+ content: "" !important;
+}
+.oe_overlay.block-w-right .w:before {
+ content: "" !important;
+}
+.oe_overlay.block-w-left.block-w-right .w:before {
+ display: none !important;
+}
+.oe_overlay.block-e-left .e:before {
+ content: "" !important;
+}
+.oe_overlay.block-e-right .e:before {
+ content: "" !important;
+}
+.oe_overlay.block-e-left.block-e-right .e:before {
+ display: none !important;
+}
+.oe_overlay.block-s-top .s:before {
+ content: "" !important;
+}
+.oe_overlay.block-s-bottom .s:before {
+ content: "" !important;
+}
+.oe_overlay.block-n-top .n:before {
+ content: "" !important;
+}
+.oe_overlay.block-n-bottom .n:before {
+ content: "" !important;
+}
+.oe_overlay.o_keypress {
+ opacity: 0;
+ -webkit-transition: 0.8s;
+ -moz-transition: 0.8s;
+ -o-transition: 0.8s;
+ -transition: 0.8s;
+}
+
+.s-resize-important, .s-resize-important * {
+ cursor: s-resize !important;
+}
+
+.n-resize-important, .n-resize-important * {
+ cursor: n-resize !important;
+}
+
+.e-resize-important, .e-resize-important * {
+ cursor: e-resize !important;
+}
+
+.w-resize-important, .w-resize-important * {
+ cursor: w-resize !important;
+}
+
+.move-important, .move-important * {
+ cursor: move !important;
+}
\ No newline at end of file
diff --git a/addons/web_editor/static/src/css/snippets.sass b/addons/web_editor/static/src/css/snippets.sass
new file mode 100644
index 00000000000..752dabafc78
--- /dev/null
+++ b/addons/web_editor/static/src/css/snippets.sass
@@ -0,0 +1,376 @@
+@import "compass/css3"
+@import "compass/css3/user-interface"
+@import "compass/css3/transition"
+
+
+/* ---- SNIPPET EDITOR ---- {{{ */
+
+#oe_snippets
+ #o_left_bar, #o_arrow
+ position: fixed
+ top: 34px
+ left: 0px
+ bottom: 0
+ background: rgb(40,40,40)
+ +box-shadow(0px 10px 10px -10px black inset)
+ #o_arrow
+ z-index: 1009
+ width: 24px
+ opacity: 0.75
+ > *
+ position: absolute
+ color: white
+ div
+ @include transform( translateX(-36%) rotate(-90deg) )
+ top: 50%
+ white-space: nowrap
+ .fa
+ width: 100%
+ left: 40%
+ #o_left_bar
+ left: -228px
+ z-index: 1010
+ width: 208px
+ @include transition(all 500ms ease)
+ ul
+ text-align: center
+ margin: 0
+ padding: 6px 0
+ list-style-type: none
+ li
+ display: inline-block
+ padding: 0 3px
+ a
+ color: #aaa
+ .fa-stack
+ overflow: hidden
+ li.active a.fa, li.active .fa-stack-1x
+ color: #428bca
+
+ &.o_open #o_left_bar
+ left: 0
+ .o_panel
+ margin: 0
+ border: 0
+ width: 208px
+ .o_panel_header
+ background: #dff0d8
+ padding: 1px 8px
+ color: #414141
+ text-align: center
+ .fa-stack
+ overflow: hidden
+ font-size: 1em
+ .o_panel_body
+ white-space: normal
+ background: black
+ padding: 7px 3px
+ > div
+ background: rgb(0,0,0)
+ label
+ width: 44px
+ color: #fff
+ padding-left: 10px
+
+ div
+ width: 100px
+ text-align: center
+ @include transform( translate(-39px, 44px) , rotate(-90deg) )
+ @include transform-origin(50%)
+
+.oe_snippet
+ display: inline-block
+ vertical-align: top
+ width: 93px
+ margin-left: 3px
+ margin-right: 3px
+ margin-top: 1px
+ position: relative
+ overflow: hidden
+ +user-select(none)
+ cursor: move
+ .oe_snippet_thumbnail
+ text-align: center
+ height: 100%
+ background: transparent
+ color: white
+ position: relative
+ &:hover
+ .oe_snippet_thumbnail_img
+ @include transform( scale(.95,.95))
+ .oe_snippet_thumbnail_title
+ font-size: 12px
+ display: block
+ +text-shadow(0 0 2px rgb(0,0,0))
+ .oe_snippet_thumbnail_img
+ height: 74px
+ width: 92px
+ background-size: cover
+ @include transform( scale(1,1))
+ span, div
+ line-height: 18px
+ & > :not(.oe_snippet_thumbnail)
+ display: none !important
+ &.disable
+ .oe_snippet_thumbnail
+ background-color: rgba(255, 0, 0, 0.75)
+ .oe_snippet_thumbnail_img
+ opacity: 0.8
+#oe_snippets .oe_snippet_thumbnail
+ @include background(#747474, radial-gradient(rgba(0,0,0,0.25),rgba(0,0,0,0.4)))
+
+// }}}}
+
+/* ---- SNIPPETS DROP ZONES ---- {{{ */
+
+.oe_drop_zone.oe_insert
+ display: block
+ height: 48px
+ margin: 0px
+ margin-top: -4px
+ margin-bottom: -44px
+ @include transition(margin 250ms linear)
+ width: 100%
+ position: absolute
+ z-index: 1000
+ &:not(.oe_vertical):before
+ content: ""
+ display: block
+ border-top: dashed 2px rgba(209, 178, 255, 0.72)
+ position: relative
+ top: 0px
+ &.oe_hover:before
+ border-top: dashed 2px rgba(116, 255, 161, 0.72)
+ &.oe_vertical
+ width: 48px
+ float: left
+ position: relative
+ margin: 0px -24px !important
+ &.oe_vertical_text
+ width: 48px
+ float: none
+ display: inline-block
+ position: relative
+ margin: 0px -24px !important
+ &.oe_overlay
+ +border-radius(3px)
+ //@include background-image( repeating-linear-gradient(45deg, rgba(255,255,255,.1) ,rgba(255,255,255,.1) 35px, rgba(0,0,0,.1) 35px, rgba(0,0,0,.1) 75px))
+ //background-size: 100px 100px
+ background: rgba(153, 0, 255,.5)
+
+.oe_drop_zone, .oe_drop_zone_style
+ border: none
+ //@include background-image( repeating-linear-gradient(45deg, rgba(255,255,255,.1) ,rgba(255,255,255,.1) 35px, rgba(0,0,0,.1) 35px, rgba(0,0,0,.1) 75px))
+ //background-size: 100px 100px
+ background: rgba(153, 0, 255, .3)
+ +border-radius(4px)
+ &.oe_hover
+ background: rgba(0, 255, 133, .3)
+ z-index: 1001
+
+.oe_drop_zone_style
+ color: white
+ height: 48px
+ margin-bottom: 32px
+ padding-top: 12px
+
+// }}}
+
+/* ---- SNIPPET MANIPULATOR ---- {{{ */
+
+.resize_editor_busy
+ background-color: rgba(0,0,0,0.3)
+
+.oe_overlay
+ display: none
+ height: 0
+ position: absolute
+ background: transparent
+ z-index: 1001
+ //@include background-image( repeating-linear-gradient(45deg, rgba(255,255,255,.02) ,rgba(255,255,255,.02) 35px, rgba(0,0,0,.02) 35px, rgba(0,0,0,.02) 75px))
+ +border-radius(3px)
+ @include transition(opacity 100ms linear)
+ +box-sizing(border-box)
+ &.oe_active
+ display: block
+ .oe_handle
+ display: block !important
+ position: absolute
+ top: 50%
+ left: 50%
+ +box-sizing(border-box)
+ width: 16px
+ height: 16px
+ margin: -2px
+ > div
+ z-index: 1
+ position: absolute
+ border-style: dashed
+ border-width: 1px
+ border-color: #666666
+ +box-shadow(0px 0px 0px 1px rgba(255,255,255,0.5), 0px 0px 0px 1px rgba(255,255,255,0.5) inset)
+ &.e:before, &.w:before, &.s:before, &.n:before, &.size .oe_handle_button
+ z-index: 2
+ position: relative
+ top: 50%
+ left: 50%
+ display: block
+ background: rgba(255, 255, 255, 1)
+ border: solid 1px rgba(0, 0, 0, .2)
+ +border-radius(5px)
+ width: 18px
+ height: 18px
+ margin: -9px
+ padding-left: 1px
+ font-size: 14px
+ line-height: 14px
+ font-family: FontAwesome
+ color: rgba(0,0,0,.5)
+ @include transition(background 100ms linear)
+ &.e, &.w, &.s, &.n
+ &:hover:before
+ background: rgba(0, 0, 0, .5)
+ color: #fff
+ +box-shadow(0 0 5px 3px rgba(255,255,255,.7))
+ &.e, &.w
+ top: 4px
+ height: 100%
+ &:before
+ content: "\f07e"
+ line-height: 16px
+ > div
+ width: 0
+ height: 100%
+ top: 2px
+ left: 8px
+ &.e
+ left: auto
+ right: -6px
+ cursor: w-resize
+ &.w
+ left: -6px
+ cursor: e-resize
+ &.s, &.n
+ left: 2px
+ width: 100%
+ &:before
+ content: "\f07d"
+ text-align: center
+ padding: 1px
+ > div
+ width: 100%
+ height: 0
+ top: 7px
+ left: 1px
+ &.s
+ top: auto
+ cursor: n-resize
+ &.n
+ cursor: s-resize
+ > div
+ top: 5px
+ &.size
+ z-index: 3
+ top: auto
+ left: 50%
+ bottom: -6px
+ margin-left: -50px
+ .oe_handle_button
+ position: relative
+ z-index: 3
+ text-align: center
+ margin-left: -52px
+ margin-top: -10px
+ left: 0px
+ &:hover
+ background: rgba(30, 30, 30, .8)
+ color: #fff
+ +box-shadow(0 0 5px 3px rgba(255,255,255,.7))
+ .size
+ position: absolute
+ width: 64px
+ cursor: row-resize
+ top: 9px
+ margin-left: 52px
+ padding: 0 5px
+ .auto_size
+ position: absolute
+ width: 100px
+ top: 9px
+ cursor: pointer
+ &.readonly
+ cursor: auto !important
+ &:before, &.size
+ display: none !important
+
+ .icon.btn
+ display: inline-block
+
+ .oe_overlay_options
+ position: absolute
+ left: 50% !important
+ text-align: center
+ top: -20px
+ z-index: 1002
+ > .btn-group
+ left: -50%
+ white-space: nowrap
+ > a
+ cursor: pointer
+ display: inline-block
+ float: none
+ margin: 0 -3px
+ > div
+ float: left
+ .btn, a
+ cursor: pointer
+ .dropdown
+ display: inline-block
+ .dropdown-menu
+ text-align: left
+ min-width: 180px
+ .dropdown-menu select,.dropdown-menu input
+ display: block
+
+ &.block-w-left .w:before
+ content: "\F061" !important
+ &.block-w-right .w:before
+ content: "\F060" !important
+ &.block-w-left.block-w-right .w:before
+ display: none !important
+ &.block-e-left .e:before
+ content: "\F061" !important
+ &.block-e-right .e:before
+ content: "\F060" !important
+ &.block-e-left.block-e-right .e:before
+ display: none !important
+
+ &.block-s-top .s:before
+ content: "\F063" !important
+ &.block-s-bottom .s:before
+ content: "\f062" !important
+ &.block-n-top .n:before
+ content: "\F063" !important
+ &.block-n-bottom .n:before
+ content: "\f062" !important
+
+ &.o_keypress
+ opacity: 0
+ -webkit-transition: .8s
+ -moz-transition: .8s
+ -o-transition: .8s
+ -transition: .8s
+
+
+.s-resize-important, .s-resize-important *
+ cursor: s-resize !important
+.n-resize-important, .n-resize-important *
+ cursor: n-resize !important
+.e-resize-important, .e-resize-important *
+ cursor: e-resize !important
+.w-resize-important, .w-resize-important *
+ cursor: w-resize !important
+.move-important, .move-important *
+ cursor: move !important
+
+// }}}
diff --git a/addons/web_editor/static/src/css/web_editor.css b/addons/web_editor/static/src/css/web_editor.css
new file mode 100644
index 00000000000..e61e5244e40
--- /dev/null
+++ b/addons/web_editor/static/src/css/web_editor.css
@@ -0,0 +1,266 @@
+@charset "utf-8";
+/* ----- EDITOR ----- */
+.css_non_editable_mode_hidden {
+ display: none !important;
+}
+
+/* ----- Editor (for public user, like forum) ----- */
+.note-toolbar {
+ margin-left: 0 !important;
+}
+
+.note-popover .popover > .arrow {
+ display: none;
+}
+
+/* ----- GENERIC LAYOUTING HELPERS ---- */
+
+.hidden {
+ display: none !important;
+}
+
+/* table */
+table.table {
+ table-layout: fixed;
+}
+
+td {
+ min-width: 20px;
+}
+
+/* Vertical Spacing */
+.mt128 {
+ margin-top: 128px !important;
+}
+
+.mt92 {
+ margin-top: 92px !important;
+}
+
+.mt64 {
+ margin-top: 64px !important;
+}
+
+.mt48 {
+ margin-top: 48px !important;
+}
+
+.mt32 {
+ margin-top: 32px !important;
+}
+
+.mt16 {
+ margin-top: 16px !important;
+}
+
+.mt8 {
+ margin-top: 8px !important;
+}
+
+.mt4 {
+ margin-top: 4px !important;
+}
+
+.mt0 {
+ margin-top: 0px !important;
+}
+
+.mb128 {
+ margin-bottom: 128px !important;
+}
+
+.mb92 {
+ margin-bottom: 92px !important;
+}
+
+.mb64 {
+ margin-bottom: 64px !important;
+}
+
+.mb48 {
+ margin-bottom: 48px !important;
+}
+
+.mb32 {
+ margin-bottom: 32px !important;
+}
+
+.mb16 {
+ margin-bottom: 16px !important;
+}
+
+.mb8 {
+ margin-bottom: 8px !important;
+}
+
+.mb4 {
+ margin-bottom: 4px !important;
+}
+
+.mb0 {
+ margin-bottom: 0px !important;
+}
+
+.ml8 {
+ margin-left: 8px !important;
+}
+
+.ml4 {
+ margin-left: 4px !important;
+}
+
+.ml0 {
+ margin-left: 0px !important;
+}
+
+.mr8 {
+ margin-right: 8px !important;
+}
+
+.mr4 {
+ margin-right: 4px !important;
+}
+
+.mr0 {
+ margin-right: 0px !important;
+}
+
+/* Media */
+img.shadow {
+ -moz-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
+ -webkit-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
+ box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
+}
+
+img.padding-small, .img.padding-small, span.fa.padding-small, iframe.padding-small {
+ padding: 4px;
+}
+img.padding-medium, .img.padding-medium, span.fa.padding-medium, iframe.padding-medium {
+ padding: 8px;
+}
+img.padding-large, .img.padding-large, span.fa.padding-large, iframe.padding-large {
+ padding: 16px;
+}
+img.padding-xl, .img.padding-xl, span.fa.padding-xl, iframe.padding-xl {
+ padding: 32px;
+}
+
+div.media_iframe_video.padding-small iframe {
+ padding: 4px;
+}
+div.media_iframe_video.padding-medium iframe {
+ padding: 8px;
+}
+div.media_iframe_video.padding-large iframe {
+ padding: 16px;
+}
+div.media_iframe_video.padding-xl iframe {
+ padding: 32px;
+}
+
+/* font awsome */
+.fa-6x {
+ font-size: 6em;
+}
+
+.fa-7x {
+ font-size: 7em;
+}
+
+.fa-8x {
+ font-size: 8em;
+}
+
+.fa-9x {
+ font-size: 9em;
+}
+
+.fa-10x {
+ font-size: 10em;
+}
+
+.fa.center-block {
+ text-align: center;
+}
+
+/* video */
+div.media_iframe_video {
+ margin: 0 auto;
+ text-align: center;
+ position: relative;
+ overflow: hidden;
+}
+div.media_iframe_video iframe {
+ width: 100%;
+ height: 100%;
+ top: 0;
+ position: absolute;
+ margin: 0 auto;
+ margin-left: -50%;
+}
+div.media_iframe_video .media_iframe_video_size {
+ padding-bottom: 66.5%;
+ position: relative;
+ width: 100%;
+ height: 0;
+}
+div.media_iframe_video .css_editable_mode_display {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ display: none;
+ z-index: 2;
+}
+
+html[data-browser^="msie"] div.media_iframe_video iframe {
+ margin-left: 0;
+}
+
+/* Mobile view */
+@media (max-width: 768px) {
+ img:not(.cke_iframe), .media_iframe_video, span.fa, i.fa {
+ -webkit-transform: none !important;
+ -moz-transform: none !important;
+ -ms-transform: none !important;
+ -o-transform: none !important;
+ transform: none !important;
+ }
+}
+/* ---- fields --- */
+address .fa.fa-mobile-phone {
+ margin: 0 3px 0 2px;
+}
+address .fa.fa-file-text-o {
+ margin-right: 1px;
+}
+
+span[data-oe-type="monetary"] {
+ white-space: nowrap;
+}
+
+/* ---- MENU ---- {{{ */
+div.oe_menu_buttons {
+ top: -8px;
+ right: -8px;
+}
+
+ul.oe_menu_editor .fa-home {
+ display: none;
+}
+ul.oe_menu_editor > li:first-child > div > span > .fa-home {
+ display: block;
+}
+ul.oe_menu_editor .oe_menu_placeholder {
+ outline: 1px dashed #4183C4;
+}
+ul.oe_menu_editor ul {
+ list-style: none;
+}
+ul.oe_menu_editor li div {
+ cursor: move;
+}
+ul.oe_menu_editor .disclose {
+ cursor: pointer;
+ width: 10px;
+ display: none;
+}
diff --git a/addons/web_editor/static/src/css/web_editor.sass b/addons/web_editor/static/src/css/web_editor.sass
new file mode 100644
index 00000000000..75b4ad6c781
--- /dev/null
+++ b/addons/web_editor/static/src/css/web_editor.sass
@@ -0,0 +1,195 @@
+@charset "utf-8"
+@import "common"
+
+/* ----- EDITOR ----- */
+
+.css_non_editable_mode_hidden
+ display: none !important
+
+/* ----- Editor (for public user, like forum) ----- */
+
+.note-toolbar
+ margin-left: 0 !important
+
+.note-popover .popover > .arrow
+ display: none
+
+/* ----- GENERIC LAYOUTING HELPERS ---- */
+
+.hidden
+ display: none !important
+
+/* table */
+
+table.table
+ table-layout: fixed
+
+td
+ min-width: 20px
+
+/* Vertical Spacing */
+
+.mt128
+ margin-top: 128px !important
+.mt92
+ margin-top: 92px !important
+.mt64
+ margin-top: 64px !important
+.mt48
+ margin-top: 48px !important
+.mt32
+ margin-top: 32px !important
+.mt16
+ margin-top: 16px !important
+.mt8
+ margin-top: 8px !important
+.mt4
+ margin-top: 4px !important
+.mt0
+ margin-top: 0px !important
+
+.mb128
+ margin-bottom: 128px !important
+.mb92
+ margin-bottom: 92px !important
+.mb64
+ margin-bottom: 64px !important
+.mb48
+ margin-bottom: 48px !important
+.mb32
+ margin-bottom: 32px !important
+.mb16
+ margin-bottom: 16px !important
+.mb8
+ margin-bottom: 8px !important
+.mb4
+ margin-bottom: 4px !important
+.mb0
+ margin-bottom: 0px !important
+
+.ml8
+ margin-left: 8px !important
+.ml4
+ margin-left: 4px !important
+.ml0
+ margin-left: 0px !important
+
+.mr8
+ margin-right: 8px !important
+.mr4
+ margin-right: 4px !important
+.mr0
+ margin-right: 0px !important
+
+/* Media */
+
+img.shadow
+ +box-shadow(0px 3px 8px rgba(0, 0, 0, 0.2))
+
+img, .img, span.fa, iframe
+ &.padding-small
+ padding: 4px
+ &.padding-medium
+ padding: 8px
+ &.padding-large
+ padding: 16px
+ &.padding-xl
+ padding: 32px
+div.media_iframe_video
+ &.padding-small iframe
+ padding: 4px
+ &.padding-medium iframe
+ padding: 8px
+ &.padding-large iframe
+ padding: 16px
+ &.padding-xl iframe
+ padding: 32px
+
+/* font awsome */
+
+.fa-6x
+ font-size: 6em
+.fa-7x
+ font-size: 7em
+.fa-8x
+ font-size: 8em
+.fa-9x
+ font-size: 9em
+.fa-10x
+ font-size: 10em
+.fa.center-block
+ text-align: center
+
+/* video */
+
+div.media_iframe_video
+ margin: 0 auto
+ text-align: center
+ position: relative
+ overflow: hidden
+ iframe
+ width: 100%
+ height: 100%
+ top: 0
+ position: absolute
+ margin: 0 auto
+ margin-left: -50%
+ .media_iframe_video_size
+ padding-bottom: 66.5%
+ position: relative
+ width: 100%
+ height: 0
+ .css_editable_mode_display
+ position: absolute
+ width: 100%
+ height: 100%
+ display: none
+ z-index: 2
+
+html[data-browser^="msie"] div.media_iframe_video iframe
+ margin-left: 0
+
+/* Mobile view */
+
+@media (max-width: 768px)
+ img:not(.cke_iframe), .media_iframe_video, span.fa, i.fa
+ -webkit-transform: none !important
+ -moz-transform: none !important
+ -ms-transform: none !important
+ -o-transform: none !important
+ transform: none !important
+
+/* ---- fields --- */
+
+address
+ .fa.fa-mobile-phone
+ margin: 0 3px 0 2px
+ .fa.fa-file-text-o
+ margin-right: 1px
+
+span[data-oe-type="monetary"]
+ white-space: nowrap
+
+/* ---- MENU ---- {{{ */
+
+div.oe_menu_buttons
+ top: -8px
+ right: -8px
+
+ul.oe_menu_editor
+ .fa-home
+ display: none
+ > li:first-child > div > span > .fa-home
+ display: block
+ .oe_menu_placeholder
+ outline: 1px dashed #4183C4
+ ul
+ list-style: none
+ li div
+ cursor: move
+ .disclose
+ cursor: pointer
+ width: 10px
+ display: none
+
+// }}}
diff --git a/addons/website/static/src/img/drag_here.png b/addons/web_editor/static/src/img/drag_here.png
similarity index 100%
rename from addons/website/static/src/img/drag_here.png
rename to addons/web_editor/static/src/img/drag_here.png
diff --git a/addons/website/static/src/img/edit_here.png b/addons/web_editor/static/src/img/edit_here.png
similarity index 100%
rename from addons/website/static/src/img/edit_here.png
rename to addons/web_editor/static/src/img/edit_here.png
diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js
new file mode 100644
index 00000000000..5725cfc39f6
--- /dev/null
+++ b/addons/web_editor/static/src/js/backend.js
@@ -0,0 +1,322 @@
+odoo.define('web_editor.backend', function (require) {
+'use strict';
+
+var core = require('web.core');
+var session = require('web.session');
+var Model = require('web.DataModel');
+var common = require('web.form_common');
+var base = require('web_editor.base');
+var editor = require('web_editor.editor');
+var summernote = require('web_editor.summernote');
+var transcoder = require('web_editor.transcoder');
+
+var QWeb = core.qweb;
+var _t = core._t;
+
+/**
+ * FieldTextHtml Widget
+ * Intended for FieldText widgets meant to display HTML content. This
+ * widget will instantiate an iframe with the editor summernote improved by odoo
+ */
+
+var widget = common.AbstractField.extend(common.ReinitializeFieldMixin);
+
+var FieldTextHtmlSimple = widget.extend({
+ template: 'web_editor.FieldTextHtmlSimple',
+ _config: function () {
+ var self = this;
+ return {
+ 'focus': false,
+ 'height': 180,
+ 'toolbar': [
+ ['style', ['style']],
+ ['font', ['bold', 'italic', 'underline', 'clear']],
+ ['fontsize', ['fontsize']],
+ ['color', ['color']],
+ ['para', ['ul', 'ol', 'paragraph']],
+ ['table', ['table']],
+ ['insert', ['link', 'picture']],
+ ['history', ['undo', 'redo']]
+ ],
+ 'styleWithSpan': false,
+ 'inlinemedia': ['p'],
+ 'lang': "odoo",
+ 'onChange': function (value) {
+ self.internal_set_value(value);
+ self.trigger('changed_value');
+ }
+ };
+ },
+ initialize_content: function() {
+ var self = this;
+ this.$textarea = this.$("textarea").val(this.get('value') || "
");
+
+ if (this.get("effective_readonly")) {
+ this.$textarea.hide().after('');
+ } else {
+ this.$textarea.summernote(this._config());
+
+ if (this.field.translate && this.view) {
+ $(QWeb.render('web_editor.FieldTextHtml.button.translate', this))
+ .appendTo(this.$('.note-toolbar'))
+ .on('click', this.on_translate);
+ }
+
+ var reset = _.bind(this.reset_history, this);
+ this.view.on('load_record', this, reset);
+ setTimeout(reset, 0);
+ }
+ this.$content = this.$('.note-editable:first');
+
+ $(".oe-view-manager-content").on("scroll", function () {
+ $('.o_table_handler').remove();
+ });
+ this._super();
+ },
+ reset_history: function () {
+ var history = this.$content.data('NoteHistory');
+ if (history) {
+ history.reset();
+ self.$('.note-toolbar').find('button[data-event="undo"]').attr('disabled', true);
+ }
+ },
+ text_to_html: function (text) {
+ var value = text || "";
+ if (value.match(/^\s*$/)) {
+ value = '
';
+ } else {
+ value = ""+value.split(/
/).join("
")+"
";
+ value = value.replace('', '
').replace('
', '');
+ }
+ return value;
+ },
+ render_value: function() {
+ var value = this.get('value');
+ this.$textarea.val(value || '');
+ this.$content.html(this.text_to_html(value));
+ this.$content.focusInEnd();
+ var history = this.$content.data('NoteHistory');
+ if (history && history.recordUndo()) {
+ this.$('.note-toolbar').find('button[data-event="undo"]').attr('disabled', false);
+ }
+ },
+ is_false: function() {
+ return !this.get('value') || this.get('value') === "
" || !this.get('value').match(/\S/);
+ },
+ before_save: function() {
+ if (this.options['style-inline']) {
+ transcoder.class_to_style(this.$content);
+ transcoder.font_to_img(this.$content);
+ this.internal_set_value(this.$content.html());
+ }
+ },
+ destroy_content: function () {
+ $(".oe-view-manager-content").off("scroll");
+ this.$textarea.destroy();
+ this._super();
+ }
+});
+
+var FieldTextHtml = widget.extend({
+ template: 'web_editor.FieldTextHtml',
+ willStart: function () {
+ var self = this;
+ return new Model('res.lang').call("search_read", [[['code', '!=', 'en_US']], ["name", "code"]]).then(function (res) {
+ self.languages = res;
+ });
+ },
+ start: function () {
+ var self = this;
+
+ this.callback = _.uniqueId('FieldTextHtml_');
+ window.odoo[this.callback+"_editor"] = function (EditorBar) {
+ setTimeout(function () {
+ self.on_editor_loaded(EditorBar);
+ },0);
+ };
+ window.odoo[this.callback+"_content"] = function (EditorBar) {
+ self.on_content_loaded();
+ };
+ window.odoo[this.callback+"_updown"] = null;
+ window.odoo[this.callback+"_downup"] = function (value) {
+ self.internal_set_value(value);
+ self.trigger('changed_value');
+ self.resize();
+ };
+
+ // init jqery objects
+ this.$iframe = this.$el.find('iframe');
+ this.document = null;
+ this.$body = $();
+ this.$content = $();
+
+ this.$iframe.css('min-height', 'calc(100vh - 360px)');
+
+ // init resize
+ this.resize = function resize() {
+ if (self.get('effective_readonly')) { return; }
+ if ($("body").hasClass("o_form_FieldTextHtml_fullscreen")) {
+ self.$iframe.css('height', (document.body.clientHeight - self.$iframe.offset().top) + 'px');
+ } else {
+ self.$iframe.css("height", (self.$body.find("#oe_snippets").length ? 500 : 300) + "px");
+ }
+ };
+ $(window).on('resize', self.resize);
+
+ return this._super();
+ },
+ get_url: function (_attr) {
+ var src = this.options.editor_url ? this.options.editor_url+"?" : "/web_editor/field/html?";
+ var datarecord = this.view.get_fields_values();
+
+ var attr = {
+ 'model': this.view.model,
+ 'field': this.name,
+ 'res_id': datarecord.id || '',
+ 'callback': this.callback
+ };
+ _attr = _attr || {};
+
+ if (this.options['style-inline']) {
+ attr.inline_mode = 1;
+ }
+ if (this.options.snippets) {
+ attr.snippets = this.options.snippets;
+ }
+ if (!this.get("effective_readonly")) {
+ attr.enable_editor = 1;
+ }
+ if (this.field.translate) {
+ attr.translatable = 1;
+ }
+ if (session.debug) {
+ attr.debug = 1;
+ }
+
+ attr.lang = attr.enable_editor ? 'en_US' : this.session.user_context.lang;
+
+ for (var k in _attr) {
+ attr[k] = _attr[k];
+ }
+
+ for (var k in attr) {
+ if (attr[k] !== null) {
+ src += "&"+k+"="+(_.isBoolean(attr[k]) ? +attr[k] : attr[k]);
+ }
+ }
+
+ delete datarecord[this.name];
+ src += "&datarecord="+ encodeURIComponent(JSON.stringify(datarecord));
+
+ return src;
+ },
+ initialize_content: function() {
+ this.$el.closest('.modal-body').css('max-height', 'none');
+ this.$iframe = this.$el.find('iframe');
+ this.document = null;
+ this.$body = $();
+ this.$content = $();
+ this.editor = false;
+ window.odoo[this.callback+"_updown"] = null;
+ this.$iframe.attr("src", this.get_url());
+ },
+ on_content_loaded: function () {
+ var self = this;
+ this.document = this.$iframe.contents()[0];
+ this.$body = $("body", this.document);
+ this.$content = this.$body.find("#editable_area");
+ this.lang = this.$iframe.attr('src').match(/[?&]lang=([^&]+)/);
+ this.lang = this.lang ? this.lang[1] : this.view.dataset.context.lang;
+ this._dirty_flag = false;
+ this.render_value();
+ setTimeout(function () {
+ self.add_button();
+ setTimeout(self.resize,0);
+ }, 0);
+ },
+ on_editor_loaded: function (EditorBar) {
+ var self = this;
+ this.editor = EditorBar;
+ if (this.get('value') && window.odoo[self.callback+"_updown"] && !(this.$content.html()||"").length) {
+ this.render_value();
+ }
+ setTimeout(function () {
+ setTimeout(self.resize,0);
+ }, 0);
+ },
+ add_button: function () {
+ var self = this;
+ var $to = this.$body.find("#web_editor-top-edit, #wrapwrap").first();
+
+ $(QWeb.render('web_editor.FieldTextHtml.translate', {'widget': this}))
+ .appendTo($to)
+ .on('change', 'select', function () {
+ var lang = $(this).val();
+ var edit = !self.get("effective_readonly");
+ var trans = lang !== 'en_US';
+ self.$iframe.attr("src", self.get_url({
+ 'edit_translations': edit && trans,
+ 'enable_editor': edit && !trans,
+ 'lang': lang
+ }));
+ });
+
+ $(QWeb.render('web_editor.FieldTextHtml.fullscreen'))
+ .appendTo($to)
+ .on('click', '.o_fullscreen', function () {
+ $("body").toggleClass("o_form_FieldTextHtml_fullscreen");
+ var full = $("body").hasClass("o_form_FieldTextHtml_fullscreen");
+ self.$iframe.parents().toggleClass('o_form_fullscreen_ancestor', full);
+ self.resize();
+ });
+
+ this.$body.on('click', '[data-action="cancel"]', function (event) {
+ event.preventDefault();
+ self.initialize_content();
+ });
+ },
+ render_value: function() {
+ if (this.lang !== this.view.dataset.context.lang || this.$iframe.attr('src').match(/[?&]edit_translations=1/)) {
+ return;
+ }
+ var value = (this.get('value') || "").replace(/^]*>(\s*|
)<\/p>$/, '');
+ if (!this.$content) {
+ return;
+ }
+ if (!this.get("effective_readonly")) {
+ if(window.odoo[this.callback+"_updown"]) {
+ window.odoo[this.callback+"_updown"](value, this.view.get_fields_values(), this.name);
+ this.resize();
+ }
+ } else {
+ this.$content.html(value);
+ this.$iframe.css("height", (this.$body.height()+20) + "px");
+ }
+ },
+ is_false: function() {
+ return this.get('value') === false || !this.$content.html() || !this.$content.html().match(/\S/);
+ },
+ before_save: function () {
+ if (this.lang !== 'en_US' && this.$body.find('.o_dirty').length) {
+ this.internal_set_value( this.view.datarecord[this.name] );
+ this._dirty_flag = false;
+ return this.editor.save();
+ } else if (this._dirty_flag && this.editor && this.editor.buildingBlock) {
+ this.editor.buildingBlock.clean_for_save();
+ this.internal_set_value( this.$content.html() );
+ }
+ },
+ destroy: function () {
+ $(window).off('resize', self.resize);
+ delete window.odoo[this.callback+"_editor"];
+ delete window.odoo[this.callback+"_content"];
+ delete window.odoo[this.callback+"_updown"];
+ delete window.odoo[this.callback+"_downup"];
+ }
+});
+
+core.form_widget_registry
+ .add('html', FieldTextHtmlSimple)
+ .add('html_frame', FieldTextHtml);
+
+});
diff --git a/addons/web_editor/static/src/js/base.js b/addons/web_editor/static/src/js/base.js
new file mode 100644
index 00000000000..28b35722edd
--- /dev/null
+++ b/addons/web_editor/static/src/js/base.js
@@ -0,0 +1,85 @@
+odoo.define('web_editor.base', function (require) {
+"use strict";
+
+var core = require('web.core');
+var ajax = require('web.ajax');
+var qweb = core.qweb;
+var _t = core._t;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+var get_context = function (dict) {
+ var html = document.documentElement;
+ return _.extend({
+ 'lang': (html.getAttribute('lang') || '').replace('-', '_'),
+ }, dict);
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* ----------------------------------------------------
+ Async Ready and Template loading
+ ---------------------------------------------------- */
+var dom_ready = $.Deferred();
+$(document).ready(function () {
+ dom_ready.resolve();
+ // fix for ie
+ if($.fn.placeholder) $('input, textarea').placeholder();
+});
+
+
+// todo: remove and load a bundle of translated templates
+var all_ready;
+var ready = function() {
+ if (!all_ready) {
+ all_ready = $.when(dom_ready, ajax.loadXML()).then(translations);
+ } else if(all_ready.state() === "resolved") { // can add async template
+ all_ready = $.when(dom_ready, ajax.loadXML());
+ }
+ return all_ready;
+};
+
+function translations() {
+ function translate_node(node){
+ if(node.nodeType === 3) { // TEXT_NODE
+ if(node.nodeValue.match(/\S/)){
+ var space = node.nodeValue.match(/^([\s]*)([\s\S]*?)([\s]*)$/);
+ node.nodeValue = space[1] + $.trim(_t(space[2])) + space[3];
+ }
+ }
+ else if(node.nodeType === 1 && node.hasChildNodes()) { // ELEMENT_NODE
+ _.each(node.childNodes, translate_node);
+ }
+ }
+ return ajax.jsonRpc(data.url_translations, 'call', {
+ 'mods': ['web_editor'],
+ 'lang': data.get_context().lang
+ })
+ .then(function(trans) {
+ _t.database.set_bundle(trans);
+ }).then(function () {
+ var keys = _.keys(qweb.templates);
+ for (var i = 0; i < keys.length; i++){
+ translate_node(qweb.templates[keys[i]]);
+ }
+ }).promise();
+}
+
+var reload = function () {
+ location.hash = "scrollTop=" + window.document.body.scrollTop;
+ if (location.search.indexOf("enable_editor") > -1) {
+ window.location.href = window.location.href.replace(/enable_editor(=[^&]*)?/g, '');
+ } else {
+ window.location.reload();
+ }
+};
+
+var data = {
+ 'get_context': get_context,
+ 'url_translations': '/web/webclient/translations',
+ 'dom_ready': dom_ready,
+ 'ready': ready,
+ 'reload': reload
+};
+return data;
+
+});
diff --git a/addons/web_editor/static/src/js/editor.js b/addons/web_editor/static/src/js/editor.js
new file mode 100644
index 00000000000..b09a69e3818
--- /dev/null
+++ b/addons/web_editor/static/src/js/editor.js
@@ -0,0 +1,164 @@
+odoo.define('web_editor.editor', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var Widget = require('web.Widget');
+var core = require('web.core');
+var base = require('web_editor.base');
+var rte = require('web_editor.rte');
+
+var qweb = core.qweb;
+var _t = core._t;
+var editor = {};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+editor.dummy = function () {return true;}; // used for snippets, options...
+
+editor.editable = !!$('html').data('editable');
+
+ajax.loadXML('/web_editor/static/src/xml/editor.xml', qweb);
+
+base.dom_ready.done(function () {
+ $(document).on('click', '.note-editable', function (ev) {
+ ev.preventDefault();
+ });
+
+ $(document).on('submit', '.note-editable form .btn', function (ev) {
+ // Disable form submition in editable mode
+ ev.preventDefault();
+ });
+
+ $(document).on('hide.bs.dropdown', '.dropdown', function (ev) {
+ // Prevent dropdown closing when a contenteditable children is focused
+ if (ev.originalEvent
+ && $(ev.target).has(ev.originalEvent.target).length
+ && $(ev.originalEvent.target).is('[contenteditable]')) {
+ ev.preventDefault();
+ }
+ });
+});
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+editor.reload = function () {
+ location.hash = "scrollTop=" + window.document.body.scrollTop;
+ if (location.search.indexOf("enable_editor") > -1) {
+ window.location.href = window.location.href.replace(/&?enable_editor(=[^&]*)?/g, '');
+ } else {
+ window.location.reload();
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/* ----- TOP EDITOR BAR FOR ADMIN ---- */
+
+base.ready().then(function () {
+ if (editor.editable && location.search.indexOf("enable_editor") >= 0 && !editor.no_editor) {
+ editor.editor_bar = new editor.Class();
+ editor.editor_bar.prependTo(document.body);
+ }
+});
+
+editor.Class = Widget.extend({
+ template: 'web_editor.editorbar',
+ events: {
+ 'click button[data-action=save]': 'save',
+ 'click a[data-action=cancel]': 'cancel',
+ },
+ init: function(parent) {
+ var self = this;
+ var res = this._super.apply(this, arguments);
+ this.parent = parent;
+ this.rte = new rte.Class(this);
+ this.rte.on('rte:start', this, function () {
+ self.trigger('rte:start');
+ });
+ return res;
+ },
+ start: function() {
+ var self = this;
+
+ this.$('button[data-action=save]').prop('disabled', true);
+ $('.dropdown-toggle').dropdown();
+
+ this.display_placeholder();
+
+ this.rte.on('change', this, this.proxy('rte_changed'));
+ this.rte.start();
+
+ var flag = false;
+ window.onbeforeunload = function(event) {
+ if ($('.o_editable.o_dirty').length && !flag) {
+ flag = true;
+ setTimeout(function () {flag=false;},0);
+ return _t('This document is not saved!');
+ }
+ };
+ return this._super();
+ },
+ display_placeholder: function () {
+ var $area = $("#wrapwrap").find("[data-oe-model] .oe_structure.oe_empty, [data-oe-model].oe_structure.oe_empty, [data-oe-type=html]")
+ .addClass("oe_empty")
+ .attr("data-oe-placeholder", _t("Press The Top-Left Edit Button"));
+
+ this.on('rte:start', this, function () {
+ $area.attr("data-oe-placeholder", _t("Write Your Text Here"));
+ });
+ this.on("snippets:ready", this, function () {
+ if ($("body").hasClass("editor_has_snippets")) {
+ $area.attr("data-oe-placeholder", _t("Write Your Text or Drag a Block Here"));
+ }
+ });
+
+ $(document).on("keyup", function (event) {
+ if((event.keyCode === 8 || event.keyCode === 46)) {
+ var $target = $(event.target).closest(".o_editable");
+ if(!$target.is(":has(*:not(p):not(br))") && !$target.text().match(/\S/)) {
+ $target.empty();
+ }
+ }
+ });
+ },
+ rte_changed: function () {
+ // todo: use !rte.history.getEditableHasUndo().length (but phantomjs unknow error)
+ this.$('button[data-action=save]').prop('disabled', !$('.o_dirty').length);
+ },
+ save: function () {
+ return this.rte.save().then(function () {
+ editor.reload();
+ });
+ },
+ /**
+ * Saves an RTE content, which always corresponds to a view section (?).
+ */
+ save_without_reload: function () {
+ return this.rte.save();
+ },
+ cancel: function () {
+ new $.Deferred(function (d) {
+ if (!rte.history.getEditableHasUndo().length) {
+ return d.resolve();
+ }
+ var $dialog = $(qweb.render('web_editor.discard')).appendTo(document.body);
+ $dialog.on('click', '.btn-danger', function () {
+ d.resolve();
+ }).on('hidden.bs.modal', function () {
+ d.reject();
+ });
+ d.always(function () {
+ $dialog.remove();
+ });
+ $dialog.modal('show');
+ }).then(function () {
+ window.onbeforeunload = null;
+ editor.reload();
+ });
+ },
+});
+
+return editor;
+
+});
+
diff --git a/addons/web_editor/static/src/js/iframe.js b/addons/web_editor/static/src/js/iframe.js
new file mode 100644
index 00000000000..8b287064b7c
--- /dev/null
+++ b/addons/web_editor/static/src/js/iframe.js
@@ -0,0 +1,78 @@
+odoo.define('web_editor.iframe', function (require) {
+'use strict';
+
+var editor = require('web_editor.editor');
+var translator = require('web_editor.translate');
+var rte = require('web_editor.rte');
+var snippet_editor = require('web_editor.snippet.editor');
+
+window.top.odoo[callback+"_updown"] = function (value, fields_values, field_name) {
+ var $editable = $("#editable_area");
+ if(value !== $editable.prop("innerHTML")) {
+ if ($('body').hasClass('editor_enable')) {
+ if (value !== fields_values[field_name]) {
+ rte.history.recordUndo($editable);
+ }
+ snippet_editor.instance.make_active(false);
+ }
+
+ $editable.html(value);
+
+ if ($('body').hasClass('editor_enable') && value !== fields_values[field_name]) {
+ $editable.trigger("content_changed");
+ }
+ }
+};
+
+editor.Class.include({
+ start: function () {
+ this.on('rte:start', this, function () {
+ this.$('form').hide();
+
+ if (window.top.odoo[callback+"_editor"]) {
+ window.top.odoo[callback+"_editor"](this);
+ }
+
+ var $editable = $("#editable_area");
+ setTimeout(function () {
+ $($editable.find("*").filter(function () {return !this.children.length;}).first()[0] || $editable)
+ .focusIn().trigger("mousedown").trigger("keyup");
+ },0);
+
+ $editable.on('content_changed', this, function () {
+ if (window.top.odoo[callback+"_downup"]) {
+ window.top.odoo[callback+"_downup"]($editable.prop('innerHTML'));
+ }
+ });
+ });
+
+ this.on("snippets:ready", this, function () {
+ $(window.top).trigger("resize");
+ });
+ return this._super();
+ }
+});
+
+snippet_editor.Class.include({
+ _get_snippet_url: function () {
+ return snippets_url;
+ }
+});
+
+translator.Class.include({
+ start: function () {
+ var self = this;
+ return this._super().then(function () {
+ $('button[data-action=save]').hide();
+ if (window.top.odoo[callback+"_editor"]) {
+ window.top.odoo[callback+"_editor"](self);
+ }
+ });
+ }
+});
+
+});
+
+$(function () {
+ odoo.init();
+});
diff --git a/addons/web_editor/static/src/js/inline.js b/addons/web_editor/static/src/js/inline.js
new file mode 100644
index 00000000000..c5fd2cfd924
--- /dev/null
+++ b/addons/web_editor/static/src/js/inline.js
@@ -0,0 +1,77 @@
+odoo.define('web_editor.inline', function (require) {
+'use strict';
+
+var editor = require('web_editor.editor');
+var rte = require('web_editor.rte');
+var widget = require('web_editor.widget');
+var transcoder = require('web_editor.transcoder');
+var snippet_editor = require('web_editor.snippet.editor');
+
+widget.MediaDialog.include({
+ start: function () {
+ this._super();
+ this.$('[href="#editor-media-video"]').addClass('hidden');
+ }
+});
+
+editor.Class.include({
+ start: function () {
+ var self = this;
+ if (location.search.indexOf("enable_editor") !== -1) {
+ this.on('rte:start', this, function () {
+ // move the caret at the end of the text when click after all content
+ $("#wrapwrap").on('click', function (event) {
+ if ($(event.target).is("#wrapwrap") || $(event.target).is("#editable_area:empty")) {
+ setTimeout(function () {
+ var node = $("#editable_area *")
+ .filter(function () { return this.textContent.match(/\S|\u00A0/); })
+ .add($("#editable_area"))
+ .last()[0];
+ $.summernote.core.range.create(node, $.summernote.core.dom.nodeLength(node)).select();
+ },0);
+ }
+ });
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+});
+
+snippet_editor.Class.include({
+ start: function () {
+ var self = this;
+ this._super();
+ setTimeout(function () {
+ var $editable = $("#editable_area");
+ transcoder.img_to_font($editable);
+ transcoder.style_to_class($editable);
+ });
+ },
+ clean_for_save: function () {
+ this._super();
+ var $editable = $("#editable_area");
+ transcoder.class_to_style($editable);
+ transcoder.font_to_img($editable);
+ },
+
+});
+
+window.top.odoo[callback+"_updown"] = function (value, fields_values) {
+ var $editable = $("#editable_area");
+ value = value || "";
+ if (value.indexOf('on_change_model_and_list') === -1 && value !== $editable.html()) {
+ rte.history.recordUndo($editable, null, true);
+ if (snippet_editor.instance) {
+ snippet_editor.instance.make_active(false);
+ }
+
+ $editable.html(value);
+
+ transcoder.img_to_font($editable);
+ transcoder.style_to_class($editable);
+ } else {
+ $editable.trigger("content_changed");
+ }
+};
+
+});
\ No newline at end of file
diff --git a/addons/web_editor/static/src/js/rte.js b/addons/web_editor/static/src/js/rte.js
new file mode 100644
index 00000000000..c8d5655cbd6
--- /dev/null
+++ b/addons/web_editor/static/src/js/rte.js
@@ -0,0 +1,597 @@
+odoo.define('web_editor.rte', function (require) {
+'use strict';
+
+var ajax = require('web.ajax');
+var core = require('web.core');
+var utils = require('web.utils');
+var Widget = require('web.Widget');
+var summernote = require('web_editor.summernote');
+var base = require('web_editor.base');
+
+var _t = core._t;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* Summernote Lib (neek change to make accessible: method and object) */
+
+var dom = summernote.core.dom;
+var range = summernote.core.range;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* Change History to have a global History for all summernote instances */
+
+var History = function History ($editable) {
+ var aUndo = [];
+ var pos = 0;
+
+ this.makeSnap = function (event, rng) {
+ rng = rng || range.create();
+ var elEditable = $(rng.sc).closest('.o_editable')[0];
+ if (!elEditable) {
+ return false;
+ }
+ return {
+ event: event,
+ editable: elEditable,
+ contents: elEditable.innerHTML,
+ bookmark: rng && rng.bookmark(elEditable),
+ scrollTop: $(elEditable).scrollTop()
+ };
+ };
+
+ this.applySnap = function (oSnap) {
+ var $editable = $(oSnap.editable);
+
+ if (!!document.documentMode) {
+ $editable.removeAttr("contentEditable").removeProp("contentEditable");
+ }
+
+ $editable.html(oSnap.contents).scrollTop(oSnap.scrollTop);
+ $(".oe_overlay").remove();
+ $(".note-control-selection").hide();
+
+ $editable.trigger("content_changed");
+
+ if (!oSnap.bookmark || oSnap.event === "blur") {
+ return;
+ }
+
+ try {
+ var r = oSnap.editable.innerHTML === "" ? range.create(oSnap.editable, 0) : range.createFromBookmark(oSnap.editable, oSnap.bookmark);
+ r.select();
+ } catch(e) {
+ console.error(e);
+ return;
+ }
+
+ $(document).trigger("click");
+ $(".o_editable *").filter(function () {
+ var $el = $(this);
+ if($el.data('snippet-editor')) {
+ $el.removeData();
+ }
+ });
+
+
+ setTimeout(function () {
+ var target = dom.isBR(r.sc) ? r.sc.parentNode : dom.node(r.sc);
+ if (!target) {
+ return;
+ }
+
+ $editable.trigger("applySnap");
+
+ var evt = document.createEvent("MouseEvents");
+ evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target);
+ target.dispatchEvent(evt);
+
+ $editable.trigger("keyup");
+ },0);
+ };
+
+ this.undo = function () {
+ if (!pos) { return; }
+ var _toSnap = toSnap;
+ if (_toSnap) {
+ this.saveSnap();
+ }
+ if (!aUndo[pos] && (!aUndo[pos] || aUndo[pos].event !== 'undo')) {
+ var temp = this.makeSnap('undo');
+ if (temp && (!pos || temp.contents !== aUndo[pos-1].contents)) {
+ aUndo[pos] = temp;
+ } else {
+ pos--;
+ }
+ } else if (_toSnap) {
+ pos--;
+ }
+ this.applySnap(aUndo[Math.max(--pos,0)]);
+ while (pos && (aUndo[pos].event === "blur" || aUndo[pos].event === "activate")) {
+ this.applySnap(aUndo[--pos]);
+ }
+ };
+ this.hasUndo = function () {
+ return (toSnap && (toSnap.event !== "blur" && toSnap.event !== "activate" && toSnap.event !== "undo")) ||
+ !!_.find(aUndo.slice(0, pos+1), function (undo) {
+ return undo.event !== "blur" && undo.event !== "activate" && undo.event !== "undo";
+ });
+ };
+
+ this.getEditableHasUndo = function () {
+ var editable = [];
+ if ((toSnap && (toSnap.event !== "blur" && toSnap.event !== "activate" && toSnap.event !== "undo"))) {
+ editable.push(toSnap.editable);
+ }
+ _.each(aUndo.slice(0, pos+1), function (undo) {
+ if (undo.event !== "blur" && undo.event !== "activate" && undo.event !== "undo") {
+ editable.push(undo.editable);
+ }
+ });
+ return _.uniq(editable);
+ };
+
+ this.redo = function () {
+ if (!aUndo[pos+1]) { return; }
+ this.applySnap(aUndo[++pos]);
+ while (aUndo[pos+1] && (aUndo[pos].event === "blur" || aUndo[pos].event === "activate" || aUndo[pos].event === "undo")) {
+ this.applySnap(aUndo[Math.max(++pos,aUndo.length-1)]);
+ }
+ };
+ this.hasRedo = function () {
+ return aUndo.length > pos+1;
+ };
+
+ var toSnap, split;
+ this.recordUndo = function ($editable, event, internal_history) {
+ var self = this;
+ if (!$editable) {
+ $editable = $(range.create().sc).closest(".o_editable");
+ }
+
+ if (aUndo[pos] && (event === "applySnap" || event === "activate")) {
+ return;
+ }
+
+ if (!internal_history) {
+ if (!event || !toSnap || !aUndo[pos-1] || toSnap.event === "activate") { // don't trigger change for all keypress
+ setTimeout(function () {
+ $editable.trigger("content_changed");
+ },0);
+ }
+ }
+
+ if (aUndo[pos]) {
+ pos = Math.min(pos, aUndo.length);
+ aUndo.splice(Math.max(pos,1), aUndo.length);
+ }
+
+ // => make a snap when the user change editable zone (because: don't make snap for each keydown)
+ if (toSnap && (toSnap.split || !event || toSnap.event !== event || toSnap.editable !== $editable[0])) {
+ this.saveSnap();
+ }
+
+ if (pos && aUndo[pos-1].editable !== $editable[0]) {
+ var snap = this.makeSnap('blur', range.create(aUndo[pos-1].editable, 0));
+ pos++;
+ aUndo.push(snap);
+ }
+
+ if (range.create()) {
+ toSnap = self.makeSnap(event);
+ } else {
+ toSnap = false;
+ }
+ };
+
+ this.splitNext = function () {
+ if (toSnap) {
+ toSnap.split = true;
+ }
+ };
+
+ this.saveSnap = function () {
+ if (toSnap) {
+ if (!aUndo[pos]) {
+ pos++;
+ }
+ aUndo.push(toSnap);
+ delete toSnap.split;
+ toSnap = null;
+ }
+ };
+};
+var history = new History();
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+// add focusIn to jQuery to allow to move caret into a div of a contentEditable area
+
+$.extend($.expr[':'],{
+ o_editable: function(node,i,m){
+ while (node) {
+ if (node.className) {
+ if (node.className.indexOf('o_not_editable')!==-1 ) {
+ return false;
+ }
+ if (node.className.indexOf('o_editable')!==-1 ) {
+ return true;
+ }
+ }
+ node = node.parentNode;
+ }
+ return false;
+ }
+});
+
+$.fn.extend({
+ focusIn: function () {
+ if (this.length) {
+ range.create(dom.firstChild(this[0]), 0).select();
+ }
+ return this;
+ },
+ focusInEnd: function () {
+ if (this.length) {
+ var last = dom.lastChild(this[0]);
+ range.create(last, dom.nodeLength(last)).select();
+ }
+ return this;
+ },
+ selectContent: function () {
+ if (this.length) {
+ var next = dom.lastChild(this[0]);
+ range.create(dom.firstChild(this[0]), 0, next, next.textContent.length).select();
+ }
+ return this;
+ }
+});
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/* ----- RICH TEXT EDITOR ---- */
+
+var RTE = Widget.extend({
+ init: function (EditorBar, config) {
+ var self = this;
+ this.EditorBar = EditorBar;
+ data.rte = this;
+ this._super.apply(this, arguments);
+
+ this.init_bootstrap_carousel = $.fn.carousel;
+ this.edit_bootstrap_carousel = function () {
+ var res = self.init_bootstrap_carousel.apply(this, arguments);
+ // off bootstrap keydown event to remove event.preventDefault()
+ // and allow to change cursor position
+ $(this).off('keydown.bs.carousel');
+ return res;
+ };
+
+ if (config) {
+ this.config = config;
+ }
+ },
+ /**
+ * Add a record undo to history
+ * @param {DOM} target where the dom is changed is editable zone
+ */
+ historyRecordUndo: function ($target, event, internal_history) {
+ var rng = range.create();
+ var $editable = $(rng && rng.sc).closest(".o_editable");
+ if (!rng || !$editable.length) {
+ $editable = $($target).closest(".o_editable");
+ rng = range.create($target.closest("*")[0],0);
+ } else {
+ rng = $editable.data('range') || rng;
+ }
+ rng.select();
+ history.recordUndo($editable, event, internal_history);
+ },
+ /**
+ * Makes the page editable
+ *
+ * @param {Boolean} [restart=false] in case the edition was already set
+ * up once and is being re-enabled.
+ * @returns {$.Deferred} deferred indicating when the RTE is ready
+ */
+ start: function () {
+ var self = this;
+
+ this.saving_mutex = new utils.Mutex();
+
+ $.fn.carousel = this.edit_bootstrap_carousel;
+
+ $(document).on('keydown', this, this.onKeydown);
+ $(document).on('mousedown activate', this, this.onMousedown);
+ $(document).on('mouseup', this, this.onMouseup);
+
+ $('.o_not_editable').attr("contentEditable", false);
+
+ var $editable = this.editable();
+
+ $editable.addClass('o_editable').data('rte', self);
+
+ $editable.each(function () {
+ var $node = $(this);
+
+ // add class to display inline-block for empty t-field
+ if(window.getComputedStyle(this).display === "inline" && $node.data('oe-type') !== "image") {
+ $node.addClass('o_is_inline_editable');
+ }
+
+ // start element observation
+ $node.one('content_changed', function () {
+ $(this).addClass('o_dirty');
+ });
+
+ $node.data('initInnerHTML', $node.html());
+ });
+
+ $(document).on('content_changed', '.o_editable', function () {
+ self.trigger('change', this);
+ });
+
+ $('#wrapwrap, .o_editable').on('click', '*', this, this.onClick);
+
+ $('body').addClass("editor_enable");
+
+ $(document)
+ .tooltip({
+ selector: '[data-oe-readonly]',
+ container: 'body',
+ trigger: 'hover',
+ delay: { "show": 1000, "hide": 100 },
+ placement: 'bottom',
+ title: _t("Readonly field")
+ })
+ .on('click', function () {
+ $(this).tooltip('hide');
+ });
+
+ $(document).trigger('mousedown');
+ self.trigger('rte:start');
+ },
+
+ save: function (context) {
+ var self = this;
+
+ this.__saved = {}; // list of allready saved views and data
+
+ var editables = history.getEditableHasUndo();
+
+ var defs = $('.o_dirty')
+ .removeAttr('contentEditable')
+ .removeClass('o_dirty oe_carlos_danger o_is_inline_editable')
+ .map(function () {
+ var $el = $(this);
+
+ $el.find('[class]').filter(function () {
+ if (!this.className.match(/\S/)) {
+ this.removeAttribute("class");
+ }
+ });
+
+ // TODO: Add a queue with concurrency limit in webclient
+ // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
+ return self.saving_mutex.exec(function () {
+ return self.saveElement($el, context)
+ .then(undefined, function (thing, response) {
+ // because ckeditor regenerates all the dom,
+ // we can't just setup the popover here as
+ // everything will be destroyed by the DOM
+ // regeneration. Add markings instead, and
+ // returns a new rejection with all relevant
+ // info
+ var id = _.uniqueId('carlos_danger_');
+ $el.addClass('o_dirty oe_carlos_danger ' + id);
+ return $.Deferred().reject({
+ id: id,
+ error: response.data,
+ });
+ });
+ });
+ }).get();
+
+ return $.when.apply(null, defs).then(function () {
+ window.onbeforeunload = null;
+ }, function (failed) {
+ // If there were errors, re-enable edition
+ self.stop();
+ self.start();
+ // jquery's deferred being a pain in the ass
+ if (!_.isArray(failed)) { failed = [failed]; }
+
+ _(failed).each(function (failure) {
+ var html = failure.error.exception_type === "except_osv";
+ if (html) {
+ var msg = $("
").text(failure.error.message).html();
+ var data = msg.substring(3,msg.length-2).split(/', u'/);
+ failure.error.message = '' + data[0] + '' + data[1];
+ }
+ $('.o_editable.' + failure.id)
+ .removeClass(failure.id)
+ .popover({
+ html: html,
+ trigger: 'hover',
+ content: failure.error.message,
+ placement: 'auto top',
+ })
+ // Force-show popovers so users will notice them.
+ .popover('show');
+ });
+ });
+ },
+
+ saveElement: function ($el, context) {
+ // remove multi edition
+ if ($el.data('oe-model')) {
+ var key = $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression');
+ if (this.__saved[key]) return true;
+ this.__saved[key] = true;
+ }
+
+ return ajax.jsonRpc('/web/dataset/call', 'call', {
+ model: 'ir.ui.view',
+ method: 'save',
+ args: [
+ $el.data('oe-id'),
+ $el.prop('outerHTML'),
+ $el.data('oe-xpath') || null,
+ context || base.get_context()
+ ],
+ });
+ },
+
+ stop: function () {
+ if (this.$last) {
+ this.$last.destroy();
+ this.$last = null;
+ }
+
+ $.fn.carousel = this.init_bootstrap_carousel;
+
+ $(document).off('keydown', this.onKeydown);
+ $(document).off('mousedown applySnap', this.onMousedown);
+ $(document).off('mouseup', this.onMouseup);
+ $('.o_not_editable').removeAttr("contentEditable");
+ $(document).off('content_changed').removeClass('o_is_inline_editable').removeData('rte');
+ $('#wrapwrap, .o_editable').off('click', this.onClick);
+ $(document).tooltip('destroy');
+ $('body').removeClass("editor_enable");
+ this.trigger('rte:stop');
+ },
+
+ cancel: function () {
+ $('.o_editable').each(function () {
+ var $node = $(this);
+ $node.data('initInnerHTML', $node.html());
+ });
+ this.stop();
+ },
+
+ onClick: function (event) {
+ event.preventDefault();
+ },
+
+ // handler for cancel editor
+ onKeydown: function (event) {
+ if (event.keyCode === 27 && !$('.modal-content:visible').length) {
+ setTimeout(function () {
+ $('#editor-top-navbar [data-action="cancel"]').click();
+ var $modal = $('.modal-content > .modal-body').parents(".modal:first");
+ $modal.off('keyup.dismiss.bs.modal');
+ setTimeout(function () {
+ $modal.on('keyup.dismiss.bs.modal', function () {
+ $(this).modal('hide');
+ });
+ },500);
+ },0);
+ }
+ },
+
+ // activate editor
+ onMousedown: function (event) {
+ var $target = $(event.target);
+ var $editable = $target.closest('.o_editable');
+
+ if (!$editable.size()) {
+ return;
+ }
+
+ var rte = $editable.data('rte') || event.data;
+
+ if (rte && rte.$last && (!$editable.size() || rte.$last[0] != $editable[0])) {
+ var $destroy = rte.$last;
+ history.splitNext();
+
+ setTimeout(function () {
+ var id = $destroy.data('note-id');
+ $destroy.destroy().removeData('note-id').removeAttr('data-note-id');
+ $('#note-popover-'+id+', #note-handle-'+id+', #note-dialog-'+id+'').remove();
+ },150); // setTimeout to remove flickering when change to editable zone (re-create an editor)
+ rte.$last = null;
+ }
+ if ($editable.size() && (!rte.$last || rte.$last[0] != $editable[0]) &&
+ ($target.closest('[contenteditable]').attr('contenteditable') || "").toLowerCase() !== 'false') {
+
+ $editable.summernote(rte.config($editable));
+
+ $editable.data('NoteHistory', history);
+ rte.$last = $editable;
+
+ // firefox & IE fix
+ try {
+ document.execCommand('enableObjectResizing', false, false);
+ document.execCommand('enableInlineTableEditing', false, false);
+ document.execCommand( '2D-position', false, false);
+ } catch (e) {}
+ document.body.addEventListener('resizestart', function (evt) {evt.preventDefault(); return false;});
+ document.body.addEventListener('movestart', function (evt) {evt.preventDefault(); return false;});
+ document.body.addEventListener('dragstart', function (evt) {evt.preventDefault(); return false;});
+
+ if (!range.create()) {
+ $editable.focusIn();
+ }
+
+ if (dom.isImg($target[0])) {
+ $target.trigger('mousedown'); // for activate selection on picture
+ }
+ }
+ },
+
+ onMouseup: function (event) {
+ var $target = $(event.target);
+ var $editable = $target.closest('.o_editable');
+
+ if (!$editable.size()) {
+ return;
+ }
+
+ var rte = event.data;
+ setTimeout(function () {
+ rte.historyRecordUndo($target, 'activate', true);
+ },0);
+ },
+
+ editable: function () {
+ return $('#wrapwrap [data-oe-model]')
+ .not('.o_not_editable')
+ .filter(function () {
+ return !$(this).closest('.o_not_editable').length;
+ })
+ .not('link, script')
+ .not('[data-oe-readonly]')
+ .not('img[data-oe-field="arch"], br[data-oe-field="arch"], input[data-oe-field="arch"]')
+ .not('.oe_snippet_editor')
+ .add('.o_editable');
+ },
+
+ config: function ($editable) {
+ return {
+ 'airMode' : true,
+ 'focus': false,
+ 'airPopover': [
+ ['style', ['style']],
+ ['font', ['bold', 'italic', 'underline', 'clear']],
+ ['fontsize', ['fontsize']],
+ ['color', ['color']],
+ ['para', ['ul', 'ol', 'paragraph']],
+ ['table', ['table']],
+ ['insert', ['link', 'picture']],
+ ['history', ['undo', 'redo']],
+ ],
+ 'styleWithSpan': false,
+ 'inlinemedia' : ['p'],
+ 'lang': "odoo",
+ 'onChange': function (html, $editable) {
+ $editable.trigger("content_changed");
+ }
+ };
+ },
+});
+
+
+var data = {
+ 'history': history,
+ 'Class': RTE
+};
+return data;
+
+});
diff --git a/addons/web_editor/static/src/js/rte.summernote.js b/addons/web_editor/static/src/js/rte.summernote.js
new file mode 100644
index 00000000000..1086f4b369c
--- /dev/null
+++ b/addons/web_editor/static/src/js/rte.summernote.js
@@ -0,0 +1,781 @@
+odoo.define('web_editor.rte.summernote', function (require) {
+'use strict';
+
+var core = require('web.core');
+var ajax = require('web.ajax');
+var base = require('web_editor.base');
+var widgets = require('web_editor.widget');
+var rte = require('web_editor.rte');
+
+var QWeb = core.qweb;
+var _t = core._t;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ajax.jsonRpc('/web/dataset/call', 'call', {
+ 'model': 'ir.ui.view',
+ 'method': 'read_template',
+ 'args': ['web_editor.colorpicker', base.get_context()]
+}).done(function (data) {
+ QWeb.add_template(data);
+});
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* Summernote Lib (neek change to make accessible: method and object) */
+
+var dom = $.summernote.core.dom;
+var range = $.summernote.core.range;
+var eventHandler = $.summernote.eventHandler;
+var renderer = $.summernote.renderer;
+var options = $.summernote.options;
+
+var tplButton = renderer.getTemplate().button;
+var tplIconButton = renderer.getTemplate().iconButton;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* update and change the popovers content, and add history button */
+
+var fn_createPalette = renderer.createPalette;
+renderer.createPalette = function ($container, options) {
+ fn_createPalette.call(this, $container, options);
+
+ if (!QWeb.has_template('web_editor.colorpicker')) {
+ return;
+ }
+
+ var $color = $container.find('.note-color');
+ var html = QWeb.render('web_editor.colorpicker');
+ $color.find('.note-color-palette').prepend(html);
+ var $bg = $color.find('.colorpicker:first button');
+ var $fore = $color.find('.colorpicker:last button');
+
+ $bg.each(function () { $(this).attr('data-event', 'backColor').attr('data-value', $(this).attr('class')); });
+ $fore.each(function () { $(this).attr('data-event', 'foreColor').attr('data-value', $(this).attr('class').replace(/bg-/, 'text-')); });
+};
+
+var fn_tplPopovers = renderer.tplPopovers;
+renderer.tplPopovers = function (lang, options) {
+ var $popover = $(fn_tplPopovers.call(this, lang, options));
+
+ var $imagePopover = $popover.find('.note-image-popover');
+ var $linkPopover = $popover.find('.note-link-popover');
+ var $airPopover = $popover.find('.note-air-popover');
+
+ if (window === window.top) {
+ $popover.children().addClass("hidden-xs");
+ }
+
+ //////////////// image popover
+
+ // add center button for images
+ var $centerbutton = $(tplIconButton('fa fa-align-center', {
+ title: _t('Center'),
+ event: 'floatMe',
+ value: 'center'
+ })).insertAfter($imagePopover.find('[data-event="floatMe"][data-value="left"]'));
+ $imagePopover.find('button[data-event="removeMedia"]').parent().remove();
+ $imagePopover.find('button[data-event="floatMe"][data-value="none"]').remove();
+
+ var $alt = $('');
+ $alt.prependTo($imagePopover.find('.popover-content'));
+ $alt.append('');
+
+ // padding button
+ var $padding = $('');
+ $padding.insertBefore($imagePopover.find('.btn-group:first'));
+ var $button = $(tplIconButton('fa fa-plus-square-o', {
+ title: _t('Padding'),
+ dropdown: true
+ })).appendTo($padding);
+ var $ul = $('').insertAfter($button);
+ $ul.append(''+_t('None')+'');
+ $ul.append(''+_t('Small')+'');
+ $ul.append(''+_t('Medium')+'');
+ $ul.append(''+_t('Large')+'');
+ $ul.append(''+_t('Xl')+'');
+
+ // circle, boxed... options became toggled
+ $imagePopover.find('[data-event="imageShape"]:not([data-value])').remove();
+ var $button = $(tplIconButton('fa fa-sun-o', {
+ title: _t('Shadow'),
+ event: 'imageShape',
+ value: 'shadow'
+ })).insertAfter($imagePopover.find('[data-event="imageShape"][data-value="img-circle"]'));
+
+ // add spin for fa
+ var $spin = $('').insertAfter($button.parent());
+ $(tplIconButton('fa fa-refresh', {
+ title: _t('Spin'),
+ event: 'imageShape',
+ value: 'fa-spin'
+ })).appendTo($spin);
+
+ // resize for fa
+ var $resizefa = $('')
+ .insertAfter($imagePopover.find('.btn-group:has([data-event="resize"])'));
+ for (var size=1; size<=5; size++) {
+ $(tplButton(''+size+'x', {
+ title: size+"x",
+ event: 'resizefa',
+ value: size+''
+ })).appendTo($resizefa);
+ }
+ var $colorfa = $airPopover.find('.note-color').clone();
+ $colorfa.find(".btn-group:first").remove();
+ $colorfa.find("ul.dropdown-menu").css('min-width', '172px');
+ $colorfa.find('button[data-event="color"]').attr('data-value', '{"foreColor": "#f00"}')
+ .find("i").css({'background': '', 'color': '#f00'});
+ $resizefa.after($colorfa);
+
+ // show dialog box and delete
+ var $imageprop = $('');
+ $imageprop.appendTo($imagePopover.find('.popover-content'));
+ $(tplIconButton('fa fa-picture-o', {
+ title: _t('Edit'),
+ event: 'showImageDialog'
+ })).appendTo($imageprop);
+ $(tplIconButton('fa fa-trash-o', {
+ title: _t('Remove'),
+ event: 'delete'
+ })).appendTo($imageprop);
+
+ $imagePopover.find('.popover-content').append($airPopover.find(".note-history").clone());
+
+ $imagePopover.find('[data-event="showImageDialog"]').before($airPopover.find('[data-event="showLinkDialog"]').clone());
+
+ //////////////// link popover
+
+ $linkPopover.find('.popover-content').append($airPopover.find(".note-history").clone());
+
+ $linkPopover.find('button[data-event="showLinkDialog"] i').attr("class", "fa fa-link");
+ $linkPopover.find('button[data-event="unlink"]').before($airPopover.find('button[data-event="showImageDialog"]').clone());
+
+ //////////////// text/air popover
+
+ //// highlight the text format
+ $airPopover.find('.note-style').on('mousedown', function () {
+ var $format = $airPopover.find('[data-event="formatBlock"]');
+ var node = range.create().sc;
+ var formats = $format.map(function () { return $(this).data("value"); }).get();
+ while (node && (!node.tagName || (!node.tagName || formats.indexOf(node.tagName.toLowerCase()) === -1))) {
+ node = node.parentNode;
+ }
+ $format.parent().removeClass('active');
+ $format.filter('[data-value="'+(node ? node.tagName.toLowerCase() : "p")+'"]')
+ .parent().addClass("active");
+ });
+
+ //////////////// tooltip
+
+ $airPopover.add($linkPopover).add($imagePopover).find("button")
+ .tooltip('destroy')
+ .tooltip({
+ container: 'body',
+ trigger: 'hover',
+ placement: 'bottom'
+ }).on('click', function () {$(this).tooltip('hide');});
+
+ return $popover;
+};
+
+var fn_boutton_update = eventHandler.popover.button.update;
+eventHandler.popover.button.update = function ($container, oStyle) {
+ // stop animation when edit content
+ var previous = $(".note-control-selection").data('target');
+ if (previous) {
+ $(previous).css({"-webkit-animation-play-state": "", "animation-play-state": "", "-webkit-transition": "", "transition": "", "-webkit-animation": "", "animation": ""});
+ }
+ if (oStyle.image) {
+ $(oStyle.image).css({"-webkit-animation": "none", "animation": "none"});
+ }
+ // end
+
+ fn_boutton_update.call(this, $container, oStyle);
+
+ if (oStyle.image) {
+ $container.find('[data-event]').parent().removeClass("active");
+
+ $container.find('a[data-event="padding"][data-value="small"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-small"));
+ $container.find('a[data-event="padding"][data-value="medium"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-medium"));
+ $container.find('a[data-event="padding"][data-value="large"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-large"));
+ $container.find('a[data-event="padding"][data-value="xl"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-xl"));
+ $container.find('a[data-event="padding"][data-value=""]').parent().toggleClass("active", !$container.find('.active a[data-event="padding"]').length);
+
+ if (dom.isImgFont(oStyle.image)) {
+
+ $container.find('.btn-group:not(.only_fa):has(button[data-event="resize"],button[data-event="imageShape"])').addClass("hidden");
+ $container.find('.only_fa').removeClass("hidden");
+ $container.find('button[data-event="resizefa"][data-value="2"]').toggleClass("active", $(oStyle.image).hasClass("fa-2x"));
+ $container.find('button[data-event="resizefa"][data-value="3"]').toggleClass("active", $(oStyle.image).hasClass("fa-3x"));
+ $container.find('button[data-event="resizefa"][data-value="4"]').toggleClass("active", $(oStyle.image).hasClass("fa-4x"));
+ $container.find('button[data-event="resizefa"][data-value="5"]').toggleClass("active", $(oStyle.image).hasClass("fa-5x"));
+ $container.find('button[data-event="resizefa"][data-value="1"]').toggleClass("active", !$container.find('.active[data-event="resizefa"]').length);
+
+ $container.find('button[data-event="imageShape"][data-value="fa-spin"]').toggleClass("active", $(oStyle.image).hasClass("fa-spin"));
+
+ $container.find('.note-color').removeClass("hidden");
+
+ } else {
+
+ $container.find('.hidden:not(.only_fa)').removeClass("hidden");
+ $container.find('.only_fa').addClass("hidden");
+ var width = ($(oStyle.image).attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+%)/);
+ if (width) {
+ width = width[2];
+ }
+ $container.find('button[data-event="resize"][data-value="1"]').toggleClass("active", width === "100%");
+ $container.find('button[data-event="resize"][data-value="0.5"]').toggleClass("active", width === "50%");
+ $container.find('button[data-event="resize"][data-value="0.25"]').toggleClass("active", width === "25%");
+
+ $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow"));
+
+ if (!$(oStyle.image).is("img")) {
+ $container.find('.btn-group:has(button[data-event="imageShape"])').addClass("hidden");
+ }
+
+ $container.find('.note-color').addClass("hidden");
+
+ }
+
+ $container.find('button[data-event="floatMe"][data-value="left"]').toggleClass("active", $(oStyle.image).hasClass("pull-left"));
+ $container.find('button[data-event="floatMe"][data-value="center"]').toggleClass("active", $(oStyle.image).hasClass("center-block"));
+ $container.find('button[data-event="floatMe"][data-value="right"]').toggleClass("active", $(oStyle.image).hasClass("pull-right"));
+
+ $(oStyle.image).trigger('attributes_change');
+ }
+};
+
+var fn_popover_update = eventHandler.popover.update;
+eventHandler.popover.update = function ($popover, oStyle, isAirMode) {
+ var $imagePopover = $popover.find('.note-image-popover');
+ var $linkPopover = $popover.find('.note-link-popover');
+ var $airPopover = $popover.find('.note-air-popover');
+
+ fn_popover_update.call(this, $popover, oStyle, isAirMode);
+
+ if (!isAirMode || $(oStyle.range.sc).closest('[data-oe-model]:not([data-oe-model="ir.ui.view"]):not([data-oe-type="html"])').length) {
+ $imagePopover.hide();
+ $linkPopover.hide();
+ $airPopover.hide();
+ return;
+ }
+
+ if (oStyle.image) {
+ if (oStyle.image.parentNode.className.match(/(^|\s)media_iframe_video(\s|$)/i)) {
+ oStyle.image = oStyle.image.parentNode;
+ }
+ var alt = $(oStyle.image).attr("alt");
+
+ $imagePopover.find('.o_image_alt').text( (alt || "").replace(/"/g, '"') ).parent().toggle(oStyle.image.tagName === "IMG");
+ $imagePopover.show();
+
+ range.createFromNode(dom.firstChild(oStyle.image)).select();
+ } else {
+ $(".note-control-selection").hide();
+ }
+
+ if (oStyle.image || (!oStyle.range.isCollapsed() || (oStyle.range.sc.tagName && !dom.isAnchor(oStyle.range.sc)) || (oStyle.image && !$(oStyle.image).closest('a').length))) {
+ $linkPopover.hide();
+ oStyle.anchor = false;
+ }
+
+ if (oStyle.image || oStyle.anchor || !$(oStyle.range.sc).closest('.note-editable').length) {
+ $airPopover.hide();
+ } else {
+ $airPopover.show();
+ }
+};
+
+eventHandler.handle.update = function ($handle, oStyle, isAirMode) {
+ $handle.toggle(!!oStyle.image);
+ if (oStyle.image) {
+ var $selection = $handle.find('.note-control-selection');
+ var $image = $(oStyle.image);
+ var szImage = {
+ w: parseInt($image.outerWidth(true), 10),
+ h: parseInt($image.outerHeight(true), 10)
+ };
+ $selection.data('target', oStyle.image); // save current image element.
+ var sSizing = szImage.w + 'x' + szImage.h;
+ $selection.find('.note-control-selection-info').text(szImage.h > 50 ? sSizing : "");
+
+ $selection.find('.note-control-sizing').toggleClass('note-control-sizing note-control-holder').css({
+ 'border-top': 0,
+ 'border-left': 0
+ });
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* hack for image and link editor */
+
+function getImgTarget () {
+ return $(".note-control-selection").data('target');
+}
+eventHandler.editor.padding = function ($editable, sValue) {
+ var $target = $(getImgTarget());
+ var paddings = "small medium large xl".split(/\s+/);
+ $editable.data('NoteHistory').recordUndo();
+ if (sValue.length) {
+ paddings.splice(paddings.indexOf(sValue),1);
+ $target.toggleClass('padding-'+sValue);
+ }
+ $target.removeClass("padding-" + paddings.join(" padding-"));
+};
+eventHandler.editor.resize = function ($editable, sValue) {
+ var $target = $(getImgTarget());
+ $editable.data('NoteHistory').recordUndo();
+ var width = ($target.attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+)%/);
+ if (width) {
+ width = width[2]/100;
+ }
+ $(getImgTarget()).css('width', width != sValue ? (sValue * 100) + '%' : '');
+};
+eventHandler.editor.resizefa = function ($editable, sValue) {
+ var $target = $(getImgTarget());
+ $editable.data('NoteHistory').recordUndo();
+ $target.attr('class', $target.attr('class').replace(/\s*fa-[0-9]+x/g, ''));
+ if (+sValue > 1) {
+ $target.addClass('fa-'+sValue+'x');
+ }
+};
+eventHandler.editor.floatMe = function ($editable, sValue) {
+ var $target = $(getImgTarget());
+ $editable.data('NoteHistory').recordUndo();
+ switch (sValue) {
+ case 'center': $target.toggleClass('center-block').removeClass('pull-right pull-left'); break;
+ case 'left': $target.toggleClass('pull-left').removeClass('pull-right center-block'); break;
+ case 'right': $target.toggleClass('pull-right').removeClass('pull-left center-block'); break;
+ }
+};
+eventHandler.editor.imageShape = function ($editable, sValue) {
+ var $target = $(getImgTarget());
+ $editable.data('NoteHistory').recordUndo();
+ $target.toggleClass(sValue);
+};
+
+eventHandler.dialog.showLinkDialog = function ($editable, $dialog, linkInfo) {
+ $editable.data('range').select();
+ $editable.data('NoteHistory').recordUndo();
+
+ var editor = new widgets.LinkDialog($editable, linkInfo);
+ editor.appendTo(document.body);
+
+ var def = new $.Deferred();
+ editor.on("save", this, function (linkInfo) {
+ linkInfo.range.select();
+ $editable.data('range', linkInfo.range);
+ def.resolve(linkInfo);
+ $editable.trigger('keyup');
+ $('.note-popover .note-link-popover').show();
+ });
+ editor.on("cancel", this, function () {
+ def.reject();
+ });
+ return def;
+};
+eventHandler.dialog.showImageDialog = function ($editable) {
+ var r = $editable.data('range');
+ if (r.sc.tagName && r.sc.childNodes.length) {
+ r.sc = r.sc.childNodes[r.so];
+ }
+ var editor = new widgets.MediaDialog($editable, dom.isImg(r.sc) ? r.sc : null);
+ editor.appendTo(document.body);
+ return new $.Deferred().reject();
+};
+$.summernote.pluginEvents.alt = function (event, editor, layoutInfo, sorted) {
+ var $editable = layoutInfo.editable();
+ var $selection = layoutInfo.handle().find('.note-control-selection');
+ var media = $selection.data('target');
+ new widgets.alt($editable, media).appendTo(document.body);
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+dom.isVoid = function (node) {
+ return node && /^BR|^IMG|^HR/.test(node.nodeName.toUpperCase()) || dom.isImg(node);
+};
+dom.isImg = function (node) {
+ return dom.isImgFont(node) || (node && (node.nodeName === "IMG" || (node.className && node.className.match(/(^|\s)media_iframe_video(\s|$)/i)) ));
+};
+dom.isForbiddenNode = function (node) {
+ return $(node).is(".media_iframe_video, .fa, img");
+};
+
+dom.isImgFont = function (node) {
+ var nodeName = node && node.nodeName.toUpperCase();
+ var className = (node && node.className || "");
+ if (node && (nodeName === "SPAN" || nodeName === "I") && className.length) {
+ var classNames = className.split(/\s+/);
+ for (var k=0; k
')[0];
+ $node.append( p );
+ range.createFromNode(p.firstChild).select();
+ }
+ return res;
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* fix ie and re-range to don't break snippet*/
+
+var initial_data = {};
+function reRangeSelectKey (event) {
+ initial_data.range = null;
+ if (event.shiftKey && event.keyCode >= 37 && event.keyCode <= 40 && !$(event.target).is("input, textarea, select")) {
+ var r = range.create();
+ if (r) {
+ var rng = r.reRange(event.keyCode <= 38);
+ if (r !== rng) {
+ rng.select();
+ }
+ }
+ }
+}
+function reRangeSelect (event, dx, dy) {
+ var r = range.create();
+ if (!r || r.isCollapsed()) return;
+
+ // check if the user move the caret on up or down
+ var data = r.reRange(dy < 0 || (dy === 0 && dx < 0));
+
+ if (data.sc !== r.sc || data.so !== r.so || data.ec !== r.ec || data.eo !== r.eo) {
+ setTimeout(function () {
+ data.select();
+ $(data.sc.parentNode).closest('.note-popover');
+ },0);
+ }
+
+ $(data.sc).closest('.o_editable').data('range', r);
+ return r;
+}
+function summernote_mouseup (event) {
+ if ($(event.target).closest("#web_editor-top-navbar, .note-popover").length) {
+ return;
+ }
+ // don't rerange if simple click
+ if (initial_data.event) {
+ var dx = event.clientX - (event.shiftKey ? initial_data.rect.left : initial_data.event.clientX);
+ var dy = event.clientY - (event.shiftKey ? initial_data.rect.top : initial_data.event.clientY);
+ if (10 < Math.pow(dx, 2)+Math.pow(dy, 2)) {
+ reRangeSelect(event, dx, dy);
+ }
+ }
+
+ if (!$(event.target).closest(".o_editable").length) {
+ return;
+ }
+ if (!initial_data.range || !event.shiftKey) {
+ setTimeout(function () {
+ initial_data.range = range.create();
+ },0);
+ }
+}
+var remember_selection;
+function summernote_mousedown (event) {
+ rte.history.splitNext();
+
+ var $editable = $(event.target).closest(".o_editable, .note-editor");
+
+ if (!!document.documentMode) {
+ summernote_ie_fix(event, function (node) { return node.tagName === "DIV" || node.tagName === "IMG" || (node.dataset && node.dataset.oeModel); });
+ } else if (last_div && event.target !== last_div) {
+ if (last_div.tagName === "A") {
+ summernote_ie_fix(event, function (node) { return node.dataset && node.dataset.oeModel; });
+ } else if ($editable.length) {
+ if (summernote_ie_fix(event, function (node) { return node.tagName === "A"; })) {
+ r = range.create();
+ r.select();
+ }
+ }
+ }
+
+ // remember_selection when click on non editable area
+ var r = range.create();
+ if ($(r ? dom.node(r.sc) : event.srcElement || event.target).closest('#web_editor-top-navbar, #oe_main_menu_navbar, .note-popover, .note-toolbar, .modal').length) {
+ if (!$(event.target).is('input, select, label, button, a')) {
+ if (!remember_selection && $editable[0]) {
+ remember_selection = range.create(dom.firstChild($editable[0]), 0);
+ }
+ if (!remember_selection && $editable[0]) {
+ try {
+ remember_selection.select();
+ } catch (e) {
+ console.warn(e);
+ }
+ }
+ }
+ } else if (r && $(dom.node(r.sc)).closest('.o_editable, .note-editable').length) {
+ remember_selection = r;
+ }
+
+ initial_data.event = event;
+
+ // keep selection when click with shift
+ if (event.shiftKey && $editable.length) {
+ if (initial_data.range) {
+ initial_data.range.select();
+ }
+ var rect = r && r.getClientRects();
+ initial_data.rect = rect && rect.length ? rect[0] : { top: 0, left: 0 };
+ }
+}
+
+var last_div;
+var last_div_change;
+var last_editable;
+function summernote_ie_fix (event, pred) {
+ var editable;
+ var div;
+ var node = event.target;
+ while(node.parentNode) {
+ if (!div && pred(node)) {
+ div = node;
+ }
+ if(last_div !== node && (node.getAttribute('contentEditable')==='false' || node.className && (node.className.indexOf('o_not_editable') !== -1))) {
+ break;
+ }
+ if (node.className && node.className.indexOf('o_editable') !== -1) {
+ if (!div) {
+ div = node;
+ }
+ editable = node;
+ break;
+ }
+ node = node.parentNode;
+ }
+
+ if (!editable) {
+ $(last_div_change).removeAttr("contentEditable").removeProp("contentEditable");
+ $(last_editable).attr("contentEditable", "true").prop("contentEditable", "true");
+ last_div_change = null;
+ last_editable = null;
+ return;
+ }
+
+ if (div === last_div) {
+ return;
+ }
+
+ last_div = div;
+
+ $(last_div_change).removeAttr("contentEditable").removeProp("contentEditable");
+
+ if (last_editable !== editable) {
+ if ($(editable).is("[contentEditable='true']")) {
+ $(editable).removeAttr("contentEditable").removeProp("contentEditable");
+ last_editable = editable;
+ } else {
+ last_editable = null;
+ }
+ }
+ if (!$(div).attr("contentEditable") && !$(div).is("[data-oe-type='many2one'], [data-oe-type='contact']")) {
+ $(div).attr("contentEditable", "true").prop("contentEditable", "true");
+ last_div_change = div;
+ } else {
+ last_div_change = null;
+ }
+ return editable !== div ? div : null;
+}
+
+var fn_attach = eventHandler.attach;
+eventHandler.attach = function (oLayoutInfo, options) {
+ fn_attach.call(this, oLayoutInfo, options);
+ oLayoutInfo.editor.on('dragstart', 'img', function (e) { e.preventDefault(); });
+ $(document).on('mousedown', summernote_mousedown);
+ $(document).on('mouseup', summernote_mouseup);
+ oLayoutInfo.editor.off('click').on('click', function (e) {e.preventDefault();}); // if the content editable is a link
+ oLayoutInfo.editor.on('dblclick', 'img, .media_iframe_video, span.fa, i.fa, span.fa', function (event) {
+ if (!$(event.target).closest(".note-toolbar").length) { // prevent icon edition of top bar for default summernote
+ new widgets.MediaDialog(oLayoutInfo.editor, event.target).appendTo(document.body);
+ }
+ });
+ $(document).on("keyup", reRangeSelectKey);
+
+ var clone_data = false;
+ var $node = oLayoutInfo.editor;
+ if ($node.data('oe-model')) {
+ $node.on('content_changed', function () {
+
+ var $nodes = $('[data-oe-model]')
+ .filter(function () { return this != $node[0];})
+ .filter('[data-oe-model="'+$node.data('oe-model')+'"]')
+ .filter('[data-oe-id="'+$node.data('oe-id')+'"]')
+ .filter('[data-oe-field="'+$node.data('oe-field')+'"]');
+ if ($node.data('oe-type')) $nodes = $nodes.filter('[data-oe-type="'+$node.data('oe-type')+'"]');
+ if ($node.data('oe-expression')) $nodes = $nodes.filter('[data-oe-expression="'+$node.data('oe-expression')+'"]');
+ if ($node.data('oe-xpath')) $nodes = $nodes.filter('[data-oe-xpath="'+$node.data('oe-xpath')+'"]');
+ if ($node.data('oe-contact-options')) $nodes = $nodes.filter('[data-oe-contact-options="'+$node.data('oe-contact-options')+'"]');
+
+ var nodes = $node.get();
+
+ if ($node.data('oe-type') === "many2one") {
+ $nodes = $nodes.add($('[data-oe-model]')
+ .filter(function () { return this != $node[0] && nodes.indexOf(this) === -1; })
+ .filter('[data-oe-many2one-model="'+$node.data('oe-many2one-model')+'"]')
+ .filter('[data-oe-many2one-id="'+$node.data('oe-many2one-id')+'"]')
+ .filter('[data-oe-type="many2one"]'));
+
+ $nodes = $nodes.add($('[data-oe-model]')
+ .filter(function () { return this != $node[0] && nodes.indexOf(this) === -1; })
+ .filter('[data-oe-model="'+$node.data('oe-many2one-model')+'"]')
+ .filter('[data-oe-id="'+$node.data('oe-many2one-id')+'"]')
+ .filter('[data-oe-field="name"]'));
+ }
+
+ if (!clone_data) {
+ clone_data = true;
+ $nodes.html(this.innerHTML);
+ clone_data = false;
+ }
+ });
+ }
+
+ var $toolbar = $(oLayoutInfo.popover).add(oLayoutInfo.toolbar);
+ $('button[data-event="undo"], button[data-event="redo"]', $toolbar).attr('disabled', true);
+
+ $(oLayoutInfo.editor)
+ .add(oLayoutInfo.handle)
+ .add(oLayoutInfo.popover)
+ .add(oLayoutInfo.toolbar)
+ .on('click keyup', function () {
+ $('button[data-event="undo"]', $toolbar).attr('disabled', !oLayoutInfo.editable.data('NoteHistory').hasUndo());
+ $('button[data-event="redo"]', $toolbar).attr('disabled', !oLayoutInfo.editable.data('NoteHistory').hasRedo());
+ });
+};
+var fn_detach = eventHandler.detach;
+eventHandler.dettach = function (oLayoutInfo, options) {
+ fn_detach.call(this, oLayoutInfo, options);
+ oLayoutInfo.editor.off("dragstart");
+ $(document).off('mousedown', summernote_mousedown);
+ $(document).off('mouseup', summernote_mouseup);
+ oLayoutInfo.editor.off("dblclick");
+ $(document).off("keyup", reRangeSelectKey);
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* Translation for odoo */
+
+$.summernote.lang.odoo = {
+ font: {
+ bold: _t('Bold'),
+ italic: _t('Italic'),
+ underline: _t('Underline'),
+ strikethrough: _t('Strikethrough'),
+ subscript: _t('Subscript'),
+ superscript: _t('Superscript'),
+ clear: _t('Remove Font Style'),
+ height: _t('Line Height'),
+ name: _t('Font Family'),
+ size: _t('Font Size')
+ },
+ image: {
+ image: _t('Picture'),
+ insert: _t('Insert Image'),
+ resizeFull: _t('Resize Full'),
+ resizeHalf: _t('Resize Half'),
+ resizeQuarter: _t('Resize Quarter'),
+ floatLeft: _t('Float Left'),
+ floatRight: _t('Float Right'),
+ floatNone: _t('Float None'),
+ dragImageHere: _t('Drag an image here'),
+ selectFromFiles: _t('Select from files'),
+ url: _t('Image URL'),
+ remove: _t('Remove Image')
+ },
+ link: {
+ link: _t('Link'),
+ insert: _t('Insert Link'),
+ unlink: _t('Unlink'),
+ edit: _t('Edit'),
+ textToDisplay: _t('Text to display'),
+ url: _t('To what URL should this link go?'),
+ openInNewWindow: _t('Open in new window')
+ },
+ video: {
+ video: _t('Video'),
+ videoLink: _t('Video Link'),
+ insert: _t('Insert Video'),
+ url: _t('Video URL?'),
+ providers: _t('(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)')
+ },
+ table: {
+ table: _t('Table')
+ },
+ hr: {
+ insert: _t('Insert Horizontal Rule')
+ },
+ style: {
+ style: _t('Style'),
+ normal: _t('Normal'),
+ blockquote: _t('Quote'),
+ pre: _t('Code'),
+ h1: _t('Header 1'),
+ h2: _t('Header 2'),
+ h3: _t('Header 3'),
+ h4: _t('Header 4'),
+ h5: _t('Header 5'),
+ h6: _t('Header 6')
+ },
+ lists: {
+ unordered: _t('Unordered list'),
+ ordered: _t('Ordered list')
+ },
+ options: {
+ help: _t('Help'),
+ fullscreen: _t('Full Screen'),
+ codeview: _t('Code View')
+ },
+ paragraph: {
+ paragraph: _t('Paragraph'),
+ outdent: _t('Outdent'),
+ indent: _t('Indent'),
+ left: _t('Align left'),
+ center: _t('Align center'),
+ right: _t('Align right'),
+ justify: _t('Justify full')
+ },
+ color: {
+ recent: _t('Recent Color'),
+ more: _t('More Color'),
+ background: _t('Background Color'),
+ foreground: _t('Foreground Color'),
+ transparent: _t('Transparent'),
+ setTransparent: _t('Set transparent'),
+ reset: _t('Reset'),
+ resetToDefault: _t('Reset to default')
+ },
+ shortcut: {
+ shortcuts: _t('Keyboard shortcuts'),
+ close: _t('Close'),
+ textFormatting: _t('Text formatting'),
+ action: _t('Action'),
+ paragraphFormatting: _t('Paragraph formatting'),
+ documentStyle: _t('Document Style')
+ },
+ history: {
+ undo: _t('Undo'),
+ redo: _t('Redo')
+ }
+};
+
+});
diff --git a/addons/web_editor/static/src/js/snippets.animation.js b/addons/web_editor/static/src/js/snippets.animation.js
new file mode 100644
index 00000000000..b3fccc5584a
--- /dev/null
+++ b/addons/web_editor/static/src/js/snippets.animation.js
@@ -0,0 +1,82 @@
+odoo.define('web_editor.snippets.animation', function (require) {
+'use strict';
+
+var Class = require('web.Class');
+var base = require('web_editor.base');
+
+var registry = [];
+var ready = [];
+
+var Animation = Class.extend({
+ selector: false,
+ $: function () {
+ return this.$el.find.apply(this.$el, arguments);
+ },
+ init: function (dom, editable_mode) {
+ this.$el = this.$target = $(dom);
+ this.start(editable_mode);
+ },
+ /*
+ * start
+ * This method is called after init
+ */
+ start: function (editable_mode) {
+ },
+ /*
+ * stop
+ * This method is called to stop the animation (e.g.: when rte is launch)
+ */
+ stop: function () {
+ },
+});
+
+var start = function (editable_mode, $target) {
+ for (var k in registry) {
+ var Animation = registry[k];
+ var selector = "";
+ if (Animation.prototype.selector) {
+ if (selector != "") selector += ", "
+ selector += Animation.prototype.selector;
+ }
+ if ($target) {
+ if ($target.is(selector)) selector = $target;
+ else continue;
+ }
+
+ $(selector).each(function() {
+ var $snipped_id = $(this);
+ if ( !$snipped_id.parents("#oe_snippets").length &&
+ !$snipped_id.parent("body").length &&
+ !$snipped_id.data("snippet-view")) {
+ ready.push($snipped_id);
+ $snipped_id.data("snippet-view", new Animation($snipped_id, editable_mode));
+ } else if ($snipped_id.data("snippet-view")) {
+ $snipped_id.data("snippet-view").start(editable_mode);
+ }
+ });
+ }
+};
+
+var stop = function () {
+ $(ready).each(function() {
+ var $snipped_id = $(this);
+ if ($snipped_id.data("snippet-view")) {
+ $snipped_id.data("snippet-view").stop();
+ }
+ });
+};
+
+base.ready(function () {
+ setTimeout(function () {
+ start();
+ },0);
+});
+
+return {
+ 'Class': Class,
+ 'registry': registry,
+ 'start': start,
+ 'stop': stop
+};
+
+});
\ No newline at end of file
diff --git a/addons/web_editor/static/src/js/snippets.editor.js b/addons/web_editor/static/src/js/snippets.editor.js
new file mode 100644
index 00000000000..9d37a9f84c8
--- /dev/null
+++ b/addons/web_editor/static/src/js/snippets.editor.js
@@ -0,0 +1,1137 @@
+odoo.define('web_editor.snippet.editor', function (require) {
+'use strict';
+
+var Class = require('web.Class');
+var ajax = require('web.ajax');
+var core = require('web.core');
+var Widget = require('web.Widget');
+var base = require('web_editor.base');
+var editor = require('web_editor.editor');
+var animation = require('web_editor.snippets.animation');
+var options = require('web_editor.snippets.options');
+
+var qweb = core.qweb;
+var dummy = editor.dummy;
+
+/* ----- SNIPPET SELECTOR ---- */
+
+ajax.loadXML('/web_editor/static/src/xml/snippets.xml', qweb);
+
+editor.Class.include({
+ init: function() {
+ var self = this;
+ var res = this._super.apply(this, arguments);
+ var $editable = this.rte.editable();
+ this.buildingBlock = new BuildingBlock(this, $editable);
+ this.buildingBlock.on("snippets:ready", this, function () {
+ self.trigger("snippets:ready");
+ });
+ return res;
+ },
+ start: function () {
+ var self = this;
+ animation.stop();
+ this.buildingBlock.insertAfter(this.$el);
+ animation.start(true);
+
+ this.rte.editable().find("*").off('mousedown mouseup click');
+ return this._super();
+ },
+ save: function () {
+ this.buildingBlock.clean_for_save();
+ this._super();
+ },
+});
+
+/* ----- SNIPPET SELECTOR ---- */
+
+$.extend($.expr[':'],{
+ hasData: function(node,i,m){
+ return !!_.toArray(node.dataset).length;
+ },
+ data: function(node,i,m){
+ return $(node).data(m[3]);
+ }
+});
+
+var globalSelector = {
+ closest: function () { return $(); },
+ all: function () { return $(); },
+ is: function () { return false; },
+};
+
+/* ----- Jquery activate block ---- */
+
+$.fn.extend({
+ activateBlock: function () {
+ var target = globalSelector.closest($(this))[0] || (dom.isBR(this) ? this.parentNode : dom.node(this));
+ var evt = document.createEvent("MouseEvents");
+ evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target);
+ target.dispatchEvent(evt);
+ return this;
+ }
+});
+
+/* ----- BuildingBlock (managment of drag&drop and focus/blur snippets) ---- */
+
+var BuildingBlock = Widget.extend({
+ template: 'web_editor.snippets',
+ activeSnippets: [],
+ init: function (parent, $editable) {
+ this.parent = parent;
+ this.$editable = $editable;
+
+ this._super.apply(this, arguments);
+ if(!$('#oe_manipulators').length){
+ $("").appendTo('body');
+ }
+ this.$active_snipped_id = false;
+ this.snippets = [];
+
+ data.instance = this;
+ },
+ start: function() {
+ var self = this;
+ this.$snippet = $("#oe_snippets");
+
+ this.$el
+ .on("mouseenter", function () { self.show(); })
+ .on("mouseleave", function (event) { if (event.clientX>0 && event.clientY>0) self.hide(); });
+
+ $(window).resize(function () {
+ setTimeout('$(document).click()',0);
+ });
+
+ this.fetch_snippet_templates();
+ this.bind_snippet_click_editor();
+
+ $(document).on('click', '.dropdown-submenu a[tabindex]', function (e) {
+ e.preventDefault();
+ });
+
+ var _isNotBreakable = $.summernote.core.dom.isNotBreakable;
+ $.summernote.core.dom.isNotBreakable = function (node) {
+ return _isNotBreakable(node) || $(node).is('div') || globalSelector.is($(node));
+ };
+
+ $(window).on('resize', function () {
+ if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) {
+ self.cover_target(self.$active_snipped_id.data("snippet-editor").$overlay, self.$active_snipped_id);
+ }
+ });
+
+ $(document).on('mousemove', function () {
+ if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) {
+ self.$active_snipped_id.data("snippet-editor").$overlay.removeClass('o_keypress');
+ }
+ });
+ $(document).on('keydown', function (event) {
+ if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) {
+ self.$active_snipped_id.data("snippet-editor").$overlay.addClass('o_keypress');
+ }
+ if ((event.metaKey || (event.ctrlKey && !event.altKey)) && event.shiftKey && event.keyCode >= 48 && event.keyCode <= 57) {
+ self.$snippet.find('.scroll:first > ul li:eq('+(event.keyCode-49)+') a').trigger("click");
+ self.show();
+ event.preventDefault();
+ }
+ });
+ },
+
+ _get_snippet_url: function () {
+ return '/web_editor/snippets';
+ },
+ _add_check_selector : function (selector, no_check) {
+ var self = this;
+ var selector = selector.split(/\s*,/).join(":not(.o_snippet_not_selectable), ") + ":not(.o_snippet_not_selectable)";
+
+ if (no_check) {
+ return {
+ closest: function ($from, parentNode) {
+ return $from.closest(selector, parentNode);
+ },
+ all: function ($from) {
+ return $from ? $from.find(selector) : $(selector);
+ },
+ is: function ($from) {
+ return $from.is(selector);
+ }
+ };
+ } else {
+ var selector = selector.split(/\s*,/).join(":o_editable, ") + ":o_editable";
+ return {
+ closest: function ($from, parentNode) {
+ var parents = self.$editable.get();
+ return $from.closest(selector, parentNode).filter(function () {
+ var node = this;
+ while (node.parentNode) {
+ if (parents.indexOf(node)!==-1) {
+ return true;
+ }
+ node = node.parentNode;
+ }
+ return false;
+ });
+ },
+ all: function ($from) {
+ return $from ? $from.find(selector) : self.$editable.filter(selector).add(self.$editable.find(selector));
+ },
+ is: function ($from) {
+ return $from.is(selector);
+ }
+ };
+ }
+ },
+
+ fetch_snippet_templates: function () {
+ var self = this;
+ var url = this._get_snippet_url();
+ if (!url || !url.length) {
+ this.$snippet.detach();
+ return;
+ }
+ this.$el.find("#o_left_bar").addClass("hidden");
+ return ajax.jsonRpc(url, 'call', {}).then(function (html) {
+ self.compute_snippet_templates(html);
+ self.trigger("snippets:ready");
+ });
+ },
+ compute_snippet_templates: function (html) {
+ var self = this;
+ var $html = $(html);
+ var $left_bar = this.$el.find("#o_left_bar");
+ var $ul = $html.siblings("ul");
+ var $scroll = $html.siblings("#o_scroll");
+
+ if (!$scroll.length) {
+ throw new Error("Wrong snippets xml definition");
+ }
+
+ $ul.children().tooltip({
+ delay: { "show": 500, "hide": 100 },
+ container: 'body',
+ title: function () {
+ return (navigator.appVersion.indexOf('Mac') > -1 ? 'CMD' : 'CTRL')+'+SHIFT+'+($(this).index()+1);
+ },
+ trigger: 'hover',
+ placement: 'top'
+ }).on('click', function () {$(this).tooltip('hide');});
+
+ // t-snippet
+ $html.find('[data-oe-type="snippet"][data-oe-name]').each(function () {
+ var $div = $('').insertAfter(this).append(this).attr('name', $(this).data('oe-name'));
+ });
+ // end
+
+ self.templateOptions = [];
+ var selector = [];
+ var $styles = $html.find("[data-js], [data-selector]");
+ $styles.each(function () {
+ var $style = $(this);
+ var no_check = $style.data('no-check');
+ var option_id = $style.data('js');
+ var option = {
+ 'option' : option_id,
+ 'base_selector': $style.data('selector'),
+ 'selector': self._add_check_selector($style.data('selector'), no_check),
+ '$el': $style,
+ 'drop-near': $style.data('drop-near') && self._add_check_selector($style.data('drop-near'), no_check),
+ 'drop-in': $style.data('drop-in') && self._add_check_selector($style.data('drop-in'), no_check),
+ 'data': $style.data()
+ };
+ self.templateOptions.push(option);
+ selector.push(option.selector);
+ });
+
+ $styles.addClass("hidden");
+ globalSelector = {
+ closest: function ($from) {
+ var $temp;
+ var $target;
+ var len = selector.length;
+ for (var i = 0; i'+
+ ''+
+ ''+
+ '');
+ $div.find('span').text($(this).attr("name"));
+ $(this).prepend($div);
+
+ // from t-snippet
+ var thumbnail = $("[data-oe-thumbnail]", this).data("oe-thumbnail");
+ if (thumbnail) {
+ $div.find('.oe_snippet_thumbnail_img').css('background-image', 'url(' + thumbnail + ')');
+ }
+ // end
+ }
+ if (!$(this).data("selector")) {
+ $("> *:not(.oe_snippet_thumbnail)", this).addClass('oe_snippet_body');
+ }
+ number++;
+ });
+
+ // hide scroll if no snippets defined
+ if (!number) {
+ this.$snippet.detach();
+ } else {
+ this.$el.find("#o_left_bar").removeClass("hidden");
+ }
+ $("body").toggleClass("editor_has_snippets", !!number);
+
+ // select all default text to edit (if snippet default text)
+ /*self.$snippets.find('.oe_snippet_body, .oe_snippet_body *')
+ .contents()
+ .filter(function() {
+ return this.nodeType === 3 && this.textContent.match(/\S/);
+ }).parent().addClass("o_default_snippet_text");*/
+ $(document).on("mouseup", ".o_default_snippet_text", function (event) {
+ $(event.target).selectContent();
+ });
+ $(document).on("keyup", function (event) {
+ var r = $.summernote.core.range.create();
+ $(r && r.sc).closest(".o_default_snippet_text").removeClass("o_default_snippet_text");
+ });
+ // end
+
+ // clean t-oe
+ $html.find('[data-oe-model], [data-oe-type]').each(function () {
+ for (var k=0; k middle) {
+ break;
+ }
+ last = $li[k];
+ }
+ $(last).addClass("active");
+ });
+ // end
+
+ // display scrollbar
+ $(window).on('resize', function () {
+ $scroll.css("overflow", "");
+ var height = $left_bar.height() - $ul.outerHeight();
+ $scroll.css("height", height);
+ var $last = $scroll.children(":visible").last().children(".o_panel_body");
+ $last.css({'min-height': (height-$last.prev().outerHeight())+'px'});
+ if ($scroll[0].scrollHeight + $ul[0].scrollHeight > document.body.clientHeight) {
+ $scroll.css("overflow", "auto").css("width", "226px");
+ } else {
+ $scroll.css("width", "");
+ }
+ }).trigger('resize');
+ // end
+
+ self.make_snippet_draggable(self.$snippets);
+ },
+
+ cover_target: function ($el, $target){
+ if($el.data('not-cover_target')) return;
+ var pos = $target.offset();
+ var mt = parseInt($target.css("margin-top") || 0);
+ var mb = parseInt($target.css("margin-bottom") || 0);
+ var width = $target.outerWidth();
+ var bigger = pos.left+width > $("body").outerWidth()-8;
+ $el.css({
+ 'width': width,
+ 'top': pos.top - mt - 5,
+ 'left': pos.left-1
+ });
+ $el.find(".oe_handle.e,.oe_handle.w").css({'height': $target.outerHeight() + mt + mb+1});
+ if (bigger) {
+ $el.find(".oe_handle.e").css({right: 0, margin: 0})
+ .find("div").css({right: 0, left: 'auto'});
+ } else {
+ $el.find(".oe_handle.e").css({right: "", margin: ""})
+ .find("div").css({right: "", left: ""});
+ }
+ $el.find(".oe_handle.s").css({'top': $target.outerHeight() + mt + mb});
+ $el.find(".oe_handle.size").css({'top': $target.outerHeight() + mt});
+ $el.find(".oe_handle.s,.oe_handle.n").css({'width': width-2});
+ },
+
+ show_blocks: function () {
+ var self = this;
+ var cache = {};
+ this.$snippet.find(".o_panel").each(function () {
+ var catcheck = false;
+ var $category = $(this);
+ $category.find(".oe_snippet_body").each(function () {
+ var $snippet = $(this);
+
+ var check = false;
+
+ for (var k in self.templateOptions) {
+ var option = self.templateOptions[k];
+ if ($snippet.is(option.base_selector)) {
+
+ cache[k] = cache[k] || {
+ 'drop-near': option['drop-near'] ? option['drop-near'].all() : [],
+ 'drop-in': option['drop-in'] ? option['drop-in'].all() : []
+ };
+
+ if (cache[k]['drop-near'].length || cache[k]['drop-in'].length) {
+ catcheck = true;
+ check = true;
+ break;
+ }
+ }
+ }
+
+ if (check) {
+ $snippet.closest(".oe_snippet").removeClass("disable");
+ } else {
+ $snippet.closest(".oe_snippet").addClass("disable");
+ }
+ });
+
+ $('#oe_snippets .scroll a[data-toggle="tab"][href="#' + $category.attr("id") + '"]').toggle(catcheck);
+ });
+ },
+ show: function () {
+ var self = this;
+ this.make_active(false);
+ this.$el.addClass("o_open");
+ this.show_blocks();
+ },
+ hide: function () {
+ this.$el.removeClass("o_open");
+ },
+ bind_snippet_click_editor: function () {
+ var self = this;
+ var snipped_event_flag;
+ $(document).on('click', '*', function (event) {
+ var srcElement = event.srcElement || (event.originalEvent && (event.originalEvent.originalTarget || event.originalEvent.target) || event.target);
+ if (self.editor_busy || snipped_event_flag===srcElement || !srcElement) {
+ return;
+ }
+ snipped_event_flag = srcElement;
+
+ setTimeout(function () {snipped_event_flag = false;}, 0);
+ var $target = $(srcElement);
+
+ if ($target.closest(".oe_overlay, .note-popover").length) {
+ return;
+ }
+
+ if (!globalSelector.is($target)) {
+ $target = globalSelector.closest($target);
+ }
+
+ if (self.$active_snipped_id && self.$active_snipped_id.is($target)) {
+ return;
+ }
+ self.make_active($target);
+ });
+ },
+ snippet_blur: function ($snippet) {
+ if ($snippet) {
+ if ($snippet.data("snippet-editor")) {
+ $snippet.data("snippet-editor").on_blur();
+ }
+ }
+ },
+ snippet_focus: function ($snippet) {
+ if ($snippet) {
+ if ($snippet.data("snippet-editor")) {
+ $snippet.data("snippet-editor").on_focus();
+ }
+ }
+ },
+ clean_for_save: function () {
+ var self = this;
+ var opt = options.registry;
+ var template = self.templateOptions;
+ for (var k in template) {
+ var Option = opt[template[k]['option']];
+ if (Option && Option.prototype.clean_for_save !== dummy) {
+ template[k].selector.all().filter(".o_dirty").each(function () {
+ new Option(self, null, $(this), k).clean_for_save();
+ });
+ }
+ }
+ this.$editable.find("*[contentEditable], *[attributeEditable]")
+ .removeAttr('contentEditable')
+ .removeProp('contentEditable')
+ .removeAttr('attributeEditable')
+ .removeProp('attributeEditable');
+ },
+ make_active: function ($snippet) {
+ if ($snippet && this.$active_snipped_id && this.$active_snipped_id.get(0) === $snippet.get(0)) {
+ return;
+ }
+ if (this.$active_snipped_id) {
+ this.snippet_blur(this.$active_snipped_id);
+ this.$active_snipped_id = false;
+ }
+ if ($snippet && $snippet.length) {
+ if(_.indexOf(this.snippets, $snippet.get(0)) === -1) {
+ this.snippets.push($snippet.get(0));
+ }
+ this.$active_snipped_id = $snippet;
+ this.create_overlay(this.$active_snipped_id);
+ this.snippet_focus($snippet);
+ }
+ this.$snippet.trigger('snippet-activated', $snippet);
+ if ($snippet) {
+ $snippet.trigger('snippet-activated', $snippet);
+ }
+ },
+ create_overlay: function ($snippet) {
+ if (typeof $snippet.data("snippet-editor") === 'undefined') {
+ var $targets = this.activate_overlay_zones($snippet);
+ if (!$targets.length) return;
+ $snippet.data("snippet-editor", new Editor(this, $snippet));
+ }
+ this.cover_target($snippet.data('overlay'), $snippet);
+ },
+
+ // activate drag and drop for the snippets in the snippet toolbar
+ make_snippet_draggable: function($snippets){
+ var self = this;
+ var $tumb = $snippets.find(".oe_snippet_thumbnail_img:first");
+ var left = $tumb.outerWidth()/2;
+ var top = $tumb.outerHeight()/2;
+ var $toInsert, dropped, $snippet, action, snipped_id;
+
+ $snippets.draggable({
+ greedy: true,
+ helper: 'clone',
+ zIndex: '1000',
+ appendTo: 'body',
+ cursor: "move",
+ handle: ".oe_snippet_thumbnail",
+ cursorAt: {
+ 'left': left,
+ 'top': top
+ },
+ start: function(){
+ self.hide();
+ dropped = false;
+ // snippet_selectors => to get drop-near, drop-in
+ $snippet = $(this);
+ var $base_body = $snippet.find('.oe_snippet_body');
+ var $selector_siblings = $();
+ var $selector_children = $();
+ var vertical = false;
+ var temp = self.templateOptions;
+ for (var k in temp) {
+ if ($base_body.is(temp[k].base_selector)) {
+ if (temp[k]['drop-near']) {
+ if (!$selector_siblings) $selector_siblings = temp[k]['drop-near'].all();
+ else $selector_siblings = $selector_siblings.add(temp[k]['drop-near'].all());
+ }
+ if (temp[k]['drop-in']) {
+ if (!$selector_children) $selector_children = temp[k]['drop-in'].all();
+ else $selector_children = $selector_children.add(temp[k]['drop-in'].all());
+ }
+ }
+ }
+
+ $toInsert = $base_body.clone();
+
+ if (!$selector_siblings.length && !$selector_children.length) {
+ console.debug($snippet.find(".oe_snippet_thumbnail_title").text() + " have not insert action: data-drop-near or data-drop-in");
+ return;
+ }
+
+ self.activate_insertion_zones($selector_siblings, $selector_children);
+
+ $('.oe_drop_zone').droppable({
+ over: function(){
+ dropped = true;
+ $(this).first().after($toInsert);
+ },
+ out: function(){
+ var prev = $toInsert.prev();
+ if(this === prev[0]){
+ dropped = false;
+ $toInsert.detach();
+ }
+ }
+ });
+ },
+ stop: function(ev, ui){
+ $toInsert.removeClass('oe_snippet_body');
+
+ if (! dropped && self.$editable.find('.oe_drop_zone') && ui.position.top > 3) {
+ var el = self.$editable.find('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first();
+ if (el.length) {
+ el.after($toInsert);
+ dropped = true;
+ }
+ }
+
+ self.$editable.find('.oe_drop_zone').droppable('destroy').remove();
+
+ if (dropped) {
+
+ var prev = $toInsert.first()[0].previousSibling;
+ var next = $toInsert.last()[0].nextSibling;
+ var rte = self.parent.rte;
+
+ if (prev) {
+ $toInsert.detach();
+ rte.historyRecordUndo($(prev));
+ $toInsert.insertAfter(prev);
+ } else if (next) {
+ $toInsert.detach();
+ rte.historyRecordUndo($(next));
+ $toInsert.insertBefore(next);
+ } else {
+ var $parent = $toInsert.parent();
+ $toInsert.detach();
+ rte.historyRecordUndo($parent);
+ $parent.prepend($toInsert);
+ }
+
+ $toInsert.closest(".o_editable").trigger("content_changed");
+
+ var $target = false;
+ $target = $toInsert;
+
+ setTimeout(function () {
+ self.$snippet.trigger('snippet-dropped', $target);
+
+ animation.start(true, $target);
+
+ // drop_and_build_snippet
+ self.create_overlay($target);
+ if ($target.data("snippet-editor")) {
+ $target.data("snippet-editor").drop_and_build_snippet();
+ }
+ for (var k in self.templateOptions) {
+ self.templateOptions[k].selector.all($target).each(function () {
+ var $snippet = $(this);
+ self.create_overlay($snippet);
+ if ($snippet.data("snippet-editor")) {
+ $snippet.data("snippet-editor").drop_and_build_snippet();
+ }
+ });
+ }
+ $target.closest(".o_editable").trigger("content_changed");
+ // end
+
+ self.make_active($target);
+ },0);
+ } else {
+ $toInsert.remove();
+ }
+ },
+ });
+ },
+
+ // return the original snippet in the editor bar from a snippet id (string)
+ get_snippet_from_id: function(id){
+ return $('.oe_snippet').filter(function(){
+ return $(this).data('option') === id;
+ }).first();
+ },
+
+ // Create element insertion drop zones. two css selectors can be provided
+ // selector.children -> will insert drop zones as direct child of the selected elements
+ // in case the selected elements have children themselves, dropzones will be interleaved
+ // with them.
+ // selector.siblings -> will insert drop zones after and before selected elements
+ activate_insertion_zones: function($selector_siblings, $selector_children){
+ var self = this;
+ var zone_template = $("");
+
+ if ($selector_children) {
+ $selector_children.each(function (){
+ var $zone = $(this);
+ var css = window.getComputedStyle(this);
+ var float = css.float || css.cssFloat;
+ var $drop = zone_template.clone();
+
+ $zone.append($drop);
+ var node = $drop[0].previousSibling;
+ var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR"));
+ if (test) {
+ $drop.addClass("oe_vertical oe_vertical_text").css({
+ 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight),
+ 'float': 'none',
+ 'display': 'inline-block'
+ });
+ } else if (float === "left" || float === "right") {
+ $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().last().outerHeight()), 30));
+ }
+
+ $drop = $drop.clone();
+
+ $zone.prepend($drop);
+ var node = $drop[0].nextSibling;
+ var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR"));
+ if (test) {
+ $drop.addClass("oe_vertical oe_vertical_text").css({
+ 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight),
+ 'float': 'none',
+ 'display': 'inline-block'
+ });
+ } else if (float === "left" || float === "right") {
+ $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().first().outerHeight()), 30));
+ }
+ if (test) {
+ $drop.css({'float': 'none', 'display': 'inline-block'});
+ }
+ });
+
+ // add children near drop zone
+ $selector_siblings = $(_.uniq(($selector_siblings || $()).add($selector_children.children()).get()));
+ }
+
+ if ($selector_siblings) {
+ $selector_siblings.filter(':not(.oe_drop_zone):not(.oe_drop_clone)').each(function (){
+ var $zone = $(this);
+ var $drop;
+ var css = window.getComputedStyle(this);
+ var float = css.float || css.cssFloat;
+
+ if($zone.prev('.oe_drop_zone:visible').length === 0){
+ $drop = zone_template.clone();
+ if (float === "left" || float === "right") {
+ $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.prev().outerHeight() || Infinity), 30));
+ }
+ $zone.before($drop);
+ }
+ if($zone.next('.oe_drop_zone:visible').length === 0){
+ $drop = zone_template.clone();
+ if (float === "left" || float === "right") {
+ $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.next().outerHeight() || Infinity), 30));
+ }
+ $zone.after($drop);
+ }
+ });
+ }
+
+ var count;
+ do {
+ count = 0;
+ $zones = self.$editable.find('.oe_drop_zone > .oe_drop_zone').remove(); // no recursive zones
+ count += $zones.length;
+ $zones.remove();
+ } while (count > 0);
+
+ // Cleaning consecutive zone and up zones placed between floating or inline elements. We do not like these kind of zones.
+ var $zones = self.$editable.find('.oe_drop_zone:not(.oe_vertical)');
+ $zones.each(function (){
+ var zone = $(this);
+ var prev = zone.prev();
+ var next = zone.next();
+ // remove consecutive zone
+ if (prev.is('.oe_drop_zone') || next.is('.oe_drop_zone')) {
+ zone.remove();
+ return;
+ }
+ var float_prev = prev.css('float') || 'none';
+ var float_next = next.css('float') || 'none';
+ var disp_prev = prev.css('display') || null;
+ var disp_next = next.css('display') || null;
+ if( (float_prev === 'left' || float_prev === 'right')
+ && (float_next === 'left' || float_next === 'right') ){
+ zone.remove();
+ }else if( !( disp_prev === null
+ || disp_next === null
+ || disp_prev === 'block'
+ || disp_next === 'block' )){
+ zone.remove();
+ }
+ });
+ },
+
+ // generate drop zones covering the elements selected by the selector
+ // we generate overlay drop zones only to get an idea of where the snippet are, the drop
+ activate_overlay_zones: function($targets){
+ var self = this;
+
+ function is_visible($el){
+ return $el.css('display') != 'none'
+ && $el.css('opacity') != '0'
+ && $el.css('visibility') != 'hidden';
+ }
+
+ // filter out invisible elements
+ $targets = $targets.filter(function(){ return is_visible($(this)); });
+
+ // filter out elements with invisible parents
+ $targets = $targets.filter(function(){
+ var parents = $(this).parents().filter(function(){ return !is_visible($(this)); });
+ return parents.length === 0;
+ });
+
+ $targets.each(function () {
+ var $target = $(this);
+ if (!$target.data('overlay')) {
+ var $zone = $(qweb.render('web_editor.snippet_overlay'));
+
+ // fix for pointer-events: none with ie9
+ if (document.body && document.body.addEventListener) {
+ $zone.on("click mousedown mousedown", function passThrough(event) {
+ event.preventDefault();
+ $target.each(function() {
+ // check if clicked point (taken from event) is inside element
+ event.srcElement = this;
+ $(this).trigger(event.type);
+ });
+ return false;
+ });
+ }
+
+ $zone.appendTo('#oe_manipulators');
+ $zone.data('target',$target);
+ $target.data('overlay',$zone);
+
+ var timer;
+ $target.closest('.o_editable').on("content_changed", function (event) {
+ if ($target.data('overlay') && $target.data('overlay').hasClass("oe_active")) {
+ clearTimeout(timer);
+ timer = setTimeout(function () {
+ if ($target.data('overlay')) {
+ self.cover_target($target.data('overlay'), $target);
+ }
+ },50);
+ }
+ });
+
+ var resize = function () {
+ if ($zone.parent().length) {
+ self.cover_target($zone, $target);
+ } else {
+ $('body').off("resize", resize);
+ }
+ };
+ $('body').on("resize", resize);
+ }
+ self.cover_target($target.data('overlay'), $target);
+ });
+ return $targets;
+ }
+});
+
+/* ----- Editor (object who contain overlay and the option list) ---- */
+
+var Editor = Class.extend({
+ init: function (BuildingBlock, dom) {
+ this.buildingBlock = BuildingBlock;
+ this.$target = $(dom);
+ this.$overlay = this.$target.data('overlay');
+ this.load_style_options();
+ this.get_parent_block();
+ this.start();
+ },
+
+ // activate drag and drop for the snippets in the snippet toolbar
+ _drag_and_drop: function(){
+ var self = this;
+ this.dropped = false;
+ this.$overlay.draggable({
+ greedy: true,
+ appendTo: 'body',
+ cursor: "move",
+ handle: ".oe_snippet_move",
+ cursorAt: {
+ left: 18,
+ top: 14
+ },
+ helper: function() {
+ var $clone = $(this).clone().css({width: "24px", height: "24px", border: 0});
+ $clone.find(".oe_overlay_options >:not(:contains(.oe_snippet_move)), .oe_handle").remove();
+ $clone.find(":not(.glyphicon)").css({position: 'absolute', top: 0, left: 0});
+ $clone.appendTo("body").removeClass("hidden");
+ return $clone;
+ },
+ start: _.bind(self._drag_and_drop_start, self),
+ stop: _.bind(self._drag_and_drop_stop, self)
+ });
+ },
+ _drag_and_drop_after_insert_dropzone: function (){},
+ _drag_and_drop_active_drop_zone: function ($zones){
+ var self = this;
+ $zones.droppable({
+ over: function(){
+ $(".oe_drop_zone.hide").removeClass("hide");
+ $(this).addClass("hide").first().after(self.$target);
+ self.dropped = true;
+ },
+ out: function(){
+ $(this).removeClass("hide");
+ self.$target.detach();
+ self.dropped = false;
+ },
+ });
+ },
+ _drag_and_drop_start: function (){
+ var self = this;
+ self.buildingBlock.hide();
+ self.buildingBlock.editor_busy = true;
+ self.size = {
+ width: self.$target.width(),
+ height: self.$target.height()
+ };
+ self.$target.after("");
+ self.$target.detach();
+ self.$overlay.addClass("hidden");
+
+ var $selector_siblings;
+ for (var i=0; i');
+ this.option= option_id;
+ this.$el = option.$el.find(">li").clone();
+ this.data = option.$el.data();
+ },
+
+ // helper for this.$target.find
+ $: function (selector) {
+ return this.$target(selector);
+ },
+
+ _bind_li_menu: function () {
+ this.$el.filter("li:hasData").find('a:first')
+ .off('mouseenter click')
+ .on('mouseenter click', _.bind(this._mouse, this));
+
+ this.$el
+ .off('mouseenter click', "li:hasData a")
+ .on('mouseenter click', "li:hasData a", _.bind(this._mouse, this));
+
+ this.$el.closest("ul").add(this.$el)
+ .off('mouseleave')
+ .on('mouseleave', _.bind(this.reset, this));
+
+ this.$el
+ .off('mouseleave', "ul")
+ .on('mouseleave', "ul", _.bind(this.reset, this));
+
+ this.reset_methods = [];
+ },
+
+ /**
+ * this method handles mouse:over and mouse:leave on the snippet editor menu
+ */
+ _time_mouseleave: null,
+ _mouse: function (event) {
+ var $next = $(event.currentTarget).parent();
+
+ // triggers preview or apply methods if a menu item has been clicked
+ this.select(event.type === "click" ? "click" : "over", $next);
+ if (event.type === 'click') {
+ this.set_active();
+ this.$target.trigger("snippet-option-change", [this]);
+ } else {
+ this.$target.trigger("snippet-option-preview", [this]);
+ }
+ },
+ /*
+ * select and set item active or not (add highlight item and his parents)
+ * called before start
+ */
+ set_active: function () {
+ var classes = _.uniq((this.$target.attr("class") || '').split(/\s+/));
+ this.$el.find('[data-toggle_class], [data-select_class]')
+ .add(this.$el)
+ .filter('[data-toggle_class], [data-select_class]')
+ .removeClass("active")
+ .filter('[data-toggle_class="' + classes.join('"], [data-toggle_class="') + '"] ,'+
+ '[data-select_class="' + classes.join('"], [data-select_class="') + '"]')
+ .addClass("active");
+ },
+
+ start: function () {
+ this.set_active();
+ this.$target.on('snippet-option-reset', _.bind(this.set_active, this));
+ this._bind_li_menu();
+ },
+
+ on_focus : function () {
+ this._bind_li_menu();
+ },
+
+ on_blur : function () {
+ },
+
+ on_clone: function ($clone) {
+ },
+
+ on_move: function () {
+ },
+
+ on_remove: function () {
+ },
+
+ drop_and_build_snippet: function () {
+ },
+
+ reset: function () {
+ var self = this;
+ var lis = self.$el.add(self.$el.find('li')).filter('.active').get();
+ lis.reverse();
+ _.each(lis, function (li) {
+ var $li = $(li);
+ for (var k in self.reset_methods) {
+ var method = self.reset_methods[k];
+ if ($li.is('[data-'+method+']') || $li.closest('[data-'+method+']').size()) {
+ delete self.reset_methods[k];
+ }
+ }
+ self.select("reset", $li);
+ });
+
+ for (var k in self.reset_methods) {
+ var method = self.reset_methods[k];
+ if (method) {
+ self[method]("reset", null);
+ }
+ }
+ self.reset_methods = [];
+ self.$target.trigger("snippet-option-reset", [this]);
+ },
+
+ // call data-method args as method (data-only can be used)
+ select: function (type, $li) {
+ var self = this,
+ $methods = [],
+ el = $li[0];
+
+ if ($li.data('only') && type !== $li.data('only')) {
+ return;
+ }
+
+ if (type==="click") {
+ this.reset();
+ this.buildingBlock.parent.rte.historyRecordUndo(this.$target);
+ }
+
+ function filter (k) { return k !== 'oeId' && k !== 'oeModel' && k !== 'oeField' && k !== 'oeXpath' && k !== 'oeSourceId' && k !== 'only';}
+ function hasData(el) {
+ for (var k in el.dataset) {
+ if (filter (k)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ function method(el) {
+ var data = {};
+ for (var k in el.dataset) {
+ if (filter (k)) {
+ data[k] = el.dataset[k];
+ }
+ }
+ return data;
+ }
+
+ while (el && this.$el.is(el) || _.some(this.$el.map(function () {return $.contains(this, el);}).get()) ) {
+ if (hasData(el)) {
+ $methods.push(el);
+ }
+ el = el.parentNode;
+ }
+
+ $methods.reverse();
+
+ _.each($methods, function (el) {
+ var $el = $(el);
+ var methods = method(el);
+
+ for (var k in methods) {
+ if (self[k]) {
+ if (type !== "reset" && self.reset_methods.indexOf(k) === -1) {
+ self.reset_methods.push(k);
+ }
+ self[k](type, methods[k], $el);
+ } else {
+ console.error("'"+self.option+"' snippet have not method '"+k+"'");
+ }
+ }
+ });
+ },
+
+ // default method for snippet
+ toggle_class: function (type, value, $li) {
+ var $lis = this.$el.find('[data-toggle_class]').add(this.$el).filter('[data-toggle_class]');
+
+ function map ($lis) {
+ return $lis.map(function () {return $(this).data("toggle_class");}).get().join(" ");
+ }
+ var classes = map($lis);
+ var active_classes = map($lis.filter('.active, :has(.active)'));
+
+ this.$target.removeClass(classes);
+ this.$target.addClass(active_classes);
+
+ if (type !== 'reset') {
+ this.$target.toggleClass(value);
+ }
+ },
+ select_class: function (type, value, $li) {
+ var $lis = this.$el.find('[data-select_class]').add(this.$el).filter('[data-select_class]');
+
+ var classes = $lis.map(function () {return $(this).data('select_class');}).get();
+
+ this.$target.removeClass(classes.join(" "));
+ if(value) this.$target.addClass(value);
+ },
+ eval: function (type, value, $li) {
+ var fn = new Function("node", "type", "value", "$li", value);
+ fn.call(this, this, type, value, $li);
+ },
+
+ clean_for_save: editor.dummy
+});
+
+/* ----- default options ---- */
+
+var media = Option.extend({
+ start: function () {
+ var self =this;
+ this._super();
+ animation.start(true, this.$target);
+ this.$target.closest(".o_editable").on("content_changed", function () {
+ if (!self.$target.parent().length) {
+ self.$target.remove();
+ self.$overlay.remove();
+ }
+ });
+ },
+ edition: function (type, value) {
+ if(type !== "click") return;
+ var self = this;
+ var _editor = new widget.MediaDialog(this.$target.closest('.o_editable'), this.$target[0]);
+ _editor.appendTo(document.body);
+ _editor.on('saved', this, function (item, old) {
+ self.editor.on_blur();
+ self.buildingBlock.make_active(false);
+ if (self.$target.parent().data("oe-field") !== "image") {
+ setTimeout(function () {
+ self.buildingBlock.make_active($(item));
+ },0);
+ }
+ });
+ },
+ on_focus : function () {
+ var self = this;
+ var $parent = this.$target.parent();
+
+ if ($parent.data("oe-field") === "image" && $parent.hasClass('o_editable')) {
+ this.$overlay.addClass("hidden");
+ self.edition('click', null);
+ self.buildingBlock.make_active(false);
+ }
+ }
+});
+
+var colorpicker = Option.extend({
+ start: function () {
+ var self = this;
+ var res = this._super();
+
+ if (!this.$el.find('.colorpicker').length) {
+ this.$el.find('li').append( qweb.render('web_editor.colorpicker') );
+ }
+ if (this.$el.data('area')) {
+ this.$target = this.$target.find(this.$el.data('area'));
+ this.$el.removeData('area').removeAttr('area');
+ }
+
+ var classes = [];
+ this.$el.find(".colorpicker button").map(function () {
+ var $color = $(this);
+ var color = $color.attr("class");
+ if (self.$target.hasClass(color)) {
+ self.color = color;
+ $color.parent().addClass("selected");
+ }
+ classes.push(color);
+ });
+ this.classes = classes.join(" ");
+
+ this.bind_events();
+ return res;
+ },
+ bind_events: function () {
+ var self = this;
+ var $td = this.$el.find(".colorpicker td");
+ var $colors = this.$el.find(".colorpicker button");
+ $colors
+ .mouseenter(function () {
+ self.$target.removeClass(self.classes).addClass($(this).attr("class"));
+ })
+ .mouseleave(function () {
+ self.$target.removeClass(self.classes)
+ .addClass($td.filter(".selected").children().attr("class"));
+ })
+ .click(function () {
+ $td.removeClass("selected");
+ $(this).parent().addClass("selected");
+ });
+ }
+});
+
+var background = Option.extend({
+ start: function ($change_target) {
+ this.$bg = $change_target || this.$target;
+ this._super();
+ var src = this.$bg.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
+ if (this.$bg.hasClass('oe_custom_bg')) {
+ this.$el.find('li[data-choose_image]').data("background", src).attr("data-background", src);
+ }
+ },
+ background: function(type, value, $li) {
+ if (value && value.length) {
+ this.$bg.attr("style", 'background-image: url(' + value + ')' + (this.$bg.attr("style") || '').replace(/background-image:[^;]+/, '') );
+ this.$bg.addClass("oe_img_bg");
+ } else {
+ this.$bg.css("background-image", "");
+ this.$bg.removeClass("oe_img_bg").removeClass("oe_custom_bg");
+ }
+ },
+ choose_image: function(type, value, $li) {
+ if(type !== "click") return;
+
+ var self = this;
+ var $image = $('
');
+ $image.attr("src", value);
+ $image.appendTo(self.$bg);
+
+ var _editor = new widget.MediaDialog(null, $image[0]);
+ _editor.appendTo(document.body);
+ _editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
+
+ _editor.on('saved', self, function () {
+ var value = $image.attr("src");
+ $image.remove();
+ self.$el.find('li[data-choose_image]').data("background", value).attr("data-background", value);
+ self.background(type, value,$li);
+ self.$bg.addClass('oe_custom_bg');
+ self.$bg.trigger("snippet-option-change", [self]);
+ self.set_active();
+ });
+ _editor.on('cancel', self, function () {
+ $image.remove();
+ });
+ },
+ set_active: function () {
+ var self = this;
+ var src = this.$bg.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
+ this._super();
+
+ this.$el.find('li[data-background]:not([data-background=""])')
+ .removeClass("active")
+ .each(function () {
+ var background = $(this).data("background") || $(this).attr("data-background");
+ if ((src.length && background.length && src.indexOf(background) !== -1) || (!src.length && !background.length)) {
+ $(this).addClass("active");
+ }
+ });
+
+ if (!this.$el.find('li[data-background].active').size()) {
+ this.$el.find('li[data-background=""]:not([data-choose_image])').addClass("active");
+ } else {
+ this.$el.find('li[data-background=""]:not([data-choose_image])').removeClass("active");
+ }
+ }
+});
+
+/* t-field options */
+
+var many2one = Option.extend({
+ start: function () {
+ var self = this;
+
+ this.Model = this.$target.data('oe-many2one-model');
+ this.ID = +this.$target.data('oe-many2one-id');
+
+ // create search button and bind search bar
+ this.$btn = $(qweb.render("web_editor.many2one.button"))
+ .insertAfter(this.$overlay.find('.oe_options'));
+
+ this.$ul = this.$btn.find("ul");
+ this.$search = this.$ul.find('li:first');
+ this.$search.find('input').on('mousedown click mouseup keyup keydown', function (e) {
+ e.stopPropagation();
+ });
+
+ // move menu item
+ setTimeout(function () {
+ if (self.$overlay.find('.oe_options').hasClass('hidden')) {
+ self.$btn.css('height', '0').find('> a').addClass('hidden');
+ self.$ul.show().css({
+ 'top': '-24px', 'margin': '0', 'padding': '2px 0', 'position': 'relative'
+ });
+ } else {
+ self.$btn.find('a').on('click', function (e) {
+ self.clear();
+ });
+ }
+ },0);
+
+ // bind search input
+ this.$search.find('input')
+ .focus()
+ .on('keyup', function(e) {
+ self.find_existing($(this).val());
+ });
+
+ // bind result
+ this.$ul.on('click', "li:not(:first) a", function (e) {
+ self.select_record(this);
+ });
+ },
+
+ on_focus: function () {
+ this.$target.attr('contentEditable', 'false');
+ this.clear();
+ this._super();
+ },
+
+ clear: function () {
+ var self = this;
+ this.$search.siblings().remove();
+ self.$search.find('input').val("");
+ setTimeout(function () {
+ self.$search.find('input').focus();
+ },0);
+ },
+
+ find_existing: function (name) {
+ var self = this;
+ var domain = [];
+ if (!name || !name.length) {
+ self.$search.siblings().remove();
+ return;
+ }
+ if (isNaN(+name)) {
+ if (this.Model === "res.partner") {
+ domain.push(['name', 'ilike', name]);
+ } else {
+ domain.push('|', ['name', 'ilike', name], ['email', 'ilike', name]);
+ }
+ } else {
+ domain.push(['id', '=', name]);
+ }
+
+ ajax.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: this.Model,
+ method: 'search_read',
+ args: [domain, this.Model === "res.partner" ? ['name', 'display_name', 'city', 'country_id'] : ['name', 'display_name']],
+ kwargs: {
+ order: 'name DESC',
+ limit: 5,
+ context: base.get_context(),
+ }
+ }).then(function (result){
+ self.$search.siblings().remove();
+ self.$search.after(qweb.render("web_editor.many2one.search",{contacts:result}));
+ });
+ },
+
+ get_contact_rendering: function (options) {
+ return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'ir.qweb.field.contact',
+ method: 'get_record_to_html',
+ args: [[this.ID]],
+ kwargs: {
+ options: options,
+ context: base.get_context(),
+ }
+ });
+ },
+
+ select_record: function (li) {
+ var self = this;
+
+ this.ID = +$(li).data("id");
+ this.$target.attr('data-oe-many2one-id', this.ID).data('oe-many2one-id', this.ID);
+
+ this.buildingBlock.parent.rte.historyRecordUndo(this.$target);
+
+ if (self.$target.data('oe-type') === "contact") {
+ $('[data-oe-contact-options]')
+ .filter('[data-oe-model="'+self.$target.data('oe-model')+'"]')
+ .filter('[data-oe-id="'+self.$target.data('oe-id')+'"]')
+ .filter('[data-oe-field="'+self.$target.data('oe-field')+'"]')
+ .filter('[data-oe-contact-options!="'+self.$target.data('oe-contact-options')+'"]')
+ .add(self.$target)
+ .attr('data-oe-many2one-id', self.ID).data('oe-many2one-id', self.ID)
+ .each(function () {
+ var $node = $(this);
+ self.get_contact_rendering($node.data('oe-contact-options'))
+ .then(function (html){
+ $node.html(html);
+ });
+ });
+ } else {
+ self.$target.html($(li).data("name"));
+ }
+
+ setTimeout(function () {
+ self.buildingBlock.make_active(false);
+ },0);
+ }
+});
+
+/* end*/
+
+return {
+ 'registry': {
+ 'media': media,
+ 'colorpicker': colorpicker,
+ 'background': background,
+ 'many2one': many2one,
+ },
+ 'Class': Option
+};
+
+});
diff --git a/addons/website/static/src/js/summernote.js b/addons/web_editor/static/src/js/summernote.js
similarity index 95%
rename from addons/website/static/src/js/summernote.js
rename to addons/web_editor/static/src/js/summernote.js
index 5303837b26a..56dd3249a6c 100644
--- a/addons/website/static/src/js/summernote.js
+++ b/addons/web_editor/static/src/js/summernote.js
@@ -1,7 +1,8 @@
-odoo.define('website.summernote', function (require) {
+odoo.define('web_editor.summernote', function (require) {
'use strict';
var core = require('web.core');
+require('summernote/summernote'); // wait that summernote is loaded
var _t = core._t;
@@ -502,7 +503,7 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) {
if (sc.tagName) {
sc = sc.childNodes[so] || dom.firstChild(ec);
so = 0;
- if (!dom.hasContentBefore(sc) && towrite) {
+ if (!dom.hasContentBefore(sc)) {
sc.parentNode.insertBefore(document.createTextNode('\u00A0'), sc);
}
}
@@ -557,19 +558,18 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) {
sc = before ? dom.lastChild(before) : dom.firstChild(after);
so = sc.textContent.length;
- if (towrite && !node.firstChild && node.parentNode && !dom.isNotBreakable(node)) {
- sc = $("
")[0];
- so = 0;
- node.appendChild(br);
- } else if (!ancestor.children.length && !ancestor.textContent.match(/\S|\u00A0/)) {
- sc = $("
")[0];
- so = 0;
- $(ancestor).prepend(sc);
- } else if (before) {
+ if (before) {
var text = sc.textContent.replace(/[ \t\n\r]+$/, '\u00A0');
so -= sc.textContent.length - text.length;
sc.textContent = text;
}
+ if (towrite && !node.firstChild && node.parentNode && !dom.isNotBreakable(node)) {
+ var br = $("
")[0];
+ node.appendChild(br);
+ sc = br;
+ so = 0;
+ }
+ dom.autoMerge(sc, false);
} else {
@@ -690,9 +690,9 @@ dom.listBetween = function (sc, ec) {
});
return list.unique(nodes);
};
-dom.isNotBreakable = function (node, sc, so, ec, eo) {
+dom.isNotBreakable = function (node) {
// avoid triple click => crappy dom
- return node === ec && !dom.isText(ec) && !dom.isBR(dom.firstChild(ec));
+ return !dom.isText(node) && !dom.isBR(dom.firstChild(node)) && dom.isVoid(dom.firstChild(node));
};
dom.isContentEditable = function (node) {
@@ -705,7 +705,7 @@ dom.isFont = function (node) {
(nodeName === "SPAN" && (
node.className.match(/(^|\s)fa(\s|$)/i) ||
node.className.match(/(^|\s)(text|bg)-/i) ||
- (node.attributes.style && node.attributes.style.value.match(/(^|\s)(color|background-color|font-size):/i)))) );
+ (node.attributes.style && node.attributes.style.value.match(/(^|\s)(color|background-color):/i)))) );
};
dom.isVisibleText = function (textNode) {
return !!textNode.textContent.match(/\S|\u00A0/);
@@ -1025,13 +1025,17 @@ $.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
var node = dom.node(r.sc);
var last = node;
- while (node && dom.isSplitable(node) && !dom.isList(node) && !dom.isEditable(node)) {
+ while (node && dom.isSplitable(node) && !dom.isList(node)) {
last = node;
node = node.parentNode;
}
if (last === node && !dom.isBR(node)) {
node = r.insertNode(br, true);
+ if (isFormatNode(last.firstChild) && $(last).closest(options.styleTags.join(',')).length) {
+ dom.moveContent(last.firstChild, last);
+ last.removeChild(last.firstChild);
+ }
do {
node = dom.hasContentAfter(node);
} while (node && dom.isBR(node));
@@ -1159,6 +1163,8 @@ $.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
}
r = r.deleteContents();
r.select();
+ event.preventDefault();
+ return false;
}
var target = r.ec;
@@ -1283,6 +1289,8 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
}
r = r.deleteContents();
r.select();
+ event.preventDefault();
+ return false;
}
var target = r.sc;
@@ -1726,19 +1734,12 @@ $.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTa
$.summernote.pluginEvents.removeFormat = function (event, editor, layoutInfo, value) {
var $editable = layoutInfo.editable();
$editable.data('NoteHistory').recordUndo($editable);
-
var node = range.create().sc.parentNode;
document.execCommand('removeFormat');
- document.execCommand('removeFormat'); // 2 times because some browser don't clean all in one time
+ document.execCommand('removeFormat');
var r = range.create();
r = dom.merge(node, r.sc, r.so, r.ec, r.eo, null, true);
- r = range.create(r.sc, r.so, r.ec, r.eo);
- r.select();
-
- $.summernote.pluginEvents.justify(event, editor, layoutInfo, "");
-
- r.clean().select();
-
+ range.create(r.sc, r.so, r.ec, r.eo).select();
event.preventDefault();
return false;
};
@@ -1770,9 +1771,6 @@ eventHandler.toolbar.button.updateRecentColor = function (elBtn, sEvent, sValue)
options.fontSizes = [_t('Default'), 8, 9, 10, 11, 12, 14, 18, 24, 36, 48, 62];
$.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color, bgcolor, size) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable);
-
var rng = range.create();
var startPoint = rng.getStartPoint();
var endPoint = rng.getEndPoint();
@@ -1962,7 +1960,7 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
if (i>0 && (font = dom.ancestor(nodes[i-1], dom.isFont))) {
className2 = font.getAttribute('class');
style2 = font.getAttribute('style');
- if (node !== font && className == className2 && style == style2 && (node.parentNode === font.parentNode || node.parentNode === font)) {
+ if (node !== font && className == className2 && style == style2) {
remove(node, font);
nodes.splice(i,1);
i--;
@@ -1975,8 +1973,6 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
};
$.summernote.pluginEvents.fontSize = function (event, editor, layoutInfo, value) {
var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable);
-
event.preventDefault();
$.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, null, value);
editor.afterCommand($editable);
@@ -2010,60 +2006,6 @@ $.summernote.pluginEvents.backColor = function (event, editor, layoutInfo, backC
}
$.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, backColor, null);
editor.afterCommand($editable);
- };
-
- $.summernote.pluginEvents.justify = function (event, editor, layoutInfo, align) {
- var rng = range.create();
- var startPoint = rng.getStartPoint();
- var endPoint = rng.getEndPoint();
-
- // get list of nodes to change
- var nodes = [];
- nodes.push(dom.ancestor(startPoint.node, dom.isPara) || dom.ancestor(startPoint.node, dom.isCell));
- nodes.push(dom.ancestor(endPoint.node, dom.isPara) || dom.ancestor(endPoint.node, dom.isCell));
- dom.walkPoint(startPoint, endPoint, function (point) {
- var node = point.node;
- if (dom.isPara(node) || dom.isCell(node)) {
- nodes.push(point.node);
- }
- });
- nodes = list.unique(nodes);
-
- $(nodes).not(":has(div,p,li,h1,h2,h3,h4,h5,h6,h7,td,th)").each(function () {
- var $node = $(this).css('text-align', '');
- var css = window.getComputedStyle(this);
- var textAlign = css.textAlign;
- if (textAlign === "start") {
- textAlign = css.direction === "ltr" ? "left" : "right";
- }
- if(textAlign !== align) {
- $node.css('text-align', align);
- }
- }).find("*").css('text-align', "");
- };
- $.summernote.pluginEvents.justifyLeft = function (event, editor, layoutInfo) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable);
- $.summernote.pluginEvents.justify(event, editor, layoutInfo, 'left');
- editor.afterCommand($editable);
- };
- $.summernote.pluginEvents.justifyCenter = function (event, editor, layoutInfo) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable);
- $.summernote.pluginEvents.justify(event, editor, layoutInfo, 'center');
- editor.afterCommand($editable);
- };
- $.summernote.pluginEvents.justifyRight = function (event, editor, layoutInfo) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable);
- $.summernote.pluginEvents.justify(event, editor, layoutInfo, 'right');
- editor.afterCommand($editable);
- };
- $.summernote.pluginEvents.justifyFull = function (event, editor, layoutInfo) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable);
- $.summernote.pluginEvents.justify(event, editor, layoutInfo, 'justify');
- editor.afterCommand($editable);
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -2245,4 +2187,6 @@ eventHandler.dettach = function (oLayoutInfo, options) {
$editable.off("scroll", summernote_table_scroll);
};
+return $.summernote;
+
});
diff --git a/addons/website/static/src/js/website.tour.rte.js b/addons/web_editor/static/src/js/tour.rte.js
similarity index 56%
rename from addons/website/static/src/js/website.tour.rte.js
rename to addons/web_editor/static/src/js/tour.rte.js
index b068c11f604..c257176fd6f 100644
--- a/addons/website/static/src/js/website.tour.rte.js
+++ b/addons/web_editor/static/src/js/tour.rte.js
@@ -1,11 +1,15 @@
-odoo.define('website.tour.rte', function (require) {
+odoo.define('web_editor.tour', function (require) {
'use strict';
var core = require('web.core');
var Tour = require('web.Tour');
+var base = require('web_editor.base');
+var snippet_editor = require('web_editor.snippet.editor');
var _t = core._t;
+base.ready().done(function () {
+
var click_event = function(el, type) {
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el);
@@ -13,47 +17,36 @@ var click_event = function(el, type) {
};
Tour.register({
- id: 'website_rte',
- name: "Test website RTE",
- path: '/page/homepage',
+ id: 'rte',
+ name: "Test RTE",
mode: 'test',
+ path: '/web_editor/field/html?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
steps: [
- {
- element: 'button[data-action=edit]',
- title: "Edit this page",
- wait: 250
- },
- {
- snippet: '#snippet_structure .oe_snippet:has(.s_text_image)',
- title: "Drag & Drop a Text-Image Block",
- },
- {
- element: '.oe_overlay_options:visible .oe_options a:first',
- title: "Customize",
- onload: function () {
- $(".oe_overlay_options:visible .snippet-option-background > ul").show();
- }
- },
- {
- element: '.oe_overlay_options:visible .snippet-option-background > ul li[data-background*="quote"]:first a',
- title: "Chose a background image",
- },
{
title: "Change html for this test",
- waitFor: '#wrapwrap > main > div > section:first[style*="background-image"]',
- element: '#wrapwrap > main > div > section .row > div:first',
onload: function () {
var $el = $(this.element);
- var html = 'Batnae municipium in Anthemusia
'+
- '\n Batnae municipium in Anthemusia conditum Macedonum manu priscorum ab Euphrate flumine brevi spatio disparatur, refertum mercatoribus opulentis, ubi annua sollemnitate prope Septembris initium mensis ad.
'+
- '\n Quam
quidem partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.
'+
- '\n Et hanc quidem praeter oppida multa duae civitates exornant Seleucia opus Seleuci regis, et Claudiopolis quam deduxit coloniam Claudius Caesar. Isaura enim antehac nimium potens, olim subversa ut rebellatrix.
'+
- 'Harum trium sententiarum nulli prorsus assentior.
';
- $el.html(html);
+ var html = '\n'+
+ '\n'+
+ ' \n'+
+ '
\n'+
+ '
\n'+
+ '
Batnae municipium in Anthemusia
\n'+
+ '
Batnae municipium in Anthemusia conditum Macedonum manu priscorum ab Euphrate flumine brevi spatio disparatur, refertum mercatoribus opulentis, ubi annua sollemnitate prope Septembris initium mensis ad.
\n'+
+ '
Quam
quidem partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.
\n'+
+ '
Et hanc quidem praeter oppida multa duae civitates exornant Seleucia opus Seleuci regis, et Claudiopolis quam deduxit coloniam Claudius Caesar. Isaura enim antehac nimium potens, olim subversa ut rebellatrix.
'+
+ '
Harum trium sententiarum nulli prorsus assentior.
\n'+
+ '
\n'+
+ '
\n'+
+ '

\n'+
+ '
\n'+
+ '
\n'+
+ '\n';
+ $("#editable_area").html(html);
}
},
{
- element: '#wrapwrap > main > div > section .row > div:first',
+ element: '#editable_area > section .row > div:first',
title: "simulate triple click and change text bg-color",
onload: function () {
var $el = $(this.element);
@@ -63,16 +56,16 @@ Tour.register({
}
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first',
+ waitFor: '#editable_area > section .row > div:first',
element: '.note-popover button[data-event="color"]:visible',
title: "change text bg-color after triple click",
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first:not(:has(p font)) h1 font',
+ waitFor: '#editable_area > section .row > div:first:not(:has(p font)) h1 font',
element: '.note-color button.dropdown-toggle:visible',
title: "change selection to change text color",
onload: function () {
- var $el = $('#wrapwrap > main > div > section .row > div:first:not(:has(p font)) h1 font');
+ var $el = $('#editable_area > section .row > div:first:not(:has(p font)) h1 font');
$.summernote.core.range.create($el[0].firstChild, 5, $el[0].firstChild, 10).select();
click_event($el[0], 'mouseup');
}
@@ -82,11 +75,11 @@ Tour.register({
title: "change text color",
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first h1 font:eq(2)',
+ waitFor: '#editable_area > section .row > div:first h1 font:eq(2)',
element: '.note-color button.dropdown-toggle:visible',
title: "change selection to change text bg-color again",
onload: function () {
- var $el = $('#wrapwrap > main > div > section .row > div:first h1 font:eq(2)');
+ var $el = $('#editable_area > section .row > div:first h1 font:eq(2)');
$.summernote.core.range.create($el.prev()[0].firstChild, 3, $el[0].firstChild, 10).select();
click_event($el.prev()[0], 'mouseup');
}
@@ -96,11 +89,11 @@ Tour.register({
title: "change text backColor again",
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first h1 font:eq(4)',
+ waitFor: '#editable_area > section .row > div:first h1 font:eq(4)',
element: '.note-color button.dropdown-toggle:visible',
title: "change selection (h1 and p) to change text color with class",
onload: function () {
- var $el = $('#wrapwrap > main > div > section .row > div:first h1 font:eq(4)');
+ var $el = $('#editable_area > section .row > div:first h1 font:eq(4)');
$.summernote.core.range.create($el.prev()[0].firstChild, 3, $el.parent("h1").next("p")[0].firstChild, 30).select();
click_event($el.prev()[0], 'mouseup');
}
@@ -110,13 +103,13 @@ Tour.register({
title: "change text foreColor again",
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first p font',
+ waitFor: '#editable_area > section .row > div:first p font',
element: '.o_editable.note-editable.o_dirty',
title: "delete selection",
keydown: 46 // delete
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first:not(:has(p font)) h1',
+ waitFor: '#editable_area > section .row > div:first:not(:has(p font)) h1',
element: '.o_editable.note-editable.o_dirty',
title: "clean and delete (backspace) an other selection",
onload: function () {
@@ -128,7 +121,7 @@ Tour.register({
keydown: 8 // backspace
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
+ waitFor: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
element: '.o_editable.note-editable.o_dirty',
title: "delete an other selection",
onload: function () {
@@ -139,24 +132,24 @@ Tour.register({
keydown: 46
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first:has( font:last:containsExact(Bat) )',
- element: '.note-image-popover button[data-event="undo"]',
+ waitFor: '#editable_area > section .row > div:first:has( font:last:containsExact(Bat) )',
+ element: '.note-air-popover button[data-event="undo"]',
title: "undo",
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first:has( font:last:containsExact(i) )',
+ waitFor: '#editable_area > section .row > div:first:has( font:last:containsExact(i) )',
element: '.note-air-popover button[data-event="undo"]',
title: "undo adain",
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first:not(:has(p font)) h1',
+ waitFor: '#editable_area > section .row > div:first:not(:has(p font)) h1',
element: '.o_editable.note-editable.o_dirty',
title: "delete (backspace) after undo",
keydown: 8 // backspace
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) )',
- element: '#wrapwrap > main > div > section .row > div:first img[style*="25%"]',
+ waitFor: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) )',
+ element: '#editable_area > section .row > div:first img[style*="25%"]',
title: "click on image",
},
{
@@ -164,7 +157,7 @@ Tour.register({
title: "Click on resize half",
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first img[style*="50%"]',
+ waitFor: '#editable_area > section .row > div:first img[style*="50%"]',
element: '.note-image-popover:visible button[data-event="showImageDialog"]',
title: "Click on edit picture",
},
@@ -192,7 +185,7 @@ Tour.register({
title: "click on float right",
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first span.fa.pull-right',
+ waitFor: '#editable_area > section .row > div:first span.fa.pull-right',
element: '.note-image-popover:visible button[data-event="showLinkDialog"]',
title: "click on create link",
},
@@ -205,26 +198,32 @@ Tour.register({
element: '.modal .dropdown ul label.btn-success',
title: "choose success style",
},
+ // {
+ // waitFor: '.modal a#link-preview.btn:containsRegex(/^
]+><\\/span>$/)',
+ // element: '.modal .select2-container.url-source a.select2-choice',
+ // title: "click to choose an internal page",
+ // },
+ // {
+ // element: '.select2-drop:visible .select2-search input',
+ // title: "search 'contact'",
+ // sampleText: "contact",
+ // },
+ // {
+ // element: '.select2-drop:visible .select2-results .select2-result div:contains(/page/)',
+ // title: "select /page/contactus",
+ // },
{
- waitFor: '.modal a#link-preview.btn:containsRegex(/^]+><\\/span>$/)',
- element: '.modal .select2-container.url-source a.select2-choice',
- title: "click to choose an internal page",
- },
- {
- element: '.select2-drop:visible .select2-search input',
- title: "search 'contact'",
- sampleText: "contact",
- },
- {
- element: '.select2-drop:visible .select2-results .select2-result div:contains(/page/)',
- title: "select /page/contactus",
- },
- {
- waitNot: '.select2-drop:visible',
+ // waitNot: '.select2-drop:visible',
element: '#link-text',
title: "change text label",
sampleText: "ABC[IMG] DEF",
},
+ {
+ waitFor: '.modal a#link-preview.btn',
+ element: '#link-external',
+ sampleText: "http://www.odoo.com",
+ title: "insert a link url",
+ },
{
waitFor: '.modal a#link-preview.btn:containsRegex(/^ABC]+><\\/span> DEF$/)',
element: '.modal button.save',
@@ -232,12 +231,12 @@ Tour.register({
},
{
waitNot: '#link-preview',
- waitFor: 'a.btn[href^="/"]:has(span.fa.fa-3x.pull-right)',
- element: '#wrapwrap > main > div > section .row > div:last img',
+ waitFor: 'a.btn[href^="http://"]:has(span.fa.fa-3x.pull-right)',
+ element: '#editable_area > section .row > div:last img',
title: "click on other picture",
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first span.fa.pull-right',
+ waitFor: '#editable_area > section .row > div:first span.fa.pull-right',
element: '.note-image-popover:visible button[data-event="showLinkDialog"]',
title: "click on create link again",
},
@@ -263,8 +262,8 @@ Tour.register({
},
{
waitNot: '#link-preview',
- waitFor: '#wrapwrap > main > div > section .row > div:eq(1) > a > img',
- element: '#wrapwrap > main > div > section .row > div:first p:eq(2)',
+ waitFor: '#editable_area > section .row > div:eq(1) > a > img',
+ element: '#editable_area > section .row > div:first p:eq(2)',
title: "triple enter then double backspace",
keydown: [66, 13, 66, 13, 13, 8, 8], // B enter B enter enter backspace backspace
onload: function () {
@@ -274,11 +273,11 @@ Tour.register({
},
},
{
- waitNot: '#wrapwrap > main > div > section .row > div:first p:eq(4), #wrapwrap > main > div > section .row > div:eq(3)',
- waitFor: '#wrapwrap > main > div > section .row > div:first p:eq(3)',
+ waitNot: '#editable_area > section .row > div:first p:eq(4), #editable_area > section .row > div:eq(3)',
+ waitFor: '#editable_area > section .row > div:first p:eq(3)',
title: "add ul content",
onload: function () {
- var $el = $('#wrapwrap > main > div > section .row > div:first');
+ var $el = $('#editable_area > section .row > div:first');
var html = ' '+
'\n -
Batnae municipium.
'+
'\n - Seleucia praeter.
'+
@@ -291,7 +290,7 @@ Tour.register({
}
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first ul li p:first',
+ waitFor: '#editable_area > section .row > div:first ul li p:first',
element: '.note-air-popover .note-style button.dropdown-toggle',
title: "click on style dropdown",
},
@@ -299,24 +298,24 @@ Tour.register({
element: '.note-air-popover .note-style ul:visible a[data-value="h3"]',
title: "select h3",
onload: function () {
- var node = $('#wrapwrap > main > div > section .row > div:first ul li p:last')[0].firstChild;
+ var node = $('#editable_area > section .row > div:first ul li p:last')[0].firstChild;
$.summernote.core.range.create(node, 0).select();
click_event(node, 'mouseup');
}
},
{
- element: '#wrapwrap > main > div > section .row > div:first > ul > li > h3',
+ element: '#editable_area > section .row > div:first > ul > li > h3',
title: "double tabulation",
keydown: [9, 9] // tabulation
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first ul > li > ul > li > ul > li > h3',
+ waitFor: '#editable_area > section .row > div:first ul > li > ul > li > ul > li > h3',
element: '.note-air-popover button[data-event="insertOrderedList"]',
title: "click on order list",
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first ul > li > ul > li > ol > li > h3',
- element: '#wrapwrap > main > div > section .row > div:first ul li > p:last',
+ waitFor: '#editable_area > section .row > div:first ul > li > ul > li > ol > li > h3',
+ element: '#editable_area > section .row > div:first ul li > p:last',
title: "enter in ul",
keydown: [66, 13], // enter
onload: function () {
@@ -326,15 +325,74 @@ Tour.register({
}
},
{
- element: '#wrapwrap > main > div > section .row > div:first ul li > p:eq(1):containsRegex(/^municipium./)',
+ element: '#editable_area > section .row > div:first ul li > p:eq(1):containsRegex(/^municipium./)',
title: "backspace in list",
keydown: 8
},
{
- waitFor: '#wrapwrap > main > div > section .row > div:first ul li p:eq(1)',
+ waitFor: '#editable_area > section .row > div:first ul li p:eq(1)',
title: "end",
},
]
});
+
+Tour.register({
+ id: 'rte_inline',
+ name: "Test RTE Inline",
+ mode: 'test',
+ path: '/web_editor/field/html/inline?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
+ steps: [
+ {
+ title: "Change html for this test",
+ onload: function () {
+ var $el = $(this.element);
+ var html = '\n'+
+ '\n'+
+ '
\n'+
+ ' \n'+
+ ' \n'+
+ ' \n'+
+ ' \n'+
+ ' | \n'+
+ ' \n'+
+ ' \n'+
+ ' \n'+
+ ' | \n'+
+ '
\n'+
+ ' \n'+
+ '
\n'+
+ '
';
+ $("#editable_area").html(html);
+ }
+ },
+ {
+ title: "call clean for save",
+ element: '#wrapwrap table',
+ onload: function () {
+ snippet_editor.instance.clean_for_save();
+ }
+ },
+ {
+ waitFor: '#wrapwrap img:first[style*="border-bottom-left-radius:50%"][style*="border-top-width"][style*="padding-top"]',
+ title: "check the image style",
+ },
+ {
+ waitFor: '#wrapwrap img:eq(1)[src^="/web_editor/font_to_img/"][src$="/rgb(66,139,202)/27"]',
+ title: "check the font image src",
+ },
+ {
+ waitFor: '#wrapwrap img:eq(1)[style*="height:2em"]',
+ title: "check the font class to css",
+ },
+ {
+ waitFor: '#wrapwrap img:eq(2)[style*="float:right"]',
+ title: "check the second font class to css",
+ },
+ ]
+});
+
+
+});
+
});
diff --git a/addons/web_editor/static/src/js/transcoder.js b/addons/web_editor/static/src/js/transcoder.js
new file mode 100644
index 00000000000..c7bcd14107f
--- /dev/null
+++ b/addons/web_editor/static/src/js/transcoder.js
@@ -0,0 +1,172 @@
+odoo.define('web_editor.transcoder', function (require) {
+'use strict';
+
+var widget = require('web_editor.widget');
+
+var cache = {};
+var rulesCache = [];
+var getMatchedCSSRules = function (a) {
+ if(cache[a.tagName + "." +a.className]) {
+ return cache[a.tagName + "." +a.className];
+ }
+ if (!rulesCache.length) {
+ var sheets = document.styleSheets;
+ for(var i = sheets.length-1; i >= 0 ; i--) {
+ var rules = sheets[i].rules || sheets[i].cssRules;
+ if (rules) {
+ for(var r = rules.length-1; r >= 0; r--) {
+ var selectorText = rules[r].selectorText;
+ if (selectorText &&
+ rules[r].cssText &&
+ selectorText.indexOf(".") !== -1 &&
+ selectorText.indexOf(".note-") === -1 &&
+
+ selectorText.indexOf(":hover") === -1 &&
+ selectorText.indexOf(":before") === -1 &&
+ selectorText.indexOf(":after") === -1 &&
+ selectorText.indexOf(":active") === -1 &&
+ selectorText.indexOf(":link") === -1 &&
+ selectorText.indexOf("::") === -1) {
+ var st = selectorText.split(/\s*,\s*/);
+ for (var k=0; k").attr("src", src).attr("data-class", $font.attr("class")).attr("style", style);
+ $font.replaceWith($img);
+ } else {
+ $font.remove();
+ }
+ });
+};
+// convert image into font awsome
+var img_to_font = function ($editable) {
+ $("img[src*='/web_editor/font_to_img/']", $editable).each(function () {
+ var $img = $(this);
+ var $font = $("").attr("class", $img.data("class")).attr("style", $img.attr("style")).css("height", "");
+ $img.replaceWith($font);
+ });
+};
+
+// convert class into inline style to send by mail
+var class_to_style = function ($editable) {
+ if (!rulesCache.length) {
+ getMatchedCSSRules($editable[0]);
+ }
+ var selector = _.map(rulesCache, function (a) { return a.selector;}).join(",");
+ $editable.find(selector).each(function () {
+ var $target = $(this);
+ var css = getMatchedCSSRules(this);
+ var style = $target.attr("style") || "";
+ _.each(css, function (v,k) {
+ if (style.indexOf(k) === -1) {
+ style = k+":"+v+";"+style;
+ }
+ });
+ $target.attr("style", style);
+ });
+};
+// convert style into inline class from mail
+var style_to_class = function ($editable) {
+ getMatchedCSSRules($editable[0]);
+ var classes = [];
+ $editable.find("[class]").each(function () {
+ classes = classes.concat(this.className.split(/\s+/));
+ });
+
+ var maybe_selector = _.filter(rulesCache, function (a) { return !!_.find(classes, function (b) { return a.selector.indexOf("."+b) !== -1; } ); });
+ var selector = _.map(maybe_selector, function (a) { return a.selector;}).join(",");
+
+ var $c = $('').appendTo("body");
+
+ $editable.find(selector).each(function () {
+ var $target = $(this);
+ var css = getMatchedCSSRules(this);
+ var style = "";
+ _.each(css, function (v,k) {
+ if (style.indexOf(k) === -1) {
+ style = k+":"+v+";"+style;
+ }
+ });
+ css = $c.attr("style", style).attr("style").split(/\s*;\s*/);
+ style = $target.attr("style") || "";
+ _.each(css, function (v) {
+ style = style.replace(v, '');
+ });
+ $target.attr("style", style.replace(/;+(\s;)*/g, ';').replace(/^;/g, ''));
+ });
+ $c.remove();
+};
+
+return {
+ 'font_to_img': font_to_img,
+ 'img_to_font': img_to_font,
+ 'class_to_style': class_to_style,
+ 'style_to_class': style_to_class,
+};
+
+});
\ No newline at end of file
diff --git a/addons/web_editor/static/src/js/translator.js b/addons/web_editor/static/src/js/translator.js
new file mode 100644
index 00000000000..190706a111c
--- /dev/null
+++ b/addons/web_editor/static/src/js/translator.js
@@ -0,0 +1,214 @@
+odoo.define('web_editor.translate', function (require) {
+'use strict';
+
+var core = require('web.core');
+var Model = require('web.Model');
+var ajax = require('web.ajax');
+var Class = require('web.Class');
+var Widget = require('web.Widget');
+var base = require('web_editor.base');
+var rte = require('web_editor.rte');
+
+var qweb = core.qweb;
+
+ajax.loadXML('/web_editor/static/src/xml/translator.xml', qweb);
+
+
+var translatable = !!$('html').data('translatable');
+var edit_translations = !!$('html').data('edit_translations');
+
+function unbind_click(event) {
+ if (event.ctrlKey || !$(event.target).is(':o_editable')) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+}
+
+
+var RTE_Translate = rte.Class.extend({
+ saveElement: function ($el, context) {
+ // remove multi edition
+ if ($el.data('oe-translation-id')) {
+ var key = 'translation:'+$el.data('oe-translation-id');
+ if (this.__saved[key]) return true;
+ this.__saved[key] = true;
+
+ return ajax.jsonRpc('/web/dataset/call', 'call', {
+ model: 'ir.translation',
+ method: 'write',
+ args: [
+ [+$el.data('oe-translation-id')],
+ {value: $el.html(), state: 'translated'},
+ context || base.get_context()
+ ],
+ });
+ }
+
+ return this._super.apply(this, arguments);
+ },
+});
+
+var Translate = Widget.extend({
+ events: {
+ 'click [data-action="save"]': 'save_and_reload',
+ 'click [data-action="cancel"]': 'cancel',
+ },
+ template: 'web_editor.translator',
+ init: function (parent, $target, lang) {
+ this.parent = parent;
+ this.ir_translation = new Model('ir.translation');
+ this.lang = lang || base.get_context().lang;
+ this.setTarget($target);
+ this._super();
+
+ this.rte = new RTE_Translate(this, this.config);
+ this.rte.on('change', this, this.onChange);
+ },
+ start: function () {
+ this._super();
+ return this.edit();
+ },
+ setTarget: function ($target) {
+ this.$target = $target.find('[data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field]');
+ },
+ find: function (selector) {
+ return selector ? this.$target.find(selector).addBack().filter(selector) : this.$target;
+ },
+ edit: function () {
+ var flag = false;
+ window.onbeforeunload = function(event) {
+ if ($('.o_editable.o_dirty').length && !flag) {
+ flag = true;
+ setTimeout(function () {flag=false;},0);
+ return _t('This document is not saved!');
+ }
+ };
+ this.$target.addClass("o_editable");
+ this.rte.start();
+ this.translations = [];
+ this.markTranslatableNodes();
+ this.onTranslateReady();
+ },
+ onTranslateReady: function () {
+ this.$el.show();
+ this.trigger("edit");
+ },
+ onChange: function (node) {
+ var $node = $(node);
+ var trans = this.getTranlationObject($node[0]);
+ $node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' '));
+ },
+ getTranlationObject: function (node) {
+ var $node = $(node);
+ var id = +$node.data('oe-translation-id');
+ if (!id) {
+ id = $node.data('oe-model')+','+$node.data('oe-id')+','+$node.data('oe-field');
+ }
+ var trans = _.find(this.translations, function (trans) {
+ return trans.id === id;
+ });
+ if (!trans) {
+ this.translations.push(trans = {'id': id});
+ }
+ return trans;
+ },
+ markTranslatableNodes: function (node) {
+ var self = this;
+ this.$target.each(function () {
+ var $node = $(this);
+ var trans = self.getTranlationObject(this);
+ trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
+ });
+ this.$target.parent().on('click', unbind_click);
+ console.info('Click on CTRL when you click in an translatable area to have the default behavior');
+ },
+ unarkTranslatableNode: function () {
+ this.$target.removeClass('o_editable').removeAttr('contentEditable');
+ this.$target.parent().off('click', this.__unbind_click);
+ this.$target_attr.off('mousedown click mouseup', this.__translate_attribute);
+ },
+ save_and_reload: function () {
+ return this.save().then(function () {
+ window.location.href = window.location.href.replace(/&?edit_translations(=[^&]*)?/g, '');
+ });
+ },
+ save: function () {
+ var context = base.get_context();
+ context.lang = this.lang;
+ return this.rte.save(context);
+ },
+ cancel: function () {
+ var self = this;
+ this.rte.cancel();
+ this.$target.each(function () {
+ $(this).html(self.getTranlationObject(this).value);
+ });
+ this.unarkTranslatableNode();
+ this.trigger("cancel");
+ this.$el.hide();
+ },
+ destroy: function () {
+ this.cancel();
+ this.$el.remove();
+ this._super();
+ },
+
+ config: function ($editable) {
+ if ($editable.data('oe-model')) {
+ return {
+ 'airMode' : true,
+ 'focus': false,
+ 'airPopover': [
+ ['history', ['undo', 'redo']],
+ ],
+ 'styleWithSpan': false,
+ 'inlinemedia' : ['p'],
+ 'lang': "odoo",
+ 'onChange': function (html, $editable) {
+ $editable.trigger("content_changed");
+ }
+ };
+ }
+ return {
+ 'airMode' : true,
+ 'focus': false,
+ 'airPopover': [
+ ['font', ['bold', 'italic', 'underline', 'clear']],
+ ['fontsize', ['fontsize']],
+ ['color', ['color']],
+ ['history', ['undo', 'redo']],
+ ],
+ 'styleWithSpan': false,
+ 'inlinemedia' : ['p'],
+ 'lang': "odoo",
+ 'onChange': function (html, $editable) {
+ $editable.trigger("content_changed");
+ }
+ };
+ }
+});
+
+
+if (edit_translations) {
+ base.ready().then(function () {
+ data.instance = new Translate(this, $('#wrapwrap'));
+ data.instance.prependTo(document.body);
+
+ $('a[href*=edit_translations]').each(function () {
+ this.href = this.href.replace(/[$?]edit_translations[^&?]+/, '');
+ });
+ $('form[action*=edit_translations]').each(function () {
+ this.action = this.action.replace(/[$?]edit_translations[^&?]+/, '');
+ });
+ });
+}
+
+var data = {
+ 'translatable': translatable,
+ 'edit_translations': edit_translations,
+ 'Class': Translate,
+};
+return data;
+
+});
diff --git a/addons/web_editor/static/src/js/widgets.js b/addons/web_editor/static/src/js/widgets.js
new file mode 100644
index 00000000000..6fab80ad802
--- /dev/null
+++ b/addons/web_editor/static/src/js/widgets.js
@@ -0,0 +1,1069 @@
+odoo.define('web_editor.widget', function (require) {
+'use strict';
+
+var core = require('web.core');
+var ajax = require('web.ajax');
+var Widget = require('web.Widget');
+var base = require('web_editor.base');
+var rte = require('web_editor.rte');
+
+var QWeb = core.qweb;
+var range = $.summernote.core.range;
+var dom = $.summernote.core.dom;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+var Dialog = Widget.extend({
+ events: {
+ 'hidden.bs.modal': 'destroy',
+ 'click button.save': 'save',
+ 'click button[data-dismiss="modal"]': 'cancel',
+ },
+ init: function () {
+ this._super();
+ },
+ start: function () {
+ var sup = this._super();
+ this.$el.modal({backdrop: 'static'});
+ this.$('input:first').focus();
+ return sup;
+ },
+ save: function () {
+ this.close();
+ this.trigger("saved");
+ },
+ cancel: function () {
+ this.trigger("cancel");
+ },
+ close: function () {
+ this.$el.modal('hide');
+ },
+});
+
+/**
+ * alt widget. Lets users change a alt & title on a media
+ */
+var alt = Dialog.extend({
+ template: 'web_editor.dialog.alt',
+ init: function ($editable, media) {
+ this.$editable = $editable;
+ this.media = media;
+ this.alt = ($(this.media).attr('alt') || "").replace(/"/g, '"');
+ this.title = ($(this.media).attr('title') || "").replace(/"/g, '"');
+ return this._super();
+ },
+ save: function () {
+ var self = this;
+ range.createFromNode(self.media).select();
+ this.$editable.data('NoteHistory').recordUndo();
+ var alt = this.$('#alt').val();
+ var title = this.$('#title').val();
+ $(this.media).attr('alt', alt ? alt.replace(/"/g, """) : null).attr('title', title ? title.replace(/"/g, """) : null);
+ setTimeout(function () {
+ click_event(self.media, "mouseup");
+ },0);
+ return this._super();
+ },
+});
+
+var click_event = function(el, type) {
+ var evt = document.createEvent("MouseEvents");
+ evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el);
+ el.dispatchEvent(evt);
+};
+
+/**
+ * MediaDialog widget. Lets users change a media, including uploading a
+ * new image, font awsome or video and can change a media into an other
+ * media
+ *
+ * options: select_images: allow the selection of more of one image
+ */
+var MediaDialog = Dialog.extend({
+ template: 'web_editor.dialog.media',
+ events : _.extend({}, Dialog.prototype.events, {
+ 'input input#icon-search': 'search',
+ }),
+ init: function ($editable, media, options) {
+ this._super();
+ if ($editable) {
+ this.$editable = $editable;
+ this.rte = this.$editable.rte || this.$editable.data('rte');
+ }
+ this.options = options || {};
+ this.old_media = media;
+ this.media = media;
+ this.isNewMedia = !media;
+ this.range = range.create();
+ },
+ start: function () {
+ var self = this;
+
+ this.only_images = this.options.only_images || this.options.select_images || (this.media && $(this.media).parent().data("oe-field") === "image");
+ if (this.only_images) {
+ this.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
+ }
+
+ if (this.media) {
+ if (this.media.nodeName === "IMG") {
+ this.$('[href="#editor-media-image"]').tab('show');
+ } else if (this.media.className.match(/(^|\s)media_iframe_video($|\s)/)) {
+ this.$('[href="#editor-media-video"]').tab('show');
+ } else if (this.media.parentNode.className.match(/(^|\s)media_iframe_video($|\s)/)) {
+ this.media = this.media.parentNode;
+ this.$('[href="#editor-media-video"]').tab('show');
+ } else if (this.media.className.match(/(^|\s)fa($|\s)/)) {
+ this.$('[href="#editor-media-icon"]').tab('show');
+ }
+ }
+
+ this.imageDialog = new ImageDialog(this, this.media, this.options);
+ this.imageDialog.appendTo(this.$("#editor-media-image"));
+ this.iconDialog = new fontIconsDialog(this, this.media, this.options);
+ this.iconDialog.appendTo(this.$("#editor-media-icon"));
+ this.videoDialog = new VideoDialog(this, this.media, this.options);
+ this.videoDialog.appendTo(this.$("#editor-media-video"));
+
+ this.active = this.imageDialog;
+
+ $('a[data-toggle="tab"]').on('shown.bs.tab', function (event) {
+ if ($(event.target).is('[href="#editor-media-image"]')) {
+ self.active = self.imageDialog;
+ self.$('li.search, li.previous, li.next').removeClass("hidden");
+ } else if ($(event.target).is('[href="#editor-media-icon"]')) {
+ self.active = self.iconDialog;
+ self.$('li.search, li.previous, li.next').removeClass("hidden");
+ self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden");
+ } else if ($(event.target).is('[href="#editor-media-video"]')) {
+ self.active = self.videoDialog;
+ self.$('.nav-tabs li.search').addClass("hidden");
+ }
+ });
+
+ return this._super();
+ },
+ save: function () {
+ if (this.options.select_images) {
+ this.trigger("saved", this.active.save());
+ this.close();
+ return;
+ }
+ if(this.rte) {
+ this.range.select();
+ this.rte.historyRecordUndo(this.media);
+ }
+
+ var self = this;
+ if (self.media) {
+ this.media.innerHTML = "";
+ if (this.active !== this.imageDialog) {
+ this.imageDialog.clear();
+ }
+ if (this.active !== this.iconDialog) {
+ this.iconDialog.clear();
+ }
+ if (this.active !== this.videoDialog) {
+ this.videoDialog.clear();
+ }
+ } else {
+ this.media = document.createElement("img");
+ this.range.insertNode(this.media, true);
+ this.active.media = this.media;
+ }
+ this.active.save();
+
+ if (this.active.add_class) {
+ $(this.media).addClass(this.active.add_class);
+ }
+
+ $(document.body).trigger("media-saved", [self.active.media, self.old_media]);
+ self.trigger("saved", [self.active.media, self.old_media]);
+ setTimeout(function () {
+ if (!self.active.media.parentNode) {
+ return;
+ }
+ range.createFromNode(self.active.media).select();
+ click_event(self.active.media, "mousedown");
+ if (!this.only_images) {
+ setTimeout(function () {
+ if($(self.active.media).parent().data("oe-field") !== "image") {
+ click_event(self.active.media, "click");
+ }
+ click_event(self.active.media, "mouseup");
+ },0);
+ }
+ },0);
+
+ this.close();
+ },
+ searchTimer: null,
+ search: function () {
+ var self = this;
+ var needle = this.$("input#icon-search").val();
+ clearTimeout(this.searchTimer);
+ this.searchTimer = setTimeout(function () {
+ self.active.search(needle || "");
+ },250);
+ }
+});
+
+/**
+ * ImageDialog widget. Lets users change an image, including uploading a
+ * new image in OpenERP or selecting the image style (if supported by
+ * the caller).
+ */
+var IMAGES_PER_ROW = 6;
+var IMAGES_ROWS = 2;
+var ImageDialog = Widget.extend({
+ template: 'web_editor.dialog.image',
+ events: _.extend({}, Dialog.prototype.events, {
+ 'change .url-source': function (e) {
+ this.changed($(e.target));
+ },
+ 'click button.filepicker': function () {
+ var filepicker = this.$('input[type=file]');
+ if (!_.isEmpty(filepicker)){
+ filepicker[0].click();
+ }
+ },
+ 'click .js_disable_optimization': function () {
+ this.$('input[name="disable_optimization"]').val('1');
+ var filepicker = this.$('button.filepicker');
+ if (!_.isEmpty(filepicker)){
+ filepicker[0].click();
+ }
+ },
+ 'change input[type=file]': 'file_selection',
+ 'submit form': 'form_submit',
+ 'change input.url': "change_input",
+ 'keyup input.url': "change_input",
+ //'change select.image-style': 'preview_image',
+ 'click .existing-attachments img': 'select_existing',
+ 'click .existing-attachment-remove': 'try_remove',
+ }),
+ init: function (parent, media, options) {
+ this._super();
+ this.options = options || {};
+ this.parent = parent;
+ this.media = media;
+ this.images = [];
+ this.page = 0;
+ },
+ start: function () {
+ this.$preview = this.$('.preview-container').detach();
+ var self = this;
+ var res = this._super();
+ var o = { url: null, alt: null };
+ // avoid typos, prevent addition of new properties to the object
+ Object.preventExtensions(o);
+
+ if ($(this.media).is("img")) {
+ o.url = this.media.getAttribute('src');
+ } else {
+ this.add_class = "img-responsive pull-left";
+ }
+ this.parent.$(".pager > li").click(function (e) {
+ e.preventDefault();
+ var $target = $(e.currentTarget);
+ if ($target.hasClass('disabled')) {
+ return;
+ }
+ self.page += $target.hasClass('previous') ? -1 : 1;
+ self.display_attachments();
+ });
+ this.set_image(o.url, o.alt);
+ this.fetch_existing();
+ return res;
+ },
+ push: function (url, alt, id) {
+ if (this.options.select_images) {
+ var img = _.select(this.images, function (v) { return v.url == url;});
+ if (img.length) {
+ this.images.splice(this.images.indexOf(img[0]),1);
+ return;
+ }
+ } else {
+ this.images = [];
+ }
+ this.images.push({'url': url, 'alt': alt, 'id': id});
+ },
+ save: function () {
+ if (this.options.select_images) {
+ this.parent.trigger("save", this.images);
+ return this.images;
+ }
+ this.parent.trigger("save", this.media);
+
+ var img = this.images[0] || {
+ 'url': this.$(".existing-attachments img:first").attr('src'),
+ 'alt': this.$(".existing-attachments img:first").attr('alt')
+ };
+
+ if (this.media.tagName !== "IMG") {
+ var media = document.createElement('img');
+ $(this.media).replaceWith(media);
+ this.media = media;
+ }
+
+ $(this.media).attr('src', img.url).attr('alt', img.alt);
+
+ var style = this.style;
+ this.media.setAttribute('src', img.url);
+ if (style) { this.media.addClass(style); }
+
+ return this.media;
+ },
+ clear: function () {
+ this.media.className = this.media.className.replace(/(^|\s)(img(\s|$)|img-[^\s]*)/g, ' ');
+ },
+ cancel: function () {
+ this.trigger('cancel');
+ },
+ change_input: function (e) {
+ var $input = $(e.target);
+ var $button = $input.parent().find("button");
+ if ($input.val() === "") {
+ $button.addClass("btn-default").removeClass("btn-primary");
+ } else {
+ $button.removeClass("btn-default").addClass("btn-primary");
+ }
+ },
+ search: function (needle) {
+ var self = this;
+ this.fetch_existing(needle).then(function () {
+ self.selected_existing();
+ });
+ },
+ set_image: function (url, alt, error) {
+ var self = this;
+ if (url) {
+ this.push(url, alt);
+ }
+ this.$('input.url').val('');
+ this.fetch_existing().then(function () {
+ self.selected_existing();
+ });
+ },
+ form_submit: function (event) {
+ var self = this;
+ var $form = this.$('form[action="/web_editor/attachment/add"]');
+ if (!$form.find('input[name="upload"]').val().length) {
+ var url = $form.find('input[name="url"]').val();
+ if (this.selected_existing().size()) {
+ event.preventDefault();
+ return false;
+ }
+ }
+ $form.find('.well > div').hide().last().after('');
+
+ var callback = _.uniqueId('func_');
+ this.$('input[name=func]').val(callback);
+ window[callback] = function (attachments, error) {
+ delete window[callback];
+ $form.find('.well > span').remove();
+ $form.find('.well > div').show();
+ if (error || !attachments.length) {
+ self.file_selected(null, error || !attachments.length);
+ }
+ for (var i=0; i= records.length));
+
+ this.selected_existing();
+ },
+ select_existing: function (e) {
+ var $img = $(e.currentTarget);
+ this.push($img.attr('src'), $img.attr('alt'), $img.data('id'));
+ this.selected_existing();
+ },
+ selected_existing: function () {
+ var self = this;
+ this.$('.existing-attachment-cell.media_selected').removeClass("media_selected");
+ var $select = this.$('.existing-attachment-cell img').filter(function () {
+ var $img = $(this);
+ var url = $img.attr("src");
+ return !!_.select(self.images, function (v) {
+ if (v.url === url) {
+ if (!v.id) {
+ v.id = $img.data('id');
+ v.alt = $img.attr('alt');
+ }
+ return true;
+ }
+ }).length;
+ });
+ $select.parent().addClass("media_selected");
+ return $select;
+ },
+ try_remove: function (e) {
+ var $help_block = this.$('.help-block').empty();
+ var self = this;
+ var $a = $(e.target);
+ var id = parseInt($a.data('id'), 10);
+ var attachment = _.findWhere(this.records, {id: id});
+ var $both = $a.parent().children();
+
+ $both.css({borderWidth: "5px", borderColor: "#f00"});
+
+ return ajax.jsonRpc('/web_editor/attachment/remove', 'call', {'ids': [id]}).then(function (prevented) {
+ if (_.isEmpty(prevented)) {
+ self.records = _.without(self.records, attachment);
+ self.display_attachments();
+ return;
+ }
+ $both.css({borderWidth: "", borderColor: ""});
+ $help_block.replaceWith(QWeb.render(
+ 'web_editor.dialog.image.existing.error', {
+ views: prevented[id]
+ }
+ ));
+ });
+ },
+});
+
+
+var cacheCssSelectors = {};
+var getCssSelectors = function(filter) {
+ var css = [];
+ if (cacheCssSelectors[filter]) {
+ return cacheCssSelectors[filter];
+ }
+ var sheets = document.styleSheets;
+ for(var i = 0; i < sheets.length; i++) {
+ var rules = sheets[i].rules || sheets[i].cssRules;
+ if (rules) {
+ for(var r = 0; r < rules.length; r++) {
+ var selectorText = rules[r].selectorText;
+ if (selectorText) {
+ var match = selectorText.match(filter);
+ if (match) {
+ css.push([match[1], rules[r].cssText.replace(/(^.*\{\s*)|(\s*\}\s*$)/g, '')]);
+ }
+ }
+ }
+ }
+ }
+ return cacheCssSelectors[filter] = css;
+};
+function computeFonts() {
+ _.each(fontIcons, function (data) {
+ data.icons = _.map(getCssSelectors(data.parser), function (css) {
+ return css[0].slice(1, css[0].length).replace(/::?before$/, '');
+ });
+ });
+}
+
+rte.Class.include({
+ init: function (EditorBar) {
+ this._super.apply(this, arguments);
+ computeFonts();
+ }
+});
+
+/* list of font icons to load by editor. The icons are displayed in the media editor and
+ * identified like font and image (can be colored, spinned, resized with fa classes).
+ * To add font, push a new object {base, parser}
+ * - base: class who appear on all fonts (eg: fa fa-refresh)
+ * - parser: regular expression used to select all font in css style sheets
+ */
+var fontIcons = [{'base': 'fa', 'parser': /(?=^|\s)(\.fa-[0-9a-z_-]+::?before)/i}];
+
+
+/**
+ * FontIconsDialog widget. Lets users change a font awsome, suport all
+ * font awsome loaded in the css files.
+ */
+var fontIconsDialog = Widget.extend({
+ template: 'web_editor.dialog.font-icons',
+ events : _.extend({}, Dialog.prototype.events, {
+ 'click .font-icons-icon': function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.$('#fa-icon').val(e.target.getAttribute('data-id'));
+ $(".font-icons-icon").removeClass("font-icons-selected");
+ $(event.target).addClass("font-icons-selected");
+ },
+ }),
+
+ // extract list of font (like awsome) from the cheatsheet.
+ renderElement: function() {
+ this.iconsParser = fontIcons;
+ this.icons = _.flatten(_.map(fontIcons, function (data) {
+ return data.icons;
+ }));
+ this._super();
+ },
+
+ init: function (parent, media) {
+ this._super();
+ this.parent = parent;
+ this.media = media;
+ if (_.find(fontIcons, function (data) {return !data.icons;})) {
+ computeFonts();
+ }
+ },
+ start: function () {
+ return this._super().then(this.proxy('load_data'));
+ },
+ search: function (needle) {
+ var iconsParser = this.iconsParser;
+ if (needle) {
+ var parser = [];
+ var fontIcon;
+
+ for (var k=0; k');
+ if (ytMatch && ytMatch[1].length === 11) {
+ var youtubeId = ytMatch[1];
+ $video = $('