summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfja-odoo <fja@odoo.com>2019-11-15 16:13:17 +0000
committerqsm-odoo <qsm@odoo.com>2020-01-16 14:29:04 +0000
commit218e1cad6152999a0f41f073b5efa3c9428da48d (patch)
tree76ebfa75fcb86195fd4b4c14af7b87b8ad3920c8
parent4946739c17488d32d3e61b1258ab48b5aec701a4 (diff)
[IMP] web_editor, *: allow saving of snippets
* website, mass_mailing, website_event, website_form, website_mass_mailing, website_sale Now the user can save snippets to use them on other pages. task-2120409 closes odoo/odoo#40408 Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com> Co-authored-by: qsm-odoo <qsm@odoo.com>
-rw-r--r--addons/mass_mailing/views/snippets_themes.xml19
-rw-r--r--addons/web_editor/models/ir_qweb.py5
-rw-r--r--addons/web_editor/models/ir_ui_view.py81
-rw-r--r--addons/web_editor/static/src/img/snippets_thumbs/s_custom_snippet.pngbin0 -> 977 bytes
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.editor.js105
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js55
-rw-r--r--addons/web_editor/static/src/scss/wysiwyg_snippets.scss27
-rw-r--r--addons/web_editor/static/src/xml/editor.xml14
-rw-r--r--addons/web_editor/views/snippets.xml18
-rw-r--r--addons/website/models/ir_ui_view.py24
-rw-r--r--addons/website/static/src/js/editor/snippets.options.js1
-rw-r--r--addons/website/views/snippets/s_title.xml2
-rw-r--r--addons/website/views/snippets/snippets.xml23
-rw-r--r--addons/website_event/views/event_snippets.xml4
-rw-r--r--addons/website_form/views/snippets.xml4
-rw-r--r--addons/website_mass_mailing/views/snippets_templates.xml4
-rw-r--r--addons/website_sale/views/snippets.xml5
17 files changed, 333 insertions, 58 deletions
diff --git a/addons/mass_mailing/views/snippets_themes.xml b/addons/mass_mailing/views/snippets_themes.xml
index 9633db6cfea..bb6ebc943d0 100644
--- a/addons/mass_mailing/views/snippets_themes.xml
+++ b/addons/mass_mailing/views/snippets_themes.xml
@@ -5,9 +5,9 @@
<xpath expr="//div[@id='snippets_menu']" position="inside">
<button type="button" disabled="disabled">Select a template</button>
</xpath>
- <xpath expr="//div[@id='o_scroll']" position="replace">
- <t t-set="company_id" t-value="res_company"/>
- <div id="o_scroll">
+ <xpath expr="//t[@id='default_snippets']" position="replace">
+ <t id="default_snippets">
+ <t t-set="company_id" t-value="res_company"/>
<div id="email_designer_themes">
<div data-name="basic"
data-nowrap="1"
@@ -70,7 +70,7 @@
<t t-snippet="mass_mailing.s_mail_block_footer_social_left" t-thumbnail="/mass_mailing/static/src/img/blocks/block_footer_social_left.png"/>
</div>
</div>
- </div>
+ </t>
</xpath>
<xpath expr="//div[@id='snippet_options']" position="inside">
<t t-call="mass_mailing.snippet_options"/>
@@ -797,12 +797,21 @@
data-exclude=".o_mail_no_options"
data-drop-near=".col_mv, td, th"/>
+ <t t-set="mailing_content_selector">.note-editable > div:not(.o_layout), .note-editable .oe_structure > div, .oe_snippet_body</t>
<div data-js="content"
- data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, .oe_snippet_body"
+ t-att-data-selector="mailing_content_selector"
data-exclude=".o_mail_no_options"
data-drop-near="[data-oe-field='body_html']:not(:has(.o_layout)) > *, .oe_structure > *"
data-drop-in="[data-oe-field='body_html']:not(:has(.o_layout)), .oe_structure"/>
+ <div data-js="SnippetSave"
+ t-att-data-selector="mailing_content_selector">
+ <we-button class="fa fa-fw fa-save"
+ title="Save the block to use it elsewhere"
+ data-save-snippet=""
+ data-no-preview="true"/>
+ </div>
+
<div data-js="sizing_y"
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, td, th"
data-exclude=".o_mail_no_resize, .o_mail_no_options"/>
diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py
index 78ebe9b2178..0cf3ff932c9 100644
--- a/addons/web_editor/models/ir_qweb.py
+++ b/addons/web_editor/models/ir_qweb.py
@@ -50,9 +50,10 @@ class QWeb(models.AbstractModel):
view_id = View.get_view_id(el.attrib.get('t-call'))
name = View.browse(view_id).name
thumbnail = el.attrib.pop('t-thumbnail', "oe-thumbnail")
- div = u'<div name="%s" data-oe-type="snippet" data-oe-thumbnail="%s">' % (
+ div = u'<div name="%s" data-oe-type="snippet" data-oe-thumbnail="%s" data-oe-snippet-id="%s">' % (
escape(pycompat.to_text(name)),
- escape(pycompat.to_text(thumbnail))
+ escape(pycompat.to_text(thumbnail)),
+ escape(pycompat.to_text(view_id)),
)
return [self._append(ast.Str(div))] + self._compile_node(el, options) + [self._append(ast.Str(u'</div>'))]
diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py
index 0aefd108432..19470d40c3c 100644
--- a/addons/web_editor/models/ir_ui_view.py
+++ b/addons/web_editor/models/ir_ui_view.py
@@ -3,10 +3,11 @@
import copy
import logging
+import uuid
from lxml import etree, html
from odoo.exceptions import AccessError
-from odoo import api, fields, models
+from odoo import api, models
_logger = logging.getLogger(__name__)
@@ -271,3 +272,81 @@ class IrUiView(models.Model):
View = self.with_context(active_test=False, lang=None)
views = View._views_get(key, bundles=bundles)
return views.filtered(lambda v: not v.groups_id or len(user_groups.intersection(v.groups_id)))
+
+ # --------------------------------------------------------------------------
+ # Snippet saving
+ # --------------------------------------------------------------------------
+
+ @api.model
+ def _get_default_snippet_thumbnail(self, snippet_class=None):
+ return '/web_editor/static/src/img/snippets_thumbs/s_custom_snippet.png'
+
+ @api.model
+ def _get_snippet_addition_view_key(self, template_key, key):
+ return '%s.%s' % (template_key, key)
+
+ @api.model
+ def _snippet_save_view_values_hook(self):
+ return {}
+
+ @api.model
+ def save_snippet(self, name, arch, template_key, snippet_class=None, thumbnail_url=None):
+ """
+ Saves a new snippet arch so that it appears with the given name when
+ using the given snippets template.
+
+ :param name: the name of the snippet to save
+ :param arch: the html structure of the snippet to save
+ :param template_key: the key of the view regrouping all snippets in
+ which the snippet to save is meant to appear
+ :param snippet_class: a className which is supposed to uniquely-identify
+ the snippet from which the snippet to save originates
+ :param thumbnail_url: the url of the thumbnail to use when displaying
+ the snippet to save (default: see '_get_default_snippet_thumbnail')
+ """
+ if not thumbnail_url:
+ thumbnail_url = self._get_default_snippet_thumbnail(snippet_class)
+
+ app_name = template_key.split('.')[0]
+ snippet_class = snippet_class or 's_custom_snippet'
+ key = '%s_%s' % (snippet_class, uuid.uuid4().hex)
+ snippet_key = '%s.%s' % (app_name, key)
+
+ # html to xml to add '/' at the end of self closing tags like br, ...
+ xml_arch = etree.tostring(html.fromstring(arch))
+ new_snippet_view_values = {
+ 'name': name,
+ 'key': snippet_key,
+ 'type': 'qweb',
+ 'arch': xml_arch,
+ }
+ new_snippet_view_values.update(self._snippet_save_view_values_hook())
+ self.create(new_snippet_view_values)
+
+ custom_section = self.search([('key', '=', template_key)])
+ snippet_addition_view_values = {
+ 'name': name + ' Block',
+ 'key': self._get_snippet_addition_view_key(template_key, key),
+ 'inherit_id': custom_section.id,
+ 'type': 'qweb',
+ 'arch': """
+ <data inherit_id="%s">
+ <xpath expr="//div[@id='snippet_custom']" position="attributes">
+ <attribute name="class" remove="d-none" separator=" "/>
+ </xpath>
+ <xpath expr="//div[@id='snippet_custom_body']" position="inside">
+ <t t-snippet="%s" t-thumbnail="%s"/>
+ </xpath>
+ </data>
+ """ % (template_key, snippet_key, thumbnail_url),
+ }
+ snippet_addition_view_values.update(self._snippet_save_view_values_hook())
+ self.create(snippet_addition_view_values)
+
+ @api.model
+ def delete_snippet(self, view_id, template_key):
+ snippet_view = self.browse(view_id)
+ key = snippet_view.key.split('.')[1]
+ custom_key = self._get_snippet_addition_view_key(template_key, key)
+ snippet_addition_view = self.search([('key', '=', custom_key)])
+ (snippet_addition_view | snippet_view).unlink()
diff --git a/addons/web_editor/static/src/img/snippets_thumbs/s_custom_snippet.png b/addons/web_editor/static/src/img/snippets_thumbs/s_custom_snippet.png
new file mode 100644
index 00000000000..76ffb0905fb
--- /dev/null
+++ b/addons/web_editor/static/src/img/snippets_thumbs/s_custom_snippet.png
Binary files differ
diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js
index 1e510ba9414..57ab6c37b73 100644
--- a/addons/web_editor/static/src/js/editor/snippets.editor.js
+++ b/addons/web_editor/static/src/js/editor/snippets.editor.js
@@ -780,6 +780,7 @@ var SnippetsMenu = Widget.extend({
events: {
'click .o_install_btn': '_onInstallBtnClick',
'click .o_we_invisible_entry': '_onInvisibleEntryClick',
+ 'click #snippet_custom .o_delete_btn': '_onDeleteBtnClick',
},
custom_events: {
'activate_insertion_zones': '_onActivateInsertionZones',
@@ -801,6 +802,7 @@ var SnippetsMenu = Widget.extend({
'block_preview_overlays': '_onBlockPreviewOverlays',
'unblock_preview_overlays': '_onUnblockPreviewOverlays',
'user_value_widget_opening': '_onUserValueWidgetOpening',
+ 'reload_snippet_template': '_onReloadSnippetTemplate',
},
/**
@@ -857,26 +859,22 @@ var SnippetsMenu = Widget.extend({
this.window = this.ownerDocument.defaultView;
this.$window = $(this.window);
- // Fetch snippet templates and compute it
+ this.customizePanel = document.createElement('div');
+ this.customizePanel.classList.add('o_we_customize_panel', 'd-none');
- defs.push(this.loadSnippets().then(html => {
- return this._computeSnippetTemplates(html);
- }).then(() => {
- this.customizePanel = document.createElement('div');
- this.customizePanel.classList.add('o_we_customize_panel', 'd-none');
- this.$el.append(this.customizePanel);
-
- this.invisibleDOMPanelEl = document.createElement('div');
- this.invisibleDOMPanelEl.classList.add('o_we_invisible_el_panel');
- this.invisibleDOMPanelEl.appendChild(
- $('<div/>', {
- text: _t('Invisible Elements'),
- class: 'o_panel_header',
- }).prepend(
- $('<i/>', {class: 'fa fa-eye-slash'})
- )[0]
- );
- this.$el.append(this.invisibleDOMPanelEl);
+ this.invisibleDOMPanelEl = document.createElement('div');
+ this.invisibleDOMPanelEl.classList.add('o_we_invisible_el_panel');
+ this.invisibleDOMPanelEl.appendChild(
+ $('<div/>', {
+ text: _t('Invisible Elements'),
+ class: 'o_panel_header',
+ }).prepend(
+ $('<i/>', {class: 'fa fa-eye-slash'})
+ )[0]
+ );
+
+ // Fetch snippet templates and compute it
+ defs.push(this._loadSnippetsTemplates().then(() => {
return this._updateInvisibleDOM();
}));
@@ -998,9 +996,10 @@ var SnippetsMenu = Widget.extend({
},
/**
* Load snippets.
+ * @param {boolean} invalidateCache
*/
- loadSnippets: function () {
- if (this.cacheSnippetTemplate[this.options.snippets]) {
+ loadSnippets: function (invalidateCache) {
+ if (!invalidateCache && this.cacheSnippetTemplate[this.options.snippets]) {
this._defLoadSnippets = this.cacheSnippetTemplate[this.options.snippets];
return this._defLoadSnippets;
}
@@ -1287,6 +1286,17 @@ var SnippetsMenu = Widget.extend({
},
/**
* @private
+ * @param {boolean} invalidateCache
+ */
+ _loadSnippetsTemplates: async function (invalidateCache) {
+ return this._mutex.exec(async () => {
+ await this._destroyEditors();
+ const html = await this.loadSnippets(invalidateCache);
+ await this._computeSnippetTemplates(html);
+ });
+ },
+ /**
+ * @private
*/
_destroyEditors: async function () {
const proms = _.map(this.snippetEditors, async function (snippetEditor) {
@@ -1502,6 +1512,7 @@ var SnippetsMenu = Widget.extend({
var $snippet = $(this);
var name = $snippet.attr('name');
var $sbody = $snippet.children(':not(.oe_snippet_thumbnail)').addClass('oe_snippet_body');
+ const isCustomSnippet = !!$snippet.parents('#snippet_custom').length;
// Associate in-page snippets to their name
if ($sbody.length) {
@@ -1525,6 +1536,13 @@ var SnippetsMenu = Widget.extend({
$snippet.find('[data-oe-thumbnail]').data('oeThumbnail'),
name
));
+ if (isCustomSnippet) {
+ const btn = document.createElement('we-button');
+ btn.dataset.snippetId = $snippet.data('oeSnippetId');
+ btn.classList.add('o_delete_btn', 'fa', 'fa-trash');
+ $thumbnail.prepend(btn);
+ $thumbnail.prepend($('<div class="o_image_ribbon"/>'));
+ }
$snippet.prepend($thumbnail);
// Create the install button (t-install feature) if necessary
@@ -1563,6 +1581,8 @@ var SnippetsMenu = Widget.extend({
// Add the computed template and make elements draggable
this.$el.html($html);
+ this.$el.append(this.customizePanel);
+ this.$el.append(this.invisibleDOMPanelEl);
this._makeSnippetDraggable(this.$snippets);
this._disableUndroppableSnippets();
@@ -1677,7 +1697,11 @@ var SnippetsMenu = Widget.extend({
$snippets.draggable({
greedy: true,
- helper: 'clone',
+ helper: function () {
+ const dragSnip = this.cloneNode(true);
+ dragSnip.querySelectorAll('.o_delete_btn, .o_image_ribbon').forEach(el => el.remove());
+ return dragSnip;
+ },
appendTo: this.$body,
cursor: 'move',
handle: '.oe_snippet_thumbnail',
@@ -1974,6 +1998,43 @@ var SnippetsMenu = Widget.extend({
/**
* @private
*/
+ _onDeleteBtnClick: function (ev) {
+ const $snippet = $(ev.target).closest('.oe_snippet');
+ new Dialog(this, {
+ size: 'medium',
+ title: _t('Confirmation'),
+ $content: $('<div><p>' + _t(`Are you sure you want to delete the snippet: ${$snippet.attr('name')} ?`) + '</p></div>'),
+ buttons: [{
+ text: _t("Yes"),
+ close: true,
+ classes: 'btn-primary',
+ click: async () => {
+ await this._rpc({
+ model: 'ir.ui.view',
+ method: 'delete_snippet',
+ kwargs: {
+ 'view_id': parseInt(ev.currentTarget.dataset.snippetId),
+ 'template_key': this.options.snippets,
+ },
+ });
+ await this._loadSnippetsTemplates(true);
+ },
+ }, {
+ text: _t("No"),
+ close: true,
+ }],
+ }).open();
+ },
+ /**
+ * @private
+ */
+ _onReloadSnippetTemplate: async function (ev) {
+ await this._activateSnippet(false);
+ await this._loadSnippetsTemplates(true);
+ },
+ /**
+ * @private
+ */
_onBlockPreviewOverlays: function (ev) {
this._blockPreviewOverlays = true;
},
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
index 86a3f70ac2b..53a36008a56 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -3,6 +3,7 @@ odoo.define('web_editor.snippets.options', function (require) {
var core = require('web.core');
const ColorpickerDialog = require('web.ColorpickerDialog');
+const Dialog = require('web.Dialog');
const time = require('web.time');
var Widget = require('web.Widget');
var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget;
@@ -2973,6 +2974,60 @@ registry.many2one = SnippetOptionWidget.extend({
}
});
+/**
+ * Handle the save of a snippet as a template that can be reused later
+ */
+registry.SnippetSave = SnippetOptionWidget.extend({
+ xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
+ isTopOption: true,
+
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
+
+ /**
+ * @see this.selectClass for parameters
+ */
+ saveSnippet: function (previewMode, widgetValue, params) {
+ return new Promise(resolve => {
+ const dialog = new Dialog(this, {
+ title: _t("Save Your Block"),
+ size: 'small',
+ $content: $(qweb.render('web_editor.dialog.save_snippet', {
+ currentSnippetName: _.str.sprintf(_t("Custom %s"), this.data.snippetName),
+ })),
+ buttons: [{
+ text: _t("Save"),
+ classes: 'btn-primary',
+ close: true,
+ click: async () => {
+ const snippetName = dialog.el.querySelector('.o_we_snippet_name_input').value;
+ const targetCopyEl = this.$target[0].cloneNode(true);
+ delete targetCopyEl.dataset.name;
+ await this._rpc({
+ model: 'ir.ui.view',
+ method: 'save_snippet',
+ kwargs: {
+ 'name': snippetName,
+ 'arch': targetCopyEl.outerHTML,
+ 'template_key': this.options.snippets,
+ 'snippet_class': [...this.$target[0].classList].filter(x => /\bs_./g.test(x))[0],
+ },
+ });
+ this.trigger_up('reload_snippet_template');
+ resolve();
+ },
+ }, {
+ text: _t("Discard"),
+ close: true,
+ click: () => resolve(),
+ }],
+ }).open();
+ });
+ },
+});
+
+
return {
SnippetOptionWidget: SnippetOptionWidget,
snippetOptionRegistry: registry,
diff --git a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss
index 3dcb6e872e9..b8e87ff69f5 100644
--- a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss
+++ b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss
@@ -171,6 +171,33 @@ body.editor_enable.editor_has_snippets {
}
}
}
+ #snippet_custom .oe_snippet .oe_snippet_thumbnail {
+ .o_delete_btn {
+ @include o-position-absolute(0, 0);
+ z-index: 1;
+ display: none;
+ width: 25px;
+ height: 25px;
+ background-color: rgba(255, 0, 0, 0.75);
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ cursor: pointer;
+ }
+ }
+ &:hover .o_delete_btn {
+ display: flex;
+ }
+ .o_image_ribbon {
+ position: absolute;
+ z-index: 1;
+ width: 0;
+ height: 0;
+ border-right: 20px solid transparent;
+ border-top: 20px solid $o-enterprise-color;
+ }
+ }
}
> .o_we_customize_panel {
diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml
index b1d7128e2c1..bb4abe15f60 100644
--- a/addons/web_editor/static/src/xml/editor.xml
+++ b/addons/web_editor/static/src/xml/editor.xml
@@ -25,4 +25,18 @@
</div>
</div>
</t>
+ <!-- Save Snippet Name option dialog -->
+ <t t-name="web_editor.dialog.save_snippet">
+ <div>
+ <div class="form-group form-row">
+ <label class="col-form-label col-md-3" for="snippetName">Name</label>
+ <div class="col-md-9">
+ <input type="text"
+ class="form-control o_we_snippet_name_input"
+ id="snippetName"
+ t-att-value="currentSnippetName"/>
+ </div>
+ </div>
+ </div>
+ </t>
</templates>
diff --git a/addons/web_editor/views/snippets.xml b/addons/web_editor/views/snippets.xml
index 8f44a8f05a4..ab920aa65fb 100644
--- a/addons/web_editor/views/snippets.xml
+++ b/addons/web_editor/views/snippets.xml
@@ -14,14 +14,22 @@
</div>
<div id="o_scroll">
- <div id="snippet_structure" class="o_panel">
+ <div id="snippet_custom" class="o_panel d-none">
<div class="o_panel_header">
- <i class="fa fa-th-large"/> First Panel
- </div>
- <div class="o_panel_body">
- <t t-snippet="web_editor.s_hr" t-thumbnail="/website/static/src/img/snippets_thumbs/s_separator.png"/>
+ <i class="fa fa-fw fa-save"/> Custom
</div>
+ <div id="snippet_custom_body" class="o_panel_body"/>
</div>
+ <t id="default_snippets">
+ <div id="snippet_structure" class="o_panel">
+ <div class="o_panel_header">
+ <i class="fa fa-th-large"/> First Panel
+ </div>
+ <div class="o_panel_body">
+ <t t-snippet="web_editor.s_hr" t-thumbnail="/website/static/src/img/snippets_thumbs/s_separator.png"/>
+ </div>
+ </div>
+ </t>
</div>
<div id="snippet_options" class="d-none">
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index 3618c0a141c..881b8355a3e 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -2,11 +2,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
-import werkzeug
+import os
import uuid
+import werkzeug
from odoo import api, fields, models
from odoo import tools
+from odoo.addons import website
from odoo.addons.http_routing.models.ir_http import url_for
from odoo.osv import expression
from odoo.http import request
@@ -462,3 +464,23 @@ class View(models.Model):
if website_specific_view:
self = website_specific_view
super(View, self).save(value, xpath=xpath)
+
+ # --------------------------------------------------------------------------
+ # Snippet saving
+ # --------------------------------------------------------------------------
+
+ @api.model
+ def _get_default_snippet_thumbnail(self, snippet_class=None):
+ if snippet_class:
+ for path in website.__path__:
+ if os.path.isfile(path + '/static/src/img/snippets_thumbs/%s.png' % snippet_class):
+ return '/website/static/src/img/snippets_thumbs/%s.png' % snippet_class
+ return super()._get_default_snippet_thumbnail()
+
+ @api.model
+ def _snippet_save_view_values_hook(self):
+ res = super()._snippet_save_view_values_hook()
+ website_id = self.env.context.get('website_id')
+ if website_id:
+ res['website_id'] = website_id
+ return res
diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js
index 9024cf7b8e0..5468ab9f321 100644
--- a/addons/website/static/src/js/editor/snippets.options.js
+++ b/addons/website/static/src/js/editor/snippets.options.js
@@ -3,7 +3,6 @@ odoo.define('website.editor.snippets.options', function (require) {
var core = require('web.core');
var Dialog = require('web.Dialog');
-var weWidgets = require('wysiwyg.widgets');
const wUtils = require('website.utils');
var options = require('web_editor.snippets.options');
diff --git a/addons/website/views/snippets/s_title.xml b/addons/website/views/snippets/s_title.xml
index 5fdbb9975af..db2791be3c6 100644
--- a/addons/website/views/snippets/s_title.xml
+++ b/addons/website/views/snippets/s_title.xml
@@ -2,7 +2,7 @@
<odoo>
<template id="s_title" name="Title">
- <section class="pt32 pb32">
+ <section class="s_title pt32 pb32">
<div class="container">
<div class="row s_nb_column_fixed">
<div class="col-lg-12 s_title pt16 pb16" style="text-align: center;">
diff --git a/addons/website/views/snippets/snippets.xml b/addons/website/views/snippets/snippets.xml
index 699f9fa6474..4594574c88f 100644
--- a/addons/website/views/snippets/snippets.xml
+++ b/addons/website/views/snippets/snippets.xml
@@ -3,8 +3,8 @@
<!-- Snippets menu -->
<template id="snippets" inherit_id="web_editor.snippets" primary="True">
- <xpath expr="//div[@id='o_scroll']" position="replace">
- <div id="o_scroll">
+ <xpath expr="//t[@id='default_snippets']" position="replace">
+ <t id="default_snippets">
<div id="snippet_mega_menu" class="o_panel d-none">
<div class="o_panel_header">
<i class="fa fa-puzzle-piece"/> Mega Menu
@@ -77,7 +77,7 @@
<t t-snippet="website.s_chart" t-thumbnail="/website/static/src/img/snippets_thumbs/s_chart.png"/>
</div>
</div>
- </div>
+ </t>
</xpath>
<xpath expr="//div[@id='snippet_options']/t" position="attributes">
@@ -308,16 +308,25 @@
data-drop-near=".row > div"
data-exclude=".s_col_no_resize.row > div"/>
+ <t t-set="so_snippet_addition_selector">section, .parallax, .s_popup</t>
<div id="so_snippet_addition"
- data-selector="section, .parallax, .s_popup"
+ t-att-data-selector="so_snippet_addition_selector"
data-drop-in=":not(p).oe_structure:not(.oe_structure_solo), :not(p)[data-oe-type=html], :not(p).oe_structure.oe_structure_solo:not(:has(> section, > div))"/>
- <!-- Main content drop -->
+ <t t-set="so_content_addition_selector">blockquote, .s_btn, .s_card, .s_alert, .o_facebook_page, .s_share, .s_rating, .s_hr, .s_countdown, .s_chart</t>
<div id="so_content_addition"
- data-selector="blockquote, .s_btn, .s_card, .s_alert, .o_facebook_page, .s_share, .s_rating, .s_hr, .s_countdown, .s_chart"
- data-drop-near="p, h1, h2, h3, blockquote, .s_btn, .s_card, .s_alert, .o_facebook_page, .s_share, .s_rating, .s_hr, .s_chart"
+ t-att-data-selector="so_content_addition_selector"
+ t-attf-data-drop-near="p, h1, h2, h3, #{so_content_addition_selector}"
data-drop-in=".content, nav"/>
+ <div data-js="SnippetSave"
+ t-attf-data-selector="#{so_snippet_addition_selector}, #{so_content_addition_selector}">
+ <we-button class="fa fa-fw fa-save"
+ title="Save the block to use it elsewhere"
+ data-save-snippet=""
+ data-no-preview="true"/>
+ </div>
+
<div data-js="ul"
data-selector=":not(li) > ul:has(ul,ol), :not(li) > ol:has(ul,ol)">
<we-checkbox string="Folded list" data-select-class="o_ul_folded"/>
diff --git a/addons/website_event/views/event_snippets.xml b/addons/website_event/views/event_snippets.xml
index 438a19fbd15..fb890988102 100644
--- a/addons/website_event/views/event_snippets.xml
+++ b/addons/website_event/views/event_snippets.xml
@@ -59,9 +59,7 @@
</template>
<template id="snippet_options" inherit_id="website.snippet_options">
- <xpath expr="//div[@id='so_content_addition']" position="attributes">
- <attribute name="data-selector" add=".oe_country_events, .s_speaker_bio" separator=","/>
- </xpath>
+ <xpath expr="//*[@t-set='so_content_addition_selector']" position="inside">, .oe_country_events, .s_speaker_bio</xpath>
</template>
<!-- Snippet - Speaker Bio -->
diff --git a/addons/website_form/views/snippets.xml b/addons/website_form/views/snippets.xml
index 0b4ef73b623..4f96ac66f4e 100644
--- a/addons/website_form/views/snippets.xml
+++ b/addons/website_form/views/snippets.xml
@@ -24,9 +24,7 @@
</template>
<template id="snippet_options" inherit_id="website.snippet_options">
- <xpath expr="//*[@id='so_snippet_addition']" position="attributes">
- <attribute name="data-selector" add=".s_website_form" separator=","/>
- </xpath>
+ <xpath expr="//*[@t-set='so_snippet_addition_selector']" position="inside">, .s_website_form</xpath>
<xpath expr="//div" position="after">
<!-- Form -->
<div data-js="website_form_editor" data-selector=".s_website_form">
diff --git a/addons/website_mass_mailing/views/snippets_templates.xml b/addons/website_mass_mailing/views/snippets_templates.xml
index 6200662243f..1411f42e89e 100644
--- a/addons/website_mass_mailing/views/snippets_templates.xml
+++ b/addons/website_mass_mailing/views/snippets_templates.xml
@@ -55,9 +55,7 @@
</template>
<template id="newsletter_subscribe_options" name="Newsletter Subscribe Options" inherit_id="website.snippet_options">
- <xpath expr="//*[@id='so_snippet_addition']" position="attributes">
- <attribute name="data-selector" add=".o_newsletter_popup" separator=","/>
- </xpath>
+ <xpath expr="//*[@t-set='so_snippet_addition_selector']" position="inside">, .o_newsletter_popup</xpath>
<xpath expr="//*[@id='row_valign_snippet_option']" position="attributes">
<attribute name="data-selector" separator="," add=".s_newsletter_block"/>
</xpath>
diff --git a/addons/website_sale/views/snippets.xml b/addons/website_sale/views/snippets.xml
index dd349a6b8ea..430cdf4e5e6 100644
--- a/addons/website_sale/views/snippets.xml
+++ b/addons/website_sale/views/snippets.xml
@@ -106,10 +106,7 @@
<we-button data-no-preview="true" data-open-searchbar-settings="">Products Search Bar</we-button>
</div>
</xpath>
- <xpath expr="//div[@id='so_content_addition']" position="attributes">
- <attribute name="data-selector" add=".s_wsale_products_searchbar_input" separator=","/>
- <attribute name="data-drop-near" add=".s_wsale_products_searchbar_input" separator=","/>
- </xpath>
+ <xpath expr="//*[@t-set='so_content_addition_selector']" position="inside">, .s_wsale_products_searchbar_input</xpath>
</template>
</odoo>