summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/web/static/src/css/base.css2
-rw-r--r--addons/web/static/src/js/framework/ajax.js34
-rw-r--r--addons/web/static/src/js/tour.js24
-rw-r--r--addons/web/static/src/js/views/form_common.js6
-rw-r--r--addons/web/static/src/js/views/form_view.js128
-rw-r--r--addons/web/views/webclient_templates.xml22
-rw-r--r--addons/web_editor/__init__.py4
-rw-r--r--addons/web_editor/__openerp__.py21
-rw-r--r--addons/web_editor/controllers/__init__.py1
-rw-r--r--addons/web_editor/controllers/main.py276
-rw-r--r--addons/web_editor/models/__init__.py7
-rw-r--r--addons/web_editor/models/ir_attachment.py149
-rw-r--r--addons/web_editor/models/ir_http.py16
-rw-r--r--addons/web_editor/models/ir_qweb.py566
-rw-r--r--addons/web_editor/models/ir_ui_view.py123
-rw-r--r--addons/web_editor/models/test_models.py (renamed from addons/website/models/test_models.py)7
-rw-r--r--addons/web_editor/security/ir.model.access.csv3
-rw-r--r--addons/web_editor/static/src/css/backend.css73
-rw-r--r--addons/web_editor/static/src/css/common.sass4
-rw-r--r--addons/web_editor/static/src/css/inline.css64
-rw-r--r--addons/web_editor/static/src/css/snippets.css490
-rw-r--r--addons/web_editor/static/src/css/snippets.sass376
-rw-r--r--addons/web_editor/static/src/css/web_editor.css266
-rw-r--r--addons/web_editor/static/src/css/web_editor.sass195
-rw-r--r--addons/web_editor/static/src/img/drag_here.png (renamed from addons/website/static/src/img/drag_here.png)bin619 -> 619 bytes
-rw-r--r--addons/web_editor/static/src/img/edit_here.png (renamed from addons/website/static/src/img/edit_here.png)bin491 -> 491 bytes
-rw-r--r--addons/web_editor/static/src/js/backend.js322
-rw-r--r--addons/web_editor/static/src/js/base.js85
-rw-r--r--addons/web_editor/static/src/js/editor.js164
-rw-r--r--addons/web_editor/static/src/js/iframe.js78
-rw-r--r--addons/web_editor/static/src/js/inline.js77
-rw-r--r--addons/web_editor/static/src/js/rte.js597
-rw-r--r--addons/web_editor/static/src/js/rte.summernote.js781
-rw-r--r--addons/web_editor/static/src/js/snippets.animation.js82
-rw-r--r--addons/web_editor/static/src/js/snippets.editor.js1137
-rw-r--r--addons/web_editor/static/src/js/snippets.options.js534
-rw-r--r--addons/web_editor/static/src/js/summernote.js (renamed from addons/website/static/src/js/summernote.js)112
-rw-r--r--addons/web_editor/static/src/js/tour.rte.js (renamed from addons/website/static/src/js/website.tour.rte.js)224
-rw-r--r--addons/web_editor/static/src/js/transcoder.js172
-rw-r--r--addons/web_editor/static/src/js/translator.js214
-rw-r--r--addons/web_editor/static/src/js/widgets.js1069
-rw-r--r--addons/web_editor/static/src/xml/backend.xml59
-rw-r--r--addons/web_editor/static/src/xml/editor.xml372
-rw-r--r--addons/web_editor/static/src/xml/snippets.xml (renamed from addons/website/static/src/xml/website.snippets.xml)12
-rw-r--r--addons/web_editor/static/src/xml/translator.xml14
-rw-r--r--addons/web_editor/tests/__init__.py3
-rw-r--r--addons/web_editor/tests/test_converter.py243
-rw-r--r--addons/web_editor/tests/test_ui.py9
-rw-r--r--addons/web_editor/views/editor.xml153
-rw-r--r--addons/web_editor/views/iframe.xml62
-rw-r--r--addons/web_editor/views/snippets.xml56
-rw-r--r--addons/web_tests/static/src/js/web.tour.js6
-rw-r--r--addons/website/__openerp__.py2
-rw-r--r--addons/website/controllers/main.py194
-rw-r--r--addons/website/models/__init__.py2
-rw-r--r--addons/website/models/ir_http.py3
-rw-r--r--addons/website/models/ir_qweb.py558
-rw-r--r--addons/website/models/ir_ui_view.py148
-rw-r--r--addons/website/models/website.py188
-rw-r--r--addons/website/security/ir.model.access.csv2
-rw-r--r--addons/website/static/lib/summernote/src/js/Renderer.js2
-rw-r--r--addons/website/static/src/css/snippets.css722
-rw-r--r--addons/website/static/src/css/snippets.sass579
-rw-r--r--addons/website/static/src/css/website.backend.css31
-rw-r--r--addons/website/static/src/css/website.css249
-rw-r--r--addons/website/static/src/css/website.editor.css263
-rw-r--r--addons/website/static/src/css/website.editor.sass234
-rw-r--r--addons/website/static/src/css/website.sass181
-rw-r--r--addons/website/static/src/js/openerp.define.js107
-rw-r--r--addons/website/static/src/js/website.backend.iframe.js75
-rw-r--r--addons/website/static/src/js/website.backend.js153
-rw-r--r--addons/website/static/src/js/website.editor.js2453
-rw-r--r--addons/website/static/src/js/website.js264
-rw-r--r--addons/website/static/src/js/website.snippets.animation.js72
-rw-r--r--addons/website/static/src/js/website.snippets.editor.js1667
-rw-r--r--addons/website/static/src/js/website.theme.js2
-rw-r--r--addons/website/static/src/js/website.translator.js254
-rw-r--r--addons/website/static/src/xml/website.backend.xml6
-rw-r--r--addons/website/static/src/xml/website.contentMenu.xml6
-rw-r--r--addons/website/static/src/xml/website.editor.xml607
-rw-r--r--addons/website/static/src/xml/website.gallery.xml2
-rw-r--r--addons/website/static/src/xml/website.translator.xml8
-rw-r--r--addons/website/tests/test_converter.py240
-rw-r--r--addons/website/tests/test_ui.py9
-rw-r--r--addons/website/views/snippets.xml17
-rw-r--r--addons/website/views/website.backend.xml65
-rw-r--r--addons/website/views/website_backend_navbar.xml6
-rw-r--r--addons/website/views/website_templates.xml131
-rw-r--r--addons/website_gengo/static/src/xml/website.gengo.xml4
-rw-r--r--addons/website_version/models/ir_ui_view.py2
-rw-r--r--openerp/addons/base/ir/ir_qweb.py5
-rw-r--r--openerp/addons/base/ir/ir_ui_view.py2
-rw-r--r--openerp/addons/base/tests/test_mail_examples.py2
-rw-r--r--openerp/tools/translate.py2
94 files changed, 10087 insertions, 8926 deletions
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;
+ }
+ });
+ }
+ });
+
+ $.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;
}
- }
- // 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();
+ 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 @@
<!-- jQuery stuff -->
<script type="text/javascript" src="/web/static/lib/jquery/jquery.js"></script>
+ <link rel="stylesheet" href="/web/static/lib/jquery.ui/jquery-ui.css"/>
+ <script type="text/javascript" src="/web/static/lib/jquery.ui/jquery-ui.js"></script>
<script type="text/javascript" src="/web/static/lib/jquery/jquery.browser.js"></script>
<script type="text/javascript" src="/web/static/lib/jquery.blockUI/jquery.blockUI.js"></script>
<script type="text/javascript" src="/web/static/lib/jquery.hotkeys/jquery.hotkeys.js"></script>
@@ -64,19 +66,6 @@
<script src="/web/static/lib/bootstrap/js/transition.js"></script>
</template>
- <template id="jqueryui_conflict" name="jquery.ui.conflict">
- <!-- TODO: get rid of this hack once jQuery.ui is removed -->
- <script type="text/javascript" charset="utf-8">
- $.fn.bstooltip = $.fn.tooltip;
- $.fn.bsbutton = $.fn.button;
- </script>
- <t t-raw="0"/>
- <script type="text/javascript" charset="utf-8">
- $.fn.tooltip = $.fn.bstooltip;
- $.fn.button = $.fn.bsbutton;
- </script>
- </template>
-
<template id="web.assets_backend">
<!-- jQuery addons -->
<script type="text/javascript" src="/web/static/lib/jquery.validate/jquery.validate.js"></script>
@@ -91,13 +80,6 @@
<t t-call="web.bootstrap"/>
- <!-- jQuery ui -->
- <link rel="stylesheet" href="/web/static/lib/jquery.ui/jquery-ui.css"/>
-
- <t t-call="web.jqueryui_conflict">
- <script type="text/javascript" src="/web/static/lib/jquery.ui/jquery-ui.js"></script>
- </t>
-
<link rel="stylesheet" href="/web/static/lib/jquery.ui.notify/css/ui.notify.css"/>
<script type="text/javascript" src="/web/static/lib/jquery.ui.notify/js/jquery.notify.js"></script>
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/<icon>',
+ '/web_editor/font_to_img/<icon>/<color>',
+ '/web_editor/font_to_img/<icon>/<color>/<int:size>',
+ '/web_editor/font_to_img/<icon>/<color>/<int:size>/<int:alpha>',
+ ], 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/<xmlid>',
+ '/web_editor/image/<xmlid>/<int:max_width>x<int:max_height>',
+ '/web_editor/image/<xmlid>/<field>',
+ '/web_editor/image/<xmlid>/<field>/<int:max_width>x<int:max_height>',
+ '/web_editor/image/<model>/<id>/<field>',
+ '/web_editor/image/<model>/<id>/<field>/<int:max_width>x<int:max_height>'
+ ], 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 """<script type='text/javascript'>
+ window.parent['%s'](%s, %s);
+ </script>""" % (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 <img> 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 = '<img class="%s" src="%s" style="%s"/>' % (classes, src, options.get('style', ''))
+ return ir_qweb.HTMLSafe(img)
+
+ local_url_re = re.compile(r'^/(?P<module>[^]]+)/static/(?P<rest>.+)$')
+
+ 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/<model>/<id>[_<checksum>]/<field>[/<width>x<height>]
+ 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 <br> 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
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
index 335c3a571bb..335c3a571bb 100644
--- a/addons/website/static/src/img/drag_here.png
+++ b/addons/web_editor/static/src/img/drag_here.png
Binary files differ
diff --git a/addons/website/static/src/img/edit_here.png b/addons/web_editor/static/src/img/edit_here.png
index 77beff11c7b..77beff11c7b 100644
--- a/addons/website/static/src/img/edit_here.png
+++ b/addons/web_editor/static/src/img/edit_here.png
Binary files differ
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') || "<p><br/></p>");
+
+ if (this.get("effective_readonly")) {
+ this.$textarea.hide().after('<div class="note-editable"/>');
+ } 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 = '<p><br/></p>';
+ } else {
+ value = "<p>"+value.split(/<br\/?>/).join("</p><p>")+"</p>";
+ value = value.replace('<p><p>', '<p>').replace('</p></p>', '</p>');
+ }
+ 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') === "<p><br/></p>" || !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(/^<p[^>]*>(\s*|<br\/?>)<\/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 = $("<div/>").text(failure.error.message).html();
+ var data = msg.substring(3,msg.length-2).split(/', u'/);
+ failure.error.message = '<b>' + data[0] + '</b>' + 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 = $('<div class="btn-group"/>');
+ $alt.prependTo($imagePopover.find('.popover-content'));
+ $alt.append('<button class="btn btn-default btn-sm btn-small" data-event="alt"><strong>Alt: </strong><span class="o_image_alt"></span></button>');
+
+ // padding button
+ var $padding = $('<div class="btn-group"/>');
+ $padding.insertBefore($imagePopover.find('.btn-group:first'));
+ var $button = $(tplIconButton('fa fa-plus-square-o', {
+ title: _t('Padding'),
+ dropdown: true
+ })).appendTo($padding);
+ var $ul = $('<ul class="dropdown-menu"/>').insertAfter($button);
+ $ul.append('<li><a data-event="padding" href="#" data-value="">'+_t('None')+'</a></li>');
+ $ul.append('<li><a data-event="padding" href="#" data-value="small">'+_t('Small')+'</a></li>');
+ $ul.append('<li><a data-event="padding" href="#" data-value="medium">'+_t('Medium')+'</a></li>');
+ $ul.append('<li><a data-event="padding" href="#" data-value="large">'+_t('Large')+'</a></li>');
+ $ul.append('<li><a data-event="padding" href="#" data-value="xl">'+_t('Xl')+'</a></li>');
+
+ // 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 = $('<div class="btn-group hidden only_fa"/>').insertAfter($button.parent());
+ $(tplIconButton('fa fa-refresh', {
+ title: _t('Spin'),
+ event: 'imageShape',
+ value: 'fa-spin'
+ })).appendTo($spin);
+
+ // resize for fa
+ var $resizefa = $('<div class="btn-group hidden only_fa"/>')
+ .insertAfter($imagePopover.find('.btn-group:has([data-event="resize"])'));
+ for (var size=1; size<=5; size++) {
+ $(tplButton('<span class="note-fontsize-10">'+size+'x</span>', {
+ 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 = $('<div class="btn-group"/>');
+ $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(/&quot;/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<widgets.fontIcons.length; k++) {
+ if (_.intersection(widgets.fontIcons[k].icons, classNames).length) {
+ return true;
+ }
+ }
+ }
+ return false;
+};
+// re-overwrite font to include theme icons
+var isFont = dom.isFont;
+dom.isFont = function (node) {
+ return dom.isImgFont(node) || isFont(node);
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+var fn_visible = $.summernote.pluginEvents.visible;
+$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
+ var res = fn_visible.call(this, event, editor, layoutInfo);
+ var $node = $(dom.node(range.create().sc));
+ if (($node.is('[data-oe-type="html"]') || $node.is('[data-oe-field="arch"]')) && $node.hasClass("o_editable") && !$node[0].children.length) {
+ var p = $('<p><br/></p>')[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){
+ $("<div id='oe_manipulators' class='o_css_editor'></div>").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 = $('<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<len; i++) {
+ $temp = selector[i].closest($from, $target && $target[0]);
+ if (!$target || $temp.length) {
+ $target = $temp;
+ }
+ }
+ return $target;
+ },
+ all: function ($from) {
+ var $target;
+ var len = selector.length;
+ for (var i = 0; i<len; i++) {
+ if (!$target) $target = selector[i].all($from);
+ else $target = $target.add(selector[i].all($from));
+ }
+ return $target;
+ },
+ is: function ($from) {
+ var len = selector.length;
+ for (var i = 0; i<len; i++) {
+ if (selector[i].is($from)) {
+ return true;
+ }
+ }
+ return false;
+ },
+ };
+
+ var number = 0;
+
+ // oe_snippet_body
+ self.$snippets = $scroll.find(".o_panel_body").children()
+ .addClass("oe_snippet")
+ .each(function () {
+ if (!$('.oe_snippet_thumbnail', this).size()) {
+ var $div = $(
+ '<div class="oe_snippet_thumbnail">'+
+ '<div class="oe_snippet_thumbnail_img"/>'+
+ '<span class="oe_snippet_thumbnail_title"></span>'+
+ '</div>');
+ $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<this.attributes.length; k++) {
+ if (this.attributes[k].name.indexOf('data-oe-') === 0) {
+ $(this).removeAttr(this.attributes[k].name);
+ k--;
+ }
+ }
+ });
+ // end
+
+ $html.find('.o_not_editable').attr("contentEditable", false);
+
+ $left_bar.append($html);
+
+ // animate for list of snippet blocks
+ $left_bar.on('click', '.scroll-link', function (event) {
+ event.preventDefault();
+ var targetOffset = $($(this).attr("href")).position().top - $ul.outerHeight() + $scroll[0].scrollTop;
+ $scroll.animate({'scrollTop': targetOffset}, 750);
+ });
+ $scroll.on('scroll', function () {
+ var middle = $scroll.height()/4;
+ var $li = $ul.find("a").parent().removeClass('active');
+ var last;
+ for (var k=0; k<$li.length; k++) {
+ var li = $($li[k]);
+ if (!li.data('target')) {
+ li.data('target', $($("a", li).attr("href")));
+ }
+ if (li.data('target').position().top > 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 = $("<div class='oe_drop_zone oe_insert'></div>");
+
+ 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("<div class='oe_drop_clone' style='display: none;'/>");
+ self.$target.detach();
+ self.$overlay.addClass("hidden");
+
+ var $selector_siblings;
+ for (var i=0; i<self.selector_siblings.length; i++) {
+ if (!$selector_siblings) $selector_siblings = self.selector_siblings[i].all();
+ else $selector_siblings = $selector_siblings.add(self.selector_siblings[i].all());
+ }
+ var $selector_children;
+ for (var i=0; i<self.selector_children.length; i++) {
+ if (!$selector_children) $selector_children = self.selector_children[i].all();
+ else $selector_children = $selector_children.add(self.selector_children[i].all());
+ }
+
+ self.buildingBlock.activate_insertion_zones($selector_siblings, $selector_children);
+
+ $("body").addClass('move-important');
+
+ self._drag_and_drop_after_insert_dropzone();
+ self._drag_and_drop_active_drop_zone($('.oe_drop_zone'));
+ },
+ _drag_and_drop_stop: function (){
+ var self = this;
+ var $dropzone = this.$target.prev();
+ var prev = $dropzone.length && $dropzone[0].previousSibling;
+ var next = this.$target.last()[0].nextSibling;
+ var $parent = this.$target.parent();
+
+ $(".oe_drop_clone").after(this.$target);
+
+ this.$overlay.removeClass("hidden");
+ $("body").removeClass('move-important');
+ $('.oe_drop_zone').droppable('destroy').remove();
+ $(".oe_drop_clone, .oe_drop_to_remove").remove();
+
+ if (this.dropped) {
+ this.buildingBlock.parent.rte.historyRecordUndo(this.$target);
+
+ if (prev) {
+ this.$target.insertAfter(prev);
+ } else if (next) {
+ this.$target.insertBefore(next);
+ } else {
+ $parent.prepend(this.$target);
+ }
+
+ for (var i in this.styles){
+ this.styles[i].on_move();
+ }
+ }
+
+ self.buildingBlock.editor_busy = false;
+
+ self.get_parent_block();
+ setTimeout(function () {self.buildingBlock.create_overlay(self.$target);},0);
+ },
+
+ load_style_options: function () {
+ var self = this;
+ var $styles = this.$overlay.find('.oe_options');
+ var $ul = $styles.find('ul:first');
+ this.styles = {};
+ this.selector_siblings = [];
+ this.selector_children = [];
+ _.each(this.buildingBlock.templateOptions, function (val, option_id) {
+ if (!val.selector.is(self.$target)) {
+ return;
+ }
+ if (val['drop-near']) self.selector_siblings.push(val['drop-near']);
+ if (val['drop-in']) self.selector_children.push(val['drop-in']);
+
+ var option = val['option'];
+ var Editor = options.registry[option] || options.Class;
+ var editor = self.styles[option] = new Editor(self.buildingBlock, self, self.$target, option_id);
+ $ul.append(editor.$el.addClass("snippet-option-" + option));
+ editor.start();
+ });
+
+ if (!this.selector_siblings.length && !this.selector_children.length) {
+ this.$overlay.find(".oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove").addClass('hidden');
+ }
+
+ if ($ul.find("li").length) {
+ $styles.removeClass("hidden");
+ }
+ this.$overlay.find('[data-toggle="dropdown"]').dropdown();
+ },
+
+ get_parent_block: function () {
+ var self = this;
+ var $button = this.$overlay.find('.oe_snippet_parent');
+ var $parent = globalSelector.closest(this.$target.parent());
+ if ($parent.length) {
+ $button.removeClass("hidden");
+ $button.off("click").on('click', function (event) {
+ event.preventDefault();
+ setTimeout(function () {
+ self.buildingBlock.make_active($parent);
+ }, 0);
+ });
+ } else {
+ $button.addClass("hidden");
+ }
+ },
+
+ /*
+ * start
+ * This method is called after init and _readXMLData
+ */
+ start: function () {
+ var self = this;
+ if (!this.$target.parent().is(':o_editable')) {
+ this.$overlay.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove();
+ } else {
+ this.$overlay.on('click', '.oe_snippet_clone', _.bind(this.on_clone, this));
+ this.$overlay.on('click', '.oe_snippet_remove', _.bind(this.on_remove, this));
+ this._drag_and_drop();
+ }
+ },
+
+ on_clone: function (event) {
+ event.preventDefault();
+ var $clone = this.$target.clone(false);
+
+ this.buildingBlock.parent.rte.historyRecordUndo(this.$target);
+
+ this.$target.after($clone);
+ for (var i in this.styles){
+ this.styles[i].on_clone($clone);
+ }
+ this.buildingBlock.create_overlay(this.$target);
+ return false;
+ },
+
+ on_remove: function (event) {
+ event.preventDefault();
+ this.on_blur();
+
+ this.buildingBlock.parent.rte.historyRecordUndo(this.$target);
+
+ var index = _.indexOf(this.buildingBlock.snippets, this.$target.get(0));
+ for (var i in this.styles){
+ this.styles[i].on_remove();
+ }
+ delete this.buildingBlock.snippets[index];
+
+ var $editable = this.$target.closest(".o_editable");
+
+ // remove node and his empty
+ var node = this.$target.parent()[0];
+
+ this.$target.remove();
+ this.$overlay.remove();
+
+ if (node && node.firstChild) {
+ $.summernote.core.dom.removeSpace(node, node.firstChild, 0, node.lastChild, 1);
+ if (!node.firstChild.tagName && node.firstChild.textContent === " ") {
+ node.firstChild.parentNode.removeChild(node.firstChild);
+ }
+ }
+
+ // clean editor if they are image or table in deleted content
+ $(".note-control-selection").hide();
+ $('.o_table_handler').remove();
+
+ return false;
+ },
+
+ /*
+ * drop_and_build_snippet
+ * This method is called just after that a thumbnail is drag and dropped into a drop zone
+ * (after the insertion of this.$body, if this.$body exists)
+ */
+ drop_and_build_snippet: function () {
+ for (var i in this.styles){
+ this.styles[i].drop_and_build_snippet();
+ }
+ },
+
+ /* on_focus
+ * This method is called when the user click inside the snippet in the dom
+ */
+ on_focus : function () {
+ this.$overlay.addClass('oe_active');
+ for (var i in this.styles){
+ this.styles[i].on_focus();
+ }
+ },
+
+ /* on_focus
+ * This method is called when the user click outside the snippet in the dom, after a focus
+ */
+ on_blur : function () {
+ for (var i in this.styles){
+ this.styles[i].on_blur();
+ }
+ this.$overlay.removeClass('oe_active');
+ },
+});
+
+var data = {
+ 'Class': BuildingBlock,
+ 'globalSelector': globalSelector,
+};
+return data;
+
+});
diff --git a/addons/web_editor/static/src/js/snippets.options.js b/addons/web_editor/static/src/js/snippets.options.js
new file mode 100644
index 00000000000..a46c00baa25
--- /dev/null
+++ b/addons/web_editor/static/src/js/snippets.options.js
@@ -0,0 +1,534 @@
+odoo.define('web_editor.snippets.options', function (require) {
+'use strict';
+
+var Class = require('web.Class');
+var ajax = require('web.ajax');
+var core = require('web.core');
+var base = require('web_editor.base');
+var editor = require('web_editor.editor');
+var widget = require('web_editor.widget');
+var animation = require('web_editor.snippets.animation');
+
+var qweb = core.qweb;
+
+/* ----- Editor option (object link the the xml with data-js) ---- */
+
+var Option = Class.extend({
+ // initialisation (don't overwrite)
+ init: function (BuildingBlock, editor, $target, option_id) {
+ this.buildingBlock = BuildingBlock;
+ this.editor = editor;
+ this.$target = $target;
+ var option = this.buildingBlock.templateOptions[option_id];
+ var styles = this.$target.data("snippet-option-ids") || {};
+ styles[option_id] = this;
+ this.$target.data("snippet-option-ids", styles);
+ this.$overlay = this.$target.data('overlay') || $('<div>');
+ 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 = $('<img class="hidden"/>');
+ $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
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 = $("<br/>")[0];
- so = 0;
- node.appendChild(br);
- } else if (!ancestor.children.length && !ancestor.textContent.match(/\S|\u00A0/)) {
- sc = $("<br/>")[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 = $("<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
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 = '<h1 id="text_title_id">Batnae municipium in Anthemusia</h1> '+
- '\n <p>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.</p>'+
- '\n <p> Quam <img style="width: 25%" src="/website/static/src/img/text_image.png"/> quidem <span class="fa fa-flag fa-2x"></span> partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.</p>'+
- '\n <p>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.</p>'+
- '<p>Harum trium sententiarum nulli prorsus assentior.</p>';
- $el.html(html);
+ var html = '\n'+
+ '<section>\n'+
+ ' <div class="container">\n'+
+ ' <div class="row">\n'+
+ ' <div class="col-md-6 mt16">\n'+
+ '<h1 id="text_title_id">Batnae municipium in Anthemusia</h1> \n'+
+ ' <p>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.</p>\n'+
+ ' <p> Quam <img style="width: 25%" src="/web_editor/static/src/img/drag_here.png"/> quidem <span class="fa fa-flag fa-2x"></span> partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.</p>\n'+
+ ' <p>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.</p>'+
+ '<p>Harum trium sententiarum nulli prorsus assentior.</p>\n'+
+ ' </div>\n'+
+ ' <div class="col-md-6 mt16">\n'+
+ ' <img class="img img-responsive shadow mb16" src="/web_editor/static/src/img/drag_here.png" alt="Odoo text and image block">\n'+
+ ' </div>\n'+
+ ' </div>\n'+
+ '</section>\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,39 +198,45 @@ Tour.register({
element: '.modal .dropdown ul label.btn-success',
title: "choose success style",
},
- {
- waitFor: '.modal a#link-preview.btn:containsRegex(/^<span [^>]+><\\/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',
+ // {
+ // waitFor: '.modal a#link-preview.btn:containsRegex(/^<span [^>]+><\\/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',
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 [^>]+><\\/span> DEF$/)',
element: '.modal button.save',
title: "save link",
},
{
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 = ' <ul> '+
'\n <li> <p>Batnae municipium. </p></li>'+
'\n <li> Seleucia praeter.</li>'+
@@ -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'+
+ '<div>\n'+
+ ' <table cellspacing="0" cellpadding="0" width="100%">\n'+
+ ' <tbody>\n'+
+ ' <tr>\n'+
+ ' <td valign="center" width="270">\n'+
+ ' <img src="/logo.png" alt="Your Logo" class="img-circle img-thumbnail">\n'+
+ ' </td>\n'+
+ ' <td valign="center" width="270">\n'+
+ ' <a href="https://www.facebook.com/Odoo"><span class="fa fa-facebook-square fa-2x text-primary"></span></a>\n'+
+ ' <span style="color: rgb(255, 0, 0);" class="fa fa-4x fa-google-plus-square pull-right"></span>\n'+
+ ' </td>\n'+
+ ' </tr>\n'+
+ ' </tbody>\n'+
+ ' </table>\n'+
+ '</div>';
+ $("#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<st.length; k++) {
+ rulesCache.push({ 'selector': st[k], 'style': rules[r].style });
+ }
+ }
+ }
+ }
+ }
+ rulesCache.reverse();
+ }
+
+ var css = [];
+ a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector;
+ for(var r = 0; r < rulesCache.length; r++) {
+ if (a.matches(rulesCache[r].selector)) {
+ var style = rulesCache[r].style;
+ if (style.parentRule) {
+ var style_obj = {};
+ for (var k=0, len=style.length; k<len; k++) {
+ style_obj[style[k]] = style[style[k]];
+ }
+ rulesCache[r].style = style = style_obj;
+ }
+ css.push([rulesCache[r].selector, style]);
+ }
+ }
+
+ function specificity (selector) {
+ // http://www.w3.org/TR/css3-selectors/#specificity
+ var a = 0;
+ selector.replace(/#[a-z0-9_-]+/gi, function () { a++; return ""; });
+ var b = 0;
+ selector.replace(/(\.[a-z0-9_-]+)|(\[.*?\])/gi, function () { b++; return ""; });
+ var c = 0;
+ selector.replace(/(\s+|:+)[a-z0-9_-]+/gi, function (a) { if(a.indexOf(':not(')===-1) c++; return ""; });
+ return a*100 + b*10 + c;
+ }
+ css.sort(function (a, b) { return specificity(a[0]) - specificity(b[0]); });
+
+ var style = {};
+ _.each(css, function (v,k) {
+ _.each(v[1], function (v,k) {
+ if (!style[k] || style[k].indexOf('important') === -1 || v.indexOf('important') !== -1) {
+ style[k] = v;
+ }
+ });
+ });
+ return cache[a.tagName + "." +a.className] = style;
+};
+
+// convert font awsome into image
+var font_to_img = function ($editable) {
+ $(".fa", $editable).each(function () {
+ var $font = $(this);
+ var content;
+ _.find(widget.fontIcons, function (font) {
+ return _.find(widget.getCssSelectors(font.parser), function (css) {
+ if ($font.is(css[0].replace(/::?before$/, ''))) {
+ content = css[1].match(/content:\s*['"]?(.)['"]?/)[1];
+ return true;
+ }
+ });
+ });
+ if (content) {
+ var size = parseInt(parseFloat($font.css("font-size"))/parseFloat($font.parent().css("font-size")),10);
+ var color = $font.css("color").replace(/\s/g, '');
+ var src = _.str.sprintf('/web_editor/font_to_img/%s/%s/'+$font.width(), window.encodeURI(content), window.encodeURI(color));
+ var style = $font.attr("style");
+ style = (style ? style.replace(/\s/g, '').replace(/(^|;)height:[^;]*/, '$1').replace(/(^|;)font-size:[^;]*/, '$1') : "") + "height:"+size+"em;";
+ var $img = $("<img/>").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 = $("<span/>").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 = $('<span/>').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(/&quot;/g, '"');
+ this.title = ($(this.media).attr('title') || "").replace(/&quot;/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, "&quot;") : null).attr('title', title ? title.replace(/"/g, "&quot;") : 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('<span class="fa fa-spin fa-3x fa-refresh"/>');
+
+ 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<attachments.length; i++) {
+ self.file_selected(attachments[i]['local_url'] || '/web_editor/image/ir.attachment/'+attachments[i].id+'_'+(new Date().getTime())+'/datas', error);
+ }
+ };
+ },
+ file_selection: function () {
+ this.$el.addClass('nosave');
+ this.$('form').removeClass('has-error').find('.help-block').empty();
+ this.$('button.filepicker').removeClass('btn-danger btn-success');
+ this.$('form').submit();
+ },
+ file_selected: function(url, error) {
+ var $button = this.$('button.filepicker');
+ if (!error) {
+ $button.addClass('btn-success');
+ } else {
+ url = null;
+ this.$('form').addClass('has-error')
+ .find('.help-block').text(error);
+ $button.addClass('btn-danger');
+ }
+ this.set_image(url, null, error);
+
+ if (!this.options.select_images) {
+ // auto save and close popup
+ this.parent.save();
+ }
+ },
+ fetch_existing: function (needle) {
+ var domain = [['res_model', '=', 'ir.ui.view'], '|',
+ ['mimetype', '=', false], ['mimetype', '=like', 'image/%']];
+ if (needle && needle.length) {
+ domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
+ }
+ return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'ir.attachment',
+ method: 'search_read',
+ args: [],
+ kwargs: {
+ domain: domain,
+ fields: ['name', 'local_url'],
+ order: 'id desc',
+ context: base.get_context()
+ }
+ }).then(this.proxy('fetched_existing'));
+ },
+ fetched_existing: function (records) {
+ this.records = records;
+ this.display_attachments();
+ },
+ display_attachments: function () {
+ this.$('.help-block').empty();
+ var per_screen = IMAGES_PER_ROW * IMAGES_ROWS;
+
+ var from = this.page * per_screen;
+ var records = this.records;
+
+ // Create rows of 3 records
+ var rows = _(records).chain()
+ .slice(from, from + per_screen)
+ .groupBy(function (_, index) { return Math.floor(index / IMAGES_PER_ROW); })
+ .values()
+ .value();
+ this.$('.existing-attachments').replaceWith(
+ QWeb.render(
+ 'web_editor.dialog.image.existing.content', {rows: rows}));
+ this.parent.$('.pager')
+ .find('li.previous').toggleClass('disabled', (from === 0)).end()
+ .find('li.next').toggleClass('disabled', (from + per_screen >= 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<fontIcons.length; k++) {
+ fontIcon = fontIcons[k];
+ var icons = _(fontIcon.icons).filter(function (icon) {
+ return icon.indexOf(needle) !== -1;
+ });
+ if (icons.length) {
+ parser.push({
+ base: fontIcon.base,
+ icons: icons
+ });
+ }
+ }
+ iconsParser = parser;
+ }
+ this.$('div.font-icons-icons').html(
+ QWeb.render('web_editor.dialog.font-icons.icons', {'iconsParser': iconsParser}));
+ },
+ /**
+ * Removes existing FontAwesome classes on the bound element, and sets
+ * all the new ones if necessary.
+ */
+ save: function () {
+ var self = this;
+ this.parent.trigger("save", this.media);
+ var icons = this.icons;
+ var style = this.media.attributes.style ? this.media.attributes.style.value : '';
+ var classes = (this.media.className||"").split(/\s+/);
+ var non_fa_classes = _.reject(classes, function (cls) {
+ return self.getFont(cls);
+ });
+ var final_classes = non_fa_classes.concat(this.get_fa_classes());
+ if (this.media.tagName !== "SPAN") {
+ var media = document.createElement('span');
+ $(this.media).replaceWith(media);
+ this.media = media;
+ style = style.replace(/\s*width:[^;]+/, '');
+ }
+ $(this.media).attr("class", _.compact(final_classes).join(' ')).attr("style", style);
+ },
+ /**
+ * return the data font object (with base, parser and icons) or null
+ */
+ getFont: function (classNames) {
+ if (!(classNames instanceof Array)) {
+ classNames = (classNames||"").split(/\s+/);
+ }
+ var fontIcon, className;
+ for (var i=0; i<classNames.length; i++) {
+ className = classNames[i];
+ for (var k=0; k<fontIcons.length; k++) {
+ fontIcon = fontIcons[k];
+ if (className === fontIcon.base || fontIcon.icons.indexOf(className) !== -1) {
+ return {
+ 'base': fontIcon.base,
+ 'parser': fontIcon.parser,
+ 'icons': fontIcon.icons,
+ 'font': className
+ };
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Looks up the various FontAwesome classes on the bound element and
+ * sets the corresponding template/form elements to the right state.
+ * If multiple classes of the same category are present on an element
+ * (e.g. fa-lg and fa-3x) the last one occurring will be selected,
+ * which may not match the visual look of the element.
+ */
+ load_data: function () {
+ var classes = (this.media&&this.media.className||"").split(/\s+/);
+ for (var i = 0; i < classes.length; i++) {
+ var cls = classes[i];
+ switch(cls) {
+ case 'fa-1x':case 'fa-2x':case 'fa-3x':case 'fa-4x':case 'fa-5x':
+ // size classes
+ this.$('#fa-size').val(cls);
+ continue;
+ case 'fa-spin':
+ case 'fa-rotate-90':case 'fa-rotate-180':case 'fa-rotate-270':
+ case 'fa-flip-horizontal':case 'fa-rotate-vertical':
+ this.$('#fa-rotation').val(cls);
+ continue;
+ case 'fa-fw':
+ continue;
+ case 'fa-border':
+ this.$('#fa-border').prop('checked', true);
+ continue;
+ case '': continue;
+ default:
+ $(".font-icons-icon").removeClass("font-icons-selected").filter("."+cls).addClass("font-icons-selected");
+ for (var k=0; k<this.icons.length; k++) {
+ if (this.icons.indexOf(cls) !== -1) {
+ this.$('#fa-icon').val(cls);
+ break;
+ }
+ }
+ }
+ }
+ },
+ /**
+ * Serializes the dialog to an array of FontAwesome classes. Includes
+ * the base ``fa``.
+ */
+ get_fa_classes: function () {
+ var font = this.getFont(this.$('#fa-icon').val());
+ return [
+ font ? font.base : 'fa',
+ font ? font.font : "",
+ this.$('#fa-size').val(),
+ this.$('#fa-rotation').val(),
+ this.$('#fa-border').prop('checked') ? 'fa-border' : ''
+ ];
+ },
+ clear: function () {
+ this.media.className = this.media.className.replace(/(^|\s)(fa(\s|$)|fa-[^\s]*)/g, ' ');
+ },
+});
+
+
+function createVideoNode(url) {
+ // video url patterns(youtube, instagram, vimeo, dailymotion, youku)
+ var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
+ var ytMatch = url.match(ytRegExp);
+
+ var igRegExp = /\/\/instagram.com\/p\/(.[a-zA-Z0-9]*)/;
+ var igMatch = url.match(igRegExp);
+
+ var vRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/;
+ var vMatch = url.match(vRegExp);
+
+ var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/;
+ var vimMatch = url.match(vimRegExp);
+
+ var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/;
+ var dmMatch = url.match(dmRegExp);
+
+ var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)\.html/;
+ var youkuMatch = url.match(youkuRegExp);
+
+ var $video = $('<iframe>');
+ if (ytMatch && ytMatch[1].length === 11) {
+ var youtubeId = ytMatch[1];
+ $video = $('<iframe>')
+ .attr('src', '//www.youtube.com/embed/' + youtubeId)
+ .attr('width', '640').attr('height', '360');
+ } else if (igMatch && igMatch[0].length) {
+ $video = $('<iframe>')
+ .attr('src', igMatch[0] + '/embed/')
+ .attr('width', '612').attr('height', '710')
+ .attr('scrolling', 'no')
+ .attr('allowtransparency', 'true');
+ } else if (vMatch && vMatch[0].length) {
+ $video = $('<iframe>')
+ .attr('src', vMatch[0] + '/embed/simple')
+ .attr('width', '600').attr('height', '600')
+ .attr('class', 'vine-embed');
+ } else if (vimMatch && vimMatch[3].length) {
+ $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
+ .attr('src', '//player.vimeo.com/video/' + vimMatch[3])
+ .attr('width', '640').attr('height', '360');
+ } else if (dmMatch && dmMatch[2].length) {
+ $video = $('<iframe>')
+ .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2])
+ .attr('width', '640').attr('height', '360');
+ } else if (youkuMatch && youkuMatch[1].length) {
+ $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
+ .attr('height', '498')
+ .attr('width', '510')
+ .attr('src', '//player.youku.com/embed/' + youkuMatch[1]);
+ } else {
+ // this is not a known video link. Now what, Cat? Now what?
+ }
+
+ $video.attr('frameborder', 0);
+
+ return $video;
+}
+
+/**
+ * VideoDialog widget. Lets users change a video, support all summernote
+ * video, and embled iframe
+ */
+var VideoDialog = Widget.extend({
+ template: 'web_editor.dialog.video',
+ events : _.extend({}, Dialog.prototype.events, {
+ 'click input#urlvideo ~ button': 'get_video',
+ 'click input#embedvideo ~ button': 'get_embed_video',
+ 'change input#urlvideo': 'change_input',
+ 'keyup input#urlvideo': 'change_input',
+ 'change input#embedvideo': 'change_input',
+ 'keyup input#embedvideo': 'change_input'
+ }),
+ init: function (parent, media) {
+ this._super();
+ this.parent = parent;
+ this.media = media;
+ },
+ start: function () {
+ this.$preview = this.$('.preview-container').detach();
+ this.$iframe = this.$("iframe");
+ var $media = $(this.media);
+ if ($media.hasClass("media_iframe_video")) {
+ var src = $media.data('src');
+ this.$("input#urlvideo").val(src);
+ this.$("#autoplay").attr("checked", src.indexOf('autoplay=1') != -1);
+ this.get_video();
+ } else {
+ this.add_class = "pull-left";
+ }
+ return this._super();
+ },
+ 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");
+ }
+ },
+ get_embed_video: function (event) {
+ event.preventDefault();
+ var embedvideo = this.$("input#embedvideo").val().match(/src=["']?([^"']+)["' ]?/);
+ if (embedvideo) {
+ this.$("input#urlvideo").val(embedvideo[1]);
+ this.get_video(event);
+ }
+ return false;
+ },
+ get_video: function (event) {
+ if (event) event.preventDefault();
+ var $video = createVideoNode(this.$("input#urlvideo").val());
+ this.$iframe.replaceWith($video);
+ this.$iframe = $video;
+ return false;
+ },
+ save: function () {
+ this.parent.trigger("save", this.media);
+ var video_id = this.$("#video_id").val();
+ if (!video_id) {
+ this.$("button.btn-primary").click();
+ video_id = this.$("#video_id").val();
+ }
+ var video_type = this.$("#video_type").val();
+ var $iframe = $(
+ '<div class="media_iframe_video" data-src="'+this.$iframe.attr("src")+'">'+
+ '<div class="css_editable_mode_display">&nbsp;</div>'+
+ '<div class="media_iframe_video_size" contentEditable="false">&nbsp;</div>'+
+ '<iframe src="'+this.$iframe.attr("src")+'" frameborder="0" allowfullscreen="allowfullscreen" contentEditable="false"></iframe>'+
+ '</div>');
+ $(this.media).replaceWith($iframe);
+ this.media = $iframe[0];
+ },
+ clear: function () {
+ if (this.media.dataset.src) {
+ try {
+ delete this.media.dataset.src;
+ } catch(e) {
+ this.media.dataset.src = undefined;
+ }
+ }
+ this.media.className = this.media.className.replace(/(^|\s)media_iframe_video(\s|$)/g, ' ');
+ },
+});
+
+/* ----- EDITOR: LINK & MEDIA ---- */
+var LinkDialog = Dialog.extend({
+ template: 'web_editor.dialog.link',
+ events: _.extend({}, Dialog.prototype.events, {
+ 'change :input.url-source': 'changed',
+ 'keyup :input.url': 'onkeyup',
+ 'keyup :input': 'preview',
+ 'click button.remove': 'remove_link',
+ 'change input#link-text': function (e) {
+ this.text = $(e.target).val();
+ },
+ 'change .link-style': function (e) {
+ this.preview();
+ },
+ }),
+ init: function (editable, linkInfo) {
+ this._super(editable, linkInfo);
+ this.editable = editable;
+ this.data = linkInfo || {};
+
+ this.data.className = "";
+ if (this.data.range) {
+ this.data.iniClassName = $(this.data.range.sc).filter("a").attr("class") || "";
+ this.data.className = this.data.iniClassName.replace(/(^|\s+)btn(-[a-z0-9_-]*)?/gi, ' ');
+
+ var is_link = this.data.range.isOnAnchor();
+ var r = this.data.range;
+
+ var sc = r.sc;
+ var so = r.so;
+ var ec = r.ec;
+ var eo = r.eo;
+
+ var nodes;
+ if (!is_link) {
+ if (sc.tagName) {
+ sc = dom.firstChild(so ? sc.childNodes[so] : sc);
+ so = 0;
+ } else if (so !== sc.textContent.length) {
+ if (sc === ec) {
+ ec = sc = sc.splitText(so);
+ eo -= so;
+ } else {
+ sc = sc.splitText(so);
+ }
+ so = 0;
+ }
+ if (ec.tagName) {
+ ec = dom.lastChild(eo ? ec.childNodes[eo-1] : ec);
+ eo = ec.textContent.length;
+ } else if (eo !== ec.textContent.length) {
+ ec.splitText(eo);
+ }
+
+ nodes = dom.listBetween(sc, ec);
+
+ // browsers can't target a picture or void node
+ if (dom.isVoid(sc) || dom.isImg(sc)) {
+ so = dom.listPrev(sc).length-1;
+ sc = sc.parentNode;
+ }
+ if (dom.isBR(ec)) {
+ eo = dom.listPrev(ec).length-1;
+ ec = ec.parentNode;
+ } else if (dom.isVoid(ec) || dom.isImg(sc)) {
+ eo = dom.listPrev(ec).length;
+ ec = ec.parentNode;
+ }
+
+ this.data.range = range.create(sc, so, ec, eo);
+ this.data.range.select();
+ } else {
+ nodes = dom.ancestor(sc, dom.isAnchor).childNodes;
+ }
+
+ if (dom.isImg(sc) && nodes.indexOf(sc) === -1) {
+ nodes.push(sc);
+ }
+ if (nodes.length > 1 || dom.ancestor(nodes[0], dom.isImg)) {
+ var text = "";
+ this.data.images = [];
+ for (var i=0; i<nodes.length; i++) {
+ if (dom.ancestor(nodes[i], dom.isImg)) {
+ this.data.images.push(dom.ancestor(nodes[i], dom.isImg));
+ text += '[IMG]';
+ } else if (!is_link && i===0) {
+ text += nodes[i].textContent.slice(so, Infinity);
+ } else if (!is_link && i===nodes.length-1) {
+ text += nodes[i].textContent.slice(0, eo);
+ } else {
+ text += nodes[i].textContent;
+ }
+ }
+ this.data.text = text;
+ }
+ }
+
+ this.data.text = this.data.text.replace(/[ \t\r\n]+/g, ' ');
+ },
+ start: function () {
+ this.bind_data();
+ this.$('input.url-source:eq(1)').closest('.list-group-item').addClass('active');
+ return this._super();
+ },
+ get_data: function (test) {
+ var self = this;
+ var def = new $.Deferred();
+ var $e = this.$('.active input.url-source');
+ if (!$e.length) {
+ $e = this.$('input.url-source:first');
+ }
+ var val = $e.val();
+ var label = this.$('#link-text').val() || val;
+
+ if (label && this.data.images) {
+ for(var i=0; i<this.data.images.length; i++) {
+ label = label.replace(/</, "&lt;").replace(/>/, "&gt;").replace(/\[IMG\]/, this.data.images[i].outerHTML);
+ }
+ }
+
+ if (!test && (!val || !$e[0].checkValidity())) {
+ // FIXME: error message
+ $e.closest('.form-group').addClass('has-error');
+ $e.focus();
+ def.reject();
+ }
+
+ var style = this.$("input[name='link-style-type']:checked").val() || '';
+ var size = this.$("input[name='link-style-size']:checked").val() || '';
+ var classes = (this.data.className || "") + (style && style.length ? " btn " : "") + style + " " + size;
+ var isNewWindow = this.$('input.window-new').prop('checked');
+
+ if ($e.hasClass('email-address') && $e.val().indexOf("@") !== -1) {
+ self.get_data_buy_mail(def, $e, isNewWindow, label, classes);
+ } else {
+ self.get_data_buy_url(def, $e, isNewWindow, label, classes);
+ }
+ return def;
+ },
+ get_data_buy_mail: function (def, $e, isNewWindow, label, classes) {
+ var val = $e.val();
+ def.resolve(val.indexOf("mailto:") === 0 ? val : 'mailto:' + val, isNewWindow, label, classes);
+ },
+ get_data_buy_url: function (def, $e, isNewWindow, label, classes) {
+ def.resolve($e.val(), isNewWindow, label, classes);
+ },
+ save: function () {
+ var self = this;
+ var _super = this._super.bind(this);
+ return this.get_data()
+ .then(function (url, new_window, label, classes) {
+ self.data.url = url;
+ self.data.newWindow = new_window;
+ self.data.text = label;
+ self.data.className = classes.replace(/\s+/gi, ' ').replace(/^\s+|\s+$/gi, '');
+
+ self.trigger("save", self.data);
+ }).then(_super);
+ },
+ bind_data: function () {
+ var href = this.data.url;
+ var new_window = this.data.isNewWindow;
+ var text = this.data.text;
+ var classes = this.data.iniClassName;
+
+ this.$('input#link-text').val(text);
+ this.$('input.window-new').prop('checked', new_window);
+
+ if (classes) {
+ this.$('input[value!=""]').each(function () {
+ var $option = $(this);
+ if (classes.indexOf($option.val()) !== -1) {
+ $option.attr("checked", "checked");
+ }
+ });
+ }
+
+ if (href) {
+ var match;
+ if(match = /mailto:(.+)/.exec(href)) {
+ this.$('input.email-address').val(match = /mailto:(.+)/.exec(href) ? match[1] : '');
+ } else {
+ this.$('input.url').val(href);
+ }
+ }
+ this.preview();
+ },
+ changed: function (e) {
+ $(e.target).closest('.list-group-item')
+ .addClass('active')
+ .siblings().removeClass('active')
+ .addBack().removeClass('has-error');
+ this.preview();
+ },
+ onkeyup: function (e) {
+ var $e = $(e.target);
+ var is_link = ($e.val()||'').length && $e.val().indexOf("@") === -1;
+ this.$('input.window-new').closest("div").toggle(is_link);
+ this.preview();
+ },
+ preview: function () {
+ var $preview = this.$("#link-preview");
+ this.get_data(true).then(function (url, new_window, label, classes) {
+ $preview.attr("target", new_window ? '_blank' : "")
+ .attr("href", url && url.length ? url : "#")
+ .html((label && label.length ? label : url))
+ .attr("class", classes.replace(/pull-\w+/, ''));
+ });
+ }
+});
+
+return {
+ 'getCssSelectors': getCssSelectors,
+ 'Dialog': Dialog,
+ 'alt': alt,
+ 'MediaDialog': MediaDialog,
+ 'fontIcons': fontIcons,
+ 'LinkDialog': LinkDialog
+};
+
+});
diff --git a/addons/web_editor/static/src/xml/backend.xml b/addons/web_editor/static/src/xml/backend.xml
new file mode 100644
index 00000000000..5e3ed9695cd
--- /dev/null
+++ b/addons/web_editor/static/src/xml/backend.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="web_editor.FieldTextHtmlSimple">
+ <div class="oe_form_field oe_form_field_html_text" t-att-style="widget.node.attrs.style">
+ <textarea readonly="readonly"/>
+ </div>
+ </t>
+
+ <t t-name="web_editor.FieldTextHtml">
+ <div class="oe_form_field oe_form_field_html oe_form_embedded_html" t-att-style="widget.node.attrs.style">
+ <iframe/>
+ </div>
+ </t>
+
+ <t t-name="web_editor.FieldTextHtml.button.reset">
+ <div class="btn-group pull-right">
+ <button class="o_reset_translation btn btn-default btn-sm btn-small" title="Reset from source"><span class="fa fa-upload"></span></button>
+ </div>
+ </t>
+
+ <t t-name="web_editor.FieldTextHtml.button.translate">
+ <div class="btn-group pull-right">
+ <button t-if="widget.field.translate &amp;&amp; widget.get('effective_readonly')" class="oe_field_translate btn btn-default btn-sm btn-small" style="height: 24px; padding: 1px 3px 0px 5px">
+ <img border="0" class="oe_input_icon" height="16" width="16" src="/web/static/src/img/icons/terp-translate.png" style="margin: 2px 0px -2px 0px;"/>
+ </button>
+ </div>
+ </t>
+
+ <t t-name="web_editor.FieldTextHtml.translate">
+ <div t-if="widget.view.datarecord.id &amp;&amp; widget.languages.length" style="margin: 5px; position: fixed; top: 0; left: 0; z-index: 2000;">
+ <select style="
+ width: 148px;
+ height: 22px;
+ padding: 2px 4px;
+ border: 1px solid #ccc;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+ background: white;">
+ <t t-foreach="widget.languages" t-as="lang">
+ <option value="en_US" t-att-selected="'en_US' == widget.lang ? 'selected' : null">
+ <t t-if="!widget.get('effective_readonly')">English (edit mode)</t>
+ <t t-if="widget.get('effective_readonly')">English</t>
+ </option>
+ <option t-att-value="lang.code" t-att-selected="lang.code == widget.lang ? 'selected' : null"><t t-esc="lang.name"/></option>
+ </t>
+ </select>
+ </div>
+ </t>
+ <t t-name="web_editor.FieldTextHtml.fullscreen">
+ <span style="margin: 5px; position: fixed; top: 0; right: 0; z-index: 2000;">
+ <button class="o_fullscreen btn btn-primary" style="height: 24px; padding: 1px 2px 1px 3px;">
+ <img border="0" class="oe_input_icon" height="16" width="16" src="/web/static/src/img/icons/STOCK_FULLSCREEN.png" style="margin: 2px 0px -2px 0px;"/>
+ </button>
+ </span>
+ </t>
+
+</templates>
diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml
new file mode 100644
index 00000000000..b2f4d583052
--- /dev/null
+++ b/addons/web_editor/static/src/xml/editor.xml
@@ -0,0 +1,372 @@
+<templates id="template" xml:space="preserve">
+ <t t-name="web_editor.editorbar">
+ <div id="web_editor-top-edit" class="o_css_editor navbar navbar-inverse navbar-fixed-top hidden-xs">
+ <form class="navbar-form text-muted">
+ <button type="button" data-action="save"
+ class="btn btn-primary btn-xs">Save</button>
+ <span> or </span>
+ <a href="#" data-action="cancel" class="btn btn-link btn-xs">Discard</a>
+ </form>
+ </div>
+ </t>
+
+ <t t-name="web_editor.dialog">
+ <div class="modal note-link-dialog" tabindex="-1" role="dialog" aria-hidden="true">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+ <h3 class="modal-title"><t t-esc="title"/></h3>
+ </div>
+ <div class="modal-body"><t t-raw="0"/></div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-primary save" t-if="!button">Save</button>
+ <button type="button" class="btn btn-primary save" t-if="button"><t t-raw="button"/></button>
+ <button type="button" class="btn hidden wait" disabled="disabled"/>
+ <span> or </span>
+ <a href="#" data-dismiss="modal" aria-hidden="true">Discard</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </t>
+ <t t-name="web_editor.dialog.media">
+ <div class="modal note-image-dialog" tabindex="-1" role="dialog" aria-hidden="true">
+ <div class="modal-dialog select-media">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+ <h3 class="modal-title">Select a Media</h3>
+ </div>
+ <div class="modal-body">
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#editor-media-image" data-toggle="tab">Image</a></li>
+ <li><a href="#editor-media-icon" data-toggle="tab">Pictogram</a></li>
+ <li><a href="#editor-media-video" data-toggle="tab">Video</a></li>
+ <li class="search" style="float: right;">
+ <ul class="pager mb0 mt0">
+ <li class="previous disabled"><a href="#">← Previous</a></li>
+ <li class="next disabled"><a href="#">Next →</a></li>
+ </ul>
+ </li>
+ <li class="search" style="float: right;">
+ <form action="#">
+ <div class="form-group font-icons fa fa-search mb0">
+ <input type="search" class="form-control" id="icon-search"/>
+ </div>
+ </form>
+ </li>
+ </ul>
+
+ <!-- Tab panes -->
+ <div class="tab-content">
+ <div class="tab-pane fade in active" id="editor-media-image">
+ </div>
+ <div class="tab-pane fade" id="editor-media-icon">
+ </div>
+ <div class="tab-pane fade" id="editor-media-video">
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-primary save">Save</button>
+ <button type="button" class="btn hidden wait" disabled="disabled"/>
+ or
+ <a href="#" data-dismiss="modal" aria-hidden="true">Discard</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </t>
+ <t t-name="web_editor.dialog.image">
+ <div>
+ <form method="POST"
+ action="/web_editor/attachment/add"
+ enctype="multipart/form-data"
+ target="fileframe"
+ class="form-inline">
+ <div class="well">
+ <div class="form-group pull-left">
+ <input type="file" name="upload" accept="image/*" multiple="multiple" style="position: absolute; opacity: 0; width: 1px; height: 1px;"/>
+ <input type="hidden" name="disable_optimization" value=""/>
+ <div class="btn-group">
+ <button type="button" class="btn btn-primary filepicker">Upload an image from your computer</button>
+ <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
+ <span class="caret"></span>
+ <span class="sr-only">Alternate Upload</span>
+ </button>
+ <ul class="dropdown-menu" role="menu">
+ <li><a href="#" class="js_disable_optimization">Upload image without optimization</a></li>
+ </ul>
+ </div>
+ <button type="button" class="btn hidden wait" disabled="disabled">Uploading...</button>
+ </div>
+ <div>
+ <span class="text-muted"> — or — </span>
+ <label for="iamgeurl">Add an image URL</label>
+ <div class="form-group btn-group">
+ <input type="text" name="url" class="form-control url pull-left" style="width: 320px;" id="iamgeurl" placeholder="http://openerp.com/logo.png"/>
+ <button class="btn btn-default" type="submit">Add</button>
+ </div>
+ </div>
+ </div>
+ <input type="hidden" name="func"/>
+ <div class="help-block"/>
+ <div class="existing-attachments">
+ <t t-call="web_editor.dialog.image.existing"/>
+ </div>
+ </form>
+ </div>
+ <iframe src="about:blank" name="fileframe" class="hidden"/>
+ </t>
+ <t t-name="web_editor.dialog.image.existing">
+ <div class="modal" tabindex="-1" role="dialog" aria-hidden="true">
+ <div class="modal-dialog select-image">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+ <h3 class="modal-title">Select a Picture</h3>
+ </div>
+ <div class="modal-body has-error">
+ <div class="existing-attachments"/>
+ <div class="help-block"/>
+ </div>
+ <div class="modal-footer">
+ <a href="#" data-dismiss="modal" aria-hidden="true">Discard</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </t>
+ <t t-name="web_editor.dialog.image.existing.content">
+ <div class="existing-attachments">
+ <div class="row mt16" t-foreach="rows" t-as="row">
+ <div class="col-sm-2 existing-attachment-cell"
+ t-foreach="row" t-as="attachment">
+ <i class="fa fa-times existing-attachment-remove" t-att-data-id="attachment.id"/>
+ <img t-att-src="attachment.local_url" t-att-alt="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" class="img img-responsive"/>
+ </div>
+ </div>
+ </div>
+ </t>
+ <t t-name="web_editor.dialog.image.existing.error">
+ <div class="help-block">
+ <p>The image could not be deleted because it is used in the
+ following pages or views:</p>
+ <ul t-foreach="views" t-as="view">
+ <li>
+ <a t-attf-href="/web#model=ir.ui.view&amp;id=#{view.id}">
+ <t t-esc="view.name"/>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </t>
+
+ <t t-name="web_editor.discard">
+ <div class="modal" tabindex="-1" role="dialog" aria-hidden="true">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+ <h3 class="modal-title">Discard edition</h3>
+ </div>
+ <div class="modal-body">
+ <p>
+ If you discard the current edition, <em>all</em>
+ unsaved changes will be lost.
+ </p>
+ <p>You can cancel to return to the edition mode.</p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-danger">Discard</button>
+ or
+ <a href="#" data-dismiss="modal" aria-hidden="true">Cancel</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </t>
+
+ <t t-name="web_editor.dialog.alt">
+ <t t-call="web_editor.dialog">
+ <t t-set="title">Change media Alt and Title</t>
+ <form action="#" class="form-horizontal">
+ <div class="form-group">
+ <label for="alt" class="col-sm-2 control-label">Alt:</label>
+ <div class="col-sm-9"><input id="alt" type="text" class="form-control" required="required" t-att-value="widget.alt"/></div>
+ </div>
+ <div class="form-group">
+ <label for="title" class="col-sm-2 control-label">Title:</label>
+ <div class="col-sm-9"><input id="title" type="text" class="form-control" required="required" t-att-value="widget.title"/></div>
+ </div>
+ </form>
+ </t>
+ </t>
+
+ <t t-name="web_editor.dialog.font-icons">
+ <form action="#">
+ <input type="hidden" id="fa-icon"/>
+ <input type="hidden" id="fa-size" value="fa-1x"/>
+ <div class="font-icons-icons">
+ <t t-call="web_editor.dialog.font-icons.icons">
+ <t t-set="iconsParser" t-value="widget.iconsParser"/>
+ </t>
+ </div>
+ </form>
+ </t>
+ <t t-name="web_editor.dialog.font-icons.icons">
+ <t t-foreach="iconsParser" t-as="data">
+ <span t-foreach="data.icons" t-as="icon"
+ t-attf-class="#{data.base} font-icons-icon #{icon}"
+ t-att-data-id="icon"/>
+ </t>
+ </t>
+
+ <t t-name="web_editor.dialog.video">
+ <form action="#" class="form-inline">
+ <div class="well">
+ <div class="form-group btn-group">
+ <span class="text-muted pull-right" style="margin-left:10px; line-height: 2em;">(Youtube, Vimeo, Dailymotion)</span>
+ <label for="urlvideo" style="width: 220px; line-height: 2em;" class="pull-left">Set a video URL</label>
+ <input type="text"
+ name="url"
+ class="form-control url pull-left"
+ style="width: 400px;"
+ id="urlvideo"
+ placeholder="//www.youtube.com/embed/yws1tbgNV7k"
+ t-translation="off"/>
+ <button class="btn btn-default">Preview</button>
+ </div>
+ <div class="form-group btn-group">
+ <label for="urlvideo" style="width: 220px; line-height: 2em;" class="pull-left"><span class="text-muted">— or —</span> Embed Video (HTML)</label>
+ <input type="text"
+ name="embed"
+ class="form-control url pull-left"
+ style="width: 400px;"
+ id="embedvideo"
+ placeholder='&lt;iframe src="//www.youtube.com/embed/yws1tbgNV7k"&gt;&lt;/iframe&gt;'
+ t-translation="off"/>
+ <button class="btn btn-default">Preview</button>
+ </div>
+ </div>
+
+ <div id="video-preview" style="width: 450px; margin: 0 auto;">
+ <div class="media_iframe_video">
+ <div class="media_iframe_video_size"></div>
+ <iframe
+ src=""
+ frameborder="0"
+ allowfullscreen="allowfullscreen"></iframe>
+ </div>
+ </div>
+
+ <div class="text-center mt32">
+ <div class="checkbox">
+ <label><input type="checkbox" id="autoplay"/> Autoplay</label>
+ </div>
+ </div>
+ </form>
+ </t>
+
+ <t t-name="web_editor.dialog.link">
+ <t t-call="web_editor.dialog">
+ <t t-set="title">Link to</t>
+ <form>
+ <div class="list-group">
+ <div class="form-group list-group-item active">
+ <h3 class="list-group-item-heading">
+ <label for="link-new" class="control-label">
+ Link Label
+ </label>
+ </h3>
+ <input type="text" class="form-control" id="link-text" required="required"/>
+ </div>
+ </div>
+ <ul class="list-group">
+ <li class="list-group-item form-group clearfix active">
+ <div class="pull-right" style="display: none;">
+ <label>
+ <input type="checkbox" class="window-new"/>
+ Open in new window
+ </label>
+ </div>
+ <h4 class="list-group-item-heading">
+ <label for="link-external" class="control-label">
+ URL or Email Address
+ </label>
+ </h4>
+ <input type="text" class="form-control url email-address url-source"
+ id="link-external" placeholder="http://openerp.com"/>
+ </li>
+ </ul>
+ <div class="form-horizontal link-style">
+ <div class="form-group">
+ <label class="col-sm-2 control-label">
+ Style
+ </label>
+ <div class="col-sm-4">
+ <div class="dropdown" style="line-height: 2em;">
+ <a data-toggle="dropdown" class="btn btn-default" href="#">Color Style <span class="caret"></span></a>
+ <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
+ <li>
+ <label class="btn btn-link">Link<input value="" class="link-style" type="radio" name="link-style-type" checked="checked"/></label>
+ </li>
+ <li>
+ <label class="btn btn-default">Basic<input value="btn-default" class="link-style" type="radio" name="link-style-type"/></label>
+ </li>
+ <li>
+ <label class="btn btn-success">Success<input value="btn-success" class="link-style" type="radio" name="link-style-type"/></label>
+ </li>
+ <li>
+ <label class="btn btn-primary">Primary<input value="btn-primary" class="link-style" type="radio" name="link-style-type"/></label>
+ </li>
+ <li>
+ <label class="btn btn-info">Info<input value="btn-info" class="link-style" type="radio" name="link-style-type"/></label>
+ </li>
+ <li>
+ <label class="btn btn-warning">Warning<input value="btn-warning" class="link-style" type="radio" name="link-style-type"/></label>
+ </li>
+ <li>
+ <label class="btn btn-danger">Danger<input value="btn-danger" class="link-style" type="radio" name="link-style-type"/></label>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="col-sm-4">
+ <div class="dropdown" style="line-height: 2em;">
+ <a data-toggle="dropdown" class="btn btn-default" href="#">Size <span class="caret"></span></a>
+ <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
+ <li>
+ <label class="btn btn-default btn-xs">Extra Small<input value="btn-xs" class="link-style" type="radio" name="link-style-size"/></label>
+ </li>
+ <li>
+ <label class="btn btn-default btn-sm">Small<input value="btn-sm" class="link-style" type="radio" name="link-style-size"/></label>
+ </li>
+ <li>
+ <label class="btn btn-default">Default<input value="" class="link-style" type="radio" name="link-style-size" checked="checked"/></label>
+ </li>
+ <li>
+ <label class="btn btn-default btn-lg">Large<input value="btn-lg" class="link-style" type="radio" name="link-style-size"/></label>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="form-horizontal link-style">
+ <div class="form-group">
+ <label class="col-sm-2 control-label">
+ Preview
+ </label>
+ <div class="col-sm-10" style="line-height: 2em;">
+ <div style="overflow: auto; max-width: 100%; max-height: 200px;">
+ <a id="link-preview" href="#"></a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+ </t>
+ </t>
+</templates>
diff --git a/addons/website/static/src/xml/website.snippets.xml b/addons/web_editor/static/src/xml/snippets.xml
index 42a925184fa..ee996548379 100644
--- a/addons/website/static/src/xml/website.snippets.xml
+++ b/addons/web_editor/static/src/xml/snippets.xml
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<!-- Snippet loader -->
- <t t-name="website.snippets_style">
+ <t t-name="web_editor.snippets_style">
<li class="navbar-form">
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
@@ -13,7 +13,7 @@
</li>
</t>
- <t t-name="website.snippets">
+ <t t-name="web_editor.snippets">
<div id='oe_snippets' class="hidden-xs o_open">
<div id="o_arrow">
<span class="fa fa-angle-double-right fa-1x" style="top: 10px;"></span>
@@ -24,7 +24,7 @@
</div>
</t>
- <t t-name="website.snippet_overlay">
+ <t t-name="web_editor.snippet_overlay">
<div class="oe_overlay">
<div class="oe_overlay_options" contentEditable="false">
<div class="btn-group">
@@ -51,13 +51,13 @@
</div>
</t>
- <t t-name="website.snippet_manipulator">
+ <t t-name="web_editor.snippet_manipulator">
<div class='oe_snippet_manipulator'></div>
</t>
<!-- t-field options -->
- <t t-name="website.many2one.button">
+ <t t-name="web_editor.many2one.button">
<div class="dropdown">
<a href="#" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" data-hover="dropdown" title="Search Contact"><i class="fa fa-search"></i></a>
<ul class="dropdown-menu contact_menu" role="menu">
@@ -66,7 +66,7 @@
</div>
</t>
- <t t-name="website.many2one.search">
+ <t t-name="web_editor.many2one.search">
<t t-foreach="contacts" t-as="item">
<li><a href="#" t-att-data-id="item.id" t-att-data-name="item.display_name"><t t-esc="item.display_name"/> <t t-if="item.city or item.country_id"><small class="text-muted">(<t t-esc="item.city"/> <t t-esc="item.country_id and item.country_id[1]"/>)</small></t></a></li>
</t>
diff --git a/addons/web_editor/static/src/xml/translator.xml b/addons/web_editor/static/src/xml/translator.xml
new file mode 100644
index 00000000000..5a825d4ad0b
--- /dev/null
+++ b/addons/web_editor/static/src/xml/translator.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates id="template" xml:space="preserve">
+
+<t t-name="web_editor.translator">
+ <div id="web_editor-top-edit" class="o_css_editor o_translate navbar navbar-inverse navbar-fixed-top hidden-xs">
+ <form class="navbar-form text-muted">
+ <button type="button" data-action="save" class="btn btn-primary btn-xs">Save</button>
+ <span>or</span>
+ <a href="#" data-action="cancel" class="btn btn-link btn-xs">Discard</a>
+ </form>
+ </div>
+</t>
+
+</templates>
diff --git a/addons/web_editor/tests/__init__.py b/addons/web_editor/tests/__init__.py
new file mode 100644
index 00000000000..ee3fba301a9
--- /dev/null
+++ b/addons/web_editor/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+import test_ui
+import test_converter
diff --git a/addons/web_editor/tests/test_converter.py b/addons/web_editor/tests/test_converter.py
new file mode 100644
index 00000000000..ec8124b66b5
--- /dev/null
+++ b/addons/web_editor/tests/test_converter.py
@@ -0,0 +1,243 @@
+# -*- coding: utf-8 -*-
+import textwrap
+import unittest2
+
+from lxml import etree, html
+from lxml.builder import E
+
+from openerp.tests import common
+from openerp.addons.base.ir import ir_qweb
+from openerp.addons.web_editor.models.ir_qweb import html_to_text
+
+
+class TestHTMLToText(unittest2.TestCase):
+ def test_rawstring(self):
+ self.assertEqual(
+ "foobar",
+ html_to_text(E.div("foobar")))
+
+ def test_br(self):
+ self.assertEqual(
+ "foo\nbar",
+ html_to_text(E.div("foo", E.br(), "bar")))
+
+ self.assertEqual(
+ "foo\n\nbar\nbaz",
+ html_to_text(E.div(
+ "foo", E.br(), E.br(),
+ "bar", E.br(),
+ "baz")))
+
+ def test_p(self):
+ self.assertEqual(
+ "foo\n\nbar\n\nbaz",
+ html_to_text(E.div(
+ "foo",
+ E.p("bar"),
+ "baz")))
+
+ self.assertEqual(
+ "foo",
+ html_to_text(E.div(E.p("foo"))))
+
+ self.assertEqual(
+ "foo\n\nbar",
+ html_to_text(E.div("foo", E.p("bar"))))
+ self.assertEqual(
+ "foo\n\nbar",
+ html_to_text(E.div(E.p("foo"), "bar")))
+
+ self.assertEqual(
+ "foo\n\nbar\n\nbaz",
+ html_to_text(E.div(
+ E.p("foo"),
+ E.p("bar"),
+ E.p("baz"),
+ )))
+
+ def test_div(self):
+ self.assertEqual(
+ "foo\nbar\nbaz",
+ html_to_text(E.div(
+ "foo",
+ E.div("bar"),
+ "baz"
+ )))
+
+ self.assertEqual(
+ "foo",
+ html_to_text(E.div(E.div("foo"))))
+
+ self.assertEqual(
+ "foo\nbar",
+ html_to_text(E.div("foo", E.div("bar"))))
+ self.assertEqual(
+ "foo\nbar",
+ html_to_text(E.div(E.div("foo"), "bar")))
+
+ self.assertEqual(
+ "foo\nbar\nbaz",
+ html_to_text(E.div(
+ "foo",
+ E.div("bar"),
+ E.div("baz")
+ )))
+
+ def test_other_block(self):
+ self.assertEqual(
+ "foo\nbar\nbaz",
+ html_to_text(E.div(
+ "foo",
+ E.section("bar"),
+ "baz"
+ )))
+
+ def test_inline(self):
+ self.assertEqual(
+ "foobarbaz",
+ html_to_text(E.div("foo", E.span("bar"), "baz")))
+
+ def test_whitespace(self):
+ self.assertEqual(
+ "foo bar\nbaz",
+ html_to_text(E.div(
+ "foo\nbar",
+ E.br(),
+ "baz")
+ ))
+
+ self.assertEqual(
+ "foo bar\nbaz",
+ html_to_text(E.div(
+ E.div(E.span("foo"), " bar"),
+ "baz")))
+
+
+class TestConvertBack(common.TransactionCase):
+ def setUp(self):
+ super(TestConvertBack, self).setUp()
+
+ def field_rountrip_result(self, field, value, expected):
+ model = 'web_editor.converter.test'
+ Model = self.registry(model)
+ id = Model.create(
+ self.cr, self.uid, {
+ field: value
+ })
+ [record] = Model.browse(self.cr, self.uid, [id])
+
+ e = etree.Element('span')
+ field_value = 'record.%s' % field
+ e.set('t-field', field_value)
+
+ rendered = self.registry('ir.qweb').render_tag_field(
+ e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, {
+ 'record': record,
+ }, context={'inherit_branding': True}))
+ element = html.fromstring(
+ rendered, parser=html.HTMLParser(encoding='utf-8'))
+
+ converter = self.registry('ir.qweb').get_converter_for(
+ element.get('data-oe-type'))
+
+ value_back = converter.from_html(
+ self.cr, self.uid, model, Model._fields[field], element)
+
+ if isinstance(expected, str):
+ expected = expected.decode('utf-8')
+ self.assertEqual(value_back, expected)
+
+ def field_roundtrip(self, field, value):
+ self.field_rountrip_result(field, value, value)
+
+ def test_integer(self):
+ self.field_roundtrip('integer', 42)
+
+ def test_float(self):
+ self.field_roundtrip('float', 42.567890)
+ self.field_roundtrip('float', 324542.567890)
+
+ def test_numeric(self):
+ self.field_roundtrip('numeric', 42.77)
+
+ def test_char(self):
+ self.field_roundtrip('char', "foo bar")
+
+ self.field_roundtrip('char', "ⒸⓄⓇⒼⒺ")
+
+ def test_selection(self):
+ self.field_roundtrip('selection', 3)
+
+ def test_selection_str(self):
+ self.field_roundtrip('selection_str', 'B')
+
+ def test_text(self):
+ self.field_roundtrip('text', textwrap.dedent("""\
+ You must obey the dance commander
+ Givin' out the order for fun
+ You must obey the dance commander
+ You know that he's the only one
+ Who gives the orders here,
+ Alright
+ Who gives the orders here,
+ Alright
+
+ It would be awesome
+ If we could dance-a
+ It would be awesome, yeah
+ Let's take the chance-a
+ It would be awesome, yeah
+ Let's start the show
+ Because you never know
+ You never know
+ You never know until you go"""))
+
+ def test_m2o(self):
+ """ the M2O field conversion (from html) is markedly different from
+ others as it directly writes into the m2o and returns nothing at all.
+ """
+ model = 'web_editor.converter.test'
+ field = 'many2one'
+
+ Sub = self.registry('web_editor.converter.test.sub')
+ sub_id = Sub.create(self.cr, self.uid, {'name': "Foo"})
+ sub2_id = Sub.create(self.cr, self.uid, {'name': "Bar"})
+
+ Model = self.registry(model)
+ id = Model.create(self.cr, self.uid, {field: sub_id})
+ [record] = Model.browse(self.cr, self.uid, [id])
+
+ e = etree.Element('span')
+ field_value = 'record.%s' % field
+ e.set('t-field', field_value)
+
+ rendered = self.registry('ir.qweb').render_tag_field(
+ e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, {
+ 'record': record,
+ }, context={'inherit_branding': True}))
+
+ element = html.fromstring(rendered, parser=html.HTMLParser(encoding='utf-8'))
+ # emulate edition
+ element.set('data-oe-many2one-id', str(sub2_id))
+ element.text = "New content"
+
+ converter = self.registry('ir.qweb').get_converter_for(
+ element.get('data-oe-type'))
+
+ value_back = converter.from_html(
+ self.cr, self.uid, model, Model._fields[field], element)
+
+ self.assertIsNone(
+ value_back, "the m2o converter should return None to avoid spurious"
+ " or useless writes on the parent record")
+
+ self.assertEqual(
+ Sub.browse(self.cr, self.uid, sub_id).name,
+ "Foo",
+ "element edition can't change directly the m2o record"
+ )
+ self.assertEqual(
+ Model.browse(self.cr, self.uid, id).many2one.name,
+ "Bar",
+ "element edition should have been change the m2o id"
+ )
diff --git a/addons/web_editor/tests/test_ui.py b/addons/web_editor/tests/test_ui.py
new file mode 100644
index 00000000000..c0887201bba
--- /dev/null
+++ b/addons/web_editor/tests/test_ui.py
@@ -0,0 +1,9 @@
+import openerp.tests
+
+
+class TestUi(openerp.tests.HttpCase):
+ def test_01_admin_rte(self):
+ self.phantom_js("/web", "odoo.__DEBUG__.services['web.Tour'].run('rte', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.rte", login='admin')
+
+ def test_02_admin_rte_inline(self):
+ self.phantom_js("/web", "odoo.__DEBUG__.services['web.Tour'].run('rte_inline', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.rte", login='admin')
diff --git a/addons/web_editor/views/editor.xml b/addons/web_editor/views/editor.xml
new file mode 100644
index 00000000000..c7d785d58f8
--- /dev/null
+++ b/addons/web_editor/views/editor.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+
+<template id="summernote" name="Summernote">
+ <script type="text/javascript">
+ (function () {
+ "use strict";
+ odoo.define('jquery', function () {return $;});
+
+ var uniqId = 0;
+ odoo.__define__ = window.define;
+ window.define = function (id) {
+ var args = Array.prototype.slice.call(arguments);
+ var factorie = args.pop();
+ var id = args[0];
+
+ if (id instanceof Array) {
+ var name = "summernote_factorie_" + (++uniqId);
+ odoo[name] = factorie;
+
+ var head = '';
+ var fn = 'var fn = odoo.'+name+';\ndelete odoo.'+name+';\n';
+ fn += 'return fn(';
+ for (var k=0; k&lt;id.length; k++) {
+ head += 'var a'+(++uniqId)+' = require("'+id[k]+'");\n';
+ fn += 'a'+uniqId+', ';
+ }
+ fn += 'null);';
+
+ odoo.define(odoo.website_next_define, new Function('require', head + fn));
+ } else {
+ odoo.define(id, factorie);
+ }
+ };
+ })();
+ </script>
+ <script>odoo.website_next_define = 'summernote/core/async';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/async.js" />
+ <script>odoo.website_next_define = 'summernote/core/func';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/func.js" />
+ <script>odoo.website_next_define = 'summernote/core/agent';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/agent.js" />
+ <script>odoo.website_next_define = 'summernote/core/list';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/list.js" />
+ <script>odoo.website_next_define = 'summernote/core/dom';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/dom.js" />
+ <script>odoo.website_next_define = 'summernote/core/key';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/key.js" />
+ <script>odoo.website_next_define = 'summernote/core/range';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/range.js" />
+
+ <script>odoo.website_next_define = 'summernote/editing/Bullet';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/editing/Bullet.js" />
+ <script>odoo.website_next_define = 'summernote/editing/History';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/editing/History.js" />
+ <script>odoo.website_next_define = 'summernote/editing/Style';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/editing/Style.js" />
+ <script>odoo.website_next_define = 'summernote/editing/Table';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/editing/Table.js" />
+ <script>odoo.website_next_define = 'summernote/editing/Typing';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/editing/Typing.js" />
+ <script>odoo.website_next_define = 'summernote/editing/Editor';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/editing/Editor.js" />
+
+ <script>odoo.website_next_define = 'summernote/module/Button';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/module/Button.js" />
+ <script>odoo.website_next_define = 'summernote/module/Dialog';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/module/Dialog.js" />
+ <script>odoo.website_next_define = 'summernote/module/Handle';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/module/Handle.js" />
+ <script>odoo.website_next_define = 'summernote/module/Popover';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/module/Popover.js" />
+ <script>odoo.website_next_define = 'summernote/module/Toolbar';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/module/Toolbar.js" />
+
+ <script>odoo.website_next_define = 'summernote/Renderer';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/Renderer.js" />
+ <script>odoo.website_next_define = 'summernote/EventHandler';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/EventHandler.js" />
+
+ <script>odoo.website_next_define = 'summernote/settings';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/settings.js" />
+
+ <script>odoo.website_next_define = 'summernote/summernote';</script>
+ <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/summernote.js" />
+
+ <script type="text/javascript" src="/web_editor/static/src/js/summernote.js" />
+ <script type="text/javascript">
+ window.define = odoo.__define__;
+ delete odoo.__define__;
+ delete odoo.website_next_define;
+ </script>
+
+ <!--link rel="stylesheet" href="/web_editor/static/lib/summernote/src/less/summernote.less"/-->
+ <link rel="stylesheet" href="/web_editor/static/lib/summernote/summernote.css"/>
+</template>
+
+<template id="editor" name="Editor">
+ <link rel='stylesheet' href='/web_editor/static/src/css/snippets.css'/>
+ <link rel="stylesheet" href='/web_editor/static/src/css/editor.css'/>
+ <script type="text/javascript" src='/web_editor/static/lib/nearest/jquery.nearest.js' />
+ <script type="text/javascript" src="/web_editor/static/src/js/editor.js" />
+ <script type="text/javascript" src="/web_editor/static/src/js/rte.js" />
+ <script type="text/javascript" src="/web_editor/static/src/js/rte.summernote.js" />
+ <script type="text/javascript" src="/web_editor/static/src/js/widgets.js" />
+ <script type="text/javascript" src="/web_editor/static/src/js/snippets.editor.js" />
+ <script type="text/javascript" src="/web_editor/static/src/js/snippets.options.js" />
+ <script type="text/javascript" src="/web_editor/static/src/js/tour.rte.js" />
+ <script type="text/javascript" src="/web_editor/static/src/js/transcoder.js"></script>
+ <script type="text/javascript" src="/web_editor/static/src/js/translator.js"></script>
+</template>
+
+<template id="assets_common" inherit_id="web.assets_common" name="web_editor base assets_common">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/web_editor/static/src/js/base.js" />
+ <script type="text/javascript" src="/web_editor/static/src/js/snippets.animation.js" />
+ </xpath>
+</template>
+
+<template id="assets_editor" name="web_editor assets for editor">
+ <t t-call="web.assets_common"/>
+ <t t-call="web.bootstrap"/>
+ <script type="text/javascript" src="/web/static/lib/select2/select2.js"></script>
+ <link rel="stylesheet" href='/web_editor/static/src/css/web_editor.css'/>
+ <t t-call="web_editor.summernote"/>
+ <t t-call="web_editor.editor"/>
+</template>
+
+<template id="assets_backend" name="web_editor assets for backend" inherit_id="web.assets_backend">
+ <xpath expr="." position="inside">
+ <t t-call="web_editor.summernote"/>
+ <t t-call="web_editor.editor"/>
+ <link rel="stylesheet" href="/web_editor/static/src/css/backend.css"/>
+ <script type="text/javascript" src="/web_editor/static/src/js/backend.js" />
+ </xpath>
+</template>
+
+<!-- color-picker -->
+<!-- HTML class to hide option for one mode : only-text, only-bg -->
+
+<template id="web_editor.colorpicker" name="Color-Picker">
+ <table class="colorpicker">
+ <tr>
+ <td><button class="bg-primary" title="Primary Color"/></td>
+ <td><button class="bg-success" title="Success Color"/></td>
+ <td><button class="bg-info" title="Info Color"/></td>
+ <td><button class="bg-warning" title="Warning Color"/></td>
+ <td><button class="bg-danger" title="Danger Color"/></td>
+ </tr>
+ </table>
+</template>
+
+</openerp>
diff --git a/addons/web_editor/views/iframe.xml b/addons/web_editor/views/iframe.xml
new file mode 100644
index 00000000000..32cbebd30ed
--- /dev/null
+++ b/addons/web_editor/views/iframe.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+
+<template id="layout" name="Editor layout">&lt;!DOCTYPE html&gt;
+ <html t-att-lang="lang and lang.replace('_', '-')"
+ t-att-data-editable="'1' if editable else None"
+ t-att-data-translatable="'1' if translatable else None"
+ t-att-data-edit_translations="'1' if edit_translations else None"
+ t-att-data-main-object="repr(main_object) if editable else None">
+ <head>
+ <meta charset="utf-8" />
+ <title>Odoo Editor layout</title>
+ <meta name="viewport" content="initial-scale=1"/>
+ <script type="text/javascript">
+ var snippets_url = '<t t-esc="snippets or ''"/>';
+ var callback = window.location.href.match(/callback=([^&amp;=]+)/)[1];
+ </script>
+ <link t-if="inline_mode" rel="stylesheet" href="/web_editor/static/src/css/inline.css" type="text/css"/>
+ <t t-if="not dont_load_assets">
+ <t t-call-assets="web_editor.assets_editor"/>
+ <style>
+ .note-popover .popover { left: 5px !important; }
+ html[data-translatable] .note-popover .popover { left: 150px !important; }
+ </style>
+ <script type="text/javascript" src="/web/static/src/js/framework/session_instance.js"></script>
+ <script type="text/javascript" src="/web_editor/static/src/js/iframe.js"></script>
+ <script t-if="enable_editor and inline_mode" type="text/javascript" src="/web_editor/static/src/js/inline.js"></script>
+ </t>
+ <script type="text/javascript">
+ var snippets_url = '<t t-esc="snippets or ''"/>';
+ var callback = window.location.href.match(/callback=([^&amp;=]+)/)[1];
+ </script>
+ <script>
+ if (window.top.odoo &amp;&amp; !window.top.odoo[callback+"_updown"]) {
+ window.top.odoo[callback+"_updown"] = function (value, fields_values) {
+ var editable = document.getElementsByClassName("o_editable")[0];
+ if (value !== editable.innerHTML) {
+ editable.innerHTML = value;
+ }
+ };
+ }
+ </script>
+ <t t-raw="head or ''"/>
+ </head>
+ <body onload="var fn=window.top.odoo &amp;&amp; window.top.odoo[callback+'_content']; if(fn) {fn();} else {console.warn('Please don\'t open iframe without the odoo backend');}">
+ <div id="wrapwrap">
+ <main>
+ <t t-raw="0"/>
+ </main>
+ </div>
+ <t t-raw="add_html or ''"/>
+ </body>
+ </html>
+</template>
+
+<template id="FieldTextHtml" name="Editor HTML">
+ <t t-call="web_editor.layout">
+ <div id="editable_area" t-att-class="'' if edit_translations else 'o_editable'"></div>
+ </t>
+</template>
+
+</openerp>
diff --git a/addons/web_editor/views/snippets.xml b/addons/web_editor/views/snippets.xml
new file mode 100644
index 00000000000..4499187f6a7
--- /dev/null
+++ b/addons/web_editor/views/snippets.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+<data>
+<template id="snippets">
+
+
+<ul>
+ <li class="active">
+ <a href="#snippet_structure" class="scroll-link">
+ <span class="fa-stack fa-lg">
+ <i class="fa fa-square fa-stack-2x fa-inverse"></i>
+ </span>
+ </a>
+ </li>
+</ul>
+
+
+<div id="o_scroll">
+ <div id="snippet_structure" class="o_panel">
+ <div class="o_panel_header">
+ <span class="fa-stack">
+ <i class="fa fa-th-large fa-stack-1x"></i>
+ </span> First Panel
+ </div>
+ <div class="o_panel_body">
+
+ </div>
+ </div>
+</div>
+
+
+<div id="snippet_options" class="hidden">
+ <t t-call="web_editor.snippet_options"/>
+</div>
+</template>
+
+<!-- options (data-selector, data-drop-in, data-drop-near, data-js to link js object ) -->
+
+<template id="snippet_options">
+ <!-- media -->
+
+ <div data-js='media'
+ data-selector="img, .media_iframe_video, span.fa, i.fa, .glyphicon">
+ <li data-edition="edition"><a>Change...</a></li>
+ </div>
+
+ <!-- t-field -->
+
+ <div data-js='many2one'
+ data-selector="[data-oe-many2one-model]:not([data-oe-readonly])"
+ data-no-check="true">
+ </div>
+</template>
+
+</data>
+</openerp>
diff --git a/addons/web_tests/static/src/js/web.tour.js b/addons/web_tests/static/src/js/web.tour.js
index bc93b1ba079..9ec5905213b 100644
--- a/addons/web_tests/static/src/js/web.tour.js
+++ b/addons/web_tests/static/src/js/web.tour.js
@@ -303,7 +303,7 @@ Tour.register({
},
{
title: "select demo user",
- element: '.modal li a:contains(Demo User)',
+ element: '.modal li:contains(Demo User)',
},
{
title: "test one2many's line onchange after many2one",
@@ -399,7 +399,7 @@ Tour.register({
},
{
title: "select an other user",
- element: '.oe_application li a:contains(Demo User)',
+ element: '.ui-autocomplete li:contains(Demo User)',
},
{
title: "test one2many's line onchange after many2one",
@@ -433,7 +433,7 @@ Tour.register({
},
{
title: "add a tag",
- element: '.ui-autocomplete a:first',
+ element: '.ui-autocomplete li:first',
},
// remove record
diff --git a/addons/website/__openerp__.py b/addons/website/__openerp__.py
index 4a40d869b0e..19cf30928da 100644
--- a/addons/website/__openerp__.py
+++ b/addons/website/__openerp__.py
@@ -11,7 +11,7 @@ OpenERP Website CMS
""",
'author': 'OpenERP SA',
- 'depends': ['web', 'web_planner'],
+ 'depends': ['web', 'web_editor', 'web_planner'],
'installable': True,
'data': [
'data/data.xml',
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 9f01f71b2a0..a755cb249b4 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
-import cStringIO
import datetime
from itertools import islice
import json
@@ -11,13 +10,12 @@ import re
import werkzeug.utils
import urllib2
import werkzeug.wrappers
-from PIL import Image
import openerp
from openerp.addons.web.controllers.main import WebClient
+from openerp.addons.web_editor.controllers.main import Web_Editor
from openerp.addons.web import http
-from openerp.http import request, STATIC_CACHE
-from openerp.tools import image_save_for_web
+from openerp.http import request
logger = logging.getLogger(__name__)
@@ -199,7 +197,7 @@ class Website(openerp.addons.web.controllers.main.Home):
return werkzeug.wrappers.Response(url, mimetype='text/plain')
return werkzeug.utils.redirect(url + "?enable_editor=1")
- @http.route(['/website/snippets'], type='json', auth="public", website=True)
+ @http.route(['/website/snippets'], type='json', auth="user", website=True)
def snippets(self):
return request.website._render('website.snippets')
@@ -234,111 +232,16 @@ class Website(openerp.addons.web.controllers.main.Home):
return request.registry["ir.ui.view"].customize_template_get(
request.cr, request.uid, key, full=full, bundles=bundles,
context=request.context)
- @http.route('/website/get_view_translations', type='json', auth='public', website=True)
- def get_view_translations(self, xml_id, lang=None):
- lang = lang or request.context.get('lang')
- return request.registry["ir.ui.view"].get_view_translations(
- request.cr, request.uid, xml_id, lang=lang, context=request.context)
-
- @http.route('/website/set_translations', type='json', auth='public', website=True)
- def set_translations(self, data, lang):
- irt = request.registry.get('ir.translation')
- for view_id, trans in data.items():
- view_id = int(view_id)
- for t in trans:
- initial_content = t['initial_content'].strip()
- new_content = t['new_content'].strip()
- tid = t['translation_id']
- if not tid:
- old_trans = irt.search_read(
- request.cr, request.uid,
- [
- ('type', '=', 'model'),
- ('name', '=', 'ir.ui.view,arch_db'),
- ('res_id', '=', view_id),
- ('lang', '=', lang),
- ('src', '=', initial_content),
- ])
- if old_trans:
- tid = old_trans[0]['id']
- if tid:
- vals = {'value': new_content, 'state': 'translated'}
- irt.write(request.cr, request.uid, [tid], vals)
- else:
- new_trans = {
- 'type': 'model',
- 'name': 'ir.ui.view,arch_db',
- 'state': 'translated',
- 'res_id': view_id,
- 'lang': lang,
- 'src': initial_content,
- 'value': new_content,
- }
- if t.get('gengo_translation'):
- new_trans['gengo_translation'] = t.get('gengo_translation')
- new_trans['gengo_comment'] = t.get('gengo_comment')
- irt.create(request.cr, request.uid, new_trans)
- return True
@http.route('/website/translations', type='json', auth="public", website=True)
- def get_website_translations(self, lang):
+ def get_website_translations(self, lang, mods=None):
module_obj = request.registry['ir.module.module']
module_ids = module_obj.search(request.cr, request.uid, [('name', 'ilike', 'website'), ('state', '=', 'installed')], context=request.context)
modules = [x['name'] for x in module_obj.read(request.cr, request.uid, module_ids, ['name'], context=request.context)]
+ if mods:
+ modules += mods
return WebClient().translations(mods=modules, lang=lang)
- @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
- def attach(self, func, upload=None, url=None, disable_optimization=None):
- # 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 = 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 """<script type='text/javascript'>
- window.parent['%s'](%s, %s);
- </script>""" % (func, json.dumps(uploads), json.dumps(message))
-
@http.route(['/website/publish'], type='json', auth="public", website=True)
def publish(self, id, object):
_id = int(id)
@@ -432,63 +335,6 @@ class Website(openerp.addons.web.controllers.main.Home):
def kanban(self, **post):
return request.website.kanban_col(**post)
- def placeholder(self, response):
- return request.registry['website']._image_placeholder(response)
-
- @http.route([
- '/website/image',
- '/website/image/<xmlid>',
- '/website/image/<xmlid>/<int:max_width>x<int:max_height>',
- '/website/image/<xmlid>/<field>',
- '/website/image/<xmlid>/<field>/<int:max_width>x<int:max_height>',
- '/website/image/<model>/<id>/<field>',
- '/website/image/<model>/<id>/<field>/<int:max_width>x<int:max_height>'
- ], auth="public", website=True)
- def website_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['website']._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)
-
#------------------------------------------------------
# Server actions
#------------------------------------------------------
@@ -529,21 +375,15 @@ class Website(openerp.addons.web.controllers.main.Home):
return request.redirect('/')
#------------------------------------------------------
- # Backend html field
+ # image route for browse record
#------------------------------------------------------
- @http.route('/website/field/html', type='http', auth="public", website=True)
- def FieldTextHtml(self, model=None, res_id=None, field=None, callback=None, **kwargs):
- record = None
- if model and res_id:
- res_id = int(res_id)
- record = request.registry[model].browse(request.cr, request.uid, res_id, request.context)
-
- datarecord = json.loads(kwargs['datarecord'])
- kwargs.update({
- 'content': record and getattr(record, field) or "",
- 'model': model,
- 'res_id': res_id,
- 'field': field,
- 'datarecord': datarecord
- })
- return request.website.render(kwargs.get("template") or "website.FieldTextHtml", kwargs)
+ @http.route([
+ '/website/image',
+ '/website/image/<xmlid>',
+ '/website/image/<xmlid>/<field>',
+ '/website/image/<model>/<id>/<field>',
+ '/website/image/<model>/<id>/<field>/<int:max_width>x<int:max_height>'
+ ], type='http', auth="public")
+ def website_image(self, model=None, id=None, field=None, xmlid=None, max_width=None, max_height=None):
+ logger.warning("Deprecated image controller, please use /web_editor/image/")
+ return Web_Editor().image(model=model, id=id, field=field, xmlid=xmlid, max_width=max_width, max_height=max_height)
diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py
index 67672babb4e..9815e0c8285 100644
--- a/addons/website/models/__init__.py
+++ b/addons/website/models/__init__.py
@@ -8,5 +8,3 @@ import ir_qweb
import ir_http
import res_config
import web_planner
-
-import test_models
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index 2668e5008d6..76e0d9a0945 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -157,7 +157,8 @@ class ir_http(orm.AbstractModel):
elif url_lang:
path.pop(1)
return self.reroute('/'.join(path) or '/')
-
+ if path[1] == request.website.default_lang_code:
+ request.context['edit_translations'] = False
if not request.context.get('tz'):
request.context['tz'] = request.session['geoip'].get('time_zone')
# bind modified context
diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py
index d10e33a4708..ea0c6e33438 100644
--- a/addons/website/models/ir_qweb.py
+++ b/addons/website/models/ir_qweb.py
@@ -1,44 +1,11 @@
# -*- coding: utf-8 -*-
-"""
-Website-context rendering needs to add some metadata to rendered 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 pytz
-import werkzeug.urls
-import werkzeug.utils
-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.web.http import request
-from openerp.addons.base.ir import ir_qweb
-import simplejson
+from openerp.osv import orm
-REMOTE_CONNECTION_TIMEOUT = 2.5
-
-logger = logging.getLogger(__name__)
class QWeb(orm.AbstractModel):
""" QWeb object for rendering stuff in the website context
"""
- _name = 'website.qweb'
_inherit = 'ir.qweb'
URL_ATTRS = {
@@ -46,7 +13,12 @@ class QWeb(orm.AbstractModel):
'a': 'href',
}
- re_remove_spaces = re.compile('\s+')
+ CDN_TRIGGERS = {
+ 'link': 'href',
+ 'script': 'src',
+ 'img': 'src',
+ }
+
PRESERVE_WHITESPACE = [
'pre',
'textarea',
@@ -54,48 +26,15 @@ class QWeb(orm.AbstractModel):
'style',
]
- CDN_TRIGGERS = {
- 'link': 'href',
- 'script': 'src',
- 'img': 'src',
- }
-
def render_attribute(self, element, name, value, qwebcontext):
context = qwebcontext.context or {}
if not context.get('rendering_bundle'):
- if name == self.URL_ATTRS.get(element.tag):
+ if name == self.URL_ATTRS.get(element.tag) and qwebcontext.get('url_for'):
value = qwebcontext.get('url_for')(value)
- if request and request.website and request.website.cdn_activated and name == self.CDN_TRIGGERS.get(element.tag):
+ elif request and request.website and request.website.cdn_activated and (name == self.URL_ATTRS.get(element.tag) or name == self.CDN_TRIGGERS.get(element.tag)):
value = request.website.get_cdn_url(value)
-
return super(QWeb, self).render_attribute(element, name, value, qwebcontext)
- 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 render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
- if request and request.website and request.website.cdn_activated:
- if qwebcontext.context is None:
- qwebcontext.context = {}
- qwebcontext.context['url_for'] = request.website.get_cdn_url
- return super(QWeb, self).render_tag_call_assets(element, template_attributes, generated_attributes, qwebcontext)
-
- def get_converter_for(self, field_type):
- return self.pool.get(
- 'website.qweb.field.' + field_type,
- self.pool['website.qweb.field'])
-
def render_text(self, text, element, qwebcontext):
compress = request and not request.debug and request.website and request.website.compress_html
if compress and element.tag not in self.PRESERVE_WHITESPACE:
@@ -108,482 +47,3 @@ class QWeb(orm.AbstractModel):
# No need to recurse because those tags children are not html5 parser friendly
tail = self.re_remove_spaces.sub(' ', tail.rstrip())
return super(QWeb, self).render_tail(tail, element, qwebcontext)
-
-class Field(orm.AbstractModel):
- _name = 'website.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 = [('data-oe-translate', 1 if getattr(field, 'translate', False) else 0)]
-
- 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['website.qweb']
-
-class Integer(orm.AbstractModel):
- _name = 'website.qweb.field.integer'
- _inherit = ['website.qweb.field']
-
- value_from_string = int
-
-class Float(orm.AbstractModel):
- _name = 'website.qweb.field.float'
- _inherit = ['website.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, '.'))
-
-
-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 = 'website.qweb.field.date'
- _inherit = ['website.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 = 'website.qweb.field.datetime'
- _inherit = ['website.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 = 'website.qweb.field.text'
- _inherit = ['website.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 = 'website.qweb.field.selection'
- _inherit = ['website.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 ManyToOne(orm.AbstractModel):
- _name = 'website.qweb.field.many2one'
- _inherit = ['website.qweb.field', '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:
- return itertools.chain(attrs, [
- ('data-oe-many2one-id', many2one.id),
- ('data-oe-many2one-model', many2one._name)])
- 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 HTML(orm.AbstractModel):
- _name = 'website.qweb.field.html'
- _inherit = ['website.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 <img> tag
- """
- _name = 'website.qweb.field.image'
- _inherit = ['website.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['website'].image_url(cr, uid, record, field_name, max_size)
- if options.get('resize'):
- src = "%s/%s" % (src, options.get('resize'))
-
- img = '<img class="%s" src="%s" style="%s"/>' % (classes, src, options.get('style', ''))
- return ir_qweb.HTMLSafe(img)
-
- local_url_re = re.compile(r'^/(?P<module>[^]]+)/static/(?P<rest>.+)$')
-
- 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('/website/image'):
- # url might be /website/image/<model>/<id>[_<checksum>]/<field>[/<width>x<height>]
- 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 = 'website.qweb.field.monetary'
- _inherit = ['website.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 = 'website.qweb.field.duration'
- _inherit = [
- 'ir.qweb.field.duration',
- 'website.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 = 'website.qweb.field.relative'
- _inherit = [
- 'ir.qweb.field.relative',
- 'website.qweb.field.datetime',
- ]
-
- # get formatting from ir.qweb.field.relative but edition/save from datetime
-
-
-class Contact(orm.AbstractModel):
- _name = 'website.qweb.field.contact'
- _inherit = ['ir.qweb.field.contact', 'website.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)
- many2one = getattr(record, field_name)
- if many2one:
- return itertools.chain(attrs, [('data-oe-contact-options', simplejson.dumps(options))])
- else:
- return attrs
-
- # helper to call the rendering of contact field in website
- 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__()
-
-class QwebView(orm.AbstractModel):
- _name = 'website.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 <br> 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/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index c63f67a191c..38c34c1bc08 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -104,55 +104,7 @@ class view(osv.osv):
result.append(r)
return result
- 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['website.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 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 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
-
- @tools.ormcache_context('uid', 'xml_id', keys=('website_id',))
+ @tools.ormcache_context('uid', 'xml_id', keys=('website_id', 'editable', 'translatable', 'edit_translations'))
def get_view_id(self, cr, uid, xml_id, context=None):
if context and 'website_id' in context and not isinstance(xml_id, (int, long)):
domain = [('key', '=', xml_id), '|', ('website_id', '=', context['website_id']), ('website_id', '=', False)]
@@ -163,29 +115,10 @@ class view(osv.osv):
view_id = super(view, self).get_view_id(cr, uid, xml_id, context=context)
return view_id
- def _prepare_qcontext(self, cr, uid, context=None):
- if not context:
- context = {}
-
- company = self.pool['res.company'].browse(cr, SUPERUSER_ID, request.website.company_id.id, context=context)
-
- qcontext = dict(
- context.copy(),
- website=request.website,
- url_for=website.url_for,
- slug=website.slug,
- res_company=company,
- user_id=self.pool.get("res.users").browse(cr, uid, uid),
- translatable=context.get('lang') != request.website.default_lang_code,
- editable=request.website.is_publisher(),
- menu_data=self.pool['ir.ui.menu'].load_menus_root(cr, uid, context=context) if request.website.is_user() else None,
- )
- return qcontext
-
@api.cr_uid_ids_context
def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
if request and getattr(request, 'website_enabled', False):
- engine = 'website.qweb'
+ engine = 'ir.qweb'
if isinstance(id_or_xml_id, list):
id_or_xml_id = id_or_xml_id[0]
@@ -197,10 +130,11 @@ class view(osv.osv):
qcontext.update(values)
# in edit mode ir.ui.view will tag nodes
- if qcontext.get('editable'):
- context = dict(context, inherit_branding=True)
- elif request.registry['res.users'].has_group(cr, uid, 'base.group_website_publisher'):
- context = dict(context, inherit_branding_auto=True)
+ if not qcontext.get('translatable'):
+ if qcontext.get('editable'):
+ context = dict(context, inherit_branding=True)
+ elif request.registry['res.users'].has_group(cr, uid, 'base.group_website_publisher'):
+ context = dict(context, inherit_branding_auto=True)
view_obj = request.website.get_template(id_or_xml_id)
if 'main_object' not in qcontext:
@@ -210,49 +144,30 @@ class view(osv.osv):
return super(view, self).render(cr, uid, id_or_xml_id, values=values, engine=engine, 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 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)
+ def _prepare_qcontext(self, cr, uid, context=None):
+ if not context:
+ context = {}
- # transform embedded field back to t-field
- el.getparent().replace(el, self.to_field_ref(cr, uid, el, context=context))
+ company = self.pool['res.company'].browse(cr, SUPERUSER_ID, request.website.company_id.id, 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)
+ editable = request.website.is_publisher()
+ translatable = editable and context.get('lang') != request.website.default_lang_code
+ editable = not translatable and editable
- view = self.browse(cr, SUPERUSER_ID, res_id, context=context)
- if view.model_data_id:
- view.model_data_id.write({'noupdate': True})
+ qcontext = dict(
+ context.copy(),
+ website=request.website,
+ url_for=website.url_for,
+ slug=website.slug,
+ res_company=company,
+ user_id=self.pool.get("res.users").browse(cr, uid, uid),
+ default_lang_code=request.website.default_lang_code,
+ languages=request.website.get_languages(),
+ translatable=translatable,
+ editable=editable,
+ menu_data=self.pool['ir.ui.menu'].load_menus_root(cr, uid, context=context) if request.website.is_user() else None,
+ )
+ return qcontext
def customize_template_get(self, cr, uid, key, full=False, bundles=False, context=None):
""" Get inherit view's informations of the template ``key``. By default, only
@@ -292,10 +207,3 @@ class view(osv.osv):
'active': v.active,
})
return result
-
- def get_view_translations(self, cr, uid, xml_id, lang, field=['id', 'res_id', 'value', 'state', 'gengo_translation'], context=None):
- views = self.customize_template_get(cr, uid, xml_id, full=True, context=context)
- views_ids = [view.get('id') for view in views if view.get('active')]
- domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
- return self.pool['ir.translation'].search_read(cr, uid, domain, field, context=context)
-
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 6fbb17f9d47..09c75336ed6 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -1,18 +1,11 @@
# -*- coding: utf-8 -*-
-import cStringIO
-import contextlib
-import datetime
-import hashlib
import inspect
import logging
import math
import unicodedata
-import os
import re
-import time
import urlparse
-from PIL import Image
from sys import maxint
import werkzeug
@@ -130,6 +123,7 @@ DEFAULT_CDN_FILTERS = [
"^/[^/]+/static/",
"^/web/(css|js)/",
"^/website/image/",
+ "^/web_editor/image/",
]
def unslug(s):
@@ -630,123 +624,15 @@ class website(osv.osv):
return html
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)
+ logger.warning("Deprecated _image_placeholder method, please use this method on ir.attachment")
+ return self.pool['ir.attachment']._image_placeholder(response)
def _image(self, cr, uid, model, id, 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]
- id = int(id)
-
- ids = Model.search(cr, uid,
- [('id', '=', id)], context=context)
- if not ids and 'website_published' in Model._fields:
- ids = Model.search(cr, openerp.SUPERUSER_ID,
- [('id', '=', id), ('website_published', '=', True)], context=context)
- if not ids:
- return self._image_placeholder(response)
-
- concurrency = '__last_update'
- [record] = Model.read(cr, openerp.SUPERUSER_ID, [id],
- [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('.', '_'), id, 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
+ logger.warning("Deprecated _image method, please use this method on ir.attachment")
+ return self.pool['ir.attachment']._image(cr, uid, model, id, field, response, max_width=max_width, max_height=max_height, cache=cache, context=context)
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 '/website/image/%s/%s/%s%s' % (model, id, field, size)
+ return self.pool['ir.attachment'].image_url(cr, uid, record, field, size=size, context=context)
class website_menu(osv.osv):
_name = "website.menu"
@@ -819,64 +705,22 @@ class website_menu(osv.osv):
class ir_attachment(osv.osv):
-
_inherit = "ir.attachment"
- def _website_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.pool['website'].image_url(cr, uid, attach, 'datas')
- return result
-
- def _datas_big(self, cr, uid, ids, name, arg, context=None):
- result = dict.fromkeys(ids, False)
- if context and context.get('bin_size'):
- return result
-
- for record in self.browse(cr, uid, ids, context=context):
- if record.res_model != 'ir.ui.view' or not record.datas: continue
- try:
- result[record.id] = openerp.tools.image_resize_image_big(record.datas)
- except IOError: # apparently the error PIL.Image.open raises
- pass
-
- return result
-
_columns = {
- 'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'),
- 'datas_big': fields.function (_datas_big, type='binary', store=True,
- string="Resized file content"),
+ 'website_url': fields.related("local_url", string="Attachment URL", type='char', deprecated=True), # related for backward compatibility with saas-6
}
- def try_remove(self, cr, uid, ids, context=None):
- """ Removes a web-based image attachment if it is used by no view
- (template)
+ def _image(self, cr, uid, model, id_or_ids, field, response, max_width=maxint, max_height=maxint, cache=None, context=None):
+ 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)
- Returns a dict mapping attachments which would not be removed (if any)
- mapped to the views preventing their removal
- """
- Views = self.pool['ir.ui.view']
- attachments_to_remove = []
- # views blocking removal of the attachment
- removal_blocked_by = {}
-
- for attachment in self.browse(cr, uid, ids, context=context):
- # in-document URLs are html-escaped, a straight search will not
- # find them
- url = escape(attachment.website_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:
- self.unlink(cr, uid, attachments_to_remove, context=context)
- return removal_blocked_by
+ if not ids and 'website_published' in Model._fields:
+ ids = Model.search(cr, openerp.SUPERUSER_ID, [('id', 'in', ids), ('website_published', '=', True)], context=context)
+
+ return super(ir_attachment, self)._image(cr, openerp.SUPERUSER_ID, model, id_or_ids, field, response,
+ max_width=max_width, max_height=max_height, cache=cache, context=context)
class res_partner(osv.osv):
_inherit = "res.partner"
diff --git a/addons/website/security/ir.model.access.csv b/addons/website/security/ir.model.access.csv
index 8682a349f70..4c91ee15c85 100644
--- a/addons/website/security/ir.model.access.csv
+++ b/addons/website/security/ir.model.access.csv
@@ -4,8 +4,6 @@ access_website,website,website.model_website,base.group_website_designer,1,1,1,1
access_website_menu,access_website_menu,model_website_menu,,1,0,0,0
access_website_menu_designer,Web Menu Manager,model_website_menu,base.group_website_designer,1,1,1,1
access_website,web menu manager,website.model_website,base.group_website_designer,1,1,1,1
-access_website_converter_test,access_website_converter_test,model_website_converter_test,,1,1,1,1
-access_website_converter_test_sub,access_website_converter_test_sub,model_website_converter_test_sub,,1,1,1,1
access_website_ir_ui_view,access_website_ir_ui_view,model_ir_ui_view,base.group_website_designer,1,1,1,1
access_seo_public,access_seo_public,model_website_seo_metadata,,1,0,0,0
access_seo_public,access_seo_manager,model_website_seo_metadata,base.group_website_designer,1,1,1,1
diff --git a/addons/website/static/lib/summernote/src/js/Renderer.js b/addons/website/static/lib/summernote/src/js/Renderer.js
index 9d0f3c7c689..07584edc453 100644
--- a/addons/website/static/lib/summernote/src/js/Renderer.js
+++ b/addons/website/static/lib/summernote/src/js/Renderer.js
@@ -884,7 +884,7 @@ define([
this.removeLayout = function ($holder, layoutInfo, options) {
if (options.airMode) {
$holder.removeClass('note-air-editor note-editable')
- .removeAttr('id contentEditable');
+ .removeAttr('contentEditable');
layoutInfo.popover.remove();
layoutInfo.handle.remove();
diff --git a/addons/website/static/src/css/snippets.css b/addons/website/static/src/css/snippets.css
index 4d3f2c6c5ff..eb0b4d3e69b 100644
--- a/addons/website/static/src/css/snippets.css
+++ b/addons/website/static/src/css/snippets.css
@@ -1,725 +1,3 @@
-/* ---- 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: none;
-}
-.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;
-}
-
-/* add CSS for bootstrap dropdown multi level */
-.dropdown-submenu {
- position: relative;
- z-index: 1000;
-}
-
-.dropdown-submenu > .dropdown-menu {
- top: 0;
- left: 100%;
- margin-top: -6px;
- margin-left: -1px;
- -webkit-border-radius: 0 6px 6px 6px;
- -moz-border-radius: 0 6px 6px 6px;
- border-radius: 0 6px 6px 6px;
-}
-.dropdown-submenu:hover > .dropdown-menu {
- display: block;
-}
-.dropdown-submenu:hover > a:after {
- border-left-color: #ffffff;
-}
-.dropdown-submenu > a:after {
- display: block;
- content: " ";
- float: right;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- border-width: 5px 0 5px 5px;
- border-left-color: #cccccc;
- margin-top: 5px;
- margin-right: -10px;
-}
-.dropdown-submenu.pull-left {
- float: none;
-}
-.dropdown-submenu.pull-left > .dropdown-menu {
- left: -100%;
- margin-left: 10px;
- -webkit-border-radius: 6px 0 6px 6px;
- -moz-border-radius: 6px 0 6px 6px;
- border-radius: 6px 0 6px 6px;
-}
-
-.oe_snippet_list {
- width: auto;
- white-space: nowrap;
- margin-left: 20px;
-}
-
-.oe_snippet_editor {
- position: fixed;
- z-index: 1000;
- bottom: 0;
- left: 0;
- right: 0;
- height: 214px;
- background: #a0a0a0;
- box-shadow: 0 1px 3px #646464 inset;
-}
-.oe_snippet_editor .oe_snippet {
- box-sizing: border-box;
- display: inline-block;
- width: 220px;
- height: 160px;
- border-radius: 3px;
- background: white;
- margin: 20px 20px 20px 0;
- cursor: pointer;
- border: 2px solid transparent;
- box-shadow: 0 1px 3px #646464;
- position: relative;
- top: 0;
- overflow: hidden;
- vertical-align: top;
- user-select: none;
- white-space: normal;
-}
-.oe_snippet_editor .oe_snippet:after {
- content: attr(name);
- position: absolute;
- bottom: 0px;
- left: 0px;
- right: 0px;
- background: rgba(255, 255, 255, 0.8);
- text-align: center;
- color: black;
- padding: 8px;
-}
-.oe_snippet_editor .oe_snippet.oe_selected, .oe_snippet_editor .oe_snippet.oe_active {
- border: 2px solid #9789ff;
- box-shadow: 0px 3px 17px rgba(99, 53, 150, 0.59);
-}
-.oe_snippet_editor .oe_snippet > * {
- margin-top: 16px;
- line-height: 1em;
- zoom: 0.6;
-}
-.oe_snippet_editor .oe_snippet > .container {
- margin-top: 15px;
- zoom: 0.17;
- line-height: 0.999em;
- line-height: 1em;
-}
-.oe_snippet_editor .oe_snippet > .row {
- margin-top: 0px;
- zoom: 0.23;
- line-height: 0.999em;
-}
-.oe_snippet_editor .oe_snippet > .span6 {
- margin-top: 18px;
- zoom: 0.34;
-}
-.oe_snippet_editor .oe_snippet > .span12 {
- margin-top: 16px;
- zoom: 0.23;
-}
-.oe_snippet_editor .oe_snippet > p {
- position: absolute;
- top: 0;
- right: 0;
- left: 0;
- bottom: 0;
- font-size: 20px;
- padding: 16px;
- zoom: 1;
- margin: 0px;
-}
-.oe_snippet_editor .oe_snippet.oe_new {
- background: gray;
- box-shadow: 0px 1px 3px #5a5a5a inset;
- border: 0px;
-}
-.oe_snippet_editor .oe_snippet.oe_new.oe_selected, .oe_snippet_editor .oe_snippet.oe_new.oe_active {
- box-shadow: 0px 2px 0px 0px #9789ff inset, 0px 3px 17px rgba(99, 53, 150, 0.59) inset;
-}
-.oe_snippet_editor .oe_snippet.oe_new > * {
- zoom: 1;
- margin: 0;
- line-height: 160px;
- text-align: center;
- color: white;
- font-size: 48px;
-}
-.oe_snippet_editor .oe_snippet.oe_new:after {
- position: absolute;
- left: 0px;
- right: 0px;
- top: 0px;
- bottom: 0px;
- background: transparent;
- color: white;
- text-shadow: 0px 1px 3px black;
- line-height: 160px;
- padding: 0px;
-}
-
-.oe_snippet_drop {
- position: relative;
- border: 2px dashed #ae34ff;
- background: rgba(147, 52, 255, 0.1);
- min-height: 28px;
- margin: -16px auto;
- border-radius: 5px;
- max-width: 50%;
-}
-.oe_snippet_drop.oe_accepting {
- border: 2px dashed #34ffa6;
- background: rgba(52, 255, 190, 0.1);
-}
-
-#website-top-edit li.oe_snippet_editorbar {
- padding: 1px 8px 2px;
- font: normal normal normal 12px Arial, Helvetica, Tahoma, Verdana, Sans-Serif;
- float: left;
- margin-top: 6px;
- border: 1px solid #a6a6a6;
- border-bottom-color: #979797;
- background: #eeeeee;
- border-radius: 3px;
-}
-#website-top-edit li.oe_snippet_editorbar > * {
- display: inline-block;
- height: 22px;
- padding: 4px 6px;
- outline: 0;
- border: 0;
-}
-#website-top-edit li.oe_snippet_editorbar a.button .icon {
- cursor: inherit;
- background-repeat: no-repeat;
- margin-top: 1px;
- width: 16px;
- height: 16px;
- display: inline-block;
-}
-
-/* ---- COLOR-PICKER ---- {{{ */
-.colorpicker {
- margin: 0 auto;
- background: transparent;
- border: 0;
-}
-.colorpicker td {
- padding: 0;
- background: transparent;
-}
-.colorpicker td > * {
- width: 16px;
- height: 16px;
- border-radius: 2px;
- margin: 3px;
- padding: 0;
- border-width: 1px;
- display: block;
-}
-.colorpicker .only-text {
- display: none;
-}
-.colorpicker .automatic-color {
- background: #fff;
- border-left: 7px solid #f33;
- border-top: 7px solid #0e0;
- border-right: 8px solid #33f;
- border-bottom: 8px solid #fe0;
- margin: 4px 3px 3px 3px;
- width: 0;
- height: 0;
-}
-
/* ---- Table Snippet ---- {{{ */
.table_desc {
margin: 0 0 20px 0;
diff --git a/addons/website/static/src/css/snippets.sass b/addons/website/static/src/css/snippets.sass
index 62a3040c19c..cb18e21db53 100644
--- a/addons/website/static/src/css/snippets.sass
+++ b/addons/website/static/src/css/snippets.sass
@@ -2,585 +2,6 @@
@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: none
- .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
-
-// }}}
-
-
-/* add CSS for bootstrap dropdown multi level */
-.dropdown-submenu
- position: relative
- z-index: 1000
-.dropdown-submenu
- &>.dropdown-menu
- top: 0
- left: 100%
- margin-top: -6px
- margin-left: -1px
- -webkit-border-radius: 0 6px 6px 6px
- -moz-border-radius: 0 6px 6px 6px
- border-radius: 0 6px 6px 6px
- &:hover
- &>.dropdown-menu
- display: block
- &>a:after
- border-left-color: #ffffff
- &>a:after
- display: block
- content: " "
- float: right
- width: 0
- height: 0
- border-color: transparent
- border-style: solid
- border-width: 5px 0 5px 5px
- border-left-color: #cccccc
- margin-top: 5px
- margin-right: -10px
- &.pull-left
- float: none
- &>.dropdown-menu
- left: -100%
- margin-left: 10px
- -webkit-border-radius: 6px 0 6px 6px
- -moz-border-radius: 6px 0 6px 6px
- border-radius: 6px 0 6px 6px
-
-.oe_snippet_list
- width: auto
- white-space: nowrap
- margin-left: 20px
-
-.oe_snippet_editor
- position: fixed
- z-index: 1000
- bottom: 0
- left: 0
- right: 0
- height: 214px
- background: rgb(160,160,160)
- box-shadow: 0 1px 3px rgb(100,100,100) inset
- .oe_snippet
- box-sizing: border-box
- display: inline-block
- width: 220px
- height: 160px
- border-radius: 3px
- background: white
- margin: 20px 20px 20px 0
- cursor: pointer
- border: 2px solid transparent
- box-shadow: 0 1px 3px rgb(100,100,100)
- position: relative
- top: 0
- overflow: hidden
- vertical-align: top
- user-select: none
- white-space: normal
- &:after
- content: attr(name)
- position: absolute
- bottom: 0px
- left: 0px
- right: 0px
- background: rgba(255,255,255,0.8)
- text-align: center
- color: black
- padding: 8px
- &.oe_selected, &.oe_active
- border: 2px solid rgb(151, 137, 255)
- box-shadow: 0px 3px 17px rgba(99, 53, 150, 0.59)
- & > *
- margin-top: 16px
- line-height: 1em
- zoom: 0.6
- & > .container
- margin-top: 15px
- zoom: 0.17
- line-height: 0.999em
- line-height: 1em
- & > .row
- margin-top: 0px
- zoom: 0.23
- line-height: 0.999em
- & > .span6
- margin-top: 18px
- zoom: 0.34
- & > .span12
- margin-top: 16px
- zoom: 0.23
- & > p
- position: absolute
- top: 0
- right: 0
- left: 0
- bottom: 0
- font-size: 20px
- padding: 16px
- zoom: 1
- margin: 0px
- &.oe_new
- background: gray
- box-shadow: 0px 1px 3px rgb(90,90,90) inset
- border: 0px
- &.oe_selected, &.oe_active
- box-shadow: 0px 2px 0px 0px rgb(151,137,255) inset, 0px 3px 17px rgba(99, 53, 150, 0.59) inset
- & > *
- zoom: 1
- margin: 0
- line-height: 160px
- text-align: center
- color: white
- font-size: 48px
- &:after
- position: absolute
- left: 0px
- right: 0px
- top: 0px
- bottom: 0px
- background: transparent
- color: white
- text-shadow: 0px 1px 3px black
- line-height: 160px
- padding: 0px
-
-.oe_snippet_drop
- position: relative
- border: 2px dashed rgb(174, 52, 255)
- background: rgba(147, 52, 255, 0.1)
- min-height: 28px
- margin: -16px auto
- border-radius: 5px
- max-width: 50%
- &.oe_accepting
- border: 2px dashed rgb(52, 255, 166)
- background: rgba(52, 255, 190, 0.1)
-
-#website-top-edit
- li.oe_snippet_editorbar
- padding: 1px 8px 2px
- font: normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif
- float: left
- margin-top: 6px
- border: 1px solid #a6a6a6
- border-bottom-color: #979797
- background: #eeeeee
- border-radius: 3px
- & > *
- display: inline-block
- height: 22px
- padding: 4px 6px
- outline: 0
- border: 0
-
- a.button .icon
- cursor: inherit
- background-repeat: no-repeat
- margin-top: 1px
- width: 16px
- height: 16px
- display: inline-block
-
-/* ---- COLOR-PICKER ---- {{{ */
-
-.colorpicker
- margin: 0 auto
- background: transparent
- border: 0
- td
- padding: 0
- background: transparent
- > *
- width: 16px
- height: 16px
- border-radius: 2px
- margin: 3px
- padding: 0
- border-width: 1px
- display: block
- .only-text
- display: none
- .automatic-color
- background: #fff
- border-left: 7px solid #f33
- border-top: 7px solid #0e0
- border-right: 8px solid #33f
- border-bottom: 8px solid #fe0
- margin: 4px 3px 3px 3px
- width: 0
- height: 0
-
-// }}}
-
/* ---- Table Snippet ---- {{{ */
.table_desc
diff --git a/addons/website/static/src/css/website.backend.css b/addons/website/static/src/css/website.backend.css
index 333118ce1b3..927b3907d6f 100644
--- a/addons/website/static/src/css/website.backend.css
+++ b/addons/website/static/src/css/website.backend.css
@@ -3,35 +3,4 @@
}
.oe_button_box .oe_stat_button .o_stat_info.published > .o_stat_text.oe_unpublish {
display: none;
-}
-.oe_form_field.oe_form_field_html_frame {
- display: block;
-}
-.oe_form_field_html_frame iframe {
- display: block;
- width: 100%;
- border: 0;
- padding: 0;
- margin: 0;
- min-height: 300px;
-}
-.o_form_FieldTextHtml_fullscreen .oe_form_field_html_frame iframe {
- position: absolute !important;
- left: 0 !important;
- right: 0 !important;
- top: 0px !important;
- bottom: 0 !important;
- width: 100% !important;
- z-index: 1001 !important;
- border: 0;
-}
-.o_form_FieldTextHtml_fullscreen .modal .oe_form_field_html_frame iframe {
- top: 0 !important;
-}
-.o_form_FieldTextHtml_fullscreen .oe_webclient *, .o_form_FieldTextHtml_fullscreen .modal * {
- position: static !important;
- top: 0 !important;
- left: 0 !important;
- overflow: visible !important;
- width: auto !important;
} \ No newline at end of file
diff --git a/addons/website/static/src/css/website.css b/addons/website/static/src/css/website.css
index 40febaf9b1b..642113e179b 100644
--- a/addons/website/static/src/css/website.css
+++ b/addons/website/static/src/css/website.css
@@ -1,158 +1,10 @@
-@charset "UTF-8";
+@charset "utf-8";
/* THIS CSS FILE IS FOR WEBSITE THEMING CUSTOMIZATION ONLY
*
* css for editor buttons, openerp widget included in the website and other
* stuff must go to the editor.css
*
*/
-/* ----- GENERIC LAYOUTING HELPERS ---- */
-/* 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;
-}
-
-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;
-}
-
-.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;
-}
-
ul {
list-style-type: disc;
}
@@ -247,12 +99,6 @@ font[style*='background'], font[class*='bg-'] {
}
/* Extra Styles */
-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);
-}
-
h1.text-muted, h2.text-muted, h3.text-muted {
margin-top: 10px;
}
@@ -269,19 +115,6 @@ hr {
padding-bottom: 10px;
}
-td {
- min-width: 20px;
-}
-
-/* ----- Editor (for public user, like forum) ----- */
-.note-toolbar {
- margin-left: 0 !important;
-}
-
-.note-popover .popover > .arrow {
- display: none;
-}
-
/* ----- Snippets Styles ----- */
.para_large {
font-size: 120%;
@@ -294,11 +127,6 @@ td {
margin-right: auto;
}
-/* ----- EDITOR ----- */
-.css_non_editable_mode_hidden {
- display: none !important;
-}
-
/* ----- BOOTSTRAP FIX ----- */
.container .container, .readable .container {
padding-left: 0;
@@ -363,26 +191,6 @@ body > nav.navbar {
width: 100%;
}
}
-/* ---- HACK FOR COVERING UP EDITOR BOGUS P INSERTION --- */
-.oe_structure.oe_empty > .oe_drop_zone.oe_insert:only-child, [data-oe-type=html] > .oe_drop_zone.oe_insert:only-child {
- position: static;
-}
-
-.oe_structure.oe_empty:empty:before, [data-oe-type=html]:empty:before, .oe_structure.oe_empty > .oe_drop_zone.oe_insert:only-child:before, [data-oe-type=html] > .oe_drop_zone.oe_insert:only-child:before {
- content: " ";
- text-align: center;
- display: block;
- min-height: 1em;
- color: grey;
- font-size: 24px;
- padding-top: 160px;
-}
-
-.css_editable_display {
- display: none;
-}
-
-/* ---- HACK FOR COVERING UP EDITOR BOGUS P INSERTION --- */
.navbar .nav > li > p {
margin-bottom: 0px;
}
@@ -844,17 +652,6 @@ table.well tr td {
color: white;
}
-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;
-}
-
.oe_template_fallback {
-moz-column-count: 3;
-webkit-column-count: 3;
@@ -879,49 +676,6 @@ span[data-oe-type="monetary"] {
text-align: center;
}
-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;
- }
-}
-/* Fix: backward compatibility saas-3 */
div.carousel .container > .carousel-caption {
position: absolute;
right: 50%;
@@ -1003,7 +757,6 @@ div.carousel .item.text_only .container > .carousel-image {
.container nav#oe_main_menu_navbar {
position: inherit;
}
-
.oe_search_clear {
position: absolute;
display:none;
diff --git a/addons/website/static/src/css/website.editor.css b/addons/website/static/src/css/website.editor.css
new file mode 100644
index 00000000000..69e6e451d6b
--- /dev/null
+++ b/addons/website/static/src/css/website.editor.css
@@ -0,0 +1,263 @@
+@charset "utf-8";
+/* ---- Common---- {{{ */
+.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
+ min-height: 24px;
+}
+
+/* ---- MOBILE PREVIEW ---- {{{ */
+.oe_mobile_preview.modal .modal-content {
+ height: 660px;
+ background-color: #000000;
+ border: 2px solid #1C1F1F;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+ border-radius: 10px;
+ margin: auto;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ max-width: 330px;
+}
+.oe_mobile_preview.modal .modal-content .modal-header {
+ background-color: #000000;
+ border-bottom: 0;
+ -moz-border-radius-topleft: 10px;
+ -webkit-border-top-left-radius: 10px;
+ border-top-left-radius: 10px;
+ -moz-border-radius-topright: 10px;
+ -webkit-border-top-right-radius: 10px;
+ border-top-right-radius: 10px;
+}
+.oe_mobile_preview.modal .modal-content .modal-header .modal-title {
+ color: #1C1F1F;
+}
+.oe_mobile_preview.modal .modal-content .modal-header .close {
+ color: lightgrey;
+ filter: progid:DXImageTransform.Microsoft.Alpha(enabled=false);
+ opacity: 1;
+}
+.oe_mobile_preview.modal .modal-content .modal-header .close:hover {
+ color: #E00101;
+ filter: progid:DXImageTransform.Microsoft.Alpha(enabled=false);
+ opacity: 1;
+}
+.oe_mobile_preview.modal .modal-content .modal-body {
+ background-color: #000000;
+ max-height: 600px;
+ padding: 0;
+ margin: 0;
+}
+.oe_mobile_preview.modal .modal-content .modal-body .oe_mobile_viewport {
+ width: 320px;
+ height: 568px;
+ padding: 5px;
+ border: none;
+}
+.oe_mobile_preview.modal .modal-content .modal-footer {
+ background-color: #000000;
+}
+
+/* ---- SEO TOOLS ---- {{{ */
+.oe_seo_configuration .modal-dialog {
+ width: 80%;
+}
+.oe_seo_configuration .oe_remove {
+ color: #E00101;
+}
+.oe_seo_configuration .oe_seo_suggestion {
+ cursor: pointer;
+}
+.oe_seo_configuration .oe_seo_keyword {
+ padding: 0.2em 0.4em 0.2em 0.5em;
+ -moz-border-radius: 0.4em;
+ -webkit-border-radius: 0.4em;
+ border-radius: 0.4em;
+}
+.oe_seo_configuration li.oe_seo_preview_g {
+ line-height: 1.2;
+ list-style: none;
+ list-style-image: none;
+ list-style-position: outside;
+ list-style-type: none;
+ font-size: small;
+ font-family: arial, sans-serif;
+}
+.oe_seo_configuration li.oe_seo_preview_g h3 {
+ font-size: medium;
+}
+.oe_seo_configuration li.oe_seo_preview_g .r {
+ margin: 0;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: normal;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ -webkit-text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.oe_seo_configuration li.oe_seo_preview_g .r a {
+ color: #1e0fbe;
+ text-decoration: underline;
+ text-transform: none;
+}
+.oe_seo_configuration li.oe_seo_preview_g .r a em {
+ font-style: normal !important;
+}
+.oe_seo_configuration li.oe_seo_preview_g .s {
+ color: #444;
+ max-width: 42em;
+}
+.oe_seo_configuration li.oe_seo_preview_g .kv, .oe_seo_configuration li.oe_seo_preview_g .slp {
+ display: block;
+ margin-bottom: 1px;
+}
+.oe_seo_configuration li.oe_seo_preview_g .f {
+ color: #666;
+ margin-bottom: 1px;
+}
+.oe_seo_configuration li.oe_seo_preview_g .f cite {
+ color: #006621;
+ font-style: normal;
+ font-size: 14px;
+}
+.oe_seo_configuration li.oe_seo_preview_g .st {
+ line-height: 1.24;
+}
+
+/* ---- ACE EDITOR ---- {{{ */
+.oe_ace_view_editor {
+ position: fixed;
+ right: 0;
+ z-index: 1001;
+ height: 100%;
+ background: #2F3129;
+ color: white;
+}
+.oe_ace_view_editor .oe_ace_view_editor_title {
+ width: 100%;
+ padding-top: 0;
+ padding-left: 0;
+ height: 30px;
+}
+.oe_ace_view_editor .oe_ace_view_editor_title .oe_view_list {
+ width: 50%;
+ height: 30px;
+ font-size: 14px;
+ font-family: "Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace;
+ line-height: normal;
+}
+.oe_ace_view_editor .oe_ace_view_editor_title .btn {
+ height: 30px;
+ padding: 0 4px 0 4px;
+ font-size: 14px;
+ font-family: "Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace;
+ line-height: normal;
+}
+.oe_ace_view_editor .ace_editor {
+ position: absolute;
+ top: 50px;
+ right: 0;
+ left: 0;
+}
+.oe_ace_view_editor .ace_editor .ace_gutter {
+ cursor: ew-resize;
+}
+.oe_ace_view_editor #ace-view-id {
+ padding: 0 1em;
+}
+.oe_ace_view_editor.oe_ace_open {
+ filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=97);
+ opacity: 0.97;
+}
+.oe_ace_view_editor.oe_ace_closed {
+ z-index: -1000;
+ filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0);
+ opacity: 0;
+}
+
+.oe_include_bundles {
+ font-weight: normal;
+ padding: 0 8px;
+}
+
+/* ---- CUSTOMIZE THEME ---- {{{ */
+#theme_error {
+ background: #ffc;
+}
+
+#theme_customize_modal {
+ overflow: visible;
+ z-index: 1020;
+ background: transparent;
+ display: block;
+}
+#theme_customize_modal .modal-dialog {
+ top: 75px;
+ width: auto;
+ margin: 0;
+ position: absolute;
+ right: 10px;
+ font-family: "Lato-Regular";
+ font-weight: normal;
+ text-transform: capitalize;
+ letter-spacing: normal;
+}
+#theme_customize_modal .modal-h5 {
+ color: #ffffff;
+ font-family: "Lato-Regular";
+ font-weight: normal;
+ text-transform: uppercase;
+ letter-spacing: normal;
+ font-size: 14px;
+ color: white;
+ padding: 4px 0 4px 4px;
+ background-color: #bdc3c7;
+}
+#theme_customize_modal table {
+ width: 100%;
+ margin-bottom: 8px;
+}
+#theme_customize_modal label {
+ display: block;
+ text-align: center;
+}
+#theme_customize_modal label > div, #theme_customize_modal label > img {
+ border: 1px solid #fff;
+ line-height: 30px;
+ font-size: 0.9em;
+ margin: 2px 4px;
+}
+#theme_customize_modal label.checked > div, #theme_customize_modal label.checked > img {
+ box-shadow: 2px 2px 3px #888;
+ border: 1px solid #666;
+}
+#theme_customize_modal label img {
+ width: 60px;
+ height: 35px;
+ margin: 2px;
+ border: 1px solid rgba(136, 136, 136, 0.5);
+}
+#theme_customize_modal label input {
+ display: none;
+}
+#theme_customize_modal .loading_backdrop {
+ display: none;
+}
+#theme_customize_modal.loading .loading_backdrop {
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: #000;
+ opacity: 0.3;
+ position: absolute;
+ z-index: 1;
+}
+
+/* ---- force the browse to re-compute the stylesheets ---- */
+body.theme_customize_css_loading {
+ magin-top: 1px;
+}
+body.theme_customize_css_loading #wrapwrap {
+ magin-top: -1px;
+}
diff --git a/addons/website/static/src/css/website.editor.sass b/addons/website/static/src/css/website.editor.sass
new file mode 100644
index 00000000000..911631bb8ee
--- /dev/null
+++ b/addons/website/static/src/css/website.editor.sass
@@ -0,0 +1,234 @@
+@charset "utf-8"
+@import "common"
+
+/* ---- Common---- {{{ */
+
+.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12
+ min-height: 24px
+
+// }}}
+
+/* ---- MOBILE PREVIEW ---- {{{ */
+
+$mobile_preview_background: #000000
+$mobile_preview_border: #1C1F1F
+$icon_close: #E00101
+.oe_mobile_preview
+ &.modal .modal-content
+ height: 660px
+ background-color: $mobile_preview_background
+ border: 2px solid $mobile_preview_border
+ +border-radius(10px)
+ margin: auto
+ top: 0
+ left: 0
+ bottom: 0
+ right: 0
+ max-width: 330px
+ .modal-header
+ background-color: $mobile_preview_background
+ border-bottom: 0
+ +border-top-left-radius(10px)
+ +border-top-right-radius(10px)
+ .modal-title
+ color: $mobile_preview_border
+ .close
+ color: lightgrey
+ +opacity(1)
+ .close:hover
+ color: $icon_close
+ +opacity(1)
+ .modal-body
+ background-color: $mobile_preview_background
+ max-height: 600px
+ padding: 0
+ margin: 0
+ .oe_mobile_viewport
+ width: 320px
+ height: 568px
+ padding: 5px
+ border: none
+ .modal-footer
+ background-color: $mobile_preview_background
+
+// }}}
+
+/* ---- SEO TOOLS ---- {{{ */
+
+$remove_color: $icon_close
+$in_title_color: #5cb85c
+$in_description_color: #428bca
+$in_body_color: #5bc0de
+$highlighted_text_color: #ffffff
+.oe_seo_configuration
+ .modal-dialog
+ width: 80%
+ .oe_remove
+ color: $remove_color
+ .oe_seo_suggestion
+ cursor: pointer
+ .oe_seo_keyword
+ padding: .2em .4em .2em .5em
+ +border-radius(.4em)
+ li.oe_seo_preview_g
+ line-height: 1.2
+ list-style: none
+ list-style-image: none
+ list-style-position: outside
+ list-style-type: none
+ font-size: small
+ font-family: arial,sans-serif
+ h3
+ font-size: medium
+ .r
+ margin: 0
+ font-size: 16px
+ font-style: normal
+ font-weight: normal
+ overflow: hidden
+ text-overflow: ellipsis
+ -webkit-text-overflow: ellipsis
+ white-space: nowrap
+ a
+ color: rgb(30, 15, 190)
+ text-decoration: underline
+ text-transform: none
+ em
+ font-style: normal !important
+ .s
+ color: #444
+ max-width: 42em
+ .kv,.slp
+ display: block
+ margin-bottom: 1px
+ .f
+ color: #666
+ margin-bottom: 1px
+ cite
+ color: #006621
+ font-style: normal
+ font-size: 14px
+ .st
+ line-height: 1.24
+
+
+// }}}
+
+/* ---- ACE EDITOR ---- {{{ */
+
+$editorbar_height: 30px
+$infobar_height: 20px
+// TODO Fix => might break with themes
+
+.oe_ace_view_editor
+ position: fixed
+ // top property is set programmatically
+ right: 0
+ z-index: 1001
+ height: 100%
+ background: #2F3129
+ color: white
+ .oe_ace_view_editor_title
+ width: 100%
+ padding-top: 0
+ padding-left: 0
+ height: $editorbar_height
+ .oe_view_list
+ width: 50%
+ height: $editorbar_height
+ @include editor-font
+ .btn
+ height: $editorbar_height
+ padding: 0 4px 0 4px
+ @include editor-font
+ .ace_editor
+ position: absolute
+ top: $editorbar_height + $infobar_height
+ right: 0
+ // bottom property is set programmatically
+ left: 0
+ .ace_gutter
+ cursor: ew-resize
+ #ace-view-id
+ padding: 0 1em
+ &.oe_ace_open
+ +opacity(0.97)
+ &.oe_ace_closed
+ z-index: -1000
+ +opacity(0)
+
+.oe_include_bundles
+ font-weight: normal
+ padding: 0 8px
+
+// }}}
+
+/* ---- CUSTOMIZE THEME ---- {{{ */
+#theme_error
+ background: #ffc
+#theme_customize_modal
+ overflow: visible
+ z-index: 1020
+ background: transparent
+ display: block
+ .modal-dialog
+ top: 75px
+ width: auto
+ margin: 0
+ position: absolute
+ right: 10px
+ font-family: 'Lato-Regular'
+ font-weight: normal
+ text-transform: capitalize
+ letter-spacing: normal
+ .modal-h5
+ color: #ffffff
+ font-family: 'Lato-Regular'
+ font-weight: normal
+ text-transform: uppercase
+ letter-spacing: normal
+ font-size: 14px
+ color: white
+ padding: 4px 0 4px 4px
+ background-color: #bdc3c7
+ table
+ width: 100%
+ margin-bottom: 8px
+ label
+ display: block
+ text-align: center
+ > div, > img
+ border: 1px solid #fff
+ line-height: 30px
+ font-size: 0.9em
+ margin: 2px 4px
+ &.checked
+ > div, > img
+ box-shadow: 2px 2px 3px #888
+ border: 1px solid #666
+ img
+ width: 60px
+ height: 35px
+ margin: 2px
+ border: 1px solid rgba(136, 136, 136, 0.5)
+ input
+ display: none
+ .loading_backdrop
+ display: none
+ &.loading
+ .loading_backdrop
+ display: block
+ width: 100%
+ height: 100%
+ background: #000
+ opacity: 0.3
+ position: absolute
+ z-index: 1
+
+/* ---- force the browse to re-compute the stylesheets ---- */
+body.theme_customize_css_loading
+ magin-top: 1px
+ #wrapwrap
+ magin-top: -1px
+
+// }}}
diff --git a/addons/website/static/src/css/website.sass b/addons/website/static/src/css/website.sass
index cb526b29b01..f8f8c4fe05f 100644
--- a/addons/website/static/src/css/website.sass
+++ b/addons/website/static/src/css/website.sass
@@ -10,93 +10,6 @@
*
*/
-/* ----- GENERIC LAYOUTING HELPERS ---- */
-
-/* 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
-
-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
-
-.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
-
ul
list-style-type: disc
li > ul
@@ -158,9 +71,6 @@ font[style*='background'], font[class*='bg-']
/* Extra Styles */
-img.shadow
- +box-shadow(0px 3px 8px rgba(0, 0, 0, 0.2))
-
h1.text-muted, h2.text-muted, h3.text-muted
margin-top: 10px
@@ -175,17 +85,6 @@ hr
margin-bottom: 10px
padding-bottom: 10px
-td
- min-width: 20px
-
-/* ----- Editor (for public user, like forum) ----- */
-
-.note-toolbar
- margin-left: 0 !important
-
-.note-popover .popover > .arrow
- display: none
-
/* ----- Snippets Styles ----- */
.para_large
@@ -197,11 +96,6 @@ td
margin-left: auto
margin-right: auto
-/* ----- EDITOR ----- */
-
-.css_non_editable_mode_hidden
- display: none !important
-
/* ----- BOOTSTRAP FIX ----- */
.container, .readable
@@ -241,7 +135,7 @@ body > nav.navbar
width: 100%
position: absolute
z-index: 10000
-@media (max-min: 767px)
+@media (min-width: 767px)
nav.navbar + #wrapwrap
padding-top: 34px
@@ -255,33 +149,9 @@ body > nav.navbar
.table .img-responsive
width: 100%
-/* ---- HACK FOR COVERING UP EDITOR BOGUS P INSERTION --- */
-.oe_structure.oe_empty > .oe_drop_zone.oe_insert:only-child, [data-oe-type=html] > .oe_drop_zone.oe_insert:only-child
- position: static
-
-.oe_structure.oe_empty:empty:before, [data-oe-type=html]:empty:before, .oe_structure.oe_empty > .oe_drop_zone.oe_insert:only-child:before, [data-oe-type=html] > .oe_drop_zone.oe_insert:only-child:before
- content: ' '
- text-align: center
- display: block
- min-height: 1em
- color: grey
- font-size: 24px
- padding-top: 160px
-
-.css_editable_display
- display: none
-
-/* ---- HACK FOR COVERING UP EDITOR BOGUS P INSERTION --- */
-
.navbar .nav > li > p
margin-bottom: 0px
-// .navbar .nav > li a
-// text-shadow: none
-
-// .nav > li a
-// display: block
-
/* ---- HOMEPAGE THEME CUSTOMIZATION ---- */
.nav-hierarchy
@@ -659,15 +529,6 @@ table.well tr
a
color: white
-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
-
.oe_template_fallback
@include column-count(3)
@@ -685,46 +546,6 @@ span[data-oe-type="monetary"]
input
text-align: center
-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
-
-
-/* Fix: backward compatibility saas-3 */
-
div.carousel
.container
> .carousel-caption
diff --git a/addons/website/static/src/js/openerp.define.js b/addons/website/static/src/js/openerp.define.js
deleted file mode 100644
index 85e0486546e..00000000000
--- a/addons/website/static/src/js/openerp.define.js
+++ /dev/null
@@ -1,107 +0,0 @@
-odoo.define('website.define', function () {
-'use strict';
-
-var containsDefine = typeof(define) !== "undefined" ? define : undefined;
-var scripts = {
- 'jquery': jQuery
-};
-
-function odoo_define(parse) {
- return function (name, list, callback) {
-
- var def = $.Deferred();
- def.openerpDefine = true;
-
- if (!callback) {
- callback = list;
- list = null;
- }
- if (!list) {
- list = name;
- if (odoo.website_next_define) {
- name = odoo.website_next_define;
- odoo.website_next_define = null;
- }
- }
-
- if (list instanceof Array) {
- var defs = [];
- var args = [];
- _.each(list, function (js, index) {
- var path = parse(js);
-
- if (!(path in scripts)) {
-
- scripts[path] = $.Deferred();
- scripts[path].openerpDefine = true;
-
- $.ajax({ 'url': path, 'dataType': 'html', 'async': false })
- .then(function (data) {
- var code = new Function("return "+data)();
- if (code.openerpDefine) {
- code.then(function (code) {
- scripts[path].resolve(code);
- scripts[path] = code;
- });
- } else {
- scripts[path].resolve(code);
- scripts[path] = code;
- }
- });
-
- }
-
- if (scripts[path] && scripts[path].openerpDefine) {
- scripts[path].then(function (code) {
- args[index] = code;
- });
- defs.push(scripts[path]);
- } else {
- args[index] = scripts[path] = scripts[path] || null;
- }
- });
-
- $.when.apply($, defs).then(function () {
- var result = callback.apply(window, args);
- if (name) {
- scripts[name] = scripts[parse(name)] = result;
- }
- def.resolve(result);
- });
-
- } else {
-
- def = scripts[name] = scripts[parse(name)] = scripts[name] || callback() || null;
-
- }
-
- return def;
- }
-}
-
-function enable() {
- window.define = odoo_define(function (id) {
-
- if (id.slice(0,10) === 'summernote') {
- return '/website/static/lib/summernote/src/js' + id.slice(10,Infinity) + '.js';
- }
- return id;
- });
-}
-
-function disable() {
- window.define = containsDefine;
-}
-
-odoo.website_next_define = undefined;
-odoo.enable_website_define = enable;
-odoo.disable_website_define = disable;
-
-return {
- enable: enable,
- disable: disable,
- define: odoo_define,
- scripts: scripts,
-};
-
-});
diff --git a/addons/website/static/src/js/website.backend.iframe.js b/addons/website/static/src/js/website.backend.iframe.js
deleted file mode 100644
index 925fb6aa4b1..00000000000
--- a/addons/website/static/src/js/website.backend.iframe.js
+++ /dev/null
@@ -1,75 +0,0 @@
-odoo.define('website.backend.iframe', function (require) {
-'use strict';
-
-var editor = require('website.editor');
-var snippet_editor = require('website.snippets.editor');
-
-window.top.odoo[callback+"_set_value"] = function (value, fields_values) {
- var $editable = $("#wrapwrap .o_editable:first");
- if (value !== $editable.html()) {
- if ($('body').hasClass('editor_enable')) {
- editor.editor_bar.rte.historyRecordUndo($editable);
- editor.editor_bar.snippets.make_active(false);
- }
- $editable.html(value);
- }
-};
-
-editor.EditorBar.include({
- start: function () {
- var self = this;
- var res = this._super.apply(this, arguments);
- if (location.search.indexOf("enable_editor") !== -1) {
- this.on('rte:ready', this, function () {
- $("#website-top-edit").hide();
- setTimeout(function () {
- if (window.top.odoo[callback+"_editor"]) {
- window.top.odoo[callback+"_editor"](this);
- }
- }, 0);
-
- var $editable = $("#wrapwrap .o_editable:first");
- setTimeout(function () {
- $($editable.find("*").filter(function () {return !this.children.length;}).first()[0] || $editable)
- .focusIn().trigger("mousedown").trigger("keyup");
- },0);
-
- this.rte.on('change', this, function () {
- window.top.odoo[callback+"_downup"]($editable.prop('innerHTML'));
- });
-
-
- $(window).on('keydown', function (event) {
- if(event.keyCode === 27 && $("body", window.top.document).hasClass("o_form_FieldTextHtml_fullscreen")) {
- $(".o_fullscreen").trigger("click");
- }
- $(window).trigger('resize');
- });
-
- });
-
- var style = document.createElement("style");
- style.textContent = "#wrapwrap > main > * { margin-top: 50px; }";
- $("head").append(style);
-
- this.on("snippets:ready", this, function () {
- $(window.top).trigger("resize");
- });
-
- } else if (window.top.odoo[callback+"_editor"]) {
- window.top.odoo[callback+"_editor"](this);
- }
- return res;
- },
- cancel: function () {
- return;
- },
-});
-
-snippet_editor.BuildingBlock.include({
- _get_snippet_url: function () {
- return snippets_url;
- }
-});
-
-});
diff --git a/addons/website/static/src/js/website.backend.js b/addons/website/static/src/js/website.backend.js
index fb93bf0dadf..f877c0e90fc 100644
--- a/addons/website/static/src/js/website.backend.js
+++ b/addons/website/static/src/js/website.backend.js
@@ -22,157 +22,4 @@ var WidgetWebsiteButton = form_common.AbstractField.extend({
core.form_widget_registry.add('website_button', WidgetWebsiteButton);
-var FieldWidget = form_common.AbstractField.extend(form_common.ReinitializeFieldMixin);
-
-
-var FieldTextHtmlFrame = FieldWidget.extend({
- template: 'FieldTextHtmlFrame',
- start: function () {
- var self = this;
-
- this.callback = _.uniqueId('FieldTextHtml_');
- odoo[this.callback+"_editor"] = function (EditorBar) {
- self.on_editor_loaded(EditorBar);
- };
- odoo[this.callback+"_content"] = function () {
- self.on_content_loaded();
- };
- odoo[this.callback+"_downup"] = function (value) {
- self.dirty = true;
- self.internal_set_value(value);
- self.trigger('changed_value');
- self.resize();
- };
-
- // init jqery objects
- this.$iframe = this.$('iframe');
- this.document = null;
- this.$body = $();
- this.$content = $();
-
- // init resize
- this.resize = function resize() {
- if (self.get('effective_readonly')) {
- $("body").removeClass("o_form_FieldTextHtml_fullscreen");
- }
- if ($("body").hasClass("o_form_FieldTextHtml_fullscreen")) {
- self.$iframe.css('height', $("body").hasClass('o_form_FieldTextHtml_fullscreen') ? (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 () {
- var src = this.options.editor_url ? this.options.editor_url+"?" : "/website/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
- };
- if (this.options.snippets) {
- attr.snippets = this.options.snippets;
- }
- if (!this.get("effective_readonly")) {
- attr.enable_editor = 1;
- }
- if (core.debug) {
- attr.debug = 1;
- }
-
- for (var k in attr) {
- src += "&"+k+"="+attr[k];
- }
-
- delete datarecord[this.name];
- src += "&datarecord="+ encodeURIComponent(JSON.stringify(datarecord));
-
- return src;
- },
- initialize_content: function() {
- if (!this.get("effective_readonly")) {
- this.$iframe = this.$el.find('iframe');
- this.document = null;
- this.$body = $();
- this.$content = $();
- this.dirty = false;
- this.editor = false;
- odoo[this.callback+"_set_value"] = null;
- this.$iframe.attr("src", this.get_url());
- }
- },
- on_content_loaded: function () {
- this.document = this.$iframe.contents()[0];
- this.$body = $("body", this.document);
- this.$content = this.$body.find("#wrapwrap .o_editable:first");
- setTimeout(this.resize,0);
- },
- on_editor_loaded: function (EditorBar) {
- var self = this;
- this.editor = EditorBar;
-
- $("body").on('click', function (event) {
- if ($("body").hasClass('o_form_FieldTextHtml_fullscreen')) {
- $("body").removeClass("o_form_FieldTextHtml_fullscreen");
- self.$iframe.css('height', '');
- event.preventDefault();
- event.stopPropagation();
- }
- });
-
- if (this.get('value') && window.odoo[this.callback+"_set_value"] && !(this.$content.prop('innerHTML')||"").length) {
- this.render_value();
- }
-
- setTimeout(function () {
- var $fullscreen = $('<span class="btn btn-primary" style="margin: 5px;padding: 1px; position: fixed; top: 0; right: 0; z-index: 2000;"><span class="o_fullscreen fa fa-arrows-alt" style="color: white;margin: 3px 5px;"></span></span>');
- $("#website-top-navbar", self.document).append($fullscreen);
- $fullscreen.on('click', function () {
- $("body").toggleClass("o_form_FieldTextHtml_fullscreen");
- self.resize();
- });
- setTimeout(self.resize,0);
- }, 500);
- },
- render_value: function() {
- var value = (this.get('value') || "").replace(/^<p[^>]*>\s*<\/p>$/, '');
- if (!this.get("effective_readonly")) {
- if (!this.$content) {
- return;
- }
- if(window.odoo[this.callback+"_set_value"]) {
- window.odoo[this.callback+"_set_value"](value || '', this.view.get_fields_values(), this.name);
- this.resize();
- }
- } else {
- this.$el.html(value);
- }
- },
- is_false: function() {
- return this.get('value') === false || this.get('value') === "";
- },
- get_value: function(save_mode) {
- if (save_mode && this.editor && this.editor.snippets && this.dirty) {
- this.editor.snippets.clean_for_save();
- this.internal_set_value( this.$content.prop('innerHTML') );
- }
- return this.get('value');
- },
- destroy: function () {
- $("body").removeClass("o_form_FieldTextHtml_fullscreen");
- $(window).off('resize', self.resize);
- $(window).off('keydown', self.escape);
- delete window.odoo[this.callback+"_content"];
- delete window.odoo[this.callback+"_downup"];
- delete window.odoo[this.callback+"_set_value"];
- }
-});
-
-core.form_widget_registry.add('html_frame', FieldTextHtmlFrame);
-
});
diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js
index 67e9b3da734..b6f463b11ba 100644
--- a/addons/website/static/src/js/website.editor.js
+++ b/addons/website/static/src/js/website.editor.js
@@ -3,1145 +3,55 @@ odoo.define('website.editor', function (require) {
var ajax = require('web.ajax');
var core = require('web.core');
-var utils = require('web.utils');
var Widget = require('web.Widget');
+var base = require('web_editor.base');
+var editor = require('web_editor.editor');
+var widget = require('web_editor.widget');
var website = require('website.website');
+var qweb = core.qweb;
var _t = core._t;
+ajax.loadXML('/website/static/src/xml/website.editor.xml', qweb);
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* 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 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 (!core.qweb.has_template('website.colorpicker')) {
- return;
- }
-
- var $color = $container.find('.note-color');
- var html = core.qweb.render('website.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
- $(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 = $('<div class="btn-group"/>');
- $alt.prependTo($imagePopover.find('.popover-content'));
- $alt.append('<button class="btn btn-default btn-sm btn-small" data-event="alt"><strong>Alt: </strong><span class="o_image_alt"></span></button>');
-
- // padding button
- var $padding = $('<div class="btn-group"/>');
- $padding.insertBefore($imagePopover.find('.btn-group:first'));
- var $button = $(tplIconButton('fa fa-plus-square-o', {
- title: _t('Padding'),
- dropdown: true
- })).appendTo($padding);
- var $ul = $('<ul class="dropdown-menu"/>').insertAfter($button);
- $ul.append('<li><a data-event="padding" href="#" data-value="">'+_t('None')+'</a></li>');
- $ul.append('<li><a data-event="padding" href="#" data-value="small">'+_t('Small')+'</a></li>');
- $ul.append('<li><a data-event="padding" href="#" data-value="medium">'+_t('Medium')+'</a></li>');
- $ul.append('<li><a data-event="padding" href="#" data-value="large">'+_t('Large')+'</a></li>');
- $ul.append('<li><a data-event="padding" href="#" data-value="xl">'+_t('Xl')+'</a></li>');
-
- // circle, boxed... options became toggled
- $imagePopover.find('[data-event="imageShape"]:not([data-value])').remove();
- $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 = $('<div class="btn-group hidden only_fa"/>').insertAfter($button.parent());
- $(tplIconButton('fa fa-refresh', {
- title: _t('Spin'),
- event: 'imageShape',
- value: 'fa-spin'
- })).appendTo($spin);
-
- // resize for fa
- var $resizefa = $('<div class="btn-group hidden only_fa"/>')
- .insertAfter($imagePopover.find('.btn-group:has([data-event="resize"])'));
- for (var size=1; size<=5; size++) {
- $(tplButton('<span class="note-fontsize-10">'+size+'x</span>', {
- 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 = $('<div class="btn-group"/>');
- $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);
-
- $container.find('button[data-event="undo"]').attr('disabled', !history.hasUndo());
- $container.find('button[data-event="redo"]').attr('disabled', !history.hasRedo());
-
- 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(/&quot;/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) {
- $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
- });
- }
-};
-
-$(document).on('click keyup', function () {
- $('button[data-event="undo"]').attr('disabled', !history.hasUndo());
- $('button[data-event="redo"]').attr('disabled', !history.hasRedo());
-});
-
-eventHandler.editor.undo = function ($popover) {
- if(!$popover.attr('disabled')) history.undo();
-};
-eventHandler.editor.redo = function ($popover) {
- if(!$popover.attr('disabled')) history.redo();
-};
-
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* 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 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.data('NoteHistory').popUndo();
- $('.note-popover .note-link-popover').show();
- });
- editor.on("cancel", this, function () {
- def.reject();
- $editable.data('NoteHistory').popUndo();
- });
- 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 MediaDialog($editable, dom.isImg(r.sc) ? r.sc : null);
- editor.appendTo(document.body);
- return new $.Deferred().reject();
-};
-$.summernote.pluginEvents.alt = function (event, editor, layoutInfo) {
- var $editable = layoutInfo.editable();
- var $selection = layoutInfo.handle().find('.note-control-selection');
- var media = $selection.data('target');
- new 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<fontIcons.length; k++) {
- if (_.intersection(fontIcons[k].icons, classNames).length) {
- return true;
- }
- }
- }
- return false;
-};
-// re-overwrite font to include theme icons
-var isFont = dom.isFont;
-dom.isFont = function (node) {
- return dom.isImgFont(node) || isFont(node);
-};
-
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-var fn_visible = $.summernote.pluginEvents.visible;
-$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
- var res = fn_visible.call(this, event, editor, layoutInfo);
- var $node = $(dom.node(range.create().sc));
- if (($node.is('[data-oe-type="html"]') || $node.is('[data-oe-field="arch"]')) && $node.hasClass("o_editable") && !$node[0].children.length) {
- var p = $('<p><br/></p>')[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("#website-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) {
- 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('#website-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);
- }
- 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 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 fn_dettach = eventHandler.dettach;
-eventHandler.dettach = function (oLayoutInfo, options) {
- fn_dettach.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')
+/**
+ * Object who contains all method and bind for the top bar, the template is create server side.
+ */
+website.TopBar.include({
+ events: {
+ 'click [data-action="edit"]': 'edit',
},
- history: {
- undo: _t('Undo'),
- redo: _t('Redo')
- }
-};
-
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* Change History to have a global History for all summernote instances */
-
-var History = function History ($editable) {
- var aUndo = [];
- var pos = 0;
-
- this.makeSnap = function () {
- var rng = range.create(),
- elEditable = dom.ancestor(rng && rng.commonAncestor(), dom.isEditable) || $('.o_editable:first')[0];
- return {
- 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) {
- return;
- }
-
- try {
- var r = 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();
+ start: function () {
+ var self = this;
+ $("#wrapwrap").on('click', '.o_editable, [data-oe-model]', function (event) {
+ var $this = $(event.srcElement);
+ var tag = $this[0] && $this[0].tagName.toLowerCase();
+ if (!(tag === 'a' || tag === "button") && !$this.parents("a, button").length) {
+ self.$('[data-action="edit"]').parent().effect('bounce', {distance: 18, times: 5}, 250);
}
});
- setTimeout(function () {
- var target = dom.isBR(r.sc) ? r.sc.parentNode : dom.node(r.sc);
- if (!target) {
- return;
- }
- var evt = document.createEvent("MouseEvents");
- evt.initMouseEvent("mousedown", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target);
- target.dispatchEvent(evt);
-
- var other_evt = document.createEvent("MouseEvents");
- other_evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target);
- target.dispatchEvent(other_evt);
- },0);
- };
-
- this.undo = function () {
- if (!pos) { return; }
- last = null;
- if (!aUndo[pos]) aUndo[pos] = this.makeSnap();
- if (aUndo[pos-1].jump) pos--;
- this.applySnap(aUndo[--pos]);
- };
- this.hasUndo = function () {
- return pos > 0;
- };
-
- this.redo = function () {
- if (aUndo.length <= pos+1) { return; }
- if (aUndo[pos].jump) pos++;
- this.applySnap(aUndo[++pos]);
- };
- this.hasRedo = function () {
- return aUndo.length > pos+1;
- };
+ $("#wrapwrap").find("[data-oe-model] .oe_structure.oe_empty, [data-oe-model].oe_structure.oe_empty, [data-oe-type=html]:empty")
+ .addClass("oe_empty")
+ .attr("data-oe-placeholder", _t("Press The Top-Left Edit Button"));
- this.popUndo = function () {
- pos--;
- aUndo.pop();
- };
-
- var last;
- this.recordUndo = function ($editable, event, internal_history) {
- if (!internal_history) {
- if (!event || !last || !aUndo[pos-1] || aUndo[pos-1].editable !== $editable[0]) { // don't trigger change for all keypress
- $(".o_editable.note-editable").trigger("content_changed");
- }
- }
- if (event) {
- if (last && aUndo[pos-1] && aUndo[pos-1].editable !== $editable[0]) {
- // => make a snap when the user change editable zone (because: don't make snap for each keydown)
- aUndo.splice(pos, aUndo.length);
- var prev = aUndo[pos-1];
- aUndo[pos] = {
- editable: prev.editable,
- contents: $(prev.editable).html(),
- bookmark: prev.bookmark,
- scrollTop: prev.scrollTop,
- jump: true
- };
- pos++;
- }
- else if (event === last) return;
+ if (location.search.indexOf("enable_editor") >= 0 && $('html').attr('lang') === "en_US") {
+ this.$el.hide();
}
- last = event;
- aUndo.splice(pos, aUndo.length);
- aUndo[pos] = this.makeSnap($editable);
- pos++;
- };
-
- this.splitNext = function () {
- last = false;
- };
-};
-var history = new History();
-
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-// add focusIn to jQuery to allow to move caret into a div of a contentEditable area
-$.fn.extend({
- focusIn: function () {
- if (this.length) {
- range.create(dom.firstChild(this[0]), 0).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;
+ return this._super();
},
- activateBlock: function () {
- var target = snippet_editor.snippet.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;
+ edit: function () {
+ this.$('button[data-action=edit]').prop('disabled', true);
+ this.$el.hide();
+ editor.editor_bar = new editor.Class(this);
+ editor.editor_bar.prependTo(document.body);
}
});
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-function change_default_bootstrap_animation_to_edit() {
- var fn_carousel = $.fn.carousel;
- $.fn.carousel = function () {
- var res = fn_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;
- };
-}
-
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-website.no_editor = !!$(document.documentElement).data('editable-no-editor');
-
-website.add_template_file('/website/static/src/xml/website.editor.xml');
-website.dom_ready.done(function () {
- website.ready().then(init_editor);
-
- $(document).on('click', 'a.js_link2post', function (ev) {
- ev.preventDefault();
- website.form(this.pathname, 'POST');
- });
-
- $(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();
- }
- });
-});
-
-
-/* ----- TOP EDITOR BAR FOR ADMIN ---- */
-var EditorBar = Widget.extend({
- template: 'website.editorbar',
- events: {
- 'click button[data-action=save]': 'save',
- 'click a[data-action=cancel]': 'cancel',
- },
- start: function() {
- var self = this;
- this.saving_mutex = new utils.Mutex();
-
- this.$buttons = {
- edit: this.$el.parents().find('button[data-action=edit]'),
- save: this.$('button[data-action=save]'),
- cancel: this.$('button[data-action=cancel]'),
- };
-
- this.$('#website-top-edit').hide();
- this.$('#website-top-view').show();
-
- var $edit_button = this.$buttons.edit
- .prop('disabled', website.no_editor);
- if (website.no_editor) {
- var help_text = $(document.documentElement).data('editable-no-editor');
- $edit_button.parent()
- // help must be set on form above button because it does
- // not appear on disabled button
- .attr('title', help_text);
- }
-
- $('.dropdown-toggle').dropdown();
-
- this.$buttons.edit.click(function(ev) {
- self.edit();
- });
-
- this.rte = new RTE(this);
- this.rte.on('change', this, this.proxy('rte_changed'));
- this.rte.on('rte:ready', this, function () {
- self.trigger('rte:ready');
- });
-
- this.rte.appendTo(this.$('#website-top-edit .nav.js_editor_placeholder'));
- return this._super.apply(this, arguments);
- },
- edit: function (no_editor) {
- this.$buttons.edit.prop('disabled', true);
- this.$('#website-top-view').hide();
- this.$el.show();
- this.$('#website-top-edit').show();
-
- if (!no_editor) {
- this.rte.start_edition();
- this.trigger('rte:called');
- }
-
- 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!');
- }
- };
- },
- rte_changed: function () {
- this.$buttons.save.prop('disabled', false);
- },
- _save: function () {
- var self = this;
-
- var saved = {}; // list of allready saved views and data
-
- var defs = $('.o_editable')
- .filter('.o_dirty')
- .removeAttr('contentEditable')
- .removeClass('o_dirty o_editable oe_carlos_danger o_is_inline_editable')
- .map(function () {
- var $el = $(this);
-
- // remove multi edition
- var key = $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression');
- if (saved[key]) return true;
- saved[key] = true;
-
- // 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)
- .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');
- $el.addClass(id);
- return $.Deferred().reject({
- id: id,
- error: response.data,
- });
- });
- });
- }).get();
- return $.when.apply(null, defs).then(function () {
- window.onbeforeunload = null;
- website.reload();
- }, function (failed) {
- // If there were errors, re-enable edition
- self.rte.start_edition(true);
- // 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 = $("<div/>").text(failure.error.message).html();
- var data = msg.substring(3,msg.length-2).split(/', u'/);
- failure.error.message = '<b>' + data[0] + '</b>' + dom.blank + data[1];
- }
- $(root).find('.' + 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');
- });
- });
- },
- save: function () {
- return this._save().then(function () {
- website.reload();
- });
- },
- /**
- * Saves an RTE content, which always corresponds to a view section (?).
- */
- save_without_reload: function () {
- return this._save();
- },
- saveElement: function ($el) {
- var markup = $el.prop('outerHTML');
- return ajax.jsonRpc('/web/dataset/call', 'call', {
- model: 'ir.ui.view',
- method: 'save',
- args: [$el.data('oe-id'), markup,
- $el.data('oe-xpath') || null,
- website.get_context()],
- });
- },
- cancel: function () {
- new $.Deferred(function (d) {
- var $dialog = $(core.qweb.render('website.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;
- website.reload();
- });
- },
-});
+/* ----- Customize Template ---- */
-var EditorBarCustomize = Widget.extend({
+website.TopBarCustomize = Widget.extend({
events: {
'mousedown a.dropdown-toggle': 'load_menu',
'click ul a[data-view-id]': 'do_customize',
@@ -1183,349 +93,29 @@ var EditorBarCustomize = Widget.extend({
args: [],
kwargs: {
ids: [parseInt(view_id, 10)],
- context: website.get_context()
+ context: base.get_context()
}
}).then( function() {
window.location.reload();
});
},
});
-
-$(document).ready(function() {
- var editorBarCustomize = new EditorBarCustomize();
- editorBarCustomize.setElement($('li[id=customize-menu]'));
- editorBarCustomize.start();
-});
-
-/* ----- RICH TEXT EDITOR ---- */
-
-var RTE = Widget.extend({
- init: function (EditorBar) {
- this.EditorBar = EditorBar;
- $('.inline-media-link').remove();
- this._super.apply(this, arguments);
-
- computeFonts();
- },
- /**
- * Add a record undo to history
- * @param {DOM} target where the dom is changed is editable zone
- */
- historyRecordUndo: function ($target, internal_history) {
- var rng = range.create();
- var $editable = $($target || (rng && rng.sc)).closest(".o_editable");
- if ($editable.length) {
- rng = $editable.data('range') || rng;
- }
- if (!rng && $target.length) {
- rng = range.create($target.closest("*")[0],0);
- }
- if (rng) {
- try {
- rng.select();
- } catch (e) {
- console.error(e);
- }
- }
- $target = $(rng.sc);
- $target.mousedown();
- this.history.recordUndo($target, null, internal_history);
- $target.mousedown();
- },
- /**
- * 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_edition: function (restart) {
- var self = this;
-
- change_default_bootstrap_animation_to_edit();
-
- this.history = history;
-
- // handler for cancel editor
- $(document).on('keydown', function (event) {
- if (event.keyCode === 27 && !$('.modal-content:visible').length) {
- setTimeout(function () {
- $('#website-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
- var $last;
- $(document).on('mousedown', function (event) {
- var $target = $(event.target);
- var $editable = $target.closest('.o_editable');
-
- if (!$editable.size()) {
- return;
- }
-
- if ($last && (!$editable.size() || $last[0] != $editable[0])) {
- var $destroy = $last;
- setTimeout(function () {$destroy.destroy();},150); // setTimeout to remove flickering when change to editable zone (re-create an editor)
- $last = null;
- }
- if ($editable.size() && (!$last || $last[0] != $editable[0]) &&
- ($target.closest('[contenteditable]').attr('contenteditable') || "").toLowerCase() !== 'false') {
- $editable.summernote(self._config());
- $editable.data('NoteHistory', self.history);
- $editable.data('rte', self);
- $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()) {
- range.create($editable[0],0).select();
- }
-
- $target.trigger('mousedown'); // for activate selection on picture
- setTimeout(function () {
- self.historyRecordUndo($editable, true);
- },0);
- }
- });
-
- $('.o_not_editable').attr("contentEditable", false);
-
- $('#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')
- .addClass('o_editable');
-
- $('.o_editable').each(function () {
- var node = this;
- var $node = $(node);
-
- // add class to display inline-block for empty t-field
- if(window.getComputedStyle(node).display === "inline" && $node.data('oe-type') !== "image") {
- $node.addClass('o_is_inline_editable');
- }
-
- // start element observation
- $(node).one('content_changed', function () {
- $node.addClass('o_dirty');
- });
- $(node).on('content_changed', function () {
- self.trigger('change');
- });
- });
-
- $(document).trigger('mousedown');
-
- if (!restart) {
- $('#wrapwrap, .o_editable').on('click', '*', function (event) {
- event.preventDefault();
- });
-
- $('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');
- });
-
- self.trigger('rte:ready');
- }
- },
-
- _config: function () {
- 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']],
- ],
- oninit: function() {
- },
- styleWithSpan: false,
- inlinemedia : ['p'],
- lang: "odoo"
- };
+website.TopBar.include({
+ start: function () {
+ this.customize_menu = new website.TopBarCustomize();
+ var def = this.customize_menu.attachTo($('#customize-menu'));
+ return $.when(this._super(), def);
}
});
/* ----- EDITOR: LINK & MEDIA ---- */
-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');
- },
- destroy: function () {
- this.$el.modal('hide').remove();
- if($(".modal.in").length>0){
- $('body').addClass('modal-open');
- }
- },
-});
-
-var LinkDialog = Dialog.extend({
- template: 'website.editor.dialog.link',
- events: _.extend({}, Dialog.prototype.events, {
- 'change :input.url-source': 'changed',
- 'keyup :input.url': 'onkeyup',
- 'keyup :input': 'preview',
- 'click button.remove': 'remove_link',
- 'change input#link-text': function (e) {
- this.text = $(e.target).val();
- },
- 'change .link-style': function (e) {
- this.preview();
- },
- }),
- init: function (editable, linkInfo) {
- this._super(editable, linkInfo);
- this.editable = editable;
- this.data = linkInfo || {};
-
- this.data.className = "";
- if (this.data.range) {
- this.data.iniClassName = $(this.data.range.sc).filter("a").attr("class") || "";
- this.data.className = this.data.iniClassName.replace(/(^|\s+)btn(-[a-z0-9_-]*)?/gi, ' ');
-
- var is_link = this.data.range.isOnAnchor();
- var r = this.data.range;
-
- var sc = r.sc;
- var so = r.so;
- var ec = r.ec;
- var eo = r.eo;
-
- var nodes;
- if (!is_link) {
- if (sc.tagName) {
- sc = dom.firstChild(so ? sc.childNodes[so] : sc);
- so = 0;
- } else if (so !== sc.textContent.length) {
- if (sc === ec) {
- ec = sc = sc.splitText(so);
- eo -= so;
- } else {
- sc = sc.splitText(so);
- }
- so = 0;
- }
- if (ec.tagName) {
- ec = dom.lastChild(eo ? ec.childNodes[eo-1] : ec);
- eo = ec.textContent.length;
- } else if (eo !== ec.textContent.length) {
- ec.splitText(eo);
- }
-
- nodes = dom.listBetween(sc, ec);
-
- // browsers can't target a picture or void node
- if (dom.isVoid(sc) || dom.isImg(sc)) {
- so = dom.listPrev(sc).length-1;
- sc = sc.parentNode;
- }
- if (dom.isBR(ec)) {
- eo = dom.listPrev(ec).length-1;
- ec = ec.parentNode;
- } else if (dom.isVoid(ec) || dom.isImg(sc)) {
- eo = dom.listPrev(ec).length;
- ec = ec.parentNode;
- }
-
- this.data.range = range.create(sc, so, ec, eo);
- this.data.range.select();
- } else {
- nodes = dom.ancestor(sc, dom.isAnchor).childNodes;
- }
-
- if (dom.isImg(sc) && nodes.indexOf(sc) === -1) {
- nodes.push(sc);
- }
- if (nodes.length > 1 || dom.ancestor(nodes[0], dom.isImg)) {
- var text = "";
- this.data.images = [];
- for (var i=0; i<nodes.length; i++) {
- if (dom.ancestor(nodes[i], dom.isImg)) {
- this.data.images.push(dom.ancestor(nodes[i], dom.isImg));
- text += '[IMG]';
- } else if (!is_link && i===0) {
- text += nodes[i].textContent.slice(so, Infinity);
- } else if (!is_link && i===nodes.length-1) {
- text += nodes[i].textContent.slice(0, eo);
- } else {
- text += nodes[i].textContent;
- }
- }
- this.data.text = text;
- }
- }
-
- this.data.text = this.data.text.replace(/[ \t\r\n]+/g, ' ');
-
- // Store last-performed request to be able to cancel/abort it.
- this.page_exists_req = null;
- this.search_pages_req = null;
- this.bind_data();
- },
- start: function () {
+widget.LinkDialog.include({
+ bind_data: function () {
var self = this;
+ var href = this.data.url;
var last;
+
this.$('#link-page').select2({
minimumInputLength: 1,
placeholder: _t("New or existing page"),
@@ -1555,37 +145,24 @@ var LinkDialog = Dialog.extend({
});
},
});
- return this._super().then(this.proxy('bind_data'));
- },
- get_data: function (test) {
- var def = new $.Deferred(),
- $e = this.$('.active input.url-source').filter(':input'),
- val = $e.val(),
- label = this.$('#link-text').val() || val;
-
- if (label && this.data.images) {
- for(var i=0; i<this.data.images.length; i++) {
- label = label.replace(/</, "&lt;").replace(/>/, "&gt;").replace(/\[IMG\]/, this.data.images[i].outerHTML);
- }
- }
- if (!test && (!val || !$e[0].checkValidity())) {
- // FIXME: error message
- $e.closest('.form-group').addClass('has-error');
- $e.focus();
- def.reject();
+ if (href) {
+ this.page_exists(href).then(function (exist) {
+ if (exist) {
+ self.$('#link-page').select2('data', {'id': href, 'text': href}).change();
+ } else {
+ self.$('input.url').val(href).change();
+ self.$('input.window-new').closest("div").show();
+ }
+ });
}
-
- var style = this.$("input[name='link-style-type']:checked").val() || '';
- var size = this.$("input[name='link-style-size']:checked").val() || '';
- var classes = (this.data.className || "") + (style && style.length ? " btn " : "") + style + " " + size;
- var isNewWindow = this.$('input.window-new').prop('checked');
-
- if ($e.hasClass('email-address') && $e.val().indexOf("@") !== -1) {
- def.resolve(val.indexOf("mailto:") === 0 ? val : 'mailto:' + val, isNewWindow, label, classes);
- } else if ($e.val() && $e.val().length && $e.hasClass('page')) {
+ return this._super();
+ },
+ get_data_buy_url: function (def, $e, isNewWindow, label, classes) {
+ var val = $e.val();
+ if (val && val.length && $e.hasClass('page')) {
var data = $e.select2('data');
- if (test || !data.create) {
+ if (!data.create) {
def.resolve(data.id, isNewWindow, label || data.text, classes);
} else {
// Create the page, get the URL back
@@ -1598,76 +175,12 @@ var LinkDialog = Dialog.extend({
} else {
def.resolve(val, isNewWindow, label, classes);
}
- return def;
- },
- save: function () {
- var self = this;
- var _super = this._super.bind(this);
- return this.get_data()
- .then(function (url, new_window, label, classes) {
- self.data.url = url;
- self.data.newWindow = new_window;
- self.data.text = label;
- self.data.className = classes.replace(/\s+/gi, ' ').replace(/^\s+|\s+$/gi, '');
-
- self.trigger("save", self.data);
- }).then(_super);
- },
- bind_data: function () {
- var self = this;
- var href = this.data.url;
- var new_window = this.data.isNewWindow;
- var text = this.data.text;
- var classes = this.data.iniClassName;
-
- this.$('input#link-text').val(text);
- this.$('input.window-new').prop('checked', new_window);
-
- if (classes) {
- this.$('input[value!=""]').each(function () {
- var $option = $(this);
- if (classes.indexOf($option.val()) !== -1) {
- $option.attr("checked", "checked");
- }
- });
- }
-
- var match, $control;
- if (href && (match = /mailto:(.+)/.exec(href))) {
- this.$('input.email-address').val(match[1]).change();
- }
- if (href && !$control) {
- this.page_exists(href).then(function (exist) {
- if (exist) {
- self.$('#link-page').select2('data', {'id': href, 'text': href});
- } else {
- self.$('input.url').val(href).change();
- self.$('input.window-new').closest("div").show();
- }
- });
- }
-
- this.page_exists(href).then(function (exist) {
- if (exist) {
- self.$('#link-page').select2('data', {'id': href, 'text': href});
- } else {
- self.$('input.url').val(href).change();
- self.$('input.window-new').closest("div").show();
- }
- });
-
- this.preview();
},
changed: function (e) {
- var $e = $(e.target);
- this.$('.url-source').filter(':input').not($e).val('')
+ this.$('.url-source').filter(':input').not($(e.target)).val('')
.filter(function () { return !!$(this).data('select2'); })
.select2('data', null);
- $e.closest('.list-group-item')
- .addClass('active')
- .siblings().removeClass('active')
- .addBack().removeClass('has-error');
- this.preview();
+ this._super(e);
},
call: function (method, args, kwargs) {
var self = this;
@@ -1684,859 +197,15 @@ var LinkDialog = Dialog.extend({
},
page_exists: function (term) {
return this.call('page_exists', [null, term], {
- context: website.get_context(),
+ context: base.get_context(),
});
},
fetch_pages: function (term) {
return this.call('search_pages', [null, term], {
limit: 9,
- context: website.get_context(),
+ context: base.get_context(),
});
},
- onkeyup: function (e) {
- var $e = $(e.target);
- var is_link = ($e.val()||'').length && $e.val().indexOf("@") === -1;
- this.$('input.window-new').closest("div").toggle(is_link);
- this.preview();
- },
- preview: function () {
- var $preview = this.$("#link-preview");
- this.get_data(true).then(function (url, new_window, label, classes) {
- $preview.attr("target", new_window ? '_blank' : "")
- .attr("href", url && url.length ? url : "#")
- .html((label && label.length ? label : url))
- .attr("class", classes.replace(/pull-\w+/, ''));
- });
- }
});
-/**
- * alt widget. Lets users change a alt & title on a media
- */
-var alt = Dialog.extend({
- template: 'website.editor.dialog.alt',
- init: function ($editable, media) {
- this.$editable = $editable;
- this.media = media;
- this.alt = ($(this.media).attr('alt') || "").replace(/&quot;/g, '"');
- this.title = ($(this.media).attr('title') || "").replace(/&quot;/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, "&quot;") : null).attr('title', title ? title.replace(/"/g, "&quot;") : 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: 'website.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: 'website.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) {
- 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="/website/attach"]');
- if (!$form.find('input[name="upload"]').val().length) {
- $form.find('input[name="url"]').val();
- if (this.selected_existing().size()) {
- event.preventDefault();
- return false;
- }
- }
- $form.find('.well > div').hide().last().after('<span class="fa fa-spin fa-3x fa-refresh"/>');
-
- 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<attachments.length; i++) {
- self.file_selected(attachments[i]['website_url'], error);
- }
- };
- },
- file_selection: function () {
- this.$el.addClass('nosave');
- this.$('form').removeClass('has-error').find('.help-block').empty();
- this.$('button.filepicker').removeClass('btn-danger btn-success');
- this.$('form').submit();
- },
- file_selected: function(url, error) {
- var $button = this.$('button.filepicker');
- if (!error) {
- $button.addClass('btn-success');
- } else {
- url = null;
- this.$('form').addClass('has-error')
- .find('.help-block').text(error);
- $button.addClass('btn-danger');
- }
- this.set_image(url, null, error);
-
- if (!this.options.select_images) {
- // auto save and close popup
- this.parent.save();
- }
- },
- fetch_existing: function (needle) {
- var domain = [['res_model', '=', 'ir.ui.view'], '|',
- ['mimetype', '=', false], ['mimetype', '=like', 'image/%']];
- if (needle && needle.length) {
- domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
- }
- return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
- model: 'ir.attachment',
- method: 'search_read',
- args: [],
- kwargs: {
- domain: domain,
- fields: ['name', 'website_url'],
- order: 'id desc',
- context: website.get_context()
- }
- }).then(this.proxy('fetched_existing'));
- },
- fetched_existing: function (records) {
- this.records = records;
- this.display_attachments();
- },
- display_attachments: function () {
- this.$('.help-block').empty();
- var per_screen = IMAGES_PER_ROW * IMAGES_ROWS;
-
- var from = this.page * per_screen;
- var records = this.records;
-
- // Create rows of 3 records
- var rows = _(records).chain()
- .slice(from, from + per_screen)
- .groupBy(function (_, index) { return Math.floor(index / IMAGES_PER_ROW); })
- .values()
- .value();
- this.$('.existing-attachments').replaceWith(
- core.qweb.render(
- 'website.editor.dialog.image.existing.content', {rows: rows}));
- this.parent.$('.pager')
- .find('li.previous').toggleClass('disabled', (from === 0)).end()
- .find('li.next').toggleClass('disabled', (from + per_screen >= 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/dataset/call_kw', 'call', {
- model: 'ir.attachment',
- method: 'try_remove',
- args: [],
- kwargs: {
- ids: [id],
- context: website.get_context()
- }
- }).then(function (prevented) {
- if (_.isEmpty(prevented)) {
- self.records = _.without(self.records, attachment);
- self.display_attachments();
- return;
- }
- $both.css({borderWidth: "", borderColor: ""});
- $help_block.replaceWith(core.qweb.render(
- 'website.editor.dialog.image.existing.error', {
- views: prevented[id]
- }
- ));
- });
- },
-});
-
-
-var cacheCssSelectors = {};
-function getCssSelectors(filter) {
- var css = [];
- if (cacheCssSelectors[filter]) {
- return cacheCssSelectors[filter];
- }
- var sheets = document.styleSheets;
- for(var i = 0; i < sheets.length; i++) {
- try {
- var rules = sheets[i].rules || sheets[i].cssRules;
- } catch(e) {
- continue;
- }
- 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$/, '');
- });
- });
-}
-
-/* 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: 'website.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;
- },
- 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<fontIcons.length; k++) {
- fontIcon = fontIcons[k];
- var icons = _(fontIcon.icons).filter(function (icon) {
- return icon.indexOf(needle) !== -1;
- });
- if (icons.length) {
- parser.push({
- base: fontIcon.base,
- icons: icons
- });
- }
- }
- iconsParser = parser;
- }
- this.$('div.font-icons-icons').html(
- core.qweb.render('website.editor.dialog.font-icons.icons', {'iconsParser': iconsParser}));
- },
- /**
- * Removes existing FontAwesome classes on the bound element, and sets
- * all the new ones if necessary.
- */
- save: function () {
- var self = this;
- this.parent.trigger("save", this.media);
- var icons = this.icons;
- var style = this.media.attributes.style ? this.media.attributes.style.value : '';
- var classes = (this.media.className||"").split(/\s+/);
- var non_fa_classes = _.reject(classes, function (cls) {
- return self.getFont(cls);
- });
- var final_classes = non_fa_classes.concat(this.get_fa_classes());
- if (this.media.tagName !== "SPAN") {
- var media = document.createElement('span');
- $(this.media).replaceWith(media);
- this.media = media;
- style = style.replace(/\s*width:[^;]+/, '');
- }
- $(this.media).attr("class", _.compact(final_classes).join(' ')).attr("style", style);
- },
- /**
- * return the data font object (with base, parser and icons) or null
- */
- getFont: function (classNames) {
- if (!(classNames instanceof Array)) {
- classNames = (classNames||"").split(/\s+/);
- }
- var fontIcon, className;
- for (var i=0; i<classNames.length; i++) {
- className = classNames[i];
- for (var k=0; k<fontIcons.length; k++) {
- fontIcon = fontIcons[k];
- if (className === fontIcon.base || fontIcon.icons.indexOf(className) !== -1) {
- return {
- 'base': fontIcon.base,
- 'parser': fontIcon.parser,
- 'icons': fontIcon.icons,
- 'font': className
- };
- }
- }
- }
- return null;
- },
-
- /**
- * Looks up the various FontAwesome classes on the bound element and
- * sets the corresponding template/form elements to the right state.
- * If multiple classes of the same category are present on an element
- * (e.g. fa-lg and fa-3x) the last one occurring will be selected,
- * which may not match the visual look of the element.
- */
- load_data: function () {
- var classes = (this.media&&this.media.className||"").split(/\s+/);
- for (var i = 0; i < classes.length; i++) {
- var cls = classes[i];
- switch(cls) {
- case 'fa-1x':case 'fa-2x':case 'fa-3x':case 'fa-4x':case 'fa-5x':
- // size classes
- this.$('#fa-size').val(cls);
- continue;
- case 'fa-spin':
- case 'fa-rotate-90':case 'fa-rotate-180':case 'fa-rotate-270':
- case 'fa-flip-horizontal':case 'fa-rotate-vertical':
- this.$('#fa-rotation').val(cls);
- continue;
- case 'fa-fw':
- continue;
- case 'fa-border':
- this.$('#fa-border').prop('checked', true);
- continue;
- case '': continue;
- default:
- $(".font-icons-icon").removeClass("font-icons-selected").filter("."+cls).addClass("font-icons-selected");
- for (var k=0; k<this.icons.length; k++) {
- if (this.icons.indexOf(cls) !== -1) {
- this.$('#fa-icon').val(cls);
- break;
- }
- }
- }
- }
- },
- /**
- * Serializes the dialog to an array of FontAwesome classes. Includes
- * the base ``fa``.
- */
- get_fa_classes: function () {
- var font = this.getFont(this.$('#fa-icon').val());
- return [
- font ? font.base : 'fa',
- font ? font.font : "",
- this.$('#fa-size').val(),
- this.$('#fa-rotation').val(),
- this.$('#fa-border').prop('checked') ? 'fa-border' : ''
- ];
- },
- clear: function () {
- this.media.className = this.media.className.replace(/(^|\s)(fa(\s|$)|fa-[^\s]*)/g, ' ');
- },
-});
-
-
-function createVideoNode(url) {
- // video url patterns(youtube, instagram, vimeo, dailymotion, youku)
- var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
- var ytMatch = url.match(ytRegExp);
-
- var igRegExp = /\/\/instagram.com\/p\/(.[a-zA-Z0-9]*)/;
- var igMatch = url.match(igRegExp);
-
- var vRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/;
- var vMatch = url.match(vRegExp);
-
- var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/;
- var vimMatch = url.match(vimRegExp);
-
- var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/;
- var dmMatch = url.match(dmRegExp);
-
- var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)\.html/;
- var youkuMatch = url.match(youkuRegExp);
-
- var $video = $('<iframe>');
- if (ytMatch && ytMatch[1].length === 11) {
- var youtubeId = ytMatch[1];
- $video = $('<iframe>')
- .attr('src', '//www.youtube.com/embed/' + youtubeId)
- .attr('width', '640').attr('height', '360');
- } else if (igMatch && igMatch[0].length) {
- $video = $('<iframe>')
- .attr('src', igMatch[0] + '/embed/')
- .attr('width', '612').attr('height', '710')
- .attr('scrolling', 'no')
- .attr('allowtransparency', 'true');
- } else if (vMatch && vMatch[0].length) {
- $video = $('<iframe>')
- .attr('src', vMatch[0] + '/embed/simple')
- .attr('width', '600').attr('height', '600')
- .attr('class', 'vine-embed');
- } else if (vimMatch && vimMatch[3].length) {
- $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
- .attr('src', '//player.vimeo.com/video/' + vimMatch[3])
- .attr('width', '640').attr('height', '360');
- } else if (dmMatch && dmMatch[2].length) {
- $video = $('<iframe>')
- .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2])
- .attr('width', '640').attr('height', '360');
- } else if (youkuMatch && youkuMatch[1].length) {
- $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
- .attr('height', '498')
- .attr('width', '510')
- .attr('src', '//player.youku.com/embed/' + youkuMatch[1]);
- } else {
- // this is not a known video link. Now what, Cat? Now what?
- }
-
- $video.attr('frameborder', 0);
-
- return $video;
-}
-
-/**
- * VideoDialog widget. Lets users change a video, support all summernote
- * video, and embled iframe
- */
-var VideoDialog = Widget.extend({
- template: 'website.editor.dialog.video',
- events : _.extend({}, Dialog.prototype.events, {
- 'click input#urlvideo ~ button': 'get_video',
- 'click input#embedvideo ~ button': 'get_embed_video',
- 'change input#urlvideo': 'change_input',
- 'keyup input#urlvideo': 'change_input',
- 'change input#embedvideo': 'change_input',
- 'keyup input#embedvideo': 'change_input'
- }),
- init: function (parent, media) {
- this._super();
- this.parent = parent;
- this.media = media;
- },
- start: function () {
- this.$preview = this.$('.preview-container').detach();
- this.$iframe = this.$("iframe");
- var $media = $(this.media);
- if ($media.hasClass("media_iframe_video")) {
- var src = $media.data('src');
- this.$("input#urlvideo").val(src);
- this.$("#autoplay").attr("checked", src.indexOf('autoplay=1') != -1);
- this.get_video();
- } else {
- this.add_class = "pull-left";
- }
- return this._super();
- },
- 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");
- }
- },
- get_embed_video: function (event) {
- event.preventDefault();
- var embedvideo = this.$("input#embedvideo").val().match(/src=["']?([^"']+)["' ]?/);
- if (embedvideo) {
- this.$("input#urlvideo").val(embedvideo[1]);
- this.get_video(event);
- }
- return false;
- },
- get_video: function (event) {
- if (event) event.preventDefault();
- var $video = createVideoNode(this.$("input#urlvideo").val());
- this.$iframe.replaceWith($video);
- this.$iframe = $video;
- return false;
- },
- save: function () {
- this.parent.trigger("save", this.media);
- var video_id = this.$("#video_id").val();
- if (!video_id) {
- this.$("button.btn-primary").click();
- video_id = this.$("#video_id").val();
- }
- var video_type = this.$("#video_type").val();
- var $iframe = $(
- '<div class="media_iframe_video" data-src="'+this.$iframe.attr("src")+'">'+
- '<div class="css_editable_mode_display">&nbsp;</div>'+
- '<div class="media_iframe_video_size" contentEditable="false">&nbsp;</div>'+
- '<iframe src="'+this.$iframe.attr("src")+'" frameborder="0" allowfullscreen="allowfullscreen" contentEditable="false"></iframe>'+
- '</div>');
- $(this.media).replaceWith($iframe);
- this.media = $iframe[0];
- },
- clear: function () {
- if (this.media.dataset.src) {
- try {
- delete this.media.dataset.src;
- } catch(e) {
- this.media.dataset.src = undefined;
- }
- }
-
- this.media.className = this.media.className.replace(/(^|\s)media_iframe_video(\s|$)/g, ' ');
- },
-});
-
-var editor_bar = new EditorBar();
-function init_editor() {
- var $body = $(document.body);
- editor_bar.prependTo($body).then(function () {
- if (location.search.indexOf("enable_editor") >= 0) {
- editor_bar.edit();
- }
- });
-};
-
-return {
- EditorBar: EditorBar,
- editor_bar: editor_bar,
- LinkDialog: LinkDialog,
- Dialog: Dialog,
- MediaDialog: MediaDialog,
- getCssSelectors: getCssSelectors,
-};
-
-});
+}); \ No newline at end of file
diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js
index a9f1f41d95c..728321b7e1f 100644
--- a/addons/website/static/src/js/website.js
+++ b/addons/website/static/src/js/website.js
@@ -3,10 +3,14 @@ odoo.define('website.website', function (require) {
var ajax = require('web.ajax');
var core = require('web.core');
+var Widget = require('web.Widget');
var session = require('web.session');
+var base = require('web_editor.base');
var Tour = require('web.Tour');
+var qweb = core.qweb;
var _t = core._t;
+base.url_translations = '/website/translations';
/* --- Set the browser into the dom for css selectors --- */
var browser;
@@ -18,66 +22,24 @@ else if ($.browser.mozilla) browser = "mozilla";
browser += ","+$.browser.version;
if (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase())) browser += ",mobile";
document.documentElement.setAttribute('data-browser', browser);
-/* ---------------------------------------------------- */
-
-
-var translatable = !!$('html').data('translatable');
-var data = {
- id: undefined,
- session: undefined,
-};
/* ----------------------------------------------------
Helpers
- ---------------------------------------------------- */
-function get_context(dict) {
+ ---------------------------------------------------- */
+
+var get_context = base.get_context;
+base.get_context = base.get_context = function (dict) {
var html = document.documentElement;
return _.extend({
- lang: html.getAttribute('lang').replace('-', '_'),
- website_id: html.getAttribute('data-website-id')|0
- }, dict);
-}
-
-function parseQS(qs) {
- var match,
- params = {},
- pl = /\+/g, // Regex for replacing addition symbol with a space
- search = /([^&=]+)=?([^&]*)/g;
-
- while ((match = search.exec(qs))) {
- var name = decodeURIComponent(match[1].replace(pl, " "));
- var value = decodeURIComponent(match[2].replace(pl, " "));
- params[name] = value;
- }
- return params;
-}
-
-var _parsedSearch;
-function parseSearch() {
- if (!_parsedSearch) {
- _parsedSearch = parseQS(window.location.search.substring(1));
- }
- return _parsedSearch;
-}
-
-function parseHash() {
- return parseQS(window.location.hash.substring(1));
-}
-
-function reload() {
- 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();
- }
-}
+ 'website_id': html.getAttribute('data-website-id')|0
+ }, get_context(dict), dict);
+};
/* ----------------------------------------------------
Widgets
---------------------------------------------------- */
-function prompt(options, qweb) {
+var prompt = function (options, _qweb) {
/**
* A bootstrapped version of prompt() albeit asynchronous
* This was built to quickly prompt the user with a single field.
@@ -111,8 +73,8 @@ function prompt(options, qweb) {
text: options
};
}
- if (_.isUndefined(qweb)) {
- qweb = 'website.prompt';
+ if (_.isUndefined(_qweb)) {
+ _qweb = 'website.prompt';
}
options = _.extend({
window_title: '',
@@ -127,7 +89,7 @@ function prompt(options, qweb) {
options.field_name = options.field_name || options[type];
var def = $.Deferred();
- var dialog = $(core.qweb.render(qweb, options)).appendTo("body");
+ var dialog = $(qweb.render(_qweb, options)).appendTo("body");
options.$dialog = dialog;
var field = dialog.find(options.field_type).first();
field.val(options['default']); // dict notation for IE<9
@@ -168,34 +130,34 @@ function prompt(options, qweb) {
});
}
return def;
-}
+};
-function error(data, url) {
- var $error = $(core.qweb.render('website.error_dialog', {
+var error = function(data, url) {
+ var $error = $(qweb.render('website.error_dialog', {
'title': data.data ? data.data.arguments[0] : "",
'message': data.data ? data.data.arguments[1] : data.statusText,
'backend_url': url
}));
$error.appendTo("body");
$error.modal('show');
-}
+};
-function form(url, method, params) {
- var htmlform = document.createElement('form');
- htmlform.setAttribute('action', url);
- htmlform.setAttribute('method', method);
+var form = function (url, method, params) {
+ var form = document.createElement('form');
+ form.setAttribute('action', url);
+ form.setAttribute('method', method);
_.each(params, function (v, k) {
var param = document.createElement('input');
param.setAttribute('type', 'hidden');
param.setAttribute('name', k);
param.setAttribute('value', v);
- htmlform.appendChild(param);
+ form.appendChild(param);
});
- document.body.appendChild(htmlform);
- htmlform.submit();
-}
+ document.body.appendChild(form);
+ form.submit();
+};
-function init_kanban($kanban) {
+var init_kanban = function ($kanban) {
$('.js_kanban_col', $kanban).each(function () {
var $col = $(this);
var $pagination = $('.pagination', $col);
@@ -223,7 +185,7 @@ function init_kanban($kanban) {
}
var page = +$a.attr("href").split(",").pop().split('-')[1];
- data.page = page;
+ data['page'] = page;
$.post('/website/kanban', data, function (col) {
$col.find("> .thumbnail").remove();
@@ -250,43 +212,9 @@ function init_kanban($kanban) {
});
});
-}
-
-/* ----------------------------------------------------
- Async Ready and Template loading
- ---------------------------------------------------- */
-var templates_def = $.Deferred().resolve();
-function add_template_file(template) {
- var def = $.Deferred();
- templates_def = templates_def.then(function() {
- core.qweb.add_template(template, function(err) {
- if (err) {
- def.reject(err);
- } else {
- def.resolve();
- }
- });
- return def;
- });
- return def;
-}
-
-add_template_file('/website/static/src/xml/website.xml');
+};
-var dom_ready = $.Deferred();
-$(document).ready(function () {
- dom_ready.resolve();
- // fix for ie
- if($.fn.placeholder) $('input, textarea').placeholder();
- $(".oe_search_box").on('input', function() {
- $(this).next('span').toggle(!!$(this).val());
- });
- $(".oe_search_clear").on('click', function() {
- $(this).prev('input').val('').focus();
- $(".oe_search_button").trigger("click");
- });
- $(".oe_search_clear").toggle(!!$('.oe_search_box').val());
-});
+ajax.loadXML('/website/static/src/xml/website.xml', qweb);
/**
* Execute a function if the dom contains at least one element matched
@@ -295,73 +223,38 @@ $(document).ready(function () {
* @param {String} selector A jQuery selector used to match the element(s)
* @param {Function} fn Callback to execute if at least one element has been matched
*/
-function if_dom_contains(selector, fn) {
- dom_ready.then(function () {
+var if_dom_contains = function(selector, fn) {
+ base.dom_ready.then(function () {
var elems = $(selector);
if (elems.length) {
fn(elems);
}
});
-}
+};
+
+/**
+ * Cancel the auto run of Tour (test) and launch the tour after tob bar all bind events
+ */
+
+Tour.autoRunning = false;
+base.ready().then(function () {
+ data.topBar = new TopBar();
+ data.topBar.attachTo($("#oe_main_menu_navbar"))
+ .then(function () {
+ setTimeout(Tour.running,0);
+ });
+});
-var all_ready = null;
/**
* Returns a deferred resolved when the templates are loaded
* and the Widgets can be instanciated.
*/
-function ready() {
- if (!all_ready) {
- all_ready = dom_ready.then(function () {
- return templates_def;
- }).then(function () {
- odoo.init();
-
- // display button if they are at least one editable zone in the page (check the branding)
- if ($('[data-oe-model]').size()) {
- $("#oe_editzone").show();
- }
-
- if ($('html').data('website-id')) {
- data.id = $('html').data('website-id');
- data.session = session;
-
- return ajax.jsonRpc('/website/translations', 'call', {'lang': get_context().lang})
- .then(function(trans) {
- _t.database.set_bundle(trans);});
- }
- }).then(function () {
- var templates = core.qweb.templates;
- var keys = _.keys(templates);
- for (var i = 0; i < keys.length; i++){
- treat_node(templates[keys[i]]);
- }
- }).promise();
- }
- return all_ready;
-}
-
-function treat_node(node){
- if(node.nodeType === 3) {
- if(node.nodeValue.match(/\S/)){
- var text_value = $.trim(node.nodeValue);
- var spaces = node.nodeValue.split(text_value);
- node.nodeValue = spaces[0] + _t(text_value) + spaces[1];
- }
- }
- else if(node.nodeType === 1 && node.hasChildNodes()) {
- _.each(node.childNodes, function(subnode) {treat_node(subnode);});
- }
-};
-
-
-function inject_tour() {
- // if a tour is active inject tour js
-}
-
-dom_ready.then(function () {
+
+base.dom_ready.then(function () {
/* ----- PUBLISHING STUFF ---- */
$(document).on('click', '.js_publish_management .js_publish_btn', function () {
var $data = $(this).parents(".js_publish_management:first");
+ var self=this;
ajax.jsonRpc($data.data('controller') || '/website/publish', 'call', {'id': +$data.data('id'), 'object': $data.data('object')})
.then(function (result) {
$data.toggleClass("css_unpublished css_published");
@@ -389,7 +282,7 @@ dom_ready.then(function () {
// retrieve the hash before the redirect
var redirect = {
lang: self.data('lang'),
- url: self.attr('href'),
+ url: encodeURIComponent(self.attr('href').replace(/[&?]edit_translations[^&?]+/, '')),
hash: location.hash
};
location.href = _.str.sprintf("/website/lang/%(lang)s?r=%(url)s%(hash)s", redirect);
@@ -412,48 +305,37 @@ dom_ready.then(function () {
window.document.body.scrollTop = +location.hash.match(/scrollTop=([0-9]+)/)[1];
}
},0);
+});
- /* ----- WEBSITE TOP BAR ---- */
- var $collapse = $('#oe_applications ul.dropdown-menu').clone()
- .attr("id", "oe_applications_collapse")
- .attr("class", "nav navbar-nav navbar-left navbar-collapse collapse");
- $('#oe_applications').before($collapse);
- $collapse.wrap('<div class="visible-xs"/>');
- $('[data-target="#oe_applications"]').attr("data-target", "#oe_applications_collapse");
- });
- Tour.autoRunning = false;
- ready().then(function () {
- setTimeout(Tour.running,0);
+/**
+ * Object who contains all method and bind for the top bar, the template is create server side.
+ */
+
+var TopBar = Widget.extend({
+ start: function () {
+ var $collapse = this.$('#oe_applications ul.dropdown-menu').clone()
+ .attr("id", "oe_applications_collapse")
+ .attr("class", "nav navbar-nav navbar-left navbar-collapse collapse");
+ this.$('#oe_applications').before($collapse);
+ $collapse.wrap('<div class="visible-xs"/>');
+ this.$('[data-target="#oe_applications"]').attr("data-target", "#oe_applications_collapse");
+
+ return this._super();
+ }
});
-return {
- translatable: translatable,
- get_context: get_context,
- parseQS: parseQS,
- parseSearch: parseSearch,
- parseHash: parseHash,
- reload: reload,
+
+var data = {
prompt: prompt,
- error: error,
form: form,
- init_kanban: init_kanban,
- add_template_file: add_template_file,
- dom_ready: dom_ready,
if_dom_contains: if_dom_contains,
- ready: ready,
- inject_tour: inject_tour,
-
- data: data,
+ TopBar: TopBar,
};
+return data;
});
-odoo.define('web.session', function (require) {
-"use strict";
-
-var Session = require('web.Session');
-
-return new Session(null, null, {modules: ['website']});
-
+$(function () {
+ odoo.init();
});
diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js
index 0b1c7befaa1..fa65484aff5 100644
--- a/addons/website/static/src/js/website.snippets.animation.js
+++ b/addons/website/static/src/js/website.snippets.animation.js
@@ -25,42 +25,6 @@ function load_called_template () {
}
}
-function start_animation(editable_mode, $target) {
- for (var k in animationRegistry) {
- var Animation = animationRegistry[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")) {
- readyAnimation.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);
- }
- });
- }
-}
-
-function stop_animation() {
- $(readyAnimation).each(function() {
- var $snipped_id = $(this);
- if ($snipped_id.data("snippet-view")) {
- $snipped_id.data("snippet-view").stop();
- }
- });
-}
-
$(document).ready(function () {
load_called_template(); // if asset is placed into head, move this call into $(document).ready
@@ -68,35 +32,9 @@ $(document).ready(function () {
// load gallery modal template
website.add_template_file('/website/static/src/xml/website.gallery.xml');
}
-
- start_animation();
});
-
-var Animation = core.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 () {
- },
- /*
- * stop
- * This method is called to stop the animation (e.g.: when rte is launch)
- */
- stop: function () {
- },
-});
-
animationRegistry.slider = Animation.extend({
selector: ".carousel",
start: function () {
@@ -272,7 +210,6 @@ animationRegistry.gallery = Animation.extend({
}
} // click_handler
});
-
animationRegistry.gallery_slider = Animation.extend({
selector: ".o_slideshow",
start: function() {
@@ -321,13 +258,4 @@ animationRegistry.gallery_slider = Animation.extend({
}
});
-return {
- Animation: Animation,
- readyAnimation: readyAnimation,
- start_animation: start_animation,
- stop_animation: stop_animation,
- registry: animationRegistry,
-};
-
});
-
diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js
index 3be81da11ce..d9e41badd8e 100644
--- a/addons/website/static/src/js/website.snippets.editor.js
+++ b/addons/website/static/src/js/website.snippets.editor.js
@@ -1,1066 +1,19 @@
odoo.define('website.snippets.editor', function (require) {
'use strict';
-var ajax = require('web.ajax');
-var Class = require('web.Class');
-var core = require('web.core');
-var Widget = require('web.Widget');
-var editor = require('website.editor');
+var widget = require('web_editor.widget');
+var animation = require('web_editor.snippets.animation');
+var options = require('web_editor.snippets.options');
+var snippet_editor = require('web_editor.snippet.editor');
-var animation = require('website.snippets.animation');
-var website = require('website.website');
-
-var dummy = function () {};
-
-var options = {};
-
-var templateOptions = [];
-var globalSelector = {
- closest: function () { return $(); },
- all: function () { return $(); },
- is: function () { return false; },
-};
-var selector = [];
-
-website.add_template_file('/website/static/src/xml/website.snippets.xml');
-
-editor.EditorBar.include({
- start: function () {
- var self = this;
- $(document).on('click', '.o_editable', function (event) {
- var $this = $(event.srcElement);
- var tag = $this[0] && $this[0].tagName.toLowerCase();
- if (!(tag === 'a' || tag === "button") && !$this.parents("a, button").length) {
- self.$('[data-action="edit"]').parent().effect('bounce', {distance: 18, times: 5}, 250);
- }
- });
- return this._super();
- },
- edit: function () {
- var self = this;
- animation.stop_animation();
- this.on('rte:ready', this, function () {
- var $editable = $(".o_editable");
- window.snippets = this.snippets = new BuildingBlock(this, $editable);
- this.snippets.insertAfter(this.$el);
- animation.start_animation(true);
- $editable.find("*").off('mousedown mouseup click');
- this.snippets.on("snippets:ready", this, function () {
- self.trigger("snippets:ready");
- });
- });
-
- return this._super.apply(this, arguments);
- },
- save: function () {
- this.snippets.clean_for_save();
- this._super();
- },
-});
-
-/* ----- SNIPPET SELECTOR ---- */
-
-$.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;
- },
- hasData: function(node,i,m){
- return !!_.toArray(node.dataset).length;
- },
- data: function(node,i,m){
- return $(node).data(m[3]);
- }
-});
-
-
-var BuildingBlock = Widget.extend({
- template: 'website.snippets',
- activeSnippets: [],
- init: function (parent, $editable) {
- this.parent = parent;
- this.$editable = $editable;
-
- this._super.apply(this, arguments);
- if(!$('#oe_manipulators').length){
- $("<div id='oe_manipulators'></div>").appendTo('body');
- }
- this.$active_snipped_id = false;
- this.snippets = [];
- },
- 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, sc, so, ec, eo) {
- return _isNotBreakable(node, sc, so, ec, eo) || $(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();
- }
- });
- },
+snippet_editor.Class.include({
_get_snippet_url: function () {
return '/website/snippets';
- },
- _add_check_selector : function (selector, no_check) {
- var self = this;
- 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 {
- 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.parent().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;
- }
- 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");
- }
-
- // t-snippet
- $html.find('[data-oe-type="snippet"][data-oe-name]').each(function () {
- $('<div/>').insertAfter(this).append(this).attr('name', $(this).data('oe-name'));
- });
- // end
-
- 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()
- };
- templateOptions.push(option);
- selector.push(option.selector);
- });
- $styles.addClass("hidden");
- _.extend(globalSelector, {
- closest: function ($from) {
- var $temp;
- var $target;
- var len = selector.length;
- for (var i = 0; i<len; i++) {
- $temp = selector[i].closest($from, $target && $target[0]);
- if (!$target || $temp.length) {
- $target = $temp;
- }
- }
- return $target;
- },
- all: function ($from) {
- var $target;
- var len = selector.length;
- for (var i = 0; i<len; i++) {
- if (!$target) $target = selector[i].all($from);
- else $target = $target.add(selector[i].all($from));
- }
- return $target;
- },
- is: function ($from) {
- var len = selector.length;
- for (var i = 0; i<len; i++) {
- if (selector[i].is($from)) {
- return true;
- }
- }
- return false;
- },
- });
-
- // oe_snippet_body
- self.$snippets = $scroll.find(".o_panel_body").children()
- .addClass("oe_snippet")
- .each(function () {
- if (!$('.oe_snippet_thumbnail', this).size()) {
- var $div = $(
- '<div class="oe_snippet_thumbnail">'+
- '<div class="oe_snippet_thumbnail_img"/>'+
- '<span class="oe_snippet_thumbnail_title"></span>'+
- '</div>');
- $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');
- }
- });
-
- // 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 () {
- 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<this.attributes.length; k++) {
- if (this.attributes[k].name.indexOf('data-oe-') === 0) {
- $(this).removeAttr(this.attributes[k].name);
- k--;
- }
- }
- });
- // end
-
- $html.find('.o_not_editable').attr("contentEditable", false);
-
- $left_bar.append($html);
-
- // animate for list of snippet blocks
- $left_bar.on('click', '.scroll-link', function (event) {
- event.preventDefault();
- var targetOffset = $($(this).attr("href")).position().top - $ul.outerHeight() + $scroll[0].scrollTop;
- $scroll.animate({'scrollTop': targetOffset}, 750);
- });
- $scroll.on('scroll', function () {
- var middle = $scroll.height()/4;
- var $li = $ul.find("a").parent().removeClass('active');
- var last;
- for (var k=0; k<$li.length; k++) {
- var li = $($li[k]);
- if (!li.data('target')) {
- li.data('target', $($("a", li).attr("href")));
- }
- if (li.data('target').position().top > 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 cache = {};
- this.$snippet.find(".tab-pane").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 templateOptions) {
- var option = 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 () {
- 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 template = templateOptions;
- for (var k in template) {
- var Option = options[template[k]['option']];
- if (Option && Option.prototype.clean_for_save !== dummy) {
- template[k].selector.all().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 SnippetEditor(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;
-
- $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 temp = 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);
- }
-
- var $target = false;
- $target = $toInsert;
-
- setTimeout(function () {
- self.$snippet.trigger('snippet-dropped', $target);
-
- animation.start_animation(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 templateOptions) {
- 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 = $("<div class='oe_drop_zone oe_insert'></div>");
-
- 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);
- node = $drop[0].nextSibling;
- 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 = $(core.qweb.render('website.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 () {
- if ($target.data('overlay') && $target.data('overlay').hasClass("oe_active")) {
- clearTimeout(timer);
- timer = setTimeout(function () {
- 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;
}
});
-
-var Option = Class.extend({
- // initialisation (don't overwrite)
- init: function (BuildingBlock, editor, $target, option_id) {
- this.BuildingBlock = BuildingBlock;
- this.editor = editor;
- this.$target = $target;
- var option = templateOptions[option_id];
- var styles = this.$target.data("snippet-option-ids") || {};
- styles[option_id] = this;
- this.$target.data("snippet-option-ids", styles);
- this.$overlay = this.$target.data('overlay') || $('<div>');
- 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 () {
- },
-
- 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;
- var $methods = [];
- var 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) {
- 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) {
- 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: dummy
-});
-
-
var preventParentEmpty = {
hide_remove_button: function() {
this.$overlay.find('.oe_snippet_remove, .oe_snippet_move').toggleClass("hidden", !this.$target.siblings().length);
@@ -1080,119 +33,7 @@ var preventParentEmpty = {
};
-options.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) {
- 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 = $('<img class="hidden"/>');
- $image.attr("src", value);
- $image.appendTo(self.$bg);
-
- var _editor = new editor.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 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");
- }
- }
-});
-
-options.colorpicker = Option.extend({
- start: function () {
- var self = this;
- var res = this._super();
-
- if (!this.$el.find('.colorpicker').length) {
- this.$el.find('li').append( core.qweb.render('website.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");
- });
- }
-});
-
-options.slider = Option.extend({
+options.registry.slider = options.Class.extend({
drop_and_build_snippet: function() {
this.id = "myCarousel" + new Date().getTime();
this.$target.attr("id", this.id);
@@ -1201,7 +42,7 @@ options.slider = Option.extend({
this.rebind_event();
},
on_clone: function ($clone) {
- this.id = "myCarousel" + new Date().getTime();
+ var id = "myCarousel" + new Date().getTime();
$clone.attr("id", id);
$clone.find("[data-slide]").attr("href", "#" + id);
$clone.find("[data-slide-to]").attr("data-target", "#" + id);
@@ -1220,6 +61,7 @@ options.slider = Option.extend({
.first().addClass("active");
},
start : function () {
+ var self = this;
this._super();
this.$target.carousel({interval: false});
this.id = this.$target.attr("id");
@@ -1228,7 +70,7 @@ options.slider = Option.extend({
this.$target.carousel('pause');
this.rebind_event();
},
- add_slide: function (type) {
+ add_slide: function (type, value) {
if(type !== "click") return;
var self = this;
@@ -1239,7 +81,7 @@ options.slider = Option.extend({
this.$indicators.append('<li data-target="#' + this.id + '" data-slide-to="' + cycle + '"></li>');
// clone the best candidate from template to use new features
- var $snippets = this.BuildingBlock.$snippets.find('.oe_snippet_body.carousel');
+ var $snippets = this.buildingBlock.$snippets.find('.oe_snippet_body.carousel');
var point = 0;
var selection;
var className = _.compact(this.$target.attr("class").split(" "));
@@ -1260,13 +102,14 @@ options.slider = Option.extend({
},0);
return $clone;
},
- remove_slide: function (type) {
+ remove_slide: function (type, value) {
if(type !== "click") return;
if (this.remove_process) {
return;
}
var self = this;
+ var new_index = 0;
var cycle = this.$inner.find('.item').length - 1;
var index = this.$inner.find('.item.active').index();
@@ -1299,7 +142,7 @@ options.slider = Option.extend({
},
});
-options.carousel = options.slider.extend({
+options.registry.carousel = options.registry.slider.extend({
getSize: function () {
this.grid = this._super();
this.grid.size = 8;
@@ -1317,6 +160,11 @@ options.carousel = options.slider.extend({
var self = this;
this._super();
+ // set background and prepare to clean for save
+ var add_class = function (c){
+ if (c) self._class = (self._class || "").replace(new RegExp("[ ]+" + c.replace(" ", "|[ ]+")), '') + ' ' + c;
+ return self._class || "";
+ };
this.$target.on('slid.bs.carousel', function () {
if(self.editor && self.editor.styles.background) {
self.editor.styles.background.$bg = self.$target.find(".item.active");
@@ -1356,7 +204,7 @@ options.carousel = options.slider.extend({
},
});
-options.marginAndResize = Option.extend({
+options.registry.marginAndResize = options.Class.extend({
start: function () {
var self = this;
this._super();
@@ -1419,7 +267,7 @@ options.marginAndResize = Option.extend({
var regClass = new RegExp("\\s*" + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');
}
- self.BuildingBlock.editor_busy = true;
+ self.buildingBlock.editor_busy = true;
var cursor = $handle.css("cursor")+'-important';
var $body = $(document.body);
@@ -1434,7 +282,7 @@ options.marginAndResize = Option.extend({
self.$target.css("height", dy+"px");
self.$target.css("overflow", "hidden");
self.on_resize(compass, null, dy);
- self.BuildingBlock.cover_target(self.$overlay, self.$target);
+ self.buildingBlock.cover_target(self.$overlay, self.$target);
return;
}
var dd = event['page'+XY] - xy + resize[1][begin];
@@ -1457,7 +305,7 @@ options.marginAndResize = Option.extend({
if (change) {
self.on_resize(compass, beginClass, current);
- self.BuildingBlock.cover_target(self.$overlay, self.$target);
+ self.buildingBlock.cover_target(self.$overlay, self.$target);
}
};
@@ -1466,19 +314,19 @@ options.marginAndResize = Option.extend({
$body.unbind('mouseup', body_mouseup);
$body.removeClass(cursor);
setTimeout(function () {
- self.BuildingBlock.editor_busy = false;
+ self.buildingBlock.editor_busy = false;
+ self.$target.closest(".o_editable").trigger("content_changed");
},0);
self.$target.removeClass("resize_editor_busy");
- self.$target.closest(".o_editable").trigger("content_changed");
};
$body.mousemove(body_mousemove);
$body.mouseup(body_mouseup);
});
- this.$overlay.find(".oe_handle.size .auto_size").on('click', function (){
+ this.$overlay.find(".oe_handle.size .auto_size").on('click', function (event){
self.$target.css("height", "");
self.$target.css("overflow", "");
- self.BuildingBlock.cover_target(self.$overlay, self.$target);
- self.$target.closest(".o_editable").trigger("content_changed");
+ self.buildingBlock.cover_target(self.$overlay, self.$target);
+ self.$target.closest(".o_editable").trigger("content_changed");
return false;
});
},
@@ -1518,7 +366,7 @@ options.marginAndResize = Option.extend({
this.$overlay.attr("class", overlay_class);
},
-
+
/* on_resize
* called when the box is resizing and the class change, before the cover_target
* @compass: resize direction : 'n', 's', 'e', 'w'
@@ -1530,7 +378,7 @@ options.marginAndResize = Option.extend({
}
});
-options["margin-y"] = options.marginAndResize.extend({
+options.registry["margin-y"] = options.registry.marginAndResize.extend({
getSize: function () {
this.grid = this._super();
var grid = [0,4,8,16,32,48,64,92,128];
@@ -1545,7 +393,7 @@ options["margin-y"] = options.marginAndResize.extend({
},
});
-options["margin-x"] = options.marginAndResize.extend(preventParentEmpty).extend({
+options.registry["margin-x"] = options.marginAndResize.extend(preventParentEmpty).extend({
getSize: function () {
this.grid = this._super();
var width = this.$target.parents(".row:first").first().outerWidth();
@@ -1553,7 +401,7 @@ options["margin-x"] = options.marginAndResize.extend(preventParentEmpty).extend(
var grid = [1,2,3,4,5,6,7,8,9,10,11,12];
this.grid.e = [_.map(grid, function (v) {return 'col-md-'+v;}), _.map(grid, function (v) {return width/12*v;})];
- grid = [-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11];
+ var grid = [-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11];
this.grid.w = [_.map(grid, function (v) {return 'col-md-offset-'+v;}), _.map(grid, function (v) {return width/12*v;}), 12];
return this.grid;
@@ -1616,7 +464,7 @@ options["margin-x"] = options.marginAndResize.extend(preventParentEmpty).extend(
},
});
-options.resize = options.marginAndResize.extend({
+options.registry.resize = options.registry.marginAndResize.extend({
getSize: function () {
this.grid = this._super();
this.grid.size = 8;
@@ -1624,7 +472,7 @@ options.resize = options.marginAndResize.extend({
},
});
-options.parallax = Option.extend({
+options.registry.parallax = options.Class.extend({
getSize: function () {
this.grid = this._super();
this.grid.size = 8;
@@ -1654,6 +502,7 @@ options.parallax = Option.extend({
this.$target.data("snippet-view").set_values();
},
set_active: function () {
+ var value = this.$target.attr('data-scroll-background-ratio') || 0;
this.$el.find('[data-scroll]').removeClass("active")
.filter('[data-scroll="' + (this.$target.attr('data-scroll-background-ratio') || 0) + '"]').addClass("active");
},
@@ -1665,7 +514,7 @@ options.parallax = Option.extend({
}
});
-options.transform = Option.extend({
+options.registry.transform = options.Class.extend({
start: function () {
var self = this;
this._super();
@@ -1703,12 +552,12 @@ options.transform = Option.extend({
}
});
},
- style: function (type) {
+ style: function (type, value) {
if (type !== 'click') return;
var settings = this.$target.data("transfo").settings;
this.$target.transfo({ 'hide': (settings.hide = !settings.hide) });
},
- clear_style: function (type) {
+ clear_style: function (type, value) {
if (type !== 'click') return;
this.$target.removeClass("fa-spin").attr("style", "");
this.resetTransfo();
@@ -1775,42 +624,16 @@ options.transform = Option.extend({
}
});
-options.media = Option.extend({
+options.registry.ul = options.Class.extend({
start: function () {
- this._super();
- animation.start_animation(true, this.$target);
- },
- edition: function (type) {
- if(type !== "click") return;
var self = this;
- var dialog = new editor.MediaDialog(this.$target.closest('.o_editable'), this.$target[0]);
- dialog.appendTo(document.body);
- dialog.on('saved', this, function (item) {
- 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);
- }
- }
-});
-
-options.ul = Option.extend({
- start: function () {
this._super();
this.$target.data("snippet-view", new animation.registry.ul(this.$target, true));
+ this.$target.on('mouseup', '.o_ul_toggle_self, .o_ul_toggle_next', function () {
+ setTimeout(function () {
+ self.buildingBlock.cover_target(self.$overlay, self.$target);
+ },0);
+ });
},
reset_ul: function () {
this.$target.find('.o_ul_toggle_self, .o_ul_toggle_next').remove();
@@ -1851,12 +674,12 @@ options.ul = Option.extend({
});
-options.collapse = Option.extend(preventParentEmpty).extend({
+options.registry.collapse = Option.extend(preventParentEmpty).extend({
start: function () {
var self = this;
this._super();
this.$target.on('shown.bs.collapse hidden.bs.collapse', '[role="tabpanel"]', function () {
- self.BuildingBlock.cover_target(self.$overlay, self.$target);
+ self.buildingBlock.cover_target(self.$overlay, self.$target);
});
},
create_ids: function ($target) {
@@ -1911,403 +734,5 @@ options.collapse = Option.extend(preventParentEmpty).extend({
});
-var SnippetEditor = 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("<div class='oe_drop_clone' style='display: none;'/>");
- self.$target.detach();
- self.$overlay.addClass("hidden");
-
- var $selector_siblings;
- for (var i=0; i<self.selector_siblings.length; i++) {
- if (!$selector_siblings) $selector_siblings = self.selector_siblings[i].all();
- else $selector_siblings = $selector_siblings.add(self.selector_siblings[i].all());
- }
- var $selector_children;
- for (i=0; i<self.selector_children.length; i++) {
- if (!$selector_children) $selector_children = self.selector_children[i].all();
- else $selector_children = $selector_children.add(self.selector_children[i].all());
- }
-
- self.BuildingBlock.activate_insertion_zones($selector_siblings, $selector_children);
-
- $("body").addClass('move-important');
-
- self._drag_and_drop_after_insert_dropzone();
- self._drag_and_drop_active_drop_zone($('.oe_drop_zone'));
- },
- _drag_and_drop_stop: function (){
- var self = this;
- var $dropzone = this.$target.prev();
- var prev = $dropzone.length && $dropzone[0].previousSibling;
- var next = this.$target.last()[0].nextSibling;
- var $parent = this.$target.parent();
-
- $(".oe_drop_clone").after(this.$target);
-
- this.$overlay.removeClass("hidden");
- $("body").removeClass('move-important');
- $('.oe_drop_zone').droppable('destroy').remove();
- $(".oe_drop_clone, .oe_drop_to_remove").remove();
-
- if (this.dropped) {
- this.BuildingBlock.parent.rte.historyRecordUndo(this.$target);
-
- if (prev) {
- this.$target.insertAfter(prev);
- } else if (next) {
- this.$target.insertBefore(next);
- } else {
- $parent.prepend(this.$target);
- }
-
- for (var i in this.styles){
- this.styles[i].on_move();
- }
- }
-
- self.BuildingBlock.editor_busy = false;
-
- self.get_parent_block();
- setTimeout(function () {self.BuildingBlock.create_overlay(self.$target);},0);
- },
-
- load_style_options: function () {
- var self = this;
- var $styles = this.$overlay.find('.oe_options');
- var $ul = $styles.find('ul:first');
- this.styles = {};
- this.selector_siblings = [];
- this.selector_children = [];
- _.each(templateOptions, function (val, option_id) {
- if (!val.selector.is(self.$target)) {
- return;
- }
- if (val['drop-near']) self.selector_siblings.push(val['drop-near']);
- if (val['drop-in']) self.selector_children.push(val['drop-in']);
-
- var option = val.option;
- var Editor = options[option] || Option;
- var editor = self.styles[option] = new Editor(self.BuildingBlock, self, self.$target, option_id);
- $ul.append(editor.$el.addClass("snippet-option-" + option));
- editor.start();
- });
-
- if (!this.selector_siblings.length && !this.selector_children.length) {
- this.$overlay.find(".oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove").addClass('hidden');
- }
-
- if ($ul.find("li").length) {
- $styles.removeClass("hidden");
- }
- this.$overlay.find('[data-toggle="dropdown"]').dropdown();
- },
-
- get_parent_block: function () {
- var self = this;
- var $button = this.$overlay.find('.oe_snippet_parent');
- var $parent = globalSelector.closest(this.$target.parent());
- if ($parent.length) {
- $button.removeClass("hidden");
- $button.off("click").on('click', function (event) {
- event.preventDefault();
- setTimeout(function () {
- self.BuildingBlock.make_active($parent);
- }, 0);
- });
- } else {
- $button.addClass("hidden");
- }
- },
-
- /*
- * start
- * This method is called after init and _readXMLData
- */
- start: function () {
- if (!this.$target.parent().is(':o_editable')) {
- this.$overlay.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove();
- } else {
- this.$overlay.on('click', '.oe_snippet_clone', _.bind(this.on_clone, this));
- this.$overlay.on('click', '.oe_snippet_remove', _.bind(this.on_remove, this));
- this._drag_and_drop();
- }
- },
-
- on_clone: function (event) {
- event.preventDefault();
- var $clone = this.$target.clone(false);
-
- this.BuildingBlock.parent.rte.historyRecordUndo(this.$target);
-
- this.$target.after($clone);
- for (var i in this.styles){
- this.styles[i].on_clone($clone);
- }
- this.BuildingBlock.create_overlay(this.$target);
- return false;
- },
-
- on_remove: function (event) {
- event.preventDefault();
- this.on_blur();
-
- this.BuildingBlock.parent.rte.historyRecordUndo(this.$target);
-
- var index = _.indexOf(this.BuildingBlock.snippets, this.$target.get(0));
- for (var i in this.styles){
- this.styles[i].on_remove();
- }
- delete this.BuildingBlock.snippets[index];
-
- // remove node and his empty
- var node = this.$target.parent()[0];
-
- this.$target.remove();
- this.$overlay.remove();
-
- if (node && node.firstChild) {
- $.summernote.core.dom.removeSpace(node, node.firstChild, 0, node.lastChild, 1);
- if (!node.firstChild.tagName && node.firstChild.textContent === " ") {
- node.firstChild.parentNode.removeChild(node.firstChild);
- }
- }
-
- // clean editor if they are image or table in deleted content
- $(".note-control-selection").hide();
- $('.o_table_handler').remove();
-
- return false;
- },
-
- /*
- * drop_and_build_snippet
- * This method is called just after that a thumbnail is drag and dropped into a drop zone
- * (after the insertion of this.$body, if this.$body exists)
- */
- drop_and_build_snippet: function () {
- for (var i in this.styles){
- this.styles[i].drop_and_build_snippet();
- }
- },
-
- /* on_focus
- * This method is called when the user click inside the snippet in the dom
- */
- on_focus : function () {
- this.$overlay.addClass('oe_active');
- for (var i in this.styles){
- this.styles[i].on_focus();
- }
- },
-
- /* on_focus
- * This method is called when the user click outside the snippet in the dom, after a focus
- */
- on_blur : function () {
- for (var i in this.styles){
- this.styles[i].on_blur();
- }
- this.$overlay.removeClass('oe_active');
- },
-});
-
-/* t-field options */
-
-options.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 = $(core.qweb.render("website.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 () {
- self.clear();
- });
- }
- },0);
-
- // bind search input
- this.$search.find('input')
- .focus()
- .on('keyup', function() {
- self.find_existing($(this).val());
- });
-
- // bind result
- this.$ul.on('click', "li:not(:first) a", function () {
- 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: website.get_context(),
- }
- }).then(function (result){
- self.$search.siblings().remove();
- self.$search.after(core.qweb.render("website.many2one.search",{contacts:result}));
- });
- },
-
- get_contact_rendering: function (options) {
- return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
- model: 'website.qweb.field.contact',
- method: 'get_record_to_html',
- args: [[this.ID]],
- kwargs: {
- options: options,
- context: website.get_context(),
- }
- });
- },
-
- select_record: function (li) {
- var self = this;
-
- this.BuildingBlock.parent.rte.historyRecordUndo(this.$target);
-
- self.ID = +$(li).data("id");
- self.$target.attr('data-oe-many2one-id', self.ID).data('oe-many2one-id', self.ID);
-
- 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"));
- }
-
- self.clear();
- }
-});
-
-return {
- BuildingBlock: BuildingBlock,
- options: options,
- Option: Option,
- globalSelector: globalSelector,
-};
-
-});
+}); \ No newline at end of file
diff --git a/addons/website/static/src/js/website.theme.js b/addons/website/static/src/js/website.theme.js
index b3dcbc9b0b6..8d5dcc80516 100644
--- a/addons/website/static/src/js/website.theme.js
+++ b/addons/website/static/src/js/website.theme.js
@@ -19,7 +19,7 @@ ajax.jsonRpc('/web/dataset/call', 'call', {
ajax.jsonRpc('/web/dataset/call', 'call', {
'model': 'ir.ui.view',
'method': 'read_template',
- 'args': ['website.colorpicker', website.get_context()]
+ 'args': ['web_editor.colorpicker', website.get_context()]
}).done(function (data) {
QWeb.add_template(data);
});
diff --git a/addons/website/static/src/js/website.translator.js b/addons/website/static/src/js/website.translator.js
index 775012e1aa8..cb02f7346e9 100644
--- a/addons/website/static/src/js/website.translator.js
+++ b/addons/website/static/src/js/website.translator.js
@@ -2,229 +2,81 @@ odoo.define('website.translator', function (require) {
'use strict';
var core = require('web.core');
-var editor = require('website.editor');
+var ajax = require('web.ajax');
+var Widget = require('web.Widget');
+var base = require('web_editor.base');
+var translate = require('web_editor.translate');
var website = require('website.website');
-var ajax = require('ajax');
-var QWeb = core.qweb;
+var qweb = core.qweb;
-if (!website.translatable) {
- // Temporary hack until the editor bar is moved to the web client
+if (!translate.translatable) {
return;
}
-website.add_template_file('/website/static/src/xml/website.translator.xml');
-var nodialog = 'website_translator_nodialog';
-
-editor.EditorBar.include({
- do_not_translate : ['-','*','!'],
- start: function () {
- var self = this;
- this.initial_content = {};
- return this._super.apply(this, arguments).then(function () {
- var $edit_button = $("button[data-action=edit]");
- $edit_button.removeClass("hidden");
-
- if(website.no_editor) {
- $edit_button.removeProp('disabled');
- } else {
- $edit_button.parent().after(QWeb.render('website.TranslatorAdditionalButtons'));
- $('a[data-action=edit_master]').on('click', self, function(ev) {
- self.edit_master(ev);
- });
- }
- $('.js_hide_on_translate').hide();
- });
- },
- edit: function () {
- var self = this;
- var mysuper = this._super;
- if (!localStorage[nodialog]) {
- var dialog = new website.TranslatorDialog();
- dialog.appendTo($(document.body));
- dialog.on('activate', this, function () {
- localStorage[nodialog] = dialog.$('input[name=do_not_show]').prop('checked') || '';
- dialog.$el.modal('hide');
- self.translate().then(function () {
- mysuper.call(self, true);
- if(self.gengo_translate){
- self.translation_gengo_display()
- }
- });
- });
+website.TopBar.include({
+ events: _.extend({}, website.TopBar.prototype.events, {
+ 'click [data-action="edit_master"]': 'edit_master',
+ 'click [data-action="translate"]': 'translate',
+ }),
+ translate: function (ev) {
+ ev.preventDefault();
+ if (translate.edit_translations) {
+ translate.instance.edit();
} else {
- this.translate().then(function () {
- mysuper.call(self, true);
- if(self.gengo_translate){
- self.translation_gengo_display()
- }
- });
+ location.search += '&edit_translations';
}
},
edit_master: function (ev) {
ev.preventDefault();
- var link = $('.js_language_selector a[data-default-lang]')[0];
- if (link) {
- link.search += (link.search ? '&' : '?') + 'enable_editor=1';
- var url = link.pathname + link.search + window.location.hash;
- link.pathname = '/website/lang/default';
- link.search = '?' + $.param({r: url});
- window.location = link.href;
- }
+ var $link = $('.js_language_selector a[data-default-lang]');
+ $link[0].search += ($link[0].search ? '&' : '?') + 'enable_editor=1';
+ $link.click();
},
- translate: function () {
- var self = this;
- this.translations = null;
- return ajax.jsonRpc('/website/get_view_translations', 'call', {
- 'xml_id': $(document.documentElement).data('view-xmlid'),
- 'lang': website.get_context().lang,
- }).then(function (translations) {
- self.translations = translations;
- self.processTranslatableNodes();
- // Disable non translatable t-fields
- $('[data-oe-type][data-oe-translate="0"]').removeAttr('data-oe-type');
- });
- },
- processTranslatableNodes: function () {
- var self = this;
- var source_attr = 'data-oe-source-id';
- var $editables = $('[data-oe-model="ir.ui.view"]')
- .not('link, script')
- .not('.oe_snippets,.oe_snippet, .oe_snippet *, .navbar-toggle')
- .not('[data-oe-type]')
- .add($('[data-oe-translate="1"]:not([data-oe-model="ir.ui.view"])').addClass('o_editable'));
+});
- $editables.each(function () {
- var $node = $(this);
- var source_id = $node.parents('[' + source_attr + ']:first').attr(source_attr)|0;
- var view_id = $node.attr('data-oe-source-id') || source_id || $node.attr('data-oe-id');
- self.transNode(this, view_id|0);
- });
- $('.oe_translatable_text').on('paste', function () {
- var node = $(this);
- setTimeout(function () {
- self.sanitizeNode(node);
- }, 0);
- });
- $(document).on('blur keyup paste', '.oe_translatable_text[contenteditable]', function(ev) {
- var $node = $(this);
- setTimeout(function () {
- // Doing stuff next tick because paste and keyup events are
- // fired before the content is changed
- if (ev.type == 'paste') {
- self.sanitizeNode($node[0]);
- }
- if (self.getInitialContent($node[0]) !== $node.text()) {
- $node.addClass('o_dirty').removeClass('oe_translatable_todo oe_translatable_inprogress');
- }
- }, 0);
- });
- },
- getInitialContent: function (node) {
- return this.initial_content[node.attributes['data-oe-nodeid'].value];
- },
- sanitizeNode: function (node) {
- node.text(node.text());
- },
- isTextNode: function (node) {
- return node.nodeType === 3 || node.nodeType === 4;
- },
- isTranslatable: function (text) {
- return text && _.str.trim(text) !== '';
- },
- markTranslatableNode: function (node, view_id) {
- // TODO: link nodes with same content
- node.className += ' oe_translatable_text';
- node.setAttribute('data-oe-translation-view-id', view_id);
- var content = $(node).text().trim();
- var trans = this.translations.filter(function (t) {
- return t.res_id === view_id && t.value.trim() === content;
- });
- if (trans.length) {
- node.setAttribute('data-oe-translation-id', trans[0].id);
- if(trans[0].gengo_translation && (trans[0].state == 'inprogress' || trans[0].state == 'to_translate')){
- node.className += ' oe_translatable_inprogress';
- }
- } else {
- node.className += this.do_not_translate.indexOf(node.textContent.trim()) ? ' oe_translatable_todo' : '';
+
+if (!translate.edit_translations) {
+ return;
+}
+
+ajax.loadXML('/website/static/src/xml/website.translator.xml', qweb);
+
+var nodialog = 'website_translator_nodialog';
+
+var Translate = translate.Class.include({
+ onTranslateReady: function () {
+ if(this.gengo_translate){
+ this.translation_gengo_display();
}
- node.contentEditable = true;
- var nid = _.uniqueId();
- $(node).attr('data-oe-nodeid', nid);
- this.initial_content[nid] = content;
+ this._super();
},
- save: function () {
- var self = this;
- var trans = {};
- // this._super.apply(this, arguments);
- $('.oe_translatable_text.o_dirty').each(function () {
- var $node = $(this);
- var data = $node.data();
- if (!trans[data.oeTranslationViewId]) {
- trans[data.oeTranslationViewId] = [];
+ edit: function () {
+ $("#oe_main_menu_navbar").hide();
+ if (localStorage[nodialog]) {
+ return;
+ }
+ var dialog = new TranslatorDialog();
+ dialog.appendTo($(document.body));
+ dialog.on('activate', this, function () {
+ if (dialog.$('input[name=do_not_show]').prop('checked')) {
+ localStorage.removeItem(nodialog);
+ } else {
+ localStorage.setItem(nodialog, true);
}
- trans[data.oeTranslationViewId].push({
- initial_content: self.getInitialContent(this),
- new_content: $node.text(),
- translation_id: data.oeTranslationId || null
- });
+ dialog.$el.modal('hide');
});
- ajax.jsonRpc('/website/set_translations', 'call', {
- 'data': trans,
- 'lang': website.get_context()['lang'],
- }).then(function () {
- self._save();
- }).fail(function () {
- // TODO: bootstrap alert with error message
- alert("Could not save translation");
- });
- },
- transNode: function (node, view_id) {
- // Mostly handling text and cdata nodes here
- // so avoid jquery usage in this function
- if (node.attributes['data-oe-type']) {
- if (node.attributes['data-oe-translate'].value == '1') {
- node.className += ' oe_translatable_field';
- }
- this.markTranslatableNode(node, view_id);
- } else if (node.childNodes.length === 1
- && this.isTextNode(node.childNodes[0])
- && !node.getAttribute('data-oe-model')) {
- this.markTranslatableNode(node, view_id);
- } else {
- for (var i = 0, l = node.childNodes.length; i < l; i ++) {
- var n = node.childNodes[i];
- if (this.isTextNode(n)) {
- if (this.isTranslatable(n.data)) {
- var container = document.createElement('span');
- node.insertBefore(container, n);
- container.appendChild(n);
- this.markTranslatableNode(container, view_id);
- }
- } else {
- this.transNode(n, view_id);
- }
- }
- }
+ return this._super();
},
-});
-
-website.RTE.include({
- start: function () {
- this._super.apply(this, arguments);
- this.$el.hide();
- },
- fetch_editables: function (root) {
- $(root).click(function (ev) {
- ev.preventDefault();
- });
- return $('[data-oe-translate="1"]');
+ cancel: function () {
+ $("#oe_main_menu_navbar").show();
+ return this._super();
}
});
-website.TranslatorDialog = core.Widget.extend({
- events: _.extend({}, website.EditorBar.prototype.events, {
+var TranslatorDialog = Widget.extend({
+ events: _.extend({}, website.TopBar.prototype.events, {
'hidden.bs.modal': 'destroy',
'click button[data-action=activate]': function (ev) {
this.trigger('activate');
diff --git a/addons/website/static/src/xml/website.backend.xml b/addons/website/static/src/xml/website.backend.xml
index 826e29e261c..5d5f8e4c1ba 100644
--- a/addons/website/static/src/xml/website.backend.xml
+++ b/addons/website/static/src/xml/website.backend.xml
@@ -6,10 +6,4 @@
<span class="o_stat_text oe_publish text-success">Published</span>
</div>
- <t t-name="FieldTextHtmlFrame">
- <div class="oe_form_field oe_form_field_html_frame oe_form_embedded_html" t-att-style="widget.node.attrs.style">
- <iframe/>
- </div>
- </t>
-
</templates>
diff --git a/addons/website/static/src/xml/website.contentMenu.xml b/addons/website/static/src/xml/website.contentMenu.xml
index 2256f4bbf33..d6b1fe40b72 100644
--- a/addons/website/static/src/xml/website.contentMenu.xml
+++ b/addons/website/static/src/xml/website.contentMenu.xml
@@ -23,7 +23,7 @@
</li>
</t>
<t t-name="website.contentMenu.dialog.select">
- <t t-call="website.editor.dialog">
+ <t t-call="web_editor.dialog">
<t t-set="title">Select a Menu</t>
<t t-set="button">Continue</t>
<select class="form-control mb16" t-if="widget.roots">
@@ -34,7 +34,7 @@
</t>
</t>
<t t-name="website.contentMenu.dialog.edit">
- <t t-call="website.editor.dialog">
+ <t t-call="web_editor.dialog">
<t t-set="title">Edit Menu</t>
<select class="form-control mb16" t-if="widget.roots">
<t t-foreach="widget.roots" t-as="root">
@@ -58,7 +58,7 @@
</div>
</t>
</t>
-<t t-name="website.contentMenu.dialog.add" t-extend="website.editor.dialog.link">
+<t t-name="website.contentMenu.dialog.add" t-extend="web_editor.dialog.link">
<t t-jquery="t[t-set='title']" t-operation="inner">
<t t-if="!widget.update_mode">Add Menu Entry</t>
<t t-if="widget.update_mode">Edit Menu Entry</t>
diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml
index 198336650a4..ce58e3a6c1e 100644
--- a/addons/website/static/src/xml/website.editor.xml
+++ b/addons/website/static/src/xml/website.editor.xml
@@ -1,601 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
- <t t-name="website.editorbar">
- <div id="website-top-navbar" class="navbar navbar-inverse navbar-fixed-top hidden-xs" style="display: none;">
- <div class="navbar-header" id="website-top-edit">
- <ul class="nav navbar-nav js_editor_placeholder" style="position: absolute; left: 180px;">
- </ul>
- <form class="navbar-form text-muted">
- <button type="button" data-action="save"
- class="btn btn-primary btn-xs">Save</button>
- <span>or</span>
- <a href="#" data-action="cancel" class="btn btn-link btn-xs">Discard</a>
- </form>
- </div>
- </div>
- </t>
-
- <t t-name="website.editor.dialog">
- <div class="modal note-link-dialog" tabindex="-1" role="dialog" aria-hidden="true">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
- <h3 class="modal-title"><t t-esc="title"/></h3>
- </div>
- <div class="modal-body"><t t-raw="0"/></div>
- <div class="modal-footer">
- <button type="button" class="btn btn-primary save" t-if="!button">Save</button>
- <button type="button" class="btn btn-primary save" t-if="button"><t t-raw="button"/></button>
- <button type="button" class="btn hidden wait" disabled="disabled"/>
- or
- <a href="#" data-dismiss="modal" aria-hidden="true">Discard</a>
- </div>
- </div>
- </div>
- </div>
- </t>
- <t t-name="website.editor.dialog.link.footer-button">
- <button type="button" class="btn pull-left remove btn-danger">Remove Link</button>
- </t>
- <t t-name="website.editor.dialog.link">
- <t t-call="website.editor.dialog">
- <t t-set="title">Link to</t>
- <form>
- <div class="list-group">
- <div class="form-group list-group-item active">
- <h3 class="list-group-item-heading">
- <label for="link-new" class="control-label">
- Link Label
- </label>
- </h3>
- <input type="text" class="form-control" id="link-text" required="required"/>
- </div>
- </div>
- <ul class="list-group">
- <li class="list-group-item form-group active">
- <h4 class="list-group-item-heading">
- <label for="link-page" class="control-label">
- Page
- </label>
- </h4>
- <input class="page form-control url-source"
- id="link-page" type="hidden"/>
- </li>
- <li class="list-group-item form-group clearfix">
- <div class="pull-right" style="display: none;">
- <label>
- <input type="checkbox" class="window-new"/>
- Open in new window
- </label>
- </div>
- <h4 class="list-group-item-heading">
- <label for="link-external" class="control-label">
- URL or Email Address
- </label>
- </h4>
- <input type="text" class="form-control url email-address url-source"
- id="link-external" placeholder="http://openerp.com"/>
- </li>
- </ul>
- <div class="form-horizontal link-style">
- <div class="form-group">
- <label class="col-sm-2 control-label">
- Style
- </label>
- <div class="col-sm-4">
- <div class="dropdown" style="line-height: 2em;">
- <a data-toggle="dropdown" class="btn btn-default" href="#">Color Style <span class="caret"></span></a>
- <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
- <li>
- <label class="btn btn-link">Link<input value="" class="link-style" type="radio" name="link-style-type" checked="checked"/></label>
- </li>
- <li>
- <label class="btn btn-default">Basic<input value="btn-default" class="link-style" type="radio" name="link-style-type"/></label>
- </li>
- <li>
- <label class="btn btn-success">Success<input value="btn-success" class="link-style" type="radio" name="link-style-type"/></label>
- </li>
- <li>
- <label class="btn btn-primary">Primary<input value="btn-primary" class="link-style" type="radio" name="link-style-type"/></label>
- </li>
- <li>
- <label class="btn btn-info">Info<input value="btn-info" class="link-style" type="radio" name="link-style-type"/></label>
- </li>
- <li>
- <label class="btn btn-warning">Warning<input value="btn-warning" class="link-style" type="radio" name="link-style-type"/></label>
- </li>
- <li>
- <label class="btn btn-danger">Danger<input value="btn-danger" class="link-style" type="radio" name="link-style-type"/></label>
- </li>
- </ul>
- </div>
- </div>
- <div class="col-sm-4">
- <div class="dropdown" style="line-height: 2em;">
- <a data-toggle="dropdown" class="btn btn-default" href="#">Size <span class="caret"></span></a>
- <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
- <li>
- <label class="btn btn-default btn-xs">Extra Small<input value="btn-xs" class="link-style" type="radio" name="link-style-size"/></label>
- </li>
- <li>
- <label class="btn btn-default btn-sm">Small<input value="btn-sm" class="link-style" type="radio" name="link-style-size"/></label>
- </li>
- <li>
- <label class="btn btn-default">Default<input value="" class="link-style" type="radio" name="link-style-size" checked="checked"/></label>
- </li>
- <li>
- <label class="btn btn-default btn-lg">Large<input value="btn-lg" class="link-style" type="radio" name="link-style-size"/></label>
- </li>
- </ul>
- </div>
- </div>
- </div>
- </div>
- <div class="form-horizontal link-style">
- <div class="form-group">
- <label class="col-sm-2 control-label">
- Preview
- </label>
- <div class="col-sm-10" style="line-height: 2em;">
- <div style="overflow: auto; max-width: 100%; max-height: 200px;">
- <a id="link-preview" href="#"></a>
- </div>
- </div>
- </div>
- </div>
- </form>
- </t>
- </t>
- <t t-name="website.editor.dialog.media">
- <div class="modal note-image-dialog" tabindex="-1" role="dialog" aria-hidden="true">
- <div class="modal-dialog select-media">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
- <h3 class="modal-title">Select a Media</h3>
- </div>
- <div class="modal-body">
- <ul class="nav nav-tabs">
- <li class="active"><a href="#editor-media-image" data-toggle="tab">Image</a></li>
- <li><a href="#editor-media-icon" data-toggle="tab">Pictogram</a></li>
- <li><a href="#editor-media-video" data-toggle="tab">Video</a></li>
- <li class="search" style="float: right;">
- <ul class="pager mb0 mt0">
- <li class="previous disabled"><a href="#">← Previous</a></li>
- <li class="next disabled"><a href="#">Next →</a></li>
- </ul>
- </li>
- <li class="search" style="float: right;">
- <form action="#">
- <div class="form-group font-icons fa fa-search mb0">
- <input type="search" class="form-control" id="icon-search"/>
- </div>
- </form>
- </li>
- </ul>
-
- <!-- Tab panes -->
- <div class="tab-content">
- <div class="tab-pane fade in active" id="editor-media-image">
- </div>
- <div class="tab-pane fade" id="editor-media-icon">
- </div>
- <div class="tab-pane fade" id="editor-media-video">
- </div>
- </div>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-primary save">Save</button>
- <button type="button" class="btn hidden wait" disabled="disabled"/>
- or
- <a href="#" data-dismiss="modal" aria-hidden="true">Discard</a>
- </div>
- </div>
- </div>
- </div>
- </t>
- <t t-name="website.editor.dialog.image">
- <div>
- <form method="POST"
- action="/website/attach"
- enctype="multipart/form-data"
- target="fileframe"
- class="form-inline">
- <div class="well">
- <div class="form-group pull-left">
- <input type="file" name="upload" accept="image/*" multiple="multiple" style="position: absolute; opacity: 0; width: 1px; height: 1px;"/>
- <input type="hidden" name="disable_optimization" value=""/>
- <div class="btn-group">
- <button type="button" class="btn btn-primary filepicker">Upload an image from your computer</button>
- <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
- <span class="caret"></span>
- <span class="sr-only">Alternate Upload</span>
- </button>
- <ul class="dropdown-menu" role="menu">
- <li><a href="#" class="js_disable_optimization">Upload image without optimization</a></li>
- </ul>
- </div>
- <button type="button" class="btn hidden wait" disabled="disabled">Uploading...</button>
- </div>
- <div>
- <span class="text-muted"> — or — </span>
- <label for="iamgeurl">Add an image URL</label>
- <div class="form-group btn-group">
- <input type="text" name="url" class="form-control url pull-left" style="width: 320px;" id="iamgeurl" placeholder="http://openerp.com/logo.png"/>
- <button class="btn btn-default" type="submit">Add</button>
- </div>
- </div>
- </div>
- <input type="hidden" name="func"/>
- <div class="help-block"/>
- <div class="existing-attachments">
- <t t-call="website.editor.dialog.image.existing"/>
- </div>
- </form>
- </div>
- <iframe src="about:blank" name="fileframe" class="hidden"/>
- </t>
- <t t-name="website.editor.dialog.image.existing">
- <div class="modal" tabindex="-1" role="dialog" aria-hidden="true">
- <div class="modal-dialog select-image">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
- <h3 class="modal-title">Select a Picture</h3>
- </div>
- <div class="modal-body has-error">
- <div class="existing-attachments"/>
- <div class="help-block"/>
- </div>
- <div class="modal-footer">
- <a href="#" data-dismiss="modal" aria-hidden="true">Discard</a>
- </div>
- </div>
- </div>
- </div>
- </t>
- <t t-name="website.editor.dialog.image.existing.content">
- <div class="existing-attachments">
- <div class="row mt16" t-foreach="rows" t-as="row">
- <div class="col-sm-2 existing-attachment-cell"
- t-foreach="row" t-as="attachment">
- <i class="fa fa-times existing-attachment-remove" t-att-data-id="attachment.id"/>
- <img t-att-src="attachment.website_url" t-att-alt="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" class="img img-responsive"/>
- </div>
- </div>
- </div>
- </t>
- <t t-name="website.editor.dialog.image.existing.error">
- <div class="help-block">
- <p>The image could not be deleted because it is used in the
- following pages or views:</p>
- <ul t-foreach="views" t-as="view">
- <li>
- <a t-attf-href="/web#model=ir.ui.view&amp;id=#{view.id}">
- <t t-esc="view.name"/>
- </a>
- </li>
- </ul>
- </div>
- </t>
- <t t-name="website.editor.table.panel">
- <table class="editorbar-panel">
- <tr t-foreach="rows"><td t-foreach="cols">&#8203;</td></tr>
- </table>
- </t>
- <t t-name="website.editor.table">
- <table class="table table-bordered table-responsive">
- <tbody>
- <tr t-foreach="rows"><td t-foreach="cols">&#8203;</td></tr>
- </tbody>
- </table>
- </t>
-
- <t t-name="website.editor.discard">
- <div class="modal" tabindex="-1" role="dialog" aria-hidden="true">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
- <h3 class="modal-title">Discard edition</h3>
- </div>
- <div class="modal-body">
- <p>
- If you discard the current edition, <em>all</em>
- unsaved changes will be lost.
- </p>
- <p>You can cancel to return to the edition mode.</p>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-danger">Discard</button>
- or
- <a href="#" data-dismiss="modal" aria-hidden="true">Cancel</a>
- </div>
- </div>
- </div>
- </div>
- </t>
-
- <t t-name="website.editor.hoverbutton.link">
- <button contentEditable="false" type="button" data-event="showLinkDialog" tabindex="-1" class="btn btn-primary hover-edition-button btn-xs">Change</button>
- </t>
-
- <t t-name="website.editor.hoverbutton.media">
- <div contentEditable="false" class="hover-edition dropdown">
- <a class="btn btn-primary btn-sm" data-toggle="dropdown" href="#">Customize</a>
- <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
- <li><a href="#" class="hover-edition-button">Change Media</a></li>
- <li><a href="#" class="hover-style-button">Style</a></li>
- <li class="dropdown-submenu">
- <a href="#" tabindex="-1">Rotation</a>
- <ul class="dropdown-menu">
- <li data-value="fa-spin"><a>Spin</a></li>
- <li data-value="fa-flip-horizontal"><a>Horizontal flip</a></li>
- <li data-value="fa-flip-vertical"><a>Vertical flip</a></li>
- </ul>
- </li>
- <li><a href="#" data-value="fa-border">border</a></li>
- </ul>
- </div>
- </t>
- <t t-name="website.editor.dialog.alt">
- <t t-call="website.editor.dialog">
- <t t-set="title">Change media Alt and Title</t>
- <form action="#" class="form-horizontal">
- <div class="form-group">
- <label for="alt" class="col-sm-2 control-label">Alt:</label>
- <div class="col-sm-9"><input id="alt" type="text" class="form-control" required="required" t-att-value="widget.alt"/></div>
- </div>
- <div class="form-group">
- <label for="title" class="col-sm-2 control-label">Title:</label>
- <div class="col-sm-9"><input id="title" type="text" class="form-control" required="required" t-att-value="widget.title"/></div>
- </div>
- </form>
+ <t t-extend="web_editor.dialog.link">
+ <t t-jquery=".list-group-item.form-group">
+ this.removeClass('active');
</t>
- </t>
-
- <t t-name="website.editor.dialog.font-icons">
- <form action="#">
- <input type="hidden" id="fa-icon"/>
- <input type="hidden" id="fa-size" value="fa-1x"/>
- <div class="font-icons-icons">
- <t t-call="website.editor.dialog.font-icons.icons">
- <t t-set="iconsParser" t-value="widget.iconsParser"/>
- </t>
- </div>
- </form>
- </t>
- <t t-name="website.editor.dialog.font-icons.icons">
- <t t-foreach="iconsParser" t-as="data">
- <span t-foreach="data.icons" t-as="icon"
- t-attf-class="#{data.base} font-icons-icon #{icon}"
- t-att-data-id="icon"/>
+ <t t-jquery=".list-group-item.form-group:has(#link-external)" t-operation="before">
+ <li class="list-group-item form-group">
+ <h4 class="list-group-item-heading">
+ <label for="link-page" class="control-label">
+ Page
+ </label>
+ </h4>
+ <input class="page form-control url-source" id="link-page" type="hidden"/>
+ </li>
</t>
</t>
- <t t-name="website.editor.dialog.video">
- <form action="#" class="form-inline">
- <div class="well">
- <div class="form-group btn-group">
- <span class="text-muted pull-right" style="margin-left:10px; line-height: 2em;">(Youtube, Vimeo, Dailymotion)</span>
- <label for="urlvideo" style="width: 220px; line-height: 2em;" class="pull-left">Set a video URL</label>
- <input type="text"
- name="url"
- class="form-control url pull-left"
- style="width: 400px;"
- id="urlvideo"
- placeholder="//www.youtube.com/embed/yws1tbgNV7k"
- t-translation="off"/>
- <button class="btn btn-default">Preview</button>
- </div>
- <div class="form-group btn-group">
- <label for="urlvideo" style="width: 220px; line-height: 2em;" class="pull-left"><span class="text-muted">— or —</span> Embed Video (HTML)</label>
- <input type="text"
- name="embed"
- class="form-control url pull-left"
- style="width: 400px;"
- id="embedvideo"
- placeholder='&lt;iframe src="//www.youtube.com/embed/yws1tbgNV7k"&gt;&lt;/iframe&gt;'
- t-translation="off"/>
- <button class="btn btn-default">Preview</button>
- </div>
- </div>
-
- <div id="video-preview" style="width: 450px; margin: 0 auto;">
- <div class="media_iframe_video">
- <div class="media_iframe_video_size"></div>
- <iframe
- src=""
- frameborder="0"
- allowfullscreen="allowfullscreen"></iframe>
- </div>
- </div>
-
- <div class="text-center mt32">
- <div class="checkbox">
- <label><input type="checkbox" id="autoplay"/> Autoplay</label>
- </div>
- </div>
- </form>
- </t>
- <t t-name="website.editor.insert.inline.media">
- <p class="inline-media-link" contenteditable="false">
- <a class="editor-insert-media fa fa-plus-circle"></a>
- </p>
- </t>
- <t t-name="website.editor.tplButton">
- <button type="button" t-att-class="'btn btn-default btn-sm btn-small ' +
- (options.className ? options.className : '') +
- (options.dropdown ? 'dropdown-toggle' : '') "
- t-att-data-toggle= "options.dropdown ? 'dropdown' : ''"
- t-att-title="options.title ? options.title : ''"
- t-att-data-event="options.event ? options.event : ''"
- t-att-data-value="options.value ? options.value : ''"
- t-att-tabindex="-1"><t t-raw="options.sLabel"/>
- <t t-if="options.dropdown">
- <span class="caret"></span>
- </t>
- </button>
- <t t-raw="options.dropdown or ''"/>
- </t>
- <t t-name="website.editor.tplAirPopover">
- <div class="popover bottom in note-air-popover" style="display: none;">
- <div class="arrow"></div>
- <div class="popover-content">
- <t t-foreach="options.airPopover" t-as="group">
- <div t-attf-class="note-#{group[0]} btn-group">
- <t t-foreach="group[1]" t-as="g">
- <t t-call="website.editor.tplButton">
- <t t-set="options" t-value="buttonInfo[g](lang ,options)"/>
- </t>
- </t>
- </div>
- </t>
- </div>
- </div>
- </t>
- <t t-name="website.editor.tplLinkPopover">
- <div class="note-link-popover popover bottom in'" style="display: none;">
- <div class="arrow"></div>
- <div class="popover-content">
- <a href="http://www.google.com" target="_blank">www.google.com</a>
- <div class="note-insert btn-group">
- <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.link.edit}" data-event="showLinkDialog" tabindex="-1">
- <i class="fa fa-pencil-square-o"></i>
- </button>
- <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.link.unlink}" data-event="unlink" tabindex="-1">
- <i class="fa fa-chain-broken"></i>
- </button>
- </div>
- </div>
- </div>
- </t>
- <t t-name="website.editor.tplLinkImagePopover">
- <div class="note-link-image-popover popover bottom in'" style="display: none;">
- <div class="arrow"></div>
- <div class="popover-content">
- <a href="http://www.google.com" target="_blank">www.google.com</a>
- <div class="note-insert btn-group">
- <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.image}" data-event="showImageDialog" tabindex="-1">
- <i class="fa fa-picture-o"></i>
- </button>
- </div>
- </div>
- </div>
- </t>
- <t t-name="website.editor.tplImagePopover">
- <div class="note-image-popover popover bottom in'" style="display: none;">
- <div class="arrow"></div>
- <div class="popover-content">
- <a href="http://www.google.com" target="_blank">www.google.com</a>
- <div class="note-insert btn-group">
- <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.resizeFull}" data-event="resize" data-value="1" tabindex="-1">
- <span class="note-fontsize-10">100%</span>
- </button>
- <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.resizeHalf}" data-event="resize" data-value="0.5" tabindex="-1">
- <span class="note-fontsize-10">50%</span>
- </button>
- <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.resizeQuarter}" data-event="resize" data-value="0.25" tabindex="-1">
- <span class="note-fontsize-10">25%</span>
- </button>
- </div>
- <div class="note-insert btn-group">
- <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.floatLeft}" data-event="floatMe" data-value="left" tabindex="-1">
- <i class="fa fa-align-left"></i>
- </button>
- <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.floatRight}" data-event="floatMe" data-value="right" tabindex="-1">
- <i class="fa fa-align-right"></i>
- </button>
- <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.floatNone}" data-event="floatMe" data-value="none" tabindex="-1">
- <i class="fa fa-align-justify"></i>
- </button>
- </div>
- <div class="note-insert btn-group">
- <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.remove}" data-event="removeMedia" data-value="none" tabindex="-1">
- <i class="fa fa-trash-o "></i>
- </button>
- </div>
- </div>
- </div>
- </t>
- <t t-name="website.editor.tplPopovers">
- <div class="note-popover">
- <t t-call="website.editor.tplLinkPopover"/>
- <t t-call="website.editor.tplLinkImagePopover"/>
- <t t-call="website.editor.tplImagePopover"/>
- <t t-if="options.airMode">
- <t t-call="website.editor.tplAirPopover"/>
- </t>
- </div>
- </t>
-
- <t t-name="website.editor.tplHandles">
- <div class="note-handle">
- <div class="note-control-selection">
- <div class="note-control-selection-bg"></div>
- <div class="note-control-holder note-control-nw"></div>
- <div class="note-control-holder note-control-ne"></div>
- <div class="note-control-holder note-control-sw"></div>
- <div class="note-control-sizing note-control-se"></div>
- <div class="note-control-selection-info"></div>
- </div>
- </div>
- </t>
- <t t-name="website.editor.table">
- <ul class="dropdown-menu">
- <div class="note-dimension-picker">
- <div class="note-dimension-picker-mousecatcher" data-event="insertTable" data-value="1x1"></div>
- <div class="note-dimension-picker-highlighted"></div>
- <div class="note-dimension-picker-unhighlighted"></div>
- </div>
- <div class="note-dimension-display"> 1 x 1 </div>
- </ul>
- </t>
- <t t-name="website.editor.color">
- <ul class="dropdown-menu">
- <li>
- <div class="btn-group">
- <div class="note-palette-title"><t t-raw="lang.color.background"/></div>
- <div class="note-color-reset" data-event="backColor" data-value="inherit" t-att-title="lang.color.transparent">
- <t t-raw="lang.color.setTransparent"/>
- </div>
- <div class="note-color-palette" data-target-event="backColor"></div>
- </div>
- <div class="btn-group">
- <div class="note-palette-title"><t t-raw="lang.color.foreground"/></div>
- <div class="note-color-reset" data-event="foreColor" data-value="inherit" t-att-title="lang.color.reset">
- <t t-raw="lang.color.resetToDefault"/>
- </div>
- <div class="note-color-palette" data-target-event="foreColor"></div>
- </div>
- </li>
- </ul>
- </t>
- <t t-name="website.editor.paragraph">
- <div class="dropdown-menu">
- <div class="note-align btn-group">
- <t t-call="website.editor.tplButton">
- <t t-set="options" t-value="leftButton"/>
- </t>
- <t t-call="website.editor.tplButton">
- <t t-set="options" t-value="centerButton"/>
- </t>
- <t t-call="website.editor.tplButton">
- <t t-set="options" t-value="rightButton"/>
- </t>
- <t t-call="website.editor.tplButton">
- <t t-set="options" t-value="justifyButton"/>
- </t>
- </div>
- <div class="note-list btn-group">
- <t t-call="website.editor.tplButton">
- <t t-set="options" t-value="indentButton"/>
- </t>
- <t t-call="website.editor.tplButton">
- <t t-set="options" t-value="outdentButton"/>
- </t>
- </div>
- </div>
- </t>
</templates>
diff --git a/addons/website/static/src/xml/website.gallery.xml b/addons/website/static/src/xml/website.gallery.xml
index bf25990a713..21fe07dca4a 100644
--- a/addons/website/static/src/xml/website.gallery.xml
+++ b/addons/website/static/src/xml/website.gallery.xml
@@ -18,7 +18,7 @@
<h4 class="modal-title">Add images</h4>
</div>
<div class="modal-body">
- <form action="/website/attach" method="post" enctype="multipart/form-data" >
+ <form action="/web_editor/attachment/add" method="post" enctype="multipart/form-data" >
<div class="form-group">
<input name="upload" type="file" value="Choose images" multiple="multiple" accept="image/*"/>
</div>
diff --git a/addons/website/static/src/xml/website.translator.xml b/addons/website/static/src/xml/website.translator.xml
index 0019f8d9d3a..8e9c9564f08 100644
--- a/addons/website/static/src/xml/website.translator.xml
+++ b/addons/website/static/src/xml/website.translator.xml
@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
+
<t t-name="website.TranslatorAdditionalButtons">
<li><a class="btn btn-link" data-action="edit_master" href="#"> or Edit Master</a></li>
</t>
+
<t t-name="website.TranslatorDialog">
<div class="modal fade oe_website_translator" tabindex="-1" role="dialog">
<div class="modal-dialog">
@@ -19,10 +21,10 @@
<p>
Here are the visuals used to help you translate efficiently:
<ul class="oe_translate_examples">
- <li class="oe_translatable_todo">
+ <li data-oe-translation-state="to_translate">
Content to translate
</li>
- <li class="oe_translatable_text">
+ <li data-oe-translation-state="translated">
Translated content
</li>
</ul>
@@ -37,7 +39,7 @@
</section>
</div>
<div class="modal-footer">
- <div class="pull-right">
+ <div class="pull-left">
<input type="checkbox" name="do_not_show"/>
Do not show this dialog later.
</div>
diff --git a/addons/website/tests/test_converter.py b/addons/website/tests/test_converter.py
index 0ab71829f9e..ba60e2fac95 100644
--- a/addons/website/tests/test_converter.py
+++ b/addons/website/tests/test_converter.py
@@ -1,15 +1,8 @@
# -*- coding: utf-8 -*-
-import textwrap
import unittest2
-
-from lxml import etree, html
-from lxml.builder import E
-
-from openerp.tests import common
-from openerp.addons.base.ir import ir_qweb
-from openerp.addons.website.models.ir_qweb import html_to_text
from openerp.addons.website.models.website import slugify, unslug
+
class TestUnslug(unittest2.TestCase):
def test_unslug(self):
tests = {
@@ -31,237 +24,6 @@ class TestUnslug(unittest2.TestCase):
self.assertEqual(unslug(slug), expected)
-class TestHTMLToText(unittest2.TestCase):
- def test_rawstring(self):
- self.assertEqual(
- "foobar",
- html_to_text(E.div("foobar")))
-
- def test_br(self):
- self.assertEqual(
- "foo\nbar",
- html_to_text(E.div("foo", E.br(), "bar")))
-
- self.assertEqual(
- "foo\n\nbar\nbaz",
- html_to_text(E.div(
- "foo", E.br(), E.br(),
- "bar", E.br(),
- "baz")))
-
- def test_p(self):
- self.assertEqual(
- "foo\n\nbar\n\nbaz",
- html_to_text(E.div(
- "foo",
- E.p("bar"),
- "baz")))
-
- self.assertEqual(
- "foo",
- html_to_text(E.div(E.p("foo"))))
-
- self.assertEqual(
- "foo\n\nbar",
- html_to_text(E.div("foo", E.p("bar"))))
- self.assertEqual(
- "foo\n\nbar",
- html_to_text(E.div(E.p("foo"), "bar")))
-
- self.assertEqual(
- "foo\n\nbar\n\nbaz",
- html_to_text(E.div(
- E.p("foo"),
- E.p("bar"),
- E.p("baz"),
- )))
-
- def test_div(self):
- self.assertEqual(
- "foo\nbar\nbaz",
- html_to_text(E.div(
- "foo",
- E.div("bar"),
- "baz"
- )))
-
- self.assertEqual(
- "foo",
- html_to_text(E.div(E.div("foo"))))
-
- self.assertEqual(
- "foo\nbar",
- html_to_text(E.div("foo", E.div("bar"))))
- self.assertEqual(
- "foo\nbar",
- html_to_text(E.div(E.div("foo"), "bar")))
-
- self.assertEqual(
- "foo\nbar\nbaz",
- html_to_text(E.div(
- "foo",
- E.div("bar"),
- E.div("baz")
- )))
-
- def test_other_block(self):
- self.assertEqual(
- "foo\nbar\nbaz",
- html_to_text(E.div(
- "foo",
- E.section("bar"),
- "baz"
- )))
-
- def test_inline(self):
- self.assertEqual(
- "foobarbaz",
- html_to_text(E.div("foo", E.span("bar"), "baz")))
-
- def test_whitespace(self):
- self.assertEqual(
- "foo bar\nbaz",
- html_to_text(E.div(
- "foo\nbar",
- E.br(),
- "baz")
- ))
-
- self.assertEqual(
- "foo bar\nbaz",
- html_to_text(E.div(
- E.div(E.span("foo"), " bar"),
- "baz")))
-
-class TestConvertBack(common.TransactionCase):
- def setUp(self):
- super(TestConvertBack, self).setUp()
-
- def field_rountrip_result(self, field, value, expected):
- model = 'website.converter.test'
- Model = self.registry(model)
- id = Model.create(
- self.cr, self.uid, {
- field: value
- })
- [record] = Model.browse(self.cr, self.uid, [id])
-
- e = etree.Element('span')
- field_value = 'record.%s' % field
- e.set('t-field', field_value)
-
- rendered = self.registry('website.qweb').render_tag_field(
- e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, {
- 'record': record,
- }, context={'inherit_branding': True}))
- element = html.fromstring(
- rendered, parser=html.HTMLParser(encoding='utf-8'))
-
- converter = self.registry('website.qweb').get_converter_for(
- element.get('data-oe-type'))
-
- value_back = converter.from_html(
- self.cr, self.uid, model, Model._fields[field], element)
-
- if isinstance(expected, str):
- expected = expected.decode('utf-8')
- self.assertEqual(value_back, expected)
-
- def field_roundtrip(self, field, value):
- self.field_rountrip_result(field, value, value)
-
- def test_integer(self):
- self.field_roundtrip('integer', 42)
-
- def test_float(self):
- self.field_roundtrip('float', 42.567890)
- self.field_roundtrip('float', 324542.567890)
-
- def test_numeric(self):
- self.field_roundtrip('numeric', 42.77)
-
- def test_char(self):
- self.field_roundtrip('char', "foo bar")
-
- self.field_roundtrip('char', "ⒸⓄⓇⒼⒺ")
-
- def test_selection(self):
- self.field_roundtrip('selection', 3)
-
- def test_selection_str(self):
- self.field_roundtrip('selection_str', 'B')
-
- def test_text(self):
- self.field_roundtrip('text', textwrap.dedent("""\
- You must obey the dance commander
- Givin' out the order for fun
- You must obey the dance commander
- You know that he's the only one
- Who gives the orders here,
- Alright
- Who gives the orders here,
- Alright
-
- It would be awesome
- If we could dance-a
- It would be awesome, yeah
- Let's take the chance-a
- It would be awesome, yeah
- Let's start the show
- Because you never know
- You never know
- You never know until you go"""))
-
- def test_m2o(self):
- """ the M2O field conversion (from html) is markedly different from
- others as it directly writes into the m2o and returns nothing at all.
- """
- model = 'website.converter.test'
- field = 'many2one'
-
- Sub = self.registry('website.converter.test.sub')
- sub_id = Sub.create(self.cr, self.uid, {'name': "Foo"})
- sub2_id = Sub.create(self.cr, self.uid, {'name': "Bar"})
-
- Model = self.registry(model)
- id = Model.create(self.cr, self.uid, {field: sub_id})
- [record] = Model.browse(self.cr, self.uid, [id])
-
- e = etree.Element('span')
- field_value = 'record.%s' % field
- e.set('t-field', field_value)
-
- rendered = self.registry('website.qweb').render_tag_field(
- e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, {
- 'record': record,
- }, context={'inherit_branding': True}))
-
- element = html.fromstring(rendered, parser=html.HTMLParser(encoding='utf-8'))
- # emulate edition
- element.set('data-oe-many2one-id', str(sub2_id))
- element.text = "New content"
-
- converter = self.registry('website.qweb').get_converter_for(
- element.get('data-oe-type'))
-
- value_back = converter.from_html(
- self.cr, self.uid, model, Model._fields[field], element)
-
- self.assertIsNone(
- value_back, "the m2o converter should return None to avoid spurious"
- " or useless writes on the parent record")
-
- self.assertEqual(
- Sub.browse(self.cr, self.uid, sub_id).name,
- "Foo",
- "element edition can't change directly the m2o record"
- )
- self.assertEqual(
- Model.browse(self.cr, self.uid, id).many2one.name,
- "Bar",
- "element edition should have been change the m2o id"
- )
-
class TestTitleToSlug(unittest2.TestCase):
"""
Those tests should pass with or without python-slugify
diff --git a/addons/website/tests/test_ui.py b/addons/website/tests/test_ui.py
index a944a9ce3d6..c1e45e743bd 100644
--- a/addons/website/tests/test_ui.py
+++ b/addons/website/tests/test_ui.py
@@ -3,13 +3,10 @@ import openerp.tests
class TestUi(openerp.tests.HttpCase):
def test_01_public_homepage(self):
- self.phantom_js("/", "console.log('ok')", "odoo.__DEBUG__.services['website.snippets.animation']")
+ self.phantom_js("/", "console.log('ok')", "'website.snippets.animation' in odoo.__DEBUG__.services")
def test_02_admin_homepage(self):
- self.phantom_js("/", "console.log('ok')", "odoo.__DEBUG__.services['website.snippets.editor']", login='admin')
+ self.phantom_js("/", "console.log('ok')", "'website.snippets.editor' in odoo.__DEBUG__.services", login='admin')
- def test_03_admin_rte(self):
- self.phantom_js("/", "odoo.__DEBUG__.services['web.Tour'].run('website_rte', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.website_rte", login='admin')
-
- def test_04_admin_tour_banner(self):
+ def test_03_admin_tour_banner(self):
self.phantom_js("/", "odoo.__DEBUG__.services['web.Tour'].run('banner', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.banner", login='admin')
diff --git a/addons/website/views/snippets.xml b/addons/website/views/snippets.xml
index af66c71d373..606e95c8503 100644
--- a/addons/website/views/snippets.xml
+++ b/addons/website/views/snippets.xml
@@ -888,13 +888,13 @@
</div>
<div id="snippet_options" class="hidden">
-
<t t-call="website.snippet_options"/>
-
</div>
</template>
<template id="snippet_options">
+
+ <t t-call="web_editor.snippet_options"/>
<div data-js='blog-style'
data-selector="section:not(.carousel):not(.parallax):not(.o_gallery)">
@@ -1018,11 +1018,6 @@
<li data-remove="remove"><a>Remove from gallery</a></li>
</div>
- <div data-js='media'
- data-selector="img, .media_iframe_video, span.fa, i.fa, .glyphicon">
- <li data-edition="edition"><a>Change...</a></li>
- </div>
-
<div data-js='carousel'
data-selector=":not(.o_gallery > .container) > .carousel">
<li class="dropdown-submenu">
@@ -1123,14 +1118,6 @@
data-selector='.panel-group > .panel:has(> .panel-collapse)'
data-drop-in='.panel-group:has(> .panel > .panel-collapse)'>
</div>
-
- <!-- t-field -->
-
- <div data-js='many2one'
- data-selector="[data-oe-many2one-model]"
- data-no-check="true">
- </div>
-
</template>
diff --git a/addons/website/views/website.backend.xml b/addons/website/views/website.backend.xml
index 2590051499c..1e10a49dfa7 100644
--- a/addons/website/views/website.backend.xml
+++ b/addons/website/views/website.backend.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
- <data>
<template id="assets_backend" name="website assets for backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
@@ -9,68 +8,4 @@
</xpath>
</template>
-<template id="assets_backend_editor" name="assets for backend editor">
- <t t-call="web.assets_common"/>
- <script type="text/javascript" src="/web/static/src/js/watch.js"></script>
- <script type="text/javascript" src="/website/static/src/js/website.js"></script>
- <script type="text/javascript" src="/website/static/src/js/website.snippets.animation.js"></script>
- <script type="text/javascript" src="/web/static/lib/bootstrap/js/bootstrap.js"></script>
- <link rel="stylesheet" href='/website/static/src/css/website.css'/>
- <link id="bootstrap_css" rel='stylesheet' href='/web/static/lib/bootstrap/css/bootstrap.css'/>
- <t t-call="website.summernote"/>
- <t t-call="website.editor"/>
- <script type="text/javascript" src="/website/static/src/js/website.backend.iframe.js"></script>
- <style>.note-popover .popover { left: 5px !important; }</style>
-</template>
-
-<template id="editor_layout" name="Editor layout">&lt;!DOCTYPE html&gt;
- <html t-att-lang="lang and lang.replace('_', '-')"
- t-att-data-website-id="website.id if editable and website else None"
- t-att-data-editable="1"
- t-att-data-translatable="'1' if translatable else None"
- t-att-data-main-object="repr(main_object) if editable else None"
- t-att-data-oe-company-name="res_company.name">
- <head>
- <meta charset="utf-8" />
- <title>Odoo Editor layout</title>
- <meta name="viewport" content="initial-scale=1"/>
- <t t-set="languages" t-value="website.get_languages() if website else None"/>
- <t t-if="request and request.website_multilang and website">
- <t t-foreach="website.get_alternate_languages(request.httprequest)" t-as="lang">
- <link rel="alternate" t-att-hreflang="lang['hreflang']" t-att-href="lang['href']"/>
- </t>
- </t>
- <script>
- var snippets_url = '<t t-esc="snippets or ''"/>';
- var callback = window.location.href.match(/callback=([^&amp;=]+)/)[1];
- window.top.odoo[callback+"_set_value"] = function (value, fields_values) {
- var editable = document.getElementsByClassName("o_editable")[0];
- if (value !== editable.innerHTML) {
- editable.innerHTML = value;
- }
- };
- </script>
- <t t-if="not dont_load_assets">
- <t t-call-assets="website.assets_backend_editor"/>
- </t>
- <t t-raw="head or ''"/>
- </head>
- <body onload="window.top.odoo[callback+'_content']();">
- <div id="wrapwrap">
- <main>
- <t t-raw="0"/>
- </main>
- </div>
- <t t-raw="add_html or ''"/>
- </body>
- </html>
-</template>
-
-<template id="FieldTextHtml" name="Editor layout">
- <t t-call="website.editor_layout">
- <div class="o_editable" t-att-data-oe-model="model" t-att-data-oe-field="field" t-att-data-oe-id="res_id" data-oe-type="html"><t t-raw="content"/></div>
- </t>
-</template>
-
- </data>
</openerp>
diff --git a/addons/website/views/website_backend_navbar.xml b/addons/website/views/website_backend_navbar.xml
index cd023bd3790..46fd18a57dd 100644
--- a/addons/website/views/website_backend_navbar.xml
+++ b/addons/website/views/website_backend_navbar.xml
@@ -35,8 +35,10 @@
</li>
</ul>
- <ul id="oe_editzone" class="nav navbar-nav hidden-xs" style="padding-left: 20px; display: none;" groups="base.group_website_publisher">
- <li><button type="button" data-action="edit" class="btn btn-primary btn-xs">Edit</button></li>
+ <ul id="oe_editzone" class="nav navbar-nav hidden-xs" style="padding-left: 20px;" groups="base.group_website_publisher">
+ <li t-if="not translatable"><button type="button" data-action="edit" class="btn btn-primary btn-xs">Edit</button></li>
+ <li t-if="translatable"><button type="button" data-action="translate" class="btn btn-primary btn-xs">Translate</button></li>
+ <li t-if="translatable"><a class="btn btn-link" data-action="edit_master" href="#"> or Edit Master</a></li>
</ul>
<ul id="oe_systray" class="nav navbar-nav navbar-right pull-right hidden-xs">
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index 309c7009fad..98fda216872 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -4,72 +4,6 @@
<!-- Assets -->
-<template id="summernote" name="Summernote">
- <script type="text/javascript" src="/website/static/src/js/openerp.define.js"></script>
- <script>odoo.enable_website_define();</script>
- <script>odoo.website_next_define = 'summernote/core/async';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/async.js"></script>
- <script>odoo.website_next_define = 'summernote/core/func';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/func.js"></script>
- <script>odoo.website_next_define = 'summernote/core/agent';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/agent.js"></script>
- <script>odoo.website_next_define = 'summernote/core/list';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/list.js"></script>
- <script>odoo.website_next_define = 'summernote/core/dom';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/dom.js"></script>
- <script>odoo.website_next_define = 'summernote/core/key';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/key.js"></script>
- <script>odoo.website_next_define = 'summernote/core/range';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/range.js"></script>
-
- <script>odoo.website_next_define = 'summernote/editing/Bullet';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/editing/Bullet.js"></script>
- <script>odoo.website_next_define = 'summernote/editing/History';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/editing/History.js"></script>
- <script>odoo.website_next_define = 'summernote/editing/Style';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/editing/Style.js"></script>
- <script>odoo.website_next_define = 'summernote/editing/Table';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/editing/Table.js"></script>
- <script>odoo.website_next_define = 'summernote/editing/Typing';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/editing/Typing.js"></script>
- <script>odoo.website_next_define = 'summernote/editing/Editor';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/editing/Editor.js"></script>
-
- <script>odoo.website_next_define = 'summernote/module/Button';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/module/Button.js"></script>
- <script>odoo.website_next_define = 'summernote/module/Dialog';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/module/Dialog.js"></script>
- <script>odoo.website_next_define = 'summernote/module/Handle';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/module/Handle.js"></script>
- <script>odoo.website_next_define = 'summernote/module/Popover';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/module/Popover.js"></script>
- <script>odoo.website_next_define = 'summernote/module/Toolbar';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/module/Toolbar.js"></script>
-
- <script>odoo.website_next_define = 'summernote/Renderer';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/Renderer.js"></script>
- <script>odoo.website_next_define = 'summernote/EventHandler';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/EventHandler.js"></script>
-
- <script>odoo.website_next_define = 'summernote/settings';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/settings.js"></script>
-
- <script>odoo.website_next_define = 'summernote/summernote';</script>
- <script type="text/javascript" src="/website/static/lib/summernote/src/js/summernote.js"></script>
- <script>odoo.website_next_define = 'summernote/openerp';</script>
- <script type="text/javascript" src="/website/static/src/js/summernote.js"></script>
- <script>odoo.disable_website_define();</script>
-
- <!--link rel="stylesheet" href="/website/static/lib/summernote/src/less/summernote.less"/-->
- <link rel="stylesheet" href="/website/static/lib/summernote/summernote.css"/>
-</template>
-
-<template id="assets_common" inherit_id="web.assets_common" name="Website common assets">
- <xpath expr='.' position="inside">
- <t t-call="website.summernote"/>
- </xpath>
-</template>
-
<template id="assets_frontend" name="Website assets">
<link id="bootstrap_css" rel="stylesheet" href='/website/static/src/less/import_bootstrap.less'/>
@@ -82,7 +16,6 @@
<script type="text/javascript" src="/website/static/src/js/website.snippets.animation.js"></script>
<t t-call="web.bootstrap"/>
-
</template>
<template id="assets_backend" name="website assets for backend" inherit_id="web.assets_backend">
@@ -92,41 +25,27 @@
</xpath>
</template>
-<template id="editor" name="Editor">
- <link rel='stylesheet' href='/website/static/src/css/snippets.css'/>
- <link rel='stylesheet' href='/website/static/src/css/editor.css'/>
- <link rel='stylesheet' href="/web/static/lib/jquery.ui/jquery-ui.css"/>
- <script type="text/javascript" src="/website/static/lib/jQuery.transfo.js"></script>
- <t t-call="web.jqueryui_conflict">
- <script type="text/javascript" src="/web/static/lib/jquery.ui/jquery-ui.js"></script>
- </t>
- <script type="text/javascript" src='/website/static/lib/nearest/jquery.nearest.js'></script>
-
- <!-- mutation observers shim backed by mutation events (8 < IE < 11, Safari < 6, FF < 14, Chrome < 17) -->
- <script type="text/javascript" src="/website/static/lib/MutationObservers/test/sidetable.js"></script>
- <script type="text/javascript" src="/website/static/lib/MutationObservers/MutationObserver.js"></script>
- <script type="text/javascript" src="/website/static/src/js/website.editor.js"></script>
- <script type="text/javascript" src="/website/static/src/js/website.snippets.editor.js"></script>
- <script type="text/javascript" src="/website/static/src/js/website.snippets.gallery.js" />
-</template>
-
<template id="assets_editor" name="Editor assets">
- <t t-call="website.editor"/>
+ <script type="text/javascript" src="/website/static/src/js/website.editor.js"></script>
+ <script type="text/javascript" src="/website/static/lib/jQuery.transfo.js"></script>
<script type="text/javascript" src="/website/static/lib/ace/ace.js"></script>
<script type="text/javascript" src="/website/static/lib/ace/theme-monokai.js"></script>
<script type="text/javascript" src="/website/static/lib/ace/mode-xml.js"></script>
<script type="text/javascript" src="/website/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js"></script>
- <script type="text/javascript" src="/website/static/lib//jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js"></script>
- <script type="text/javascript" src="/website/static/src/js/website.contentMenu.js"></script> <!-- groups="base.group_website_designer" -->
+ <script type="text/javascript" src="/website/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.mobile.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.seo.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.tour.js"></script>
- <script type="text/javascript" src="/website/static/src/js/website.tour.banner.js"></script> <!-- groups="base.group_website_designer" -->
- <script type="text/javascript" src="/website/static/src/js/website.tour.rte.js"></script>
+ <script type="text/javascript" src="/website/static/src/js/website.tour.banner.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.ace.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.translator.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.theme.js"></script>
<script type="text/javascript" src="/website/static/src/js/web_planner_website.js"></script>
+ <script type="text/javascript" src="/website/static/src/js/website.contentMenu.js"></script>
+ <link rel='stylesheet' href='/web_editor/static/src/css/web_editor.css'/>
+ <link rel="stylesheet" href="/website/static/src/css/website.editor.css"/>
+ <script type="text/javascript" src="/website/static/src/js/website.snippets.editor.js"></script>
+ <script type="text/javascript" src="/website/static/src/js/website.snippets.gallery.js"></script>
</template>
<!-- Layout -->
@@ -160,7 +79,8 @@
t-att-data-website-id="website.id if editable and website else None"
t-att-data-editable="'1' if editable else None"
t-att-data-translatable="'1' if translatable else None"
- t-att-data-view-xmlid="xmlid if editable else None"
+ t-att-data-edit_translations="'1' if edit_translations else None"
+ t-att-data-view-xmlid="xmlid if editable or translatable else None"
t-att-data-main-object="repr(main_object) if editable else None"
t-att-data-oe-company-name="res_company.name">
<head>
@@ -236,7 +156,7 @@
</b>
</a>
<ul class="dropdown-menu js_usermenu" role="menu">
- <li><a t-attf-href="/web/session/logout?redirect=/" role="menuitem">Logout</a></li>
+ <li id="o_logout"><a t-attf-href="/web/session/logout?redirect=/" role="menuitem">Logout</a></li>
</ul>
</li>
</ul>
@@ -266,6 +186,10 @@
</template>
<template id="layout_editor" inherit_id="website.layout" name="Editor" groups="base.group_website_publisher,base.group_website_designer">
+ <xpath expr='//t[@t-call-assets="website.assets_frontend"][@t-css="false"]' position="before">
+ <t groups="base.group_website_designer" t-call-assets="web_editor.summernote"/>
+ <t groups="base.group_website_designer" t-call-assets="web_editor.editor"/>
+ </xpath>
<xpath expr='//t[@t-call-assets="website.assets_frontend"][@t-css="false"]' position="after">
<t t-call-assets="website.assets_editor" async="True"/>
</xpath>
@@ -293,8 +217,6 @@
</xpath>
</template>
-
-
<!--
Optional Bundle for PDFJS lib
Since PDFJS is quite huge (40000 lines), please only load it when it is necessary.
@@ -368,7 +290,7 @@
<script type="text/javascript" src="/website/static/lib/pdfjs/pdf.js"></script>
</template>
-
+<!-- Features template -->
<template id="login_layout" inherit_id="web.login_layout" name="Website Login Layout">
<xpath expr="t" position="replace">
@@ -421,11 +343,11 @@
</template>
<template id="language_selector">
- <ul class="list-inline js_language_selector mt16" t-if="(request and request.website_multilang and len(languages) &gt; 1) or (website and editable)">
+ <ul class="list-inline js_language_selector mt16" t-if="(request and request.website_multilang and len(languages) &gt; 1) or (website and (editable or translatable))">
<li t-foreach="languages" t-as="lg">
<a
t-att-href="url_for(request.httprequest.path + '?' + keep_query(), lang=lg[0])"
- t-att-data-default-lang="editable and 'true' if website and lg[0] == website.default_lang_code else None"
+ t-att-data-default-lang="(editable or translatable) and 'true' if website and lg[0] == website.default_lang_code else None"
t-att-data-lang="lg[0]"
class="js_change_lang"
>
@@ -576,21 +498,6 @@
</div>
</template>
-<!-- color-picker -->
-<!-- HTML class to hide option for one mode : only-text, only-bg -->
-
-<template id="website.colorpicker" name="Color-Picker">
- <table class="colorpicker">
- <tr>
- <td><button class="bg-primary" title="Primary Color"/></td>
- <td><button class="bg-success" title="Success Color"/></td>
- <td><button class="bg-info" title="Info Color"/></td>
- <td><button class="bg-warning" title="Warning Color"/></td>
- <td><button class="bg-danger" title="Danger Color"/></td>
- </tr>
- </table>
-</template>
-
<template id="kanban">
<t t-set="step"><t t-esc="step or 0"/></t>
<t t-set="scope"><t t-esc="scope or 0"/></t>
diff --git a/addons/website_gengo/static/src/xml/website.gengo.xml b/addons/website_gengo/static/src/xml/website.gengo.xml
index a867f6eb478..b4d8c720744 100644
--- a/addons/website_gengo/static/src/xml/website.gengo.xml
+++ b/addons/website_gengo/static/src/xml/website.gengo.xml
@@ -23,10 +23,10 @@
<li style="background:#ffffb6;">
Content to translate or you can post them to <b><a href="http://gengo.com/" >Gengo</a></b> for translation.
</li>
- <li class="oe_translatable_inprogress">
+ <li data-oe-translation-state="inprogress">
Translation in process (Gengo)
</li>
- <li class="oe_translatable_text">
+ <li data-oe-translation-state="translated">
Already translated content
</li>
</ul>
diff --git a/addons/website_version/models/ir_ui_view.py b/addons/website_version/models/ir_ui_view.py
index 4a3ded70f43..549953254ba 100644
--- a/addons/website_version/models/ir_ui_view.py
+++ b/addons/website_version/models/ir_ui_view.py
@@ -63,7 +63,7 @@ class view(models.Model):
return xml_id
@tools.ormcache_context('uid', 'view_id',
- keys=('lang', 'inherit_branding', 'editable', 'translatable', 'website_id', 'version_id'))
+ keys=('lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations', 'website_id', 'version_id'))
def _read_template(self, cr, uid, view_id, context=None):
arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
arch_tree = etree.fromstring(arch)
diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py
index 7fe0d8cc75d..f7489a5379d 100644
--- a/openerp/addons/base/ir/ir_qweb.py
+++ b/openerp/addons/base/ir/ir_qweb.py
@@ -592,7 +592,7 @@ class FieldConverter(osv.AbstractModel):
('data-oe-type', field_type),
('data-oe-expression', t_att['field']),
]
- if record._fields[field_name].readonly:
+ if field.readonly:
data.append(('data-oe-readonly', 1))
return data
@@ -645,7 +645,8 @@ class FieldConverter(osv.AbstractModel):
_build_attribute(name, value)
for name, value in self.attributes(
cr, uid, field_name, record, options,
- source_element, g_att, t_att, qweb_context)
+ source_element, g_att, t_att, qweb_context,
+ context=context)
)
return self.render_element(cr, uid, source_element, t_att, g_att,
diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py
index 58f3b12a412..ce2603809ab 100644
--- a/openerp/addons/base/ir/ir_ui_view.py
+++ b/openerp/addons/base/ir/ir_ui_view.py
@@ -901,7 +901,7 @@ class view(osv.osv):
# apply ormcache_context decorator unless in dev mode...
@tools.conditional(not config['dev_mode'],
tools.ormcache_context('uid', 'view_id',
- keys=('lang', 'inherit_branding', 'editable', 'translatable')))
+ keys=('lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations')))
def _read_template(self, cr, uid, view_id, context=None):
arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
arch_tree = etree.fromstring(arch)
diff --git a/openerp/addons/base/tests/test_mail_examples.py b/openerp/addons/base/tests/test_mail_examples.py
index 78f5cf55269..27ec3ee3e10 100644
--- a/openerp/addons/base/tests/test_mail_examples.py
+++ b/openerp/addons/base/tests/test_mail_examples.py
@@ -100,7 +100,7 @@ OERP_WEBSITE_HTML_1_OUT = [
]
OERP_WEBSITE_HTML_2 = """
-<div class="mt16 cke_widget_editable cke_widget_element oe_editable oe_dirty" data-oe-model="blog.post" data-oe-id="6" data-oe-field="content" data-oe-type="html" data-oe-translate="0" data-oe-expression="blog_post.content" data-cke-widget-data="{}" data-cke-widget-keep-attr="0" data-widget="oeref" contenteditable="true" data-cke-widget-editable="text">
+<div class="mt16 cke_widget_editable cke_widget_element o_editable o_dirty" data-oe-model="blog.post" data-oe-id="6" data-oe-field="content" data-oe-type="html" data-oe-expression="blog_post.content" data-cke-widget-data="{}" data-cke-widget-keep-attr="0" data-widget="oeref" contenteditable="true" data-cke-widget-editable="text">
<section class="mt16 mb16">
<div class="container">
<div class="row">
diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py
index 67885926e61..e38ae437319 100644
--- a/openerp/tools/translate.py
+++ b/openerp/tools/translate.py
@@ -270,7 +270,7 @@ def xml_translate(callback, value):
trans.process(root)
return trans.get_done()
except etree.ParseError:
- wrapped = "<div>%s</div>" % value
+ wrapped = "<div>%s</div>" % encode(value)
root = etree.fromstring(wrapped, etree.HTMLParser())
# html > body > div
trans.process(root[0][0])