From 95d0ca4b2cb58536b7af1d10d905d8d85998e4b2 Mon Sep 17 00:00:00 2001
From: Goaman
Date: Thu, 17 Sep 2020 16:52:55 +0000
Subject: [PATCH] [REF] web_editor,web: adapt to new jabberwock editor
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Nicolas Bayet
Co-authored-by: Sébastien Geelen
Co-authored-by: Antoine Guenet
Co-authored-by: Christophe Matthieu
Co-authored-by: David Monjoie
---
.../tours/mail_full_composer_test_tour.js | 2 +-
addons/web/static/src/js/core/dom.js | 11 +-
.../src/js/core/smooth_scroll_on_drag.js | 317 ++--
.../static/src/js/backend/field_html.js | 345 ++--
.../static/src/js/editor/custom_colors.js | 16 +
.../web_editor/static/src/js/editor/editor.js | 289 ----
addons/web_editor/static/src/js/editor/rte.js | 812 ---------
.../static/src/js/editor/rte.summernote.js | 1232 --------------
.../static/src/js/editor/snippets.editor.js | 1307 +++++++++------
.../static/src/js/editor/snippets.options.js | 248 ++-
.../static/src/js/frontend/loader.js | 86 +-
.../web_editor/static/src/js/wysiwyg/root.js | 87 +-
.../src/js/wysiwyg/widgets/color_palette.js | 2 +-
.../src/js/wysiwyg/widgets/link_dialog.js | 210 +--
.../static/src/js/wysiwyg/widgets/media.js | 144 +-
.../static/src/js/wysiwyg/wysiwyg.js | 1452 +++++++++++++++--
.../static/src/js/wysiwyg/wysiwyg_iframe.js | 131 --
.../static/src/js/wysiwyg/wysiwyg_snippets.js | 56 -
.../wysiwyg/wysiwyg_translate_attributes.js | 64 +
.../static/src/scss/web_editor.backend.scss | 7 +-
.../web_editor/static/src/scss/wysiwyg.scss | 226 ++-
.../static/src/scss/wysiwyg_iframe.scss | 23 -
.../static/src/scss/wysiwyg_snippets.scss | 121 +-
addons/web_editor/static/src/xml/wysiwyg.xml | 10 +-
.../static/tests/field_html_tests.js | 133 +-
addons/web_editor/static/tests/test_utils.js | 259 +--
addons/web_editor/views/editor.xml | 13 -
addons/web_editor/views/snippets.xml | 15 +-
.../src/js/running_tour_action_helper.js | 13 +-
29 files changed, 3350 insertions(+), 4281 deletions(-)
diff --git a/addons/mail/static/tests/tours/mail_full_composer_test_tour.js b/addons/mail/static/tests/tours/mail_full_composer_test_tour.js
index 6d9e519ee87..be61c08bb86 100644
--- a/addons/mail/static/tests/tours/mail_full_composer_test_tour.js
+++ b/addons/mail/static/tests/tours/mail_full_composer_test_tour.js
@@ -60,7 +60,7 @@ tour.register('mail/static/tests/tours/mail_full_composer_test_tour.js', {
content: "Check composer content is kept",
trigger: '.oe_form_field[name="body"]',
run() {
- const bodyContent = document.querySelector('.oe_form_field[name="body"] textarea').textContent;
+ const bodyContent = document.querySelector('.oe_form_field[name="body"] jw-shadow').shadowRoot.textContent;
if (!bodyContent.includes("blahblah")) {
console.error(
`Full composer should contain text from small composer ("blahblah") in body input (actual: ${bodyContent})`
diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js
index 4af811d41bc..ffc6638f167 100644
--- a/addons/web/static/src/js/core/dom.js
+++ b/addons/web/static/src/js/core/dom.js
@@ -551,9 +551,14 @@ var dom = {
return;
}
$scrollable.stop();
- dom.scrollTo(el, Object.assign({}, options, {
- duration: remainingMs,
- })).then(() => resolve());
+ if (document.body.contains(el)) {
+ dom.scrollTo(el, Object.assign({}, options, {
+ duration: remainingMs,
+ })).then(resolve);
+ } else {
+ console.warn('This snippet are wrongly removed', el);
+ resolve();
+ }
};
// Detect the end of the animation to be able to indicate it to
diff --git a/addons/web/static/src/js/core/smooth_scroll_on_drag.js b/addons/web/static/src/js/core/smooth_scroll_on_drag.js
index 28a4beff4c4..2ac495bd47a 100644
--- a/addons/web/static/src/js/core/smooth_scroll_on_drag.js
+++ b/addons/web/static/src/js/core/smooth_scroll_on_drag.js
@@ -4,48 +4,6 @@ odoo.define('web/static/src/js/core/smooth_scroll_on_drag.js', function (require
const Class = require('web.Class');
const mixins = require('web.mixins');
-/**
- * Provides a helper for SmoothScrollOnDrag options.offsetElements
- */
-const OffsetElementsHelper = Class.extend({
-
- /**
- * @constructor
- * @param {Object} offsetElements
- * @param {jQuery} [offsetElements.$top] top offset element
- * @param {jQuery} [offsetElements.$right] right offset element
- * @param {jQuery} [offsetElements.$bottom] bottom offset element
- * @param {jQuery} [offsetElements.$left] left offset element
- */
- init: function (offsetElements) {
- this.offsetElements = offsetElements;
- },
- top: function () {
- if (!this.offsetElements.$top || !this.offsetElements.$top.length) {
- return 0;
- }
- return this.offsetElements.$top.get(0).getBoundingClientRect().bottom;
- },
- right: function () {
- if (!this.offsetElements.$right || !this.offsetElements.$right.length) {
- return 0;
- }
- return this.offsetElements.$right.get(0).getBoundingClientRect().left;
- },
- bottom: function () {
- if (!this.offsetElements.$bottom || !this.offsetElements.$bottom.length) {
- return 0;
- }
- return this.offsetElements.$bottom.get(0).getBoundingClientRect().top;
- },
- left: function () {
- if (!this.offsetElements.$left || !this.offsetElements.$left.length) {
- return 0;
- }
- return this.offsetElements.$left.get(0).getBoundingClientRect().right;
- },
-});
-
/**
* Provides smooth scroll behaviour on drag.
*/
@@ -55,7 +13,8 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, {
* @constructor
* @param {Object} parent The parent widget that uses this class.
* @param {jQuery} $element The element the smooth scroll on drag has to be set on.
- * @param {jQuery} $scrollTarget The element the scroll will be triggered on.
+ * @param {jQuery} $container Container for drop element.
+ * scrollTarget will be found with the closest scrollable parents.
* @param {Object} [options={}]
* @param {Object} [options.jQueryDraggableOptions={}] The configuration to be passed to
* the jQuery draggable function (all will be passed except scroll which will
@@ -76,39 +35,25 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, {
* when dragging $element bellow the bottom edge of target.
* @param {Object} [options.scrollBoundaries.left = true] Specifies whether scroll can still be triggered
* when dragging $element before the left edge of target.
- * @param {Object} [options.offsetElements={}] Visible elements in $scrollTarget that
- * reduce $scrollTarget drag visible area (scroll will be triggered sooner than
- * normally). A selector is passed so that elements such as automatically hidden
- * menu can then be correctly handled.
- * @param {jQuery} [options.offsetElements.$top] Visible top offset element which height will
- * be taken into account when triggering scroll at the top of the $scrollTarget.
- * @param {jQuery} [options.offsetElements.$right] Visible right offset element which width
- * will be taken into account when triggering scroll at the right side of the
- * $scrollTarget.
- * @param {jQuery} [options.offsetElements.$bottom] Visible bottom offset element which height
- * will be taken into account when triggering scroll at bottom of the $scrollTarget.
- * @param {jQuery} [options.offsetElements.$left] Visible left offset element which width
- * will be taken into account when triggering scroll at the left side of the
- * $scrollTarget.
+ * @param {Function} [options.dropzones] Function must return a JQuery list of dropzone elements.
+ * @param {Function} [options.over] Callback triggered when the draggable element is
+ * over a dropzone (requiered with options.dropzones).
+ * @param {Function} [options.out] Callback triggered when the draggable element is
+ * no longer above the dropzone (requiered with options.dropzones).
*/
- init(parent, $element, $scrollTarget, options = {}) {
+ init(parent, $element, $container, options = {}) {
mixins.ParentedMixin.init.call(this);
this.setParent(parent);
this.$element = $element;
- this.$scrollTarget = $scrollTarget;
+ this.$container = $container;
this.options = options;
// Setting optional options to their default value if not provided
this.options.jQueryDraggableOptions = this.options.jQueryDraggableOptions || {};
- if (!this.options.jQueryDraggableOptions.cursorAt) {
- this.$element.on('mousedown.smooth_scroll', this._onElementMouseDown.bind(this));
- }
this.options.scrollOffsetThreshold = this.options.scrollOffsetThreshold || 150;
this.options.scrollStep = this.options.scrollStep || 20;
- this.options.scrollTimerInterval = this.options.scrollTimerInterval || 5;
- this.options.offsetElements = this.options.offsetElements || {};
- this.options.offsetElementsManager = new OffsetElementsHelper(this.options.offsetElements);
+ this.options.scrollTimerInterval = this.options.scrollTimerInterval || 16;
this.options.scrollBoundaries = Object.assign({
top: true,
right: true,
@@ -128,8 +73,8 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, {
this.options.jQueryDraggableOptions.scroll = false;
const draggableOptions = Object.assign({}, this.options.jQueryDraggableOptions, {
start: (ev, ui) => this._onSmoothDragStart(ev, ui, this.options.jQueryDraggableOptions.start),
- drag: (ev, ui) => this._onSmoothDrag(ev, ui, this.options.jQueryDraggableOptions.drag),
- stop: (ev, ui) => this._onSmoothDragStop(ev, ui, this.options.jQueryDraggableOptions.stop),
+ drag: (ev, ui, inst) => this._onSmoothDrag(ev, ui, this.options.jQueryDraggableOptions.drag),
+ stop: (ev, ui, inst) => this._onSmoothDragStop(ev, ui, this.options.jQueryDraggableOptions.stop),
});
this.$element.draggable(draggableOptions);
},
@@ -138,7 +83,6 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, {
*/
destroy: function () {
mixins.ParentedMixin.destroy.call(this);
- this.$element.off('.smooth_scroll');
this._stopSmoothScroll();
},
@@ -159,56 +103,44 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, {
this._stopSmoothScroll();
this.autoScrollHandler = setInterval(
() => {
- // Prevents Delta's from being different from 0 when scroll should not occur (except when
- // helper is dragged outside of this.$scrollTarget's visible area as it increases
- // this.$scrollTarget's scrollHeight).
- // Also, this code prevents the helper from being incorrectly repositioned when target is
- // a child of this.$scrollTarget.
- this.verticalDelta = Math.min(
- // Ensures scrolling stops when dragging bellow this.$scrollTarget bottom.
- Math.max(
- 0,
- this.$scrollTarget.get(0).scrollHeight
- - (this.$scrollTarget.scrollTop() + this.$scrollTarget.innerHeight())
- ),
- // Ensures scrolling stops when dragging above this.$scrollTarget top.
- Math.max(
- this.verticalDelta,
- -this.$scrollTarget.scrollTop()
- )
- );
- this.horizontalDelta = Math.min(
- //Ensures scrolling stops when dragging left to this.$scrollTarget.
- Math.max(
- 0,
- this.$scrollTarget.get(0).scrollWidth
- - (this.$scrollTarget.scrollLeft() + this.$scrollTarget.innerWidth())
- ),
- //Ensures scrolling stops when dragging right to this.$scrollTarget.
- Math.max(
- this.horizontalDelta,
- -this.$scrollTarget.scrollLeft()
- )
- );
-
- // Keep helper at right position while scrolling when helper is a child of this.$scrollTarget.
- if (this.scrollTargetIsParent) {
- const offset = ui.helper.offset();
- ui.helper.offset({
- top: offset.top + this.verticalDelta,
- left: offset.left + this.horizontalDelta
- });
+ for (const scrollTarget of this.scrollableTargets) {
+ this._smoothScroll(ui, scrollTarget);
}
- this.$scrollTarget.scrollTop(
- this.$scrollTarget.scrollTop() +
- this.verticalDelta
- );
- this.$scrollTarget.scrollLeft(
- this.$scrollTarget.scrollLeft() +
- this.horizontalDelta
- );
},
- this.options.scrollTimerInterval
+ this.options.scrollTimerInterval,
+ );
+ },
+ _smoothScroll(ui, scrollTarget) {
+ const delta = this.deltaScrollableTargets.get(scrollTarget);
+ // Prevents Delta's from being different from 0 when scroll should not occur (except when
+ // helper is dragged outside of scrollTarget's visible area as it increases
+ // scrollTarget's scrollHeight).
+ // Also, this code prevents the helper from being incorrectly repositioned when target is
+ // a child of scrollTarget.
+ delta.vertical = Math.min(
+ // Ensures scrolling stops when dragging bellow scrollTarget bottom.
+ Math.max( 0, scrollTarget.scrollHeight - (scrollTarget.scrollTop + scrollTarget.clientHeight)),
+ // Ensures scrolling stops when dragging above scrollTarget top.
+ Math.max(delta.vertical, - scrollTarget.scrollTop)
+ );
+ delta.horizontal = Math.min(
+ //Ensures scrolling stops when dragging left to scrollTarget.
+ Math.max(0, scrollTarget.scrollWidth - (scrollTarget.scrollLeft + scrollTarget.clientWidth)),
+ //Ensures scrolling stops when dragging right to scrollTarget.
+ Math.max(delta.horizontal, - scrollTarget.scrollLeft)
+ );
+
+ // Keep helper at right position while scrolling when helper is a child of scrollTarget.
+ const scrollTargetIsParent = scrollTarget.contains(this.$element.get(0));
+ if (scrollTargetIsParent) {
+ ui.helper.css({
+ 'margin-left': parseInt(ui.helper.css('margin-left') || 0) + delta.horizontal,
+ 'margin-top': parseInt(ui.helper.css('margin-top') || 0) + delta.vertical,
+ });
+ }
+ scrollTarget.scrollTo(
+ scrollTarget.scrollLeft + delta.horizontal,
+ scrollTarget.scrollTop + delta.vertical,
);
},
/**
@@ -228,39 +160,29 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, {
* @private
* @param {Object} ui The jQuery drag handler ui parameter.
*/
- _updatePositionOptions(ui) {
- const draggableHelperOffset = ui.offset;
- const scrollTargetOffset = this.$scrollTarget.offset();
+ _getPositionDelta(ui, scrollTarget) {
+ const $scrollTarget = $(scrollTarget);
+ const scrollTargetIsDocument = $scrollTarget.is('html, body');
+ const draggableHelperOffset = ui.helper[0].getBoundingClientRect();
+ const scrollTargetOffset = scrollTarget.getBoundingClientRect();
+
let visibleOffset = {
top: draggableHelperOffset.top
- scrollTargetOffset.top
- + this.options.jQueryDraggableOptions.cursorAt.top
- - this.options.offsetElementsManager.top(),
- right: scrollTargetOffset.left + this.$scrollTarget.outerWidth()
+ + this.options.jQueryDraggableOptions.cursorAt.top,
+ right: scrollTargetOffset.left
+ + scrollTarget.clientWidth
- draggableHelperOffset.left
- - this.options.jQueryDraggableOptions.cursorAt.left
- - this.options.offsetElementsManager.right(),
- bottom: scrollTargetOffset.top + this.$scrollTarget.outerHeight()
+ - this.options.jQueryDraggableOptions.cursorAt.right,
+ bottom: scrollTargetOffset.top
+ + scrollTarget.clientHeight
- draggableHelperOffset.top
- - this.options.jQueryDraggableOptions.cursorAt.top
- - this.options.offsetElementsManager.bottom(),
+ - this.options.jQueryDraggableOptions.cursorAt.bottom,
left: draggableHelperOffset.left
- scrollTargetOffset.left
- + this.options.jQueryDraggableOptions.cursorAt.left
- - this.options.offsetElementsManager.left(),
+ + this.options.jQueryDraggableOptions.cursorAt.left,
};
- // If this.$scrollTarget is the html tag, we need to take the scroll position in to account
- // as offsets positions are calculated relative to the document (thus ).
- if (this.scrollTargetIsDocument) {
- const scrollTargetScrollTop = this.$scrollTarget.scrollTop();
- const scrollTargetScrollLeft = this.$scrollTarget.scrollLeft();
- visibleOffset.top -= scrollTargetScrollTop;
- visibleOffset.right += scrollTargetScrollLeft;
- visibleOffset.bottom += scrollTargetScrollTop;
- visibleOffset.left -= scrollTargetScrollLeft;
- }
-
const scrollDecelerator = {
vertical: 0,
horizontal: 0,
@@ -304,35 +226,26 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, {
}
}
- this.verticalDelta = Math.ceil(scrollStepDirection.vertical *
- this.options.scrollStep *
- (1 - Math.sqrt(scrollDecelerator.vertical)));
- this.horizontalDelta = Math.ceil(scrollStepDirection.horizontal *
- this.options.scrollStep *
- (1 - Math.sqrt(scrollDecelerator.horizontal)));
+ return {
+ vertical: Math.ceil(scrollStepDirection.vertical *
+ this.options.scrollStep *
+ (1 - Math.sqrt(scrollDecelerator.vertical))),
+ horizontal: Math.ceil(scrollStepDirection.horizontal *
+ this.options.scrollStep *
+ (1 - Math.sqrt(scrollDecelerator.horizontal))),
+ };
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
- /**
- * Called when mouse button is down on this.$element.
- * Updates the mouse cursor position variable.
- *
- * @private
- * @param {Object} ev The jQuery mousedown handler event parameter.
- */
- _onElementMouseDown(ev) {
- const elementOffset = $(ev.target).offset();
- this.options.jQueryDraggableOptions.cursorAt = {
- top: ev.pageY - elementOffset.top,
- left: ev.pageX - elementOffset.left,
- };
- },
/**
* Called when dragging the element.
* Updates the position options and call the provided callback if any.
+ * When use dropzones options, call 'over' option when the helper is
+ * over a dropzone and call 'out' when the helper is no longer above
+ * the dropzone.
*
* @private
* @param {Object} ev The jQuery drag handler event parameter.
@@ -340,10 +253,59 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, {
* @param {Function} onDragCallback The jQuery drag callback.
*/
_onSmoothDrag(ev, ui, onDragCallback) {
- this._updatePositionOptions(ui);
+ for (const scrollTarget of this.scrollableTargets) {
+ const delta = this._getPositionDelta(ui, scrollTarget);
+ this.deltaScrollableTargets.set(scrollTarget, delta);
+ }
+
if (typeof onDragCallback === 'function') {
onDragCallback.call(ui.helper, ev, ui);
}
+
+ if (this.options.dropzones) {
+ if (!this.$dropzones) {
+ this.$dropzones = this.options.dropzones.call(ui.helper, ev, ui).filter(function () {
+ if (ui.helper[0].contains(this)) {
+ console.warn('One drop zone is include inside the draggable item.');
+ } else {
+ return true;
+ }
+ });
+ }
+ if (!this.$dropzones.length) {
+ return;
+ }
+ const draggableBox = ui.helper[0].getBoundingClientRect();
+
+ let over;
+ for (const zone of this.$dropzones.get()) {
+ let box;
+ if (this.overDropzone && this.overDropzone[0] === zone) {
+ box = this.overDropzone[1];
+ } else {
+ box = zone.getBoundingClientRect();
+ }
+ if (box.top < draggableBox.top + draggableBox.height && box.top + box.height > draggableBox.top &&
+ box.left < draggableBox.left + draggableBox.width && box.left + box.width > draggableBox.left) {
+ over = zone;
+ break;
+ }
+ }
+ if (this.overDropzone && this.overDropzone[0] !== over) {
+ this.options.out.call(ui.helper, ui, this.overDropzone[0]);
+ this.overDropzone = null;
+ }
+ if (over && !this.overDropzone) {
+ const box = over.getBoundingClientRect();
+ this.overDropzone = [over, {
+ top: box.top,
+ height: box.height,
+ left: box.left,
+ width: box.width,
+ }];
+ this.options.over.call(ui.helper, ui, over);
+ }
+ }
},
/**
* Called when starting to drag the element.
@@ -356,9 +318,34 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, {
* @param {Function} onDragStartCallBack The jQuery drag callback.
*/
_onSmoothDragStart(ev, ui, onDragStartCallBack) {
- this.scrollTargetIsDocument = this.$scrollTarget.is('html');
- this.scrollTargetIsParent = this.$scrollTarget.get(0).contains(this.$element.get(0));
- this._updatePositionOptions(ui);
+ const elementOffset = ev.target.getBoundingClientRect();
+ this.options.jQueryDraggableOptions.cursorAt = {
+ top: ev.pageY - elementOffset.top,
+ right: elementOffset.right - ev.pageX,
+ bottom: elementOffset.bottom - ev.pageY,
+ left: ev.pageX - elementOffset.left,
+ };
+
+ this.scrollableTargets = [];
+ this.deltaScrollableTargets = new WeakMap();
+ let scrollTarget = this.$container[0];
+ while (scrollTarget) {
+ if (scrollTarget.nodeType === 1 && scrollTarget.scrollHeight > scrollTarget.clientHeight) {
+ const overflow = window.getComputedStyle(scrollTarget).overflow;
+ if (overflow.includes('auto') || overflow.includes('scroll')) {
+ this.scrollableTargets.push(scrollTarget);
+ this.deltaScrollableTargets.set(scrollTarget, {
+ vertical: 0,
+ horizontal: 0,
+ });
+ }
+ }
+ scrollTarget = scrollTarget.parentNode;
+ if (scrollTarget && scrollTarget.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scrollTarget.host) {
+ scrollTarget = scrollTarget.host;
+ }
+ }
+
this._startSmoothScroll(ui);
if (typeof onDragStartCallBack === 'function') {
onDragStartCallBack.call(ui.helper, ev, ui);
@@ -378,6 +365,8 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, {
if (typeof onDragEndCallBack === 'function') {
onDragEndCallBack.call(ui.helper, ev, ui);
}
+ this.$dropzones = null;
+ this.overDropzone = null;
},
});
diff --git a/addons/web_editor/static/src/js/backend/field_html.js b/addons/web_editor/static/src/js/backend/field_html.js
index 7005318ef28..6b39ec7bf16 100644
--- a/addons/web_editor/static/src/js/backend/field_html.js
+++ b/addons/web_editor/static/src/js/backend/field_html.js
@@ -3,9 +3,8 @@ odoo.define('web_editor.field.html', function (require) {
var ajax = require('web.ajax');
var basic_fields = require('web.basic_fields');
-var config = require('web.config');
var core = require('web.core');
-var Wysiwyg = require('web_editor.wysiwyg.root');
+var wysiwygLoader = require('web_editor.loader');
var field_registry = require('web.field_registry');
// must wait for web/ to add the default html widget, otherwise it would override the web_editor one
require('web._field_registry');
@@ -13,7 +12,6 @@ require('web._field_registry');
var _lt = core._lt;
var TranslatableFieldMixin = basic_fields.TranslatableFieldMixin;
var QWeb = core.qweb;
-var assetsLoaded;
var jinjaRegex = /(^|\n)\s*%\s(end|set\s)/;
@@ -32,7 +30,7 @@ var jinjaRegex = /(^|\n)\s*%\s(end|set\s)/;
*/
var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
description: _lt("Html"),
- className: 'oe_form_field oe_form_field_html',
+ className: 'oe_form_field oe_form_field_html d-flex',
supportedFieldTypes: ['html'],
custom_events: {
@@ -45,32 +43,20 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
/**
* @override
*/
- willStart: function () {
- var self = this;
- this.isRendered = false;
- this._onUpdateIframeId = 'onLoad_' + _.uniqueId('FieldHtml');
- var defAsset;
+ willStart: async function () {
+ await this._super();
if (this.nodeOptions.cssReadonly) {
- defAsset = ajax.loadAsset(this.nodeOptions.cssReadonly);
+ this.cssReadonly = await ajax.loadAsset(this.nodeOptions.cssReadonly);
}
-
- if (!assetsLoaded) { // avoid flickering when begin to edit
- assetsLoaded = new Promise(function (resolve) {
- var wysiwyg = new Wysiwyg(self, {});
- wysiwyg.attachTo($('
')[0];
- $node.append( p );
- range.createFromNode(p.firstChild).select();
- }
- return res;
-};
-
-function prettify_html(html) {
- html = html.trim();
- var result = '',
- level = 0,
- get_space = function (level) {
- var i = level, space = '';
- while (i--) space += ' ';
- return space;
- },
- reg = /^<\/?(a|span|font|u|em|i|strong|b)(\s|>)/i,
- inline_level = Infinity,
- tokens = _.compact(_.flatten(_.map(html.split(/), function (value) {
- value = value.replace(/\s+/g, ' ').split(/>/);
- value[0] = /\S/.test(value[0]) ? '<' + value[0] + '>' : '';
- return value;
- })));
-
- // reduce => merge inline style + text
-
- for (var i = 0, l = tokens.length; i < l; i++) {
- var token = tokens[i];
- var inline_tag = reg.test(token);
- var inline = inline_tag || inline_level <= level;
-
- if (token[0] === '<' && token[1] === '/') {
- if (inline_tag && inline_level === level) {
- inline_level = Infinity;
- }
- level--;
- }
-
- if (!inline && !/\S/.test(token)) {
- continue;
- }
- if (!inline || (token[1] !== '/' && inline_level > level)) {
- result += get_space(level);
- }
-
- if (token[0] === '<' && token[1] !== '/') {
- level++;
- if (inline_tag && inline_level > level) {
- inline_level = level;
- }
- }
-
- if (token.match(/^<(img|hr|br)/)) {
- level--;
- }
-
- // don't trim inline content (which could change appearance)
- if (!inline) {
- token = token.trim();
- }
-
- result += token.replace(/\s+/, ' ');
-
- if (inline_level > level) {
- result += '\n';
- }
- }
- return result;
-}
-
-/*
- * This override when clicking on the 'Code View' button has two aims:
- *
- * - have our own code view implementation for FieldTextHtml
- * - add an 'enable' paramater to call the function directly and allow us to
- * disable (false) or enable (true) the code view mode.
- */
-$.summernote.pluginEvents.codeview = function (event, editor, layoutInfo, enable) {
- if (!layoutInfo) {
- return;
- }
- if (layoutInfo.toolbar) {
- // if editor inline (FieldTextHtmlSimple)
- var is_activated = $.summernote.eventHandler.modules.codeview.isActivated(layoutInfo);
- if (is_activated === enable) {
- return;
- }
- return eventHandler.modules.codeview.toggle(layoutInfo);
- } else {
- // if editor iframe (FieldTextHtml)
- var $editor = layoutInfo.editor();
- var $textarea = $editor.prev('textarea');
- if ($textarea.is('textarea') === enable) {
- return;
- }
-
- if (!$textarea.length) {
- // init and create texarea
- var html = prettify_html($editor.prop("innerHTML"));
- $editor.parent().css({
- 'position': 'absolute',
- 'top': 0,
- 'bottom': 0,
- 'left': 0,
- 'right': 0
- });
- $textarea = $('').css({
- 'margin': '0 -4px',
- 'padding': '0 4px',
- 'border': 0,
- 'top': '51px',
- 'left': '620px',
- 'width': '100%',
- 'font-family': 'sans-serif',
- 'font-size': '13px',
- 'height': '98%',
- 'white-space': 'pre',
- 'word-wrap': 'normal'
- }).val(html).data('init', html);
- $editor.before($textarea);
- $editor.hide();
- } else {
- // save changes
- $editor.prop('innerHTML', $textarea.val().replace(/\s*\n\s*/g, '')).trigger('content_changed');
- $textarea.remove();
- $editor.show();
- }
- }
-};
-
-// Fix ie and re-range to don't break snippet
-var last_div;
-var last_div_change;
-var last_editable;
-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 ? initial_data.rect.left : initial_data.event.clientX);
- var dy = event.clientY - (event.shiftKey && initial_data.rect ? 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");
- var r;
-
- 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();
- }
- }
- }
-
- // restore range if range lost after clicking on non-editable area
- try {
- r = range.create();
- } catch (e) {
- // If this code is running inside an iframe-editor and that the range
- // is outside of this iframe, this will fail as the iframe does not have
- // the permission to check the outside content this way. In that case,
- // we simply ignore the exception as it is as if there was no range.
- return;
- }
- var editables = $(".o_editable[contenteditable], .note-editable[contenteditable]");
- var r_editable = editables.has((r||{}).sc).addBack(editables.filter((r||{}).sc));
- if (!r_editable.closest('.note-editor').is($editable) && !r_editable.filter('.o_editable').is(editables)) {
- var saved_editable = editables.has((remember_selection||{}).sc);
- if ($editable.length && !saved_editable.closest('.o_editable, .note-editor').is($editable)) {
- remember_selection = range.create(dom.firstChild($editable[0]), 0);
- } else if (!saved_editable.length) {
- remember_selection = undefined;
- }
- if (remember_selection) {
- try {
- remember_selection.select();
- } catch (e) {
- console.warn(e);
- }
- }
- } else if (r_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 };
- }
-}
-
-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).on('mouseup', summernote_mouseup);
- oLayoutInfo.editor().off('click').on('click', function (e) {e.preventDefault();}); // if the content editable is a link
-
- /**
- * Open Media Dialog on double click on an image/video/icon.
- * Shows a tooltip on click to say to the user he can double click.
- */
- create_dblclick_feature("img, .media_iframe_video, i.fa, span.fa, a.o_image", function () {
- eventHandler.modules.imageDialog.show(oLayoutInfo);
- });
-
- /**
- * Open Link Dialog on double click on a link/button.
- * Shows a tooltip on click to say to the user he can double click.
- */
- create_dblclick_feature("a[href], a.btn, button.btn", function () {
- eventHandler.modules.linkDialog.show(oLayoutInfo);
- });
-
- oLayoutInfo.editable().on('mousedown', function (e) {
- if (dom.isImg(e.target) && dom.isContentEditable(e.target)) {
- range.createFromNode(e.target).select();
- }
- });
- $(document).on("keyup", reRangeSelectKey);
-
- var clone_data = false;
-
- if (options.model) {
- oLayoutInfo.editable().data({'oe-model': options.model, 'oe-id': options.id});
- }
- if (options.getMediaDomain) {
- oLayoutInfo.editable().data('oe-media-domain', options.getMediaDomain);
- }
-
- var $node = oLayoutInfo.editor();
- if ($node.data('oe-model') || $node.data('oe-translation-id')) {
- $node.on('content_changed', function () {
- var $nodes = $('[data-oe-model], [data-oe-translation-id]')
- .filter(function () { return this !== $node[0];});
-
- if ($node.data('oe-model')) {
- $nodes = $nodes.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-translation-id')) $nodes = $nodes.filter('[data-oe-translation-id="'+$node.data('oe-translation-id')+'"]');
- 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 custom_toolbar = oLayoutInfo.toolbar ? oLayoutInfo.toolbar() : undefined;
- var $toolbar = $(oLayoutInfo.popover()).add(custom_toolbar);
- $('button[data-event="undo"], button[data-event="redo"]', $toolbar).attr('disabled', true);
-
- $(oLayoutInfo.editor())
- .add(oLayoutInfo.handle())
- .add(oLayoutInfo.popover())
- .add(custom_toolbar)
- .on('click content_changed', 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());
- });
-
- function create_dblclick_feature(selector, callback) {
- var show_tooltip = true;
-
- oLayoutInfo.editor().on("dblclick", selector, function (e) {
- var $target = $(e.target);
- if (!dom.isContentEditable($target)) {
- // Prevent edition of non editable parts
- return;
- }
-
- show_tooltip = false;
- callback();
- e.stopImmediatePropagation();
- });
-
- oLayoutInfo.editor().on("click", selector, function (e) {
- var $target = $(e.target);
- if (!dom.isContentEditable($target)) {
- // Prevent edition of non editable parts
- return;
- }
-
- show_tooltip = true;
- setTimeout(function () {
- // Do not show tooltip on double-click and if there is already one
- if (!show_tooltip || $target.attr('title') !== undefined) {
- return;
- }
- $target.tooltip({title: _t('Double-click to edit'), trigger: 'manuel', container: 'body'}).tooltip('show');
- setTimeout(function () {
- $target.tooltip('dispose');
- }, 800);
- }, 400);
- });
- }
-};
-var fn_detach = eventHandler.detach;
-eventHandler.detach = function (oLayoutInfo, options) {
- fn_detach.call(this, oLayoutInfo, options);
- oLayoutInfo.editable().off('mousedown');
- oLayoutInfo.editor().off("dragstart");
- oLayoutInfo.editor().off('click');
- $(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('File / Image'),
- 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'),
- small: _t('Small'),
- 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: {
- custom: _t('Custom Color'),
- background: _t('Background Color'),
- foreground: _t('Font Color'),
- transparent: _t('Transparent'),
- setTransparent: _t('None'),
- },
- 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')
- }
-};
-
-//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
-
-/**
- * @todo get rid of this. This has been implemented as a fix to be able to
- * instantiate media, link and alt dialogs outside the main editor: in the
- * simple HTML fields and forum textarea.
- */
-var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
- /**
- * @constructor
- */
- init: function (parent) {
- mixins.EventDispatcherMixin.init.call(this);
- this.setParent(parent);
-
- topBus.on('alt_dialog_demand', this, this._onAltDialogDemand);
- topBus.on('crop_image_demand', this, this._onCropImageDemand);
- topBus.on('link_dialog_demand', this, this._onLinkDialogDemand);
- topBus.on('media_dialog_demand', this, this._onMediaDialogDemand);
- },
- /**
- * @override
- */
- destroy: function () {
- mixins.EventDispatcherMixin.destroy.call(this);
-
- topBus.off('alt_dialog_demand', this, this._onAltDialogDemand);
- topBus.off('crop_image_demand', this, this._onCropImageDemand);
- topBus.off('link_dialog_demand', this, this._onLinkDialogDemand);
- topBus.off('media_dialog_demand', this, this._onMediaDialogDemand);
- },
-
- /**
- * Create modified image attachments.
- *
- * @param {jQuery} $editable
- * @returns {Promise}
- */
- saveModifiedImages: function ($editable) {
- const defs = _.map($editable, async editableEl => {
- const {oeModel: resModel, oeId: resId} = editableEl.dataset;
- const proms = [...editableEl.querySelectorAll('.o_modified_image_to_save')].map(async el => {
- const isBackground = !el.matches('img');
- el.classList.remove('o_modified_image_to_save');
- // Modifying an image always creates a copy of the original, even if
- // it was modified previously, as the other modified image may be used
- // elsewhere if the snippet was duplicated or was saved as a custom one.
- const newAttachmentSrc = await this._rpc({
- route: `/web_editor/modify_image/${el.dataset.originalId}`,
- params: {
- res_model: resModel,
- res_id: parseInt(resId),
- data: (isBackground ? el.dataset.bgSrc : el.getAttribute('src')).split(',')[1],
- },
- });
- if (isBackground) {
- $(el).css('background-image', `url('${newAttachmentSrc}')`);
- delete el.dataset.bgSrc;
- } else {
- el.setAttribute('src', newAttachmentSrc);
- }
- });
- return Promise.all(proms);
- });
- return Promise.all(defs);
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when a demand to open a alt dialog is received on the bus.
- *
- * @private
- * @param {Object} data
- */
- _onAltDialogDemand: function (data) {
- if (data.__alreadyDone) {
- return;
- }
- data.__alreadyDone = true;
- var altDialog = new weWidgets.AltDialog(this,
- data.options || {},
- data.media
- );
- if (data.onSave) {
- altDialog.on('save', this, data.onSave);
- }
- if (data.onCancel) {
- altDialog.on('cancel', this, data.onCancel);
- }
- altDialog.open();
- },
- /**
- * Called when a demand to crop an image is received on the bus.
- *
- * @private
- * @param {Object} data
- */
- _onCropImageDemand: function (data) {
- if (data.__alreadyDone) {
- return;
- }
- data.__alreadyDone = true;
- new weWidgets.ImageCropWidget(this, data.media)
- .appendTo(data.$editable);
- },
- /**
- * Called when a demand to open a link dialog is received on the bus.
- *
- * @private
- * @param {Object} data
- */
- _onLinkDialogDemand: function (data) {
- if (data.__alreadyDone) {
- return;
- }
- data.__alreadyDone = true;
- var linkDialog = new weWidgets.LinkDialog(this,
- data.options || {},
- data.$editable,
- data.linkInfo
- );
- if (data.onSave) {
- linkDialog.on('save', this, data.onSave);
- }
- if (data.onCancel) {
- linkDialog.on('cancel', this, data.onCancel);
- }
- linkDialog.open();
- },
- /**
- * Called when a demand to open a media dialog is received on the bus.
- *
- * @private
- * @param {Object} data
- */
- _onMediaDialogDemand: function (data) {
- if (data.__alreadyDone) {
- return;
- }
- data.__alreadyDone = true;
-
- var mediaDialog = new weWidgets.MediaDialog(this,
- _.extend({
- res_model: data.$editable.data('oe-model'),
- res_id: data.$editable.data('oe-id'),
- domain: data.$editable.data('oe-media-domain'),
- }, data.options),
- data.media
- );
- if (data.onSave) {
- mediaDialog.on('save', this, data.onSave);
- }
- if (data.onCancel) {
- mediaDialog.on('cancel', this, data.onCancel);
- }
- mediaDialog.open();
- },
-});
-return SummernoteManager;
-});
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 f72aa15720b..eeb48a71ffc 100644
--- a/addons/web_editor/static/src/js/editor/snippets.editor.js
+++ b/addons/web_editor/static/src/js/editor/snippets.editor.js
@@ -6,8 +6,7 @@ var core = require('web.core');
var Dialog = require('web.Dialog');
var dom = require('web.dom');
var Widget = require('web.Widget');
-var options = require('web_editor.snippets.options');
-var Wysiwyg = require('web_editor.wysiwyg');
+var snippetOptions = require('web_editor.snippets.options');
const {ColorPaletteWidget} = require('web_editor.ColorPalette');
const SmoothScrollOnDrag = require('web/static/src/js/core/smooth_scroll_on_drag.js');
const {getCSSVariableValue} = require('web_editor.utils');
@@ -20,6 +19,24 @@ var globalSelector = {
is: () => false,
};
+// jQuery extensions
+$.extend($.expr[':'], {
+ o_editable: function (node, i, m) {
+ while (node) {
+ if (node.className && _.isString(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;
+ },
+});
+
/**
* Management of the overlay and option list for a snippet.
*/
@@ -44,17 +61,35 @@ var SnippetEditor = Widget.extend({
* @param {jQuery} $editable
* @param {Object} options
*/
- init: function (parent, target, templateOptions, $editable, options) {
+ init: function (parent, snippetElement, templateOptions, $editable, snippetMenu, options) {
this._super.apply(this, arguments);
this.options = options;
this.$editable = $editable;
- this.ownerDocument = this.$editable[0].ownerDocument;
- this.$body = $(this.ownerDocument.body);
- this.$target = $(target);
- this.$target.data('snippet-editor', this);
+ this.$snippetBlock = $(snippetElement);
+ this.$editor = this.getParent().$editor;
+ this.$snippetBlock.data('snippet-editor', this);
+ // The following class is a hack. There is a possibility of the
+ // `$snippetBlock` to be destroyed at some point.
+ // For example: The method `updateChangesInWysiwyg` of an "editor option" migth
+ // erase $snippetBlock that are children on the current "editor option".
+ // In that case, the editor of the child will be erased with the method
+ // `updateCurrentSnippetEditorOverlay` because it's target will not be
+ // present on the DOM.
+ // But because an editor has been previously created, it might need to
+ // be cleaned with `cleanForSave`. Therfore we flag the $snippetBlock
+ // with "o_snippet_editor_updated" to recreate editor in order to be
+ // able to call `cleanForSave`.
+ this.$snippetBlock.addClass("o_snippet_editor_updated");
+ this.$body = $(document.body);
this.templateOptions = templateOptions;
this.isTargetParentEditable = false;
this.isTargetMovable = false;
+ this.JWEditorLib = options.JWEditorLib;
+ this.wysiwyg = options.wysiwyg;
+ this.editor = options.wysiwyg.editor;
+ this.editorHelpers = this.wysiwyg.editorHelpers;
+
+ this.snippetMenu = snippetMenu;
this.__isStarted = new Promise(resolve => {
this.__isStartedResolveFunc = resolve;
@@ -64,15 +99,16 @@ var SnippetEditor = Widget.extend({
* @override
*/
start: function () {
+ const self = this;
var defs = [this._super.apply(this, arguments)];
// Initialize the associated options (see snippets.options.js)
defs.push(this._initializeOptions());
var $customize = this._customize$Elements[this._customize$Elements.length - 1];
- this.isTargetParentEditable = this.$target.parent().is(':o_editable');
+ this.isTargetParentEditable = this.$snippetBlock.parent().is(':o_editable');
this.isTargetMovable = this.isTargetParentEditable && this.isTargetMovable;
- this.isTargetRemovable = this.isTargetParentEditable && !this.$target.parent().is('[data-oe-type="image"]');
+ this.isTargetRemovable = this.isTargetParentEditable && !this.$snippetBlock.parent().is('[data-oe-type="image"]');
// Initialize move/clone/remove buttons
if (this.isTargetMovable) {
@@ -80,16 +116,15 @@ var SnippetEditor = Widget.extend({
const smoothScrollOptions = this.options.getScrollOptions({
jQueryDraggableOptions: {
cursorAt: {
- left: 10,
- top: 10
+ left: 15,
+ top: 11,
},
+ iframeFix: true,
handle: '.o_move_handle',
helper: () => {
- var $clone = this.$el.clone().css({width: '24px', height: '24px', border: 0});
- $clone.appendTo(this.$body).removeClass('d-none');
- return $clone;
+ return $('');
},
- start: this._onDragAndDropStart.bind(this),
+ start: this._dragAndDropStart.bind(this),
stop: (...args) => {
// Delay our stop handler so that some summernote handlers
// which occur on mouseup (and are themself delayed) are
@@ -100,8 +135,24 @@ var SnippetEditor = Widget.extend({
}, 0);
},
},
+ dropzones: function () {
+ return self.$editor.find('.oe_drop_zone');
+ },
+ over: function (ui, droppable) {
+ if (!self.dropped) {
+ $(droppable).addClass('d-none').after(self.$snippetBlock);
+ self.dropped = true;
+ }
+ },
+ out: function (ui, droppable) {
+ if (self.dropped) {
+ $(droppable).removeClass('d-none');
+ self.$snippetBlock.detach();
+ self.dropped = false;
+ }
+ },
});
- this.draggableComponent = new SmoothScrollOnDrag(this, this.$el, $().getScrollingElement(), smoothScrollOptions);
+ this.draggableComponent = new SmoothScrollOnDrag(this, this.$el, this.$editable.find('#wrapwrap').addBack().last(), smoothScrollOptions);
} else {
this.$('.o_overlay_move_options').addClass('d-none');
$customize.find('.oe_snippet_clone').addClass('d-none');
@@ -113,7 +164,7 @@ var SnippetEditor = Widget.extend({
var _animationsCount = 0;
var postAnimationCover = _.throttle(() => this.cover(), 100);
- this.$target.on('transitionstart.snippet_editor, animationstart.snippet_editor', () => {
+ this.$snippetBlock.on('transitionstart.snippet_editor, animationstart.snippet_editor', () => {
// We cannot rely on the fact each transition/animation start will
// trigger a transition/animation end as the element may be removed
// from the DOM before or it could simply be an infinite animation.
@@ -134,7 +185,7 @@ var SnippetEditor = Widget.extend({
// On top of what is explained above, do the post animation cover for
// each detected transition/animation end so that the user does not see
// a flickering when not needed.
- this.$target.on('transitionend.snippet_editor, animationend.snippet_editor', postAnimationCover);
+ this.$snippetBlock.on('transitionend.snippet_editor, animationend.snippet_editor', postAnimationCover);
return Promise.all(defs).then(() => {
this.__isStartedResolveFunc(this);
@@ -149,8 +200,8 @@ var SnippetEditor = Widget.extend({
this.trigger_up('snippet_editor_destroyed');
this._super(...arguments);
- this.$target.removeData('snippet-editor');
- this.$target.off('.snippet_editor');
+ this.$snippetBlock.removeData('snippet-editor');
+ this.$snippetBlock.off('.snippet_editor');
},
//--------------------------------------------------------------------------
@@ -171,8 +222,8 @@ var SnippetEditor = Widget.extend({
* been dropped in the page.
*/
buildSnippet: async function () {
- for (var i in this.styles) {
- this.styles[i].onBuilt();
+ for (var i in this.snippetOptionInstances) {
+ this.snippetOptionInstances[i].onBuilt();
}
await this.toggleTargetVisibility(true);
},
@@ -184,9 +235,9 @@ var SnippetEditor = Widget.extend({
if (this.isDestroyed()) {
return;
}
- await this.toggleTargetVisibility(!this.$target.hasClass('o_snippet_invisible'));
- const proms = _.map(this.styles, option => {
- return option.cleanForSave();
+ await this.toggleTargetVisibility(!this.$snippetBlock.hasClass('o_snippet_invisible'));
+ const proms = _.map(this.snippetOptionInstances, async option => {
+ await option.cleanForSave();
});
await Promise.all(proms);
},
@@ -194,22 +245,22 @@ var SnippetEditor = Widget.extend({
* Closes all widgets of all options.
*/
closeWidgets: function () {
- if (!this.styles || !this.areOptionsShown()) {
+ if (!this.snippetOptionInstances || !this.areOptionsShown()) {
return;
}
- Object.keys(this.styles).forEach(key => {
- this.styles[key].closeWidgets();
+ Object.keys(this.snippetOptionInstances).forEach(key => {
+ this.snippetOptionInstances[key].closeWidgets();
});
},
/**
* Makes the editor overlay cover the associated snippet.
*/
cover: function () {
- if (!this.isShown() || !this.$target.length || !this.$target.is(':visible')) {
+ if (!this.isShown() || !this.$snippetBlock.length || !this.$snippetBlock.is(':visible')) {
return;
}
- const $modal = this.$target.find('.modal');
- const $target = $modal.length ? $modal : this.$target;
+ const $modal = this.$snippetBlock.find('.modal');
+ const $target = $modal.length ? $modal : this.$snippetBlock;
const offset = $target.offset();
var manipulatorOffset = this.$el.parent().offset();
offset.top -= manipulatorOffset.top;
@@ -228,13 +279,13 @@ var SnippetEditor = Widget.extend({
* directly in the DOM thanks to the `data-name` attribute.
*/
getName: function () {
- if (this.$target.data('name') !== undefined) {
- return this.$target.data('name');
+ if (this.$snippetBlock.data('name') !== undefined) {
+ return this.$snippetBlock.data('name');
}
- if (this.$target.is('img')) {
+ if (this.$snippetBlock.is('img')) {
return _t("Image");
}
- if (this.$target.parent('.row').length) {
+ if (this.$snippetBlock.parent('.row').length) {
return _t("Column");
}
return _t("Block");
@@ -255,7 +306,7 @@ var SnippetEditor = Widget.extend({
* @returns {boolean}
*/
isTargetVisible: function () {
- return (this.$target[0].dataset.invisible !== '1');
+ return (this.$snippetBlock[0].dataset.invisible !== '1');
},
/**
* Removes the associated snippet from the DOM and destroys the associated
@@ -267,61 +318,75 @@ var SnippetEditor = Widget.extend({
this.toggleOverlay(false);
this.toggleOptions(false);
- await new Promise(resolve => {
- this.trigger_up('call_for_each_child_snippet', {
- $snippet: this.$target,
- callback: function (editor, $snippet) {
- for (var i in editor.styles) {
- editor.styles[i].onRemove();
- }
- resolve();
- },
+ const removeSnippet = async (context) => {
+ await new Promise(resolve => {
+ this.trigger_up('call_for_each_child_snippet', {
+ $snippet: this.$snippetBlock,
+ callback: function (editor, $snippet) {
+ for (var i in editor.snippetOptionInstances) {
+ editor.snippetOptionInstances[i].onRemove();
+ }
+ resolve();
+ },
+ });
});
- });
- this.trigger_up('go_to_parent', {$snippet: this.$target});
- var $parent = this.$target.parent();
- this.$target.find('*').addBack().tooltip('dispose');
- this.$target.remove();
- this.$el.remove();
+ this.trigger_up('go_to_parent', {$snippet: this.$snippetBlock});
+ var $parent = this.$snippetBlock.parent();
+ this.$snippetBlock.find('*').addBack().tooltip('dispose');
+ // The snippet has to be removed directly in jquery as well for the
+ // synchronous bubbling up system to remove the empty parent.
+ // Ideally, we should simply call remove inside a mutations observer
+ // callback such that the removal is synchronous while the vdom is
+ // updated through observed mutations without calling remove twice.
+ this.$snippetBlock.remove();
+ await this.editorHelpers.remove(context, this.$snippetBlock[0]);
+ this.$el.remove();
- var node = $parent[0];
- if (node && node.firstChild) {
- if (!node.firstChild.tagName && node.firstChild.textContent === ' ') {
- node.removeChild(node.firstChild);
- }
- }
-
- if ($parent.closest(':data("snippet-editor")').length) {
- var editor = $parent.data('snippet-editor');
- while (!editor) {
- var $nextParent = $parent.parent();
- if (isEmptyAndRemovable($parent)) {
- $parent.remove();
+ var node = $parent[0];
+ if (node && node.firstChild) {
+ if (!node.firstChild.tagName && node.firstChild.textContent === ' ') {
+ await this.editorHelpers.remove(context, node.firstChild);
}
- $parent = $nextParent;
- editor = $parent.data('snippet-editor');
}
- if (isEmptyAndRemovable($parent, editor)) {
- // TODO maybe this should be part of the actual Promise being
- // returned by the function ?
- setTimeout(() => editor.removeSnippet());
+
+ if ($parent.closest(':data("snippet-editor")').length) {
+ var editor = $parent.data('snippet-editor');
+ while (!editor) {
+ var $nextParent = $parent.parent();
+ if (isEmptyAndRemovable($parent)) {
+ await this.editorHelpers.remove(context, this.$parent[0]);
+ }
+ $parent = $nextParent;
+ editor = $parent.data('snippet-editor');
+ }
+ if (isEmptyAndRemovable($parent, editor)) {
+ // TODO maybe this should be part of the actual Promise being
+ // returned by the function ?
+ await new Promise((resolve)=> {
+ setTimeout(() => editor.removeSnippet().then(resolve));
+ });
+ }
}
- }
- // clean editor if they are image or table in deleted content
- this.$body.find('.note-control-selection').hide();
- this.$body.find('.o_table_handler').remove();
+ await new Promise((resolve) => this.trigger_up('snippet_removed', {onFinish: resolve, context: context}));
+ this.destroy();
+ const childs = this.snippetMenu.getChildrenSnippetBlock(this.$snippetBlock);
+ for (const child of childs) {
+ const snippetEditor = $(child).data('snippet-editor');
+ if (snippetEditor) {
+ snippetEditor.destroy();
+ }
+ }
+ $parent.trigger('content_changed');
- this.trigger_up('snippet_removed');
- this.destroy();
- $parent.trigger('content_changed');
-
- function isEmptyAndRemovable($el, editor) {
- editor = editor || $el.data('snippet-editor');
- return $el.children().length === 0 && $el.text().trim() === ''
- && !$el.hasClass('oe_structure') && (!editor || editor.isTargetParentEditable);
- }
+ function isEmptyAndRemovable($el, editor) {
+ editor = editor || $el.data('snippet-editor');
+ return $el.children().length === 0 && $el.text().trim() === ''
+ && !$el.hasClass('oe_structure') && (!editor || editor.isTargetParentEditable);
+ }
+ };
+ await this.wysiwyg.editor.execCommand(removeSnippet);
},
/**
* Displays/Hides the editor overlay.
@@ -366,21 +431,23 @@ var SnippetEditor = Widget.extend({
this.trigger_up('update_customize_elements', {
customize$Elements: show ? this._customize$Elements : [],
});
+ const proms = [];
this._customize$Elements.forEach(($el, i) => {
const editor = $el.data('editor');
- const styles = _.chain(editor.styles).values().sortBy('__order')
+ const options = _.chain(editor.snippetOptionInstances).values().sortBy('__order')
.value();
// TODO ideally: should account the async parts of updateUI and
// allow async parts in onFocus/onBlur.
if (show) {
// All onFocus before all updateUI as the onFocus of an option
// might affect another option (like updating the $target)
- styles.forEach(style => style.onFocus());
- styles.forEach(style => style.updateUI());
+ options.forEach(option => proms.push(option.onFocus()));
+ options.forEach(option => proms.push(option.updateUI()));
} else {
- styles.forEach(style => style.onBlur());
+ options.forEach(option => proms.push(option.onBlur()));
}
});
+ return Promise.all(proms);
},
/**
* @param {boolean} [show]
@@ -388,9 +455,9 @@ var SnippetEditor = Widget.extend({
*/
toggleTargetVisibility: async function (show) {
show = this._toggleVisibilityStatus(show);
- var styles = _.values(this.styles);
- const proms = _.sortBy(styles, '__order').map(style => {
- return show ? style.onTargetShow() : style.onTargetHide();
+ var options = _.values(this.snippetOptionInstances);
+ const proms = _.sortBy(options, '__order').map(option => {
+ return show ? option.onTargetShow() : option.onTargetHide();
});
await Promise.all(proms);
return show;
@@ -410,21 +477,19 @@ var SnippetEditor = Widget.extend({
* @param {boolean} recordUndo
*/
clone: async function (recordUndo) {
- this.trigger_up('snippet_will_be_cloned', {$target: this.$target});
+ this.trigger_up('snippet_will_be_cloned', {$target: this.$snippetBlock});
- var $clone = this.$target.clone(false);
+ const $clonedContent = this.$snippetBlock.clone(false);
- if (recordUndo) {
- this.trigger_up('request_history_undo_record', {$target: this.$target});
- }
+ const vNodes = await this.editorHelpers.insertHtml(this.wysiwyg.editor, $clonedContent[0].outerHTML, this.$snippetBlock[0], 'AFTER');
+ const $clone = $(this.editorHelpers.getDomNodes(vNodes)[0]);
- this.$target.after($clone);
await new Promise(resolve => {
this.trigger_up('call_for_each_child_snippet', {
$snippet: $clone,
callback: function (editor, $snippet) {
- for (var i in editor.styles) {
- editor.styles[i].onClone({
+ for (const i in editor.snippetOptionInstances) {
+ editor.snippetOptionInstances[i].onClone({
isCurrent: ($snippet.is($clone)),
});
}
@@ -432,7 +497,7 @@ var SnippetEditor = Widget.extend({
},
});
});
- this.trigger_up('snippet_cloned', {$target: $clone, $origin: this.$target});
+ this.trigger_up('snippet_cloned', {$target: $clone, $origin: this.$snippetBlock});
$clone.trigger('content_changed');
},
@@ -448,11 +513,11 @@ var SnippetEditor = Widget.extend({
*/
_initializeOptions: function () {
this._customize$Elements = [];
- this.styles = {};
+ this.snippetOptionInstances = {};
this.selectorSiblings = [];
this.selectorChildren = [];
- var $element = this.$target.parent();
+ var $element = this.$snippetBlock.parent();
while ($element.length) {
var parentEditor = $element.data('snippet-editor');
if (parentEditor) {
@@ -483,45 +548,44 @@ var SnippetEditor = Widget.extend({
// theme options)
this.$el.data('$optionsSection', $optionsSection);
- var i = 0;
- var defs = _.map(this.templateOptions, val => {
- if (!val.selector.is(this.$target)) {
+ var orderIndex = 0;
+ var defs = _.map(this.templateOptions, option => {
+ if (!option.selector.is(this.$snippetBlock)) {
return;
}
- if (val['drop-near']) {
- this.selectorSiblings.push(val['drop-near']);
+ if (option['drop-near']) {
+ this.selectorSiblings.push(option['drop-near']);
}
- if (val['drop-in']) {
- this.selectorChildren.push(val['drop-in']);
+ if (option['drop-in']) {
+ this.selectorChildren.push(option['drop-in']);
}
- var optionName = val.option;
- var option = new (options.registry[optionName] || options.Class)(
+ var optionName = option.id;
+ const optionInstance = new (snippetOptions.registry[optionName] || snippetOptions.SnippetOptionWidget)(
this,
- val.$el.children(),
- val.base_target ? this.$target.find(val.base_target).eq(0) : this.$target,
+ option.$el.children(),
+ () => option.base_target ? this.$snippetBlock.find(option.base_target).eq(0) : this.$snippetBlock,
this.$el,
_.extend({
optionName: optionName,
snippetName: this.getName(),
- }, val.data),
+ }, option.data),
this.options
);
- var key = optionName || _.uniqueId('option');
- if (this.styles[key]) {
+ var optionId = optionName || _.uniqueId('option');
+ if (this.snippetOptionInstances[optionId]) {
// If two snippet options use the same option name (and so use
// the same JS option), store the subsequent ones with a unique
// ID (TODO improve)
- key = _.uniqueId(key);
+ optionId = _.uniqueId(optionId);
}
- this.styles[key] = option;
- option.__order = i++;
+ this.snippetOptionInstances[optionId] = optionInstance;
+ optionInstance.__order = orderIndex++;
if (option.forceNoDeleteButton) {
this.$el.add($optionsSection).find('.oe_snippet_remove').addClass('d-none');
}
-
- return option.appendTo(document.createDocumentFragment());
+ return optionInstance.appendTo(document.createDocumentFragment());
});
this.isTargetMovable = (this.selectorSiblings.length > 0 || this.selectorChildren.length > 0);
@@ -529,7 +593,7 @@ var SnippetEditor = Widget.extend({
this.$el.find('[data-toggle="dropdown"]').dropdown();
return Promise.all(defs).then(() => {
- const options = _.sortBy(this.styles, '__order');
+ const options = _.sortBy(this.snippetOptionInstances, '__order');
options.forEach(option => {
if (option.isTopOption) {
$optionsSectionBtnGroup.prepend(option.$el);
@@ -540,6 +604,19 @@ var SnippetEditor = Widget.extend({
$optionsSection.toggleClass('d-none', options.length === 0);
});
},
+ /**
+ * Reset the options target in case the reference is outdated.
+ *
+ * This can happen with the method `updateChangesInWysiwyg` on a `SnippetOption`.
+ *
+ * @private
+ */
+ _resetOptionsTarget() {
+ if (!this.snippetOptionInstances) return;
+ for (const snippetOption of Object.values(this.snippetOptionInstances)) {
+ snippetOption.resetOptionTarget();
+ }
+ },
/**
* @private
* @param {boolean} [show]
@@ -549,9 +626,9 @@ var SnippetEditor = Widget.extend({
show = !this.isTargetVisible();
}
if (show) {
- delete this.$target[0].dataset.invisible;
+ delete this.$snippetBlock[0].dataset.invisible;
} else {
- this.$target[0].dataset.invisible = '1';
+ this.$snippetBlock[0].dataset.invisible = '1';
}
return show;
},
@@ -576,15 +653,15 @@ var SnippetEditor = Widget.extend({
*
* @private
*/
- _onDragAndDropStart: function () {
+ _dragAndDropStart: function () {
var self = this;
this.dropped = false;
self.size = {
- width: self.$target.width(),
- height: self.$target.height()
+ width: self.$snippetBlock.width(),
+ height: self.$snippetBlock.height()
};
- self.$target.after('');
- self.$target.detach();
+ self.$snippetBlock.after('');
+ self.$snippetBlock.detach();
self.$el.addClass('d-none');
var $selectorSiblings;
@@ -604,7 +681,7 @@ var SnippetEditor = Widget.extend({
}
}
- this.trigger_up('go_to_parent', {$snippet: this.$target});
+ this.trigger_up('go_to_parent', {$snippet: this.$snippetBlock});
this.trigger_up('activate_insertion_zones', {
$selectorSiblings: $selectorSiblings,
$selectorChildren: $selectorChildren,
@@ -612,17 +689,8 @@ var SnippetEditor = Widget.extend({
this.$body.addClass('move-important');
- this.$editable.find('.oe_drop_zone').droppable({
- over: function () {
- self.$editable.find('.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;
- },
+ this.trigger_up('drag_and_drop_start', {
+ $snippet: this.$snippetBlock,
});
},
/**
@@ -633,67 +701,92 @@ var SnippetEditor = Widget.extend({
* @param {Event} ev
* @param {Object} ui
*/
- _onDragAndDropStop: function (ev, ui) {
+ _onDragAndDropStop: async function (ev, ui) {
// TODO lot of this is duplicated code of the d&d feature of snippets
if (!this.dropped) {
var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone', {container: document.body}).first();
if ($el.length) {
- $el.after(this.$target);
+ $el.after(this.$snippetBlock);
this.dropped = true;
}
}
- this.$editable.find('.oe_drop_zone').droppable('destroy').remove();
+ this.$editable.find('.oe_drop_zone').remove();
- var prev = this.$target.first()[0].previousSibling;
- var next = this.$target.last()[0].nextSibling;
- var $parent = this.$target.parent();
+ var prev = this.$snippetBlock.first()[0].previousSibling;
+ var next = this.$snippetBlock.last()[0].nextSibling;
+ var $parent = this.$snippetBlock.parent();
var $clone = this.$editable.find('.oe_drop_clone');
if (prev === $clone[0]) {
prev = $clone[0].previousSibling;
+ this.dropped = false;
} else if (next === $clone[0]) {
next = $clone[0].nextSibling;
+ this.dropped = false;
}
- $clone.after(this.$target);
+ $clone.after(this.$snippetBlock);
var $from = $clone.parent();
this.$el.removeClass('d-none');
this.$body.removeClass('move-important');
$clone.remove();
+ dom.scrollTo(this.$snippetBlock[0], {extraOffset: 50});
+
if (this.dropped) {
- this.trigger_up('request_history_undo_record', {$target: this.$target});
-
if (prev) {
- this.$target.insertAfter(prev);
+ this.$snippetBlock.insertAfter(prev);
} else if (next) {
- this.$target.insertBefore(next);
+ this.$snippetBlock.insertBefore(next);
} else {
- $parent.prepend(this.$target);
+ $parent.prepend(this.$snippetBlock);
}
- for (var i in this.styles) {
- this.styles[i].onMove();
- }
+ const jwEditor = this.wysiwyg.editor;
+ const moveSnippet = async (context) => {
+ const node = this.editorHelpers.getNodes(this.$snippetBlock[0])[0];
+ const prevNodes = this.editorHelpers.getNodes(prev);
+ if (prevNodes.length) {
+ prevNodes.pop().after(node);
+ return;
+ }
+ const nextNodes = this.editorHelpers.getNodes(next);
+ if (nextNodes.length) {
+ nextNodes[0].before(node);
+ return;
+ }
+ const parentNodes = this.editorHelpers.getNodes($parent[0]);
+ if (parentNodes.length) {
+ parentNodes.pop().append(node);
+ return;
+ }
+ };
+ await jwEditor.execCommand(moveSnippet);
- this.$target.trigger('content_changed');
- $from.trigger('content_changed');
+ for (var i in this.snippetOptionInstances) {
+ await this.snippetOptionInstances[i].onMove();
+ }
}
this.trigger_up('drag_and_drop_stop', {
- $snippet: this.$target,
+ $snippet: this.$snippetBlock,
});
+
+ if (this.dropped) {
+ this.$snippetBlock.trigger('content_changed');
+ $from.trigger('content_changed');
+ }
},
/**
* @private
*/
_onOptionsSectionMouseEnter: function (ev) {
- if (!this.$target.is(':visible')) {
+ if (!this.$snippetBlock.is(':visible')) {
return;
}
this.trigger_up('activate_snippet', {
- $snippet: this.$target,
+ $element: this.$snippetBlock,
previewMode: true,
});
},
@@ -701,17 +794,14 @@ var SnippetEditor = Widget.extend({
* @private
*/
_onOptionsSectionMouseLeave: function (ev) {
- this.trigger_up('activate_snippet', {
- $snippet: false,
- previewMode: true,
- });
+ // TODO: this.trigger_up('deactivate_snippet');
},
/**
* @private
*/
_onOptionsSectionClick: function (ev) {
this.trigger_up('activate_snippet', {
- $snippet: this.$target,
+ $element: this.$snippetBlock,
previewMode: false,
});
},
@@ -744,9 +834,9 @@ var SnippetEditor = Widget.extend({
function notifyForEachMatchedOption(name) {
var regex = new RegExp('^' + name + '\\d+$');
var hasOption = false;
- for (var key in self.styles) {
+ for (var key in self.snippetOptionInstances) {
if (key === name || regex.test(key)) {
- self.styles[key].notify(ev.data.name, ev.data.data);
+ self.snippetOptionInstances[key].notify(ev.data.name, ev.data.data);
hasOption = true;
}
}
@@ -762,7 +852,6 @@ var SnippetEditor = Widget.extend({
_onRemoveClick: function (ev) {
ev.preventDefault();
ev.stopPropagation();
- this.trigger_up('request_history_undo_record', {$target: this.$target});
this.removeSnippet();
},
/**
@@ -770,15 +859,15 @@ var SnippetEditor = Widget.extend({
* @param {OdooEvent} ev
*/
_onSnippetOptionUpdate: async function (ev) {
- const proms1 = Object.keys(this.styles).map(key => {
- return this.styles[key].updateUI({
+ const proms1 = Object.keys(this.snippetOptionInstances).map(key => {
+ return this.snippetOptionInstances[key].updateUI({
noVisibility: true,
});
});
await Promise.all(proms1);
- const proms2 = Object.keys(this.styles).map(key => {
- return this.styles[key].updateUIVisibility();
+ const proms2 = Object.keys(this.snippetOptionInstances).map(key => {
+ return this.snippetOptionInstances[key].updateUIVisibility();
});
await Promise.all(proms2);
@@ -797,8 +886,8 @@ var SnippetEditor = Widget.extend({
*/
_onUserValueWidgetRequest: function (ev) {
ev.stopPropagation();
- for (const key of Object.keys(this.styles)) {
- const widget = this.styles[key].findWidget(ev.data.name);
+ for (const key of Object.keys(this.snippetOptionInstances)) {
+ const widget = this.snippetOptionInstances[key].findWidget(ev.data.name);
if (widget) {
ev.data.onSuccess(widget);
return;
@@ -808,8 +897,8 @@ var SnippetEditor = Widget.extend({
});
/**
- * Management of drag&drop menu and snippet related behaviors in the page.
- */
+* Management of drag&drop menu and snippet related behaviors in the page.
+*/
var SnippetsMenu = Widget.extend({
id: 'oe_snippets',
cacheSnippetTemplate: {},
@@ -821,7 +910,9 @@ var SnippetsMenu = Widget.extend({
'mousedown': '_onMouseDown',
'input .o_snippet_search_filter_input': '_onSnippetSearchInput',
'click .o_snippet_search_filter_reset': '_onSnippetSearchResetClick',
- 'summernote_popover_update_call .o_we_snippet_text_tools': '_onSummernoteToolsUpdate',
+ 'click .o_we_website_top_actions button[data-action=save]': '_onSaveClick',
+ 'click .o_we_website_top_actions button[data-action=cancel]': '_onDiscardClick',
+ 'click .o_we_website_top_actions button[data-action=mobile]': '_onMobilePreviewClick',
},
custom_events: {
'activate_insertion_zones': '_onActivateInsertionZones',
@@ -830,6 +921,7 @@ var SnippetsMenu = Widget.extend({
'clone_snippet': '_onCloneSnippet',
'cover_update': '_onOverlaysCoverUpdate',
'deactivate_snippet': '_onDeactivateSnippet',
+ 'drag_and_drop_start': '_onDragAndDropStart',
'drag_and_drop_stop': '_onDragAndDropStop',
'get_snippet_versions': '_onGetSnippetVersions',
'go_to_parent': '_onGoToParent',
@@ -854,6 +946,7 @@ var SnippetsMenu = Widget.extend({
tabs: {
BLOCKS: 'blocks',
OPTIONS: 'options',
+ THEME: 'theme',
},
/**
@@ -882,11 +975,24 @@ var SnippetsMenu = Widget.extend({
this.options.snippets = 'web_editor.snippets';
}
this.snippetEditors = [];
- this._enabledEditorHierarchy = [];
+ this._enabledSnippetEditorsHierarchy = [];
this._mutex = new concurrency.Mutex();
- this.setSelectorEditableArea(options.$el, options.selectorEditableArea);
+ this.selectorEditableArea = options.selectorEditableArea;
+ this.$editor = options.$el;
+ this.$body = $(document.body);
+
+ this.wysiwyg = options.wysiwyg;
+
+ this.JWEditorLib = options.JWEditorLib;
+ if (this.JWEditorLib) {
+ const jwEditor = this.wysiwyg.editor;
+ const layout = jwEditor.plugins.get(this.JWEditorLib.Layout);
+ this.layoutEngine = layout.engines.dom;
+ this.nodeToEditor = new Map();
+ this.editorHelpers = this.wysiwyg.editorHelpers;
+ }
this._notActivableElementsSelector = [
'#web_editor-top-edit',
@@ -904,6 +1010,7 @@ var SnippetsMenu = Widget.extend({
this.loadingTimers = {};
this.loadingElements = {};
+ this.$snippetEditorArea = options.$snippetEditorArea;
},
/**
* @override
@@ -932,6 +1039,19 @@ var SnippetsMenu = Widget.extend({
this.textEditorPanelEl = document.createElement('div');
this.textEditorPanelEl.classList.add('o_we_snippet_text_tools', 'd-none');
+ if (this.options.onlyStyleTab) {
+ await this._loadSnippetsTemplates();
+ this.$el.find('.o_we_website_top_actions').removeClass('d-none');
+ this.$('.o_snippet_search_filter').addClass('d-none');
+ this.$('#o_scroll').addClass('d-none');
+ this.$('#snippets_menu button').removeClass('active').prop('disabled', true);
+ this.$('.o_we_customize_snippet_btn').addClass('active').prop('disabled', false);
+ this.$('o_we_ui_loading').addClass('d-none');
+ $(this.customizePanel).removeClass('d-none');
+ this._addJabberwockToolbar();
+ return Promise.all(defs);
+ }
+
this.invisibleDOMPanelEl = document.createElement('div');
this.invisibleDOMPanelEl.classList.add('o_we_invisible_el_panel');
this.invisibleDOMPanelEl.appendChild(
@@ -946,47 +1066,22 @@ var SnippetsMenu = Widget.extend({
// Fetch snippet templates and compute it
defs.push((async () => {
await this._loadSnippetsTemplates();
+ // enable the toolbar actions for the jabberwock editor
+ this.$el.find('.o_we_website_top_actions').removeClass('d-none');
await this._updateInvisibleDOM();
})());
- // Prepare snippets editor environment
- this.$snippetEditorArea = $('', {
- id: 'oe_manipulators',
- }).insertAfter(this.$el);
-
- // Active snippet editor on click in the page
- var lastElement;
- this.$document.on('click.snippets_menu', '*', ev => {
- var srcElement = ev.target || (ev.originalEvent && (ev.originalEvent.target || ev.originalEvent.originalTarget)) || ev.srcElement;
- if (!srcElement || lastElement === srcElement) {
- return;
- }
- lastElement = srcElement;
- _.defer(function () {
- lastElement = false;
- });
-
- var $target = $(srcElement);
- if (!$target.closest('we-button, we-toggler, .o_we_color_preview').length) {
- this._closeWidgets();
- }
- if (!$target.closest('body > *').length) {
- return;
- }
- if ($target.closest(this._notActivableElementsSelector).length) {
- return;
- }
- this._activateSnippet($target);
- });
-
core.bus.on('deactivate_snippet', this, this._onDeactivateSnippet);
+ this.$window.on('mousedown.snippets_menu', this._onSnippetMouseDown.bind(this));
+ this.$window.on('mousedown-iframe.snippets_menu', this._onSnippetMouseDown.bind(this));
+
// Adapt overlay covering when the window is resized / content changes
- var debouncedCoverUpdate = _.throttle(() => {
+ var throttledCoverUpdate = _.throttle(() => {
this.updateCurrentSnippetEditorOverlay();
}, 50);
- this.$window.on('resize.snippets_menu', debouncedCoverUpdate);
- this.$window.on('content_changed.snippets_menu', debouncedCoverUpdate);
+ this.$window.on('resize.snippets_menu', throttledCoverUpdate);
+ this.$window.on('content_changed.snippets_menu', throttledCoverUpdate);
// On keydown add a class on the active overlay to hide it and show it
// again when the mouse moves
@@ -999,8 +1094,11 @@ var SnippetsMenu = Widget.extend({
this.snippetEditors.forEach(editor => {
editor.toggleOverlayVisibility(true);
});
+ this.updateCurrentSnippetEditorOverlay();
}, 250));
-
+ this.$editor.find('#wrapwrap').on('scroll.snippets_menu', () => {
+ this.updateCurrentSnippetEditorOverlay();
+ });
// Hide the active overlay when scrolling.
// Show it again and recompute all the overlays after the scroll.
this.$scrollingElement = $().getScrollingElement();
@@ -1020,18 +1118,24 @@ var SnippetsMenu = Widget.extend({
// Auto-selects text elements with a specific class and remove this
// on text changes
- this.$document.on('click.snippets_menu', '.o_default_snippet_text', function (ev) {
- $(ev.target).closest('.o_default_snippet_text').removeClass('o_default_snippet_text');
- $(ev.target).selectContent();
- $(ev.target).removeClass('o_default_snippet_text');
- });
- this.$document.on('keyup.snippets_menu', function () {
- var range = Wysiwyg.getRange(this);
- $(range && range.sc).closest('.o_default_snippet_text').removeClass('o_default_snippet_text');
+ this.$document.on('click.snippets_menu', '.o_default_snippet_text', (ev) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ ev.stopImmediatePropagation();
+ const autoSelectDefaultText = async (context) => {
+ const $target = $(ev.target);
+ const $defaultSnippetText = $target.closest('.o_default_snippet_text');
+ await this.editorHelpers.removeClass(context, $defaultSnippetText[0], 'o_default_snippet_text');
+ const nodes = this.editorHelpers.getNodes($target[0]);
+ this.wysiwyg.editor.selection.select(nodes[0], nodes[nodes.length - 1]);
+ };
+ this.wysiwyg.editor.execCommand(autoSelectDefaultText);
});
const $autoFocusEls = $('.o_we_snippet_autofocus');
- this._activateSnippet($autoFocusEls.length ? $autoFocusEls.first() : false);
+ if ($autoFocusEls.length) {
+ this._activateSnippet($autoFocusEls.first());
+ }
this._textToolsSwitchingEnabled = true;
// Add tooltips on we-title elements whose text overflows
@@ -1058,13 +1162,6 @@ var SnippetsMenu = Widget.extend({
return this.classList.contains('active') ? false : this.dataset.title;
},
});
-
- // Trigger a resize event once entering edit mode as the snippets
- // menu will take part of the screen width (delayed because of
- // animation). (TODO wait for real animation end)
- setTimeout(() => {
- this.$window.trigger('resize');
- }, 1000);
});
},
/**
@@ -1073,10 +1170,10 @@ var SnippetsMenu = Widget.extend({
destroy: function () {
this._super.apply(this, arguments);
if (this.$window) {
- this.$snippetEditorArea.remove();
this.$window.off('.snippets_menu');
this.$document.off('.snippets_menu');
this.$scrollingElement.off('.snippets_menu');
+ this.$editor.find('#wrapwrap').off('.snippets_menu');
}
core.bus.off('deactivate_snippet', this, this._onDeactivateSnippet);
delete this.cacheSnippetTemplate[this.options.snippets];
@@ -1092,16 +1189,10 @@ var SnippetsMenu = Widget.extend({
* - Remove the 'contentEditable' attributes
*/
cleanForSave: async function () {
- await this._activateSnippet(false);
+ await this._enableLastEditor();
+ await this._removeLastSnippetActivated();
this.trigger_up('ready_to_clean_for_save');
await this._destroyEditors();
-
- this.getEditableArea().find('[contentEditable]')
- .removeAttr('contentEditable')
- .removeProp('contentEditable');
-
- this.getEditableArea().find('.o_we_selected_image')
- .removeClass('o_we_selected_image');
},
/**
* Load snippets.
@@ -1123,17 +1214,6 @@ var SnippetsMenu = Widget.extend({
this.cacheSnippetTemplate[this.options.snippets] = this._defLoadSnippets;
return this._defLoadSnippets;
},
- /**
- * Sets the instance variables $editor, $body and selectorEditableArea.
- *
- * @param {JQuery} $editor
- * @param {String} selectorEditableArea
- */
- setSelectorEditableArea: function ($editor, selectorEditableArea) {
- this.selectorEditableArea = selectorEditableArea;
- this.$editor = $editor;
- this.$body = $editor.closest('body');
- },
/**
* Get the editable area.
*
@@ -1148,7 +1228,7 @@ var SnippetsMenu = Widget.extend({
*/
updateCurrentSnippetEditorOverlay: function () {
for (const snippetEditor of this.snippetEditors) {
- if (snippetEditor.$target.closest('body').length) {
+ if (snippetEditor.$snippetBlock.closest('body, .note-editable').length) {
snippetEditor.cover();
continue;
}
@@ -1157,6 +1237,22 @@ var SnippetsMenu = Widget.extend({
this._mutex.exec(() => snippetEditor.destroy());
}
},
+ /**
+ * @param {JQuery} $snippetBlock
+ * @returns {JQuery}
+ */
+ getChildrenSnippetBlock($snippetBlock) {
+ return $snippetBlock.add(globalSelector.all($snippetBlock));
+ },
+ /**
+ * Activate the last snippet that has been activated
+ */
+ activateLastSnippetBlock() {
+ const $lastSnippet = this.$editor.find('.o_we_last_snippet_activated');
+ if ($lastSnippet) {
+ this._activateSnippet($lastSnippet, $lastSnippet.hasClass('o_we_last_snippet_preview'));
+ }
+ },
//--------------------------------------------------------------------------
// Private
@@ -1275,14 +1371,14 @@ var SnippetsMenu = Widget.extend({
var $zones;
do {
count = 0;
- $zones = this.getEditableArea().find('.oe_drop_zone > .oe_drop_zone').remove(); // no recursive zones
+ $zones = this.$editor.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.
- $zones = this.getEditableArea().find('.oe_drop_zone:not(.oe_vertical)');
+ $zones = this.$editor.find('.oe_drop_zone:not(.oe_vertical)');
$zones.each(function () {
var zone = $(this);
var prev = zone.prev();
@@ -1317,12 +1413,12 @@ var SnippetsMenu = Widget.extend({
this.invisibleDOMMap = new Map();
const $invisibleDOMPanelEl = $(this.invisibleDOMPanelEl);
$invisibleDOMPanelEl.find('.o_we_invisible_entry').remove();
- const $invisibleSnippets = globalSelector.all().find('.o_snippet_invisible').addBack('.o_snippet_invisible');
+ const $invisibleSnippets = this.$editor.find('.o_snippet_invisible').addBack('.o_snippet_invisible');
$invisibleDOMPanelEl.toggleClass('d-none', !$invisibleSnippets.length);
const proms = _.map($invisibleSnippets, async el => {
- const editor = await this._createSnippetEditor($(el));
+ const editor = await this._getSnippetEditor($(el));
const $invisEntry = $('', {
class: 'o_we_invisible_entry d-flex align-items-center justify-content-between',
text: editor.getName(),
@@ -1334,14 +1430,14 @@ var SnippetsMenu = Widget.extend({
}, false);
},
/**
- * Disable the overlay editor of the active snippet and activate the new one
- * if given.
+ * Disable the overlay editor of the active snippet and activate the new one.
* Note 1: if the snippet editor associated to the given snippet is not
* created yet, this method will create it.
* Note 2: if the given DOM element is not a snippet (no editor option), the
* first parent which is one is used instead.
*
- * @param {jQuery|false} $snippet
+ * @private
+ * @param {jQuery|false} $snippetBlock
* The DOM element whose editor (and its parent ones) need to be
* enabled. Only disable the current one if false is given.
* @param {boolean} [previewMode=false]
@@ -1349,62 +1445,75 @@ var SnippetsMenu = Widget.extend({
* @returns {Promise}
* (might be async when an editor must be created)
*/
- _activateSnippet: async function ($snippet, previewMode, ifInactiveOptions) {
+ _activateSnippet: async function ($snippetBlock, previewMode, ifInactiveOptions) {
if (this._blockPreviewOverlays && previewMode) {
return;
}
- if ($snippet && !$snippet.is(':visible')) {
+ if (!$snippetBlock.is(':visible')) {
return;
}
+
const exec = previewMode
? action => this._mutex.exec(action)
: action => this._execWithLoadingEffect(action, false);
- return exec(() => {
- return new Promise(resolve => {
- // Take the first parent of the provided DOM (or itself) which
- // should have an associated snippet editor and create + enable it.
- if ($snippet && $snippet.length) {
- $snippet = globalSelector.closest($snippet);
- if ($snippet.length) {
- return this._createSnippetEditor($snippet).then(resolve);
- }
- }
- resolve(null);
- }).then(editorToEnable => {
- if (ifInactiveOptions && this._enabledEditorHierarchy.includes(editorToEnable)) {
- return editorToEnable;
- }
- const editorToEnableHierarchy = [];
- let current = editorToEnable;
- while (current && current.$target) {
- editorToEnableHierarchy.push(current);
- current = current.getParent();
+ return exec(async () => {
+ let snippetEditor;
+ // Take the first parent of the provided DOM (or itself) which
+ // should have an associated snippet editor and create + enable it.
+ if ($snippetBlock.length) {
+ const $snippet = globalSelector.closest($snippetBlock);
+ if ($snippet.length) {
+ snippetEditor = await this._getSnippetEditor($snippet);
}
+ }
+ if (ifInactiveOptions && this._enabledSnippetEditorsHierarchy.includes(snippetEditor)) {
+ return snippetEditor;
+ }
- // First disable all editors...
- for (let i = this.snippetEditors.length; i--;) {
- const editor = this.snippetEditors[i];
- editor.toggleOverlay(false, previewMode);
- if (!previewMode && !editorToEnableHierarchy.includes(editor)) {
- editor.toggleOptions(false);
- }
- }
- // ... if no editors are to be enabled, look if any have been
- // enabled previously by a click
- if (!editorToEnable) {
- editorToEnable = this.snippetEditors.find(editor => editor.isSticky());
- previewMode = false;
- }
- // ... then enable the right editor
- if (editorToEnable) {
- editorToEnable.toggleOverlay(true, previewMode);
- editorToEnable.toggleOptions(true);
- }
+ const snippetEditorHierarchy = [];
+ let currentSnippetEditor = snippetEditor;
+ while (currentSnippetEditor && currentSnippetEditor.$snippetBlock) {
+ snippetEditorHierarchy.push(currentSnippetEditor);
+ currentSnippetEditor = currentSnippetEditor.getParent();
+ }
- this._enabledEditorHierarchy = editorToEnableHierarchy;
- return editorToEnable;
- });
+
+ // First disable all snippet editors...
+ for (const currentSnippetEditor of this.snippetEditors) {
+ currentSnippetEditor.toggleOverlay(false, previewMode);
+ if (!previewMode && !snippetEditorHierarchy.includes(currentSnippetEditor)) {
+ await currentSnippetEditor.toggleOptions(false);
+ }
+ }
+
+ // ... then enable the right snippet editor
+ if (snippetEditor) {
+ snippetEditor.toggleOverlay(true, previewMode);
+ await snippetEditor.toggleOptions(true);
+ }
+
+ this._enabledSnippetEditorsHierarchy = snippetEditorHierarchy;
+ return snippetEditor;
+ });
+ },
+ /**
+ * Disable all snippet editors.
+ *
+ * @private
+ */
+ _enableLastEditor() {
+ this._mutex.exec(() => {
+ // First disable all snippet editors...
+ for (const currentSnippetEditor of this.snippetEditors) {
+ currentSnippetEditor.toggleOverlay(false, false);
+ }
+ // ... then enable the last snippet editor
+ const editorToEnable = this.snippetEditors.find(editor => editor.isSticky());
+ if (editorToEnable) {
+ editorToEnable.toggleOverlay(true, true);
+ editorToEnable.toggleOptions(true);
+ }
});
},
/**
@@ -1443,19 +1552,19 @@ var SnippetsMenu = Widget.extend({
* @returns {Promise} (might be async if snippet editors need to be created
* and/or the callback is async)
*/
- _callForEachChildSnippet: function ($snippet, callback) {
- var self = this;
- var defs = _.map($snippet.add(globalSelector.all($snippet)), function (el) {
- var $snippet = $(el);
- return self._createSnippetEditor($snippet).then(function (editor) {
- if (editor) {
- return callback.call(self, editor, $snippet);
- }
- });
+ _callForEachChildSnippet: function ($snippetBlock, callback) {
+ const defs = _.map(this.getChildrenSnippetBlock($snippetBlock), async (child) => {
+ const $childSnippet = $(child);
+ const snippetEditor = await this._getSnippetEditor($childSnippet);
+ if (snippetEditor) {
+ return callback.call(this, snippetEditor, $childSnippet);
+ }
});
return Promise.all(defs);
},
/**
+ * Close widget for all editors.
+ *
* @private
*/
_closeWidgets: function () {
@@ -1524,11 +1633,11 @@ var SnippetsMenu = Widget.extend({
};
} else {
functions.closest = function ($from, parentNode) {
- var parents = self.getEditableArea().get();
+ var snippetEditors = self.getEditableArea().get();
return $from.closest(selector, parentNode).filter(function () {
var node = this;
while (node.parentNode) {
- if (parents.indexOf(node) !== -1) {
+ if (snippetEditors.indexOf(node) !== -1) {
return true;
}
node = node.parentNode;
@@ -1537,9 +1646,9 @@ var SnippetsMenu = Widget.extend({
}).filter(filterFunc);
};
functions.all = isChildren ? function ($from) {
- return dom.cssFind($from || self.getEditableArea(), selector).filter(filterFunc);
+ return dom.cssFind($from || self.$editor, selector).filter(filterFunc);
} : function ($from) {
- $from = $from || self.getEditableArea();
+ $from = $from || self.$editor;
return $from.filter(selector).add(dom.cssFind($from, selector)).filter(filterFunc);
};
}
@@ -1552,36 +1661,36 @@ var SnippetsMenu = Widget.extend({
* @private
* @param {string} html
*/
- _computeSnippetTemplates: function (html) {
+ _computeSnippetTemplates: async function (html) {
var self = this;
var $html = $(html);
var $scroll = $html.siblings('#o_scroll');
this.templateOptions = [];
var selectors = [];
- var $styles = $html.find('[data-selector]');
- $styles.each(function () {
- var $style = $(this);
- var selector = $style.data('selector');
- var exclude = $style.data('exclude') || '';
- var target = $style.data('target');
- var noCheck = $style.data('no-check');
- var optionID = $style.data('js') || $style.data('option-name'); // used in tour js as selector
+ var $dataSelectors = $html.find('[data-selector]');
+ $dataSelectors.each(function () {
+ var $dataSelector = $(this);
+ var selector = $dataSelector.data('selector');
+ var exclude = $dataSelector.data('exclude') || '';
+ var target = $dataSelector.data('target');
+ var noCheck = $dataSelector.data('no-check');
+ var optionID = $dataSelector.data('js') || $dataSelector.data('option-name'); // used in tour js as selector
var option = {
- 'option': optionID,
+ 'id': optionID,
'base_selector': selector,
'base_exclude': exclude,
'base_target': target,
'selector': self._computeSelectorFunctions(selector, exclude, target, noCheck),
- '$el': $style,
- 'drop-near': $style.data('drop-near') && self._computeSelectorFunctions($style.data('drop-near'), '', false, noCheck, true),
- 'drop-in': $style.data('drop-in') && self._computeSelectorFunctions($style.data('drop-in'), '', false, noCheck),
- 'data': _.extend({string: $style.attr('string')}, $style.data()),
+ '$el': $dataSelector,
+ 'drop-near': $dataSelector.data('drop-near') && self._computeSelectorFunctions($dataSelector.data('drop-near'), '', false, noCheck, true),
+ 'drop-in': $dataSelector.data('drop-in') && self._computeSelectorFunctions($dataSelector.data('drop-in'), '', false, noCheck),
+ 'data': _.extend({string: $dataSelector.attr('string')}, $dataSelector.data()),
};
self.templateOptions.push(option);
selectors.push(option.selector);
});
- $styles.addClass('d-none');
+ $dataSelectors.addClass('d-none');
globalSelector.closest = function ($from) {
var $temp;
@@ -1615,17 +1724,17 @@ var SnippetsMenu = Widget.extend({
.each((i, el) => {
const $snippet = $(el);
const name = el.getAttribute('name');
- const $sbody = $snippet.children().addClass('oe_snippet_body');
+ const $snippetBody = $snippet.children().addClass('oe_snippet_body');
const isCustomSnippet = !!el.closest('#snippet_custom');
// Associate in-page snippets to their name
// TODO I am not sure this is useful anymore and it should at
// least be made more robust using data-snippet
- let snippetClasses = $sbody.attr('class').match(/s_[^ ]+/g);
+ let snippetClasses = $snippetBody.attr('class').match(/s_[^ ]+/g);
if (snippetClasses && snippetClasses.length) {
snippetClasses = '.' + snippetClasses.join('.');
}
- const $els = $(snippetClasses).not('[data-name]').add($sbody);
+ const $els = $(snippetClasses).not('[data-name]').add($snippetBody);
$els.attr('data-name', name).data('name', name);
// Create the thumbnail
@@ -1676,9 +1785,9 @@ var SnippetsMenu = Widget.extend({
this.$el.append(this.textEditorPanelEl);
this.$el.append(this.invisibleDOMPanelEl);
this._makeSnippetDraggable(this.$snippets);
- this._disableUndroppableSnippets();
+ await this._disableUndroppableSnippets();
- this.$el.addClass('o_loaded');
+ this.$el.closest('.o_main_sidebar').addBack().addClass('o_loaded');
$('body.editor_enable').addClass('editor_has_snippets');
this.trigger_up('snippets_loaded', self.$el);
},
@@ -1693,36 +1802,38 @@ var SnippetsMenu = Widget.extend({
* @param {jQuery} $snippet
* @returns {Promise}
*/
- _createSnippetEditor: function ($snippet) {
- var self = this;
+ _getSnippetEditor: async function ($snippet) {
var snippetEditor = $snippet.data('snippet-editor');
+ if (snippetEditor) {
+ snippetEditor._resetOptionsTarget();
+ return snippetEditor.__isStarted;
+ }
+
+ var $parent = globalSelector.closest($snippet.parent());
+ let parentEditor;
+ if ($parent.length) {
+ parentEditor = await this._getSnippetEditor($parent);
+ }
+
+ // When reaching this position, after the Promise resolution, the
+ // snippet editor instance might have been created by another call
+ // to _getSnippetEditor... the whole logic should be improved
+ // to avoid doing this here.
if (snippetEditor) {
return snippetEditor.__isStarted;
}
- var def;
- var $parent = globalSelector.closest($snippet.parent());
- if ($parent.length) {
- def = this._createSnippetEditor($parent);
- }
+ let editableArea = this.$editor;
+ snippetEditor = new SnippetEditor(parentEditor || this,
+ $snippet,
+ this.templateOptions,
+ $snippet.closest('[data-oe-type="html"], .oe_structure').add(editableArea),
+ this,
+ this.options);
+ this.snippetEditors.push(snippetEditor);
+ await snippetEditor.appendTo(this.$snippetEditorArea);
- return Promise.resolve(def).then(function (parentEditor) {
- // When reaching this position, after the Promise resolution, the
- // snippet editor instance might have been created by another call
- // to _createSnippetEditor... the whole logic should be improved
- // to avoid doing this here.
- snippetEditor = $snippet.data('snippet-editor');
- if (snippetEditor) {
- return snippetEditor.__isStarted;
- }
-
- let editableArea = self.getEditableArea();
- snippetEditor = new SnippetEditor(parentEditor || self, $snippet, self.templateOptions, $snippet.closest('[data-oe-type="html"], .oe_structure').add(editableArea), self.options);
- self.snippetEditors.push(snippetEditor);
- return snippetEditor.appendTo(self.$snippetEditorArea);
- }).then(function () {
- return snippetEditor;
- });
+ return snippetEditor;
},
/**
* There may be no location where some snippets might be dropped. This mades
@@ -1731,38 +1842,38 @@ var SnippetsMenu = Widget.extend({
* @todo make them undraggable
* @private
*/
- _disableUndroppableSnippets: function () {
+ _disableUndroppableSnippets: async function () {
var self = this;
var cache = {};
- this.$snippets.each(function () {
- var $snippet = $(this);
- var $snippetBody = $snippet.find('.oe_snippet_body');
- var check = false;
+ for (const snippetDraggable of this.$snippets.toArray()) {
+ var $snippetDraggable = $(snippetDraggable);
+ var $snippetTemplate = $snippetDraggable.find('.oe_snippet_body');
+
+ var isEnabled = false;
_.each(self.templateOptions, function (option, k) {
- if (check || !($snippetBody.is(option.base_selector) && !$snippetBody.is(option.base_exclude))) {
+ if (isEnabled || !($snippetTemplate.is(option.base_selector) && !$snippetTemplate.is(option.base_exclude))) {
return;
}
-
cache[k] = cache[k] || {
'drop-near': option['drop-near'] ? option['drop-near'].all().length : 0,
'drop-in': option['drop-in'] ? option['drop-in'].all().length : 0
};
- check = (cache[k]['drop-near'] || cache[k]['drop-in']);
+ isEnabled = (cache[k]['drop-near'] || cache[k]['drop-in']);
});
-
- $snippet.toggleClass('o_disabled', !check);
- $snippet.attr('title', check ? '' : _t("No location to drop in"));
- const $icon = $snippet.find('.o_snippet_undroppable').remove();
- if (check) {
- $icon.remove();
- } else if (!$icon.length) {
+ $snippetDraggable.find('.o_snippet_undroppable').remove();
+ if (isEnabled) {
+ $snippetDraggable.removeClass('o_disabled');
+ $snippetDraggable.attr('title', '');
+ } else {
+ $snippetDraggable.addClass('o_disabled');
+ $snippetDraggable.attr('title', _t("No location to drop in"));
const imgEl = document.createElement('img');
imgEl.classList.add('o_snippet_undroppable');
imgEl.src = '/web_editor/static/src/img/snippet_disabled.svg';
- $snippet.append(imgEl);
+ $snippetDraggable.append(imgEl);
}
- });
+ }
},
/**
* @private
@@ -1806,7 +1917,7 @@ var SnippetsMenu = Widget.extend({
right: false,
}, options.scrollBoundaries),
jQueryDraggableOptions: Object.assign({
- appendTo: this.$body,
+ appendTo: this.$editor,
cursor: 'move',
greedy: true,
scroll: false,
@@ -1841,12 +1952,13 @@ var SnippetsMenu = Widget.extend({
*/
_makeSnippetDraggable: function ($snippets) {
var self = this;
- var $toInsert, dropped, $snippet;
-
- let dragAndDropResolve;
+ var dragStarted;
+ var dropped;
+ let scrollValue;
const smoothScrollOptions = this._getScrollOptions({
jQueryDraggableOptions: {
+ iframeFix: true,
distance: 0,
handle: '.oe_snippet_thumbnail:not(.o_we_already_dragging)',
helper: function () {
@@ -1856,29 +1968,40 @@ var SnippetsMenu = Widget.extend({
);
return dragSnip;
},
- start: function () {
+ start: function (ev, ui) {
self.$el.find('.oe_snippet_thumbnail').addClass('o_we_already_dragging');
-
- dropped = false;
- $snippet = $(this);
+ var $snippet = this;
var $baseBody = $snippet.find('.oe_snippet_body');
+
+ if (dragStarted) {
+ // If the previous drop are not finished, the position of the current snippet can be wrong.
+ // And JWEditor can have a mismatch for the redrawing.
+ $snippet.data('to-insert', $());
+ return;
+ }
+ dragStarted = true;
+
+ const $snippetToInsert = $baseBody.clone();
+
var $selectorSiblings = $();
var $selectorChildren = $();
- var temp = self.templateOptions;
- for (var k in temp) {
- if ($baseBody.is(temp[k].base_selector) && !$baseBody.is(temp[k].base_exclude)) {
- if (temp[k]['drop-near']) {
- $selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all());
+ for (const option of self.templateOptions) {
+ if ($baseBody.is(option.base_selector) && !$baseBody.is(option.base_exclude)) {
+ if (option['drop-near']) {
+ $selectorSiblings = $selectorSiblings.add(option['drop-near'].all()).filter(function () {
+ return !$snippet[0].contains(this);
+ });
}
- if (temp[k]['drop-in']) {
- $selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all());
+ if (option['drop-in']) {
+ $selectorChildren = $selectorChildren.add(option['drop-in'].all()).filter(function () {
+ return !$snippet[0].contains(this);
+ });
}
}
}
- $toInsert = $baseBody.clone();
// Color-customize dynamic SVGs in dropped snippets with current theme colors.
- [...$toInsert.find('img[src^="/web_editor/shape/"]')].forEach(dynamicSvg => {
+ [...$snippetToInsert.find('img[src^="/web_editor/shape/"]')].forEach(dynamicSvg => {
const colorCustomizedURL = new URL(dynamicSvg.getAttribute('src'), window.location.origin);
colorCustomizedURL.searchParams.set('c1', getCSSVariableValue('o-color-1'));
dynamicSvg.src = colorCustomizedURL.pathname + colorCustomizedURL.search;
@@ -1889,89 +2012,112 @@ var SnippetsMenu = Widget.extend({
return;
}
- self._activateSnippet(false);
+ self._enableLastEditor();
self._activateInsertionZones($selectorSiblings, $selectorChildren);
- self.getEditableArea().find('.oe_drop_zone').droppable({
- over: function () {
- if (!dropped) {
- dropped = true;
- $(this).first().after($toInsert).addClass('d-none');
- $toInsert.removeClass('oe_snippet_body');
- }
- },
- out: function () {
- var prev = $toInsert.prev();
- if (this === prev[0]) {
- dropped = false;
- $toInsert.detach();
- $(this).removeClass('d-none');
- $toInsert.addClass('oe_snippet_body');
- }
- },
- });
+ self._dragAndDropStart();
- const prom = new Promise(resolve => dragAndDropResolve = () => resolve());
- self._mutex.exec(() => prom);
+ let $el = self.$editor;
+ if ($el[0].ownerDocument !== document) {
+ $el = $($el[0].ownerDocument.defaultView.frameElement);
+ if ($el) {
+ const offset = $el.offset();
+ const style = window.getComputedStyle($el[0]);
+ ui.helper.css({
+ marginTop: (- offset.top - parseInt(style.borderTop)) + 'px',
+ marginLeft: (- offset.left - parseInt(style.borderLeft)) + 'px',
+ });
+ }
+ }
+
+ $snippet.data('to-insert', $snippetToInsert);
},
stop: async function (ev, ui) {
- $toInsert.removeClass('oe_snippet_body');
+ const $snippetToInsert = this.data('to-insert');
+ $snippetToInsert.removeClass('oe_snippet_body');
if (!dropped && ui.position.top > 3 && ui.position.left + ui.helper.outerHeight() < self.el.getBoundingClientRect().left) {
var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone', {container: document.body}).first();
if ($el.length) {
- $el.after($toInsert);
+ scrollValue = $el.offset().top;
+ $el.after($snippetToInsert);
dropped = true;
}
}
- self.getEditableArea().find('.oe_drop_zone').droppable('destroy').remove();
+ self.$editor.find('.oe_drop_zone').remove();
if (dropped) {
- var prev = $toInsert.first()[0].previousSibling;
- var next = $toInsert.last()[0].nextSibling;
+ var prev = $snippetToInsert.first()[0].previousSibling;
+ var next = $snippetToInsert.last()[0].nextSibling;
if (prev) {
- $toInsert.detach();
- self.trigger_up('request_history_undo_record', {$target: $(prev)});
- $toInsert.insertAfter(prev);
+ $snippetToInsert.detach();
+ $snippetToInsert.insertAfter(prev);
} else if (next) {
- $toInsert.detach();
- self.trigger_up('request_history_undo_record', {$target: $(next)});
- $toInsert.insertBefore(next);
+ $snippetToInsert.detach();
+ $snippetToInsert.insertBefore(next);
} else {
- var $parent = $toInsert.parent();
- $toInsert.detach();
- self.trigger_up('request_history_undo_record', {$target: $parent});
- $parent.prepend($toInsert);
+ var $parent = $snippetToInsert.parent();
+ $snippetToInsert.detach();
+ $parent.prepend($snippetToInsert);
}
- var $target = $toInsert;
- await self._scrollToSnippet($target);
+ self._scrollToSnippet($snippetToInsert, scrollValue);
- _.defer(async function () {
- self.trigger_up('snippet_dropped', {$target: $target});
+ _.defer(async () => {
+ self.trigger_up('snippet_dropped', {$target: $snippetToInsert});
+ const jwEditor = self.wysiwyg.editor;
+ const vNodes = await self._insertSnippet($snippetToInsert);
+ const layout = jwEditor.plugins.get(self.JWEditorLib.Layout);
+ const domLayout = layout.engines.dom;
+ const domNode = domLayout.getDomNodes(vNodes[0])[0];
self._disableUndroppableSnippets();
- dragAndDropResolve();
+ self.dragAndDropResolve();
- await self._callForEachChildSnippet($target, function (editor, $snippet) {
+ await self._callForEachChildSnippet($(domNode), function (editor) {
return editor.buildSnippet();
});
- $target.trigger('content_changed');
+
+ $snippetToInsert.trigger('content_changed');
await self._updateInvisibleDOM();
self.$el.find('.oe_snippet_thumbnail').removeClass('o_we_already_dragging');
});
} else {
- $toInsert.remove();
- dragAndDropResolve();
+ $snippetToInsert.remove();
+ self.dragAndDropResolve();
self.$el.find('.oe_snippet_thumbnail').removeClass('o_we_already_dragging');
}
+ dropped = false;
+ dragStarted = false;
},
},
+ dropzones: function () {
+ return self.$editor.find('.oe_drop_zone');
+ },
+ over: function (ui, droppable) {
+ if (!dropped) {
+ dropped = true;
+ scrollValue = $(droppable).offset().top;
+ const $snippetToInsert = this.data('to-insert');
+ $(droppable).after($snippetToInsert).addClass('d-none');
+ $snippetToInsert.removeClass('oe_snippet_body');
+ }
+ },
+ out: function (ui, droppable) {
+ const $snippetToInsert = this.data('to-insert');
+ var prev = $snippetToInsert.prev();
+ if (droppable === prev[0]) {
+ dropped = false;
+ $snippetToInsert.detach();
+ $(droppable).removeClass('d-none');
+ $snippetToInsert.addClass('oe_snippet_body');
+ }
+ },
});
- this.draggableComponent = new SmoothScrollOnDrag(this, $snippets, $().getScrollingElement(), smoothScrollOptions);
+ this.draggableComponent = new SmoothScrollOnDrag(this, $snippets, this.$editor.find('#wrapwrap').addBack().last(), smoothScrollOptions);
},
/**
* Adds the 'o_default_snippet_text' class on nodes which contain only
@@ -2001,17 +2147,27 @@ var SnippetsMenu = Widget.extend({
* the new content of the customizePanel
* @param {this.tabs.VALUE} [tab='blocks'] - the tab to select
*/
- _updateLeftPanelContent: function ({content, tab}) {
+ _updateRightPanelContent: function ({content, tab}) {
clearTimeout(this._textToolsSwitchingTimeout);
this._closeWidgets();
-
tab = tab || this.tabs.BLOCKS;
+ this.wysiwyg.$toolbar.detach();
if (content) {
while (this.customizePanel.firstChild) {
this.customizePanel.removeChild(this.customizePanel.firstChild);
}
$(this.customizePanel).append(content);
+
+ if (tab === this.tabs.OPTIONS) {
+ const isImage = $(this.customizePanel).find('we-customizeblock-option.snippet-option-ImageOptimize').length > 0
+ this._addJabberwockToolbar(isImage ? 'image' : 'text');
+ }
+ }
+
+ if (tab === this.tabs.THEME) {
+ // Ensure the toolbar container is not in the side bar when we select the themes options tab.
+ $("#o_we_jw_toolbar_container").remove();
}
this.$('.o_snippet_search_filter').toggleClass('d-none', tab !== this.tabs.BLOCKS);
@@ -2024,6 +2180,58 @@ var SnippetsMenu = Widget.extend({
.prop('disabled', tab !== this.tabs.OPTIONS);
},
+ /**
+ * update the jabberwock toolbar container box.
+ */
+ _currentJabberwockToolbarMode: undefined,
+ updateJabberwockToolbarContainer : _.debounce(function (toolbarMode) {
+ if(toolbarMode !== this._currentJabberwockToolbarMode) {
+ const $oldToolbar = this.wysiwyg.$toolbar.parent();
+ this._addJabberwockToolbar(toolbarMode);
+ $oldToolbar.remove();
+ }
+ }, 100),
+ /**
+ * Add the jabberwock toolbar.
+ */
+ _addJabberwockToolbar(toolbarMode = "text") {
+ this._currentJabberwockToolbarMode = toolbarMode;
+ const $toolbar = this.wysiwyg.$toolbar;
+
+ let titleText = _t("Text Formatting");
+ switch (toolbarMode) {
+ case "image":
+ titleText = _t("Image Formatting");
+ break;
+ case "video":
+ titleText = _t("Video Formatting");
+ break;
+ case "picto":
+ titleText = _t("Icon Formatting");
+ break;
+ }
+ const customizeBlock = $('');
+ const $title = $("" + titleText + "");
+ if (toolbarMode === "text") {
+ const $removeFormatButton = $('', {
+ class: 'fa fa-fw fa-eraser o_we_link o_we_hover_danger',
+ title: 'Clear Formatting',
+ })
+ $removeFormatButton.on('click', () => {
+ this.wysiwyg.editor.execCommand('removeFormat');
+ });
+ const $group = $('');
+ $group.append($removeFormatButton);
+ $title.append($group);
+ }
+ customizeBlock.append($title);
+ customizeBlock.append($toolbar);
+ $(this.customizePanel).append(customizeBlock);
+
+ $toolbar.off('click.we_oe_snippets').on('click.we_oe_snippets', 'jw-select', ev => {
+ this.$el.toggleClass('o_we_backdrop', ev.currentTarget.getAttribute('aria-pressed') === 'true');
+ });
+ },
/**
* Scrolls to given snippet.
*
@@ -2098,6 +2306,20 @@ var SnippetsMenu = Widget.extend({
return mutexExecResult;
},
+ /**
+ * Called when a snippet will be moved in the page.
+ *
+ * @private
+ */
+ _dragAndDropStart: async function () {
+ this._mutex.exec(() => {
+ let dragAndDropResolve;
+ const promise = new Promise(resolve => dragAndDropResolve = () => resolve());
+ this.dragAndDropResolve = dragAndDropResolve;
+ return promise;
+ });
+ },
+
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -2118,7 +2340,10 @@ var SnippetsMenu = Widget.extend({
* @private
*/
_onActivateSnippet: function (ev) {
- this._activateSnippet(ev.data.$snippet, ev.data.previewMode, ev.data.ifInactiveOptions);
+ if (ev.data.saveTarget) {
+ this._setLastSnippet(ev.data.$element, ev.data.savePreview && ev.data.previewMode);
+ }
+ this._activateSnippet(ev.data.$element, ev.data.previewMode, ev.data.ifInactiveOptions);
},
/**
* Called when a child editor asks to operate some operation on all child
@@ -2149,8 +2374,8 @@ var SnippetsMenu = Widget.extend({
*/
_onCloneSnippet: async function (ev) {
ev.stopPropagation();
- const editor = await this._createSnippetEditor(ev.data.$snippet);
- await editor.clone();
+ const snippetEditor = await this._getSnippetEditor(ev.data.$snippet);
+ await snippetEditor.clone();
if (ev.data.onSuccess) {
ev.data.onSuccess();
}
@@ -2162,7 +2387,17 @@ var SnippetsMenu = Widget.extend({
* @private
*/
_onDeactivateSnippet: function () {
- this._activateSnippet(false);
+ this._enableLastEditor();
+ },
+
+ /**
+ * Called when a snippet will be moved in the page.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onDragAndDropStart: async function (ev) {
+ this._dragAndDropStart()
},
/**
* Called when a snippet has moved in the page.
@@ -2171,7 +2406,7 @@ var SnippetsMenu = Widget.extend({
* @param {OdooEvent} ev
*/
_onDragAndDropStop: async function (ev) {
- await this._destroyEditors();
+ this.dragAndDropResolve();
await this._activateSnippet(ev.data.$snippet);
},
/**
@@ -2189,8 +2424,8 @@ var SnippetsMenu = Widget.extend({
* @private
*/
_onHideOverlay: function () {
- for (const editor of this.snippetEditors) {
- editor.toggleOverlay(false);
+ for (const snippetEditor of this.snippetEditors) {
+ snippetEditor.toggleOverlay(false);
}
},
/**
@@ -2255,23 +2490,26 @@ var SnippetsMenu = Widget.extend({
ev.preventDefault();
const $snippet = $(this.invisibleDOMMap.get(ev.currentTarget));
const isVisible = await this._execWithLoadingEffect(async () => {
- const editor = await this._createSnippetEditor($snippet);
+ const editor = await this._getSnippetEditor($snippet);
return editor.toggleTargetVisibility();
}, true);
$(ev.currentTarget).find('.fa')
.toggleClass('fa-eye', isVisible)
.toggleClass('fa-eye-slash', !isVisible);
- return this._activateSnippet(isVisible ? $snippet : false);
+ if (isVisible) {
+ return this._activateSnippet($snippet);
+ } else {
+ return this._enableLastEditor();
+ }
},
/**
* @private
*/
- _onBlocksTabClick: function (ev) {
- this._activateSnippet(false).then(() => {
- this._updateLeftPanelContent({
- content: [],
- tab: this.tabs.BLOCKS,
- });
+ _onBlocksTabClick: async function (ev) {
+ await this._enableLastEditor();
+ this._updateRightPanelContent({
+ content: [],
+ tab: this.tabs.BLOCKS,
});
},
/**
@@ -2310,18 +2548,38 @@ var SnippetsMenu = Widget.extend({
*
* @private
*/
- _onMouseDown: function () {
- const $blockedArea = $('#wrapwrap'); // TODO should get that element another way
- $blockedArea.addClass('o_we_no_pointer_events');
- const reenable = () => $blockedArea.removeClass('o_we_no_pointer_events');
+ _onMouseDown: function (ev) {
+ this.$editor.addClass('o_we_no_pointer_events');
+ const reenable = () => this.$editor.removeClass('o_we_no_pointer_events');
// Use a setTimeout fallback to avoid locking the editor if the mouseup
// is fired over an element which stops propagation for example.
- const enableTimeoutID = setTimeout(() => reenable(), 5000);
- $(document).one('mouseup', () => {
+ const enableTimeoutID = setTimeout(reenable, 5000);
+ $(window).one('mouseup', () => {
clearTimeout(enableTimeoutID);
reenable();
});
},
+ _onSnippetMouseDown: function (ev) {
+ const el = this.editorHelpers.elementFromPoint(ev.clientX, ev.clientY);
+
+ const editable = el && el.closest('.note-editable');
+
+ if (!editable || !this.$editor.is(editable) || this.lastElement === el) {
+ return;
+ }
+ this.lastElement = el;
+ setTimeout(() => {this.lastElement = false;});
+
+ var $snippet = $(el);
+ if (!$snippet.closest('we-button, we-toggler, .o_we_color_preview').length) {
+ this._closeWidgets();
+ }
+ if ($snippet.closest(this._notActivableElementsSelector).length) {
+ return;
+ }
+ this._setLastSnippet($snippet);
+ this._activateSnippet($snippet);
+ },
/**
* @private
* @param {OdooEvent} ev
@@ -2338,7 +2596,7 @@ var SnippetsMenu = Widget.extend({
* @private
*/
_onReloadSnippetTemplate: async function (ev) {
- await this._activateSnippet(false);
+ await this._enableLastEditor();
await this._loadSnippetsTemplates(true);
},
/**
@@ -2359,8 +2617,8 @@ var SnippetsMenu = Widget.extend({
*/
_onRemoveSnippet: async function (ev) {
ev.stopPropagation();
- const editor = await this._createSnippetEditor(ev.data.$snippet);
- await editor.removeSnippet();
+ const snippetEditor = await this._getSnippetEditor(ev.data.$snippet);
+ await snippetEditor.removeSnippet();
if (ev.data.onSuccess) {
ev.data.onSuccess();
}
@@ -2422,9 +2680,10 @@ var SnippetsMenu = Widget.extend({
*
* @private
*/
- _onSnippetRemoved: function () {
- this._disableUndroppableSnippets();
+ _onSnippetRemoved: async function (ev) {
+ await this._disableUndroppableSnippets();
this._updateInvisibleDOM();
+ ev.data.onFinish();
},
/**
* @private
@@ -2432,7 +2691,7 @@ var SnippetsMenu = Widget.extend({
*/
_onSnippetOptionVisibilityUpdate: async function (ev) {
if (!ev.data.show) {
- await this._activateSnippet(false);
+ this._enableLastEditor();
}
await this._updateInvisibleDOM(); // Re-render to update status
},
@@ -2463,12 +2722,12 @@ var SnippetsMenu = Widget.extend({
// visible text tools.
return;
}
- // Only switch tab without changing content (_updateLeftPanelContent
+ // Only switch tab without changing content (_updateRightPanelContent
// make text tools visible only on that specific tab). Also do it with
// a slight delay to avoid flickering doing it twice.
clearTimeout(this._textToolsSwitchingTimeout);
this._textToolsSwitchingTimeout = setTimeout(() => {
- this._updateLeftPanelContent({tab: this.tabs.OPTIONS});
+ this._updateRightPanelContent({tab: this.tabs.OPTIONS});
}, 250);
},
/**
@@ -2476,7 +2735,7 @@ var SnippetsMenu = Widget.extend({
* @param {OdooEvent} ev
*/
_onUpdateCustomizeElements: function (ev) {
- this._updateLeftPanelContent({
+ this._updateRightPanelContent({
content: ev.data.customize$Elements,
tab: ev.data.customize$Elements.length ? this.tabs.OPTIONS : this.tabs.BLOCKS,
});
@@ -2512,11 +2771,115 @@ var SnippetsMenu = Widget.extend({
_onSnippetSearchResetClick: function () {
this._filterSnippets('');
},
+
+ /**
+ * Retrieve the relative position of an element.
+ * An element's position is 'BEFORE', 'AFTER' or 'INSIDE' another element
+ * (in that order of priority).
+ * Eg: the element is located before the node `a` -> return [`a`, 'BEFORE'].
+ *
+ * @param {JQuery} $snippet
+ * @returns {[Node, 'BEFORE'|'AFTER'|'INSIDE']}
+ */
+ _getRelativePosition(element) {
+ let currentNode = element.nextSibling;
+ while (currentNode) {
+ const nodes = this.editorHelpers.getNodes(currentNode);
+ const node = nodes && nodes[0];
+ if (node) {
+ return [currentNode, 'BEFORE'];
+ }
+ currentNode = currentNode.nextSibling;
+ }
+ currentNode = element.previousSibling;
+ while (currentNode) {
+ const nodes = this.editorHelpers.getNodes(currentNode);
+ const node = nodes && nodes[0];
+ if (node) {
+ return [currentNode, 'AFTER'];
+ }
+ currentNode = currentNode.previousSibling;
+ }
+ currentNode = element.parentElement;
+ while (currentNode) {
+ const nodes = this.editorHelpers.getNodes(currentNode);
+ const node = nodes && nodes[0];
+ if (node) {
+ return [currentNode, 'INSIDE'];
+ }
+ currentNode = currentNode.parentElement;
+ }
+ },
+ /**
+ * Insert a snippet at range.
+ *
+ * @param {JQuery} $snippet
+ * @returns {VNode[]}
+ */
+
+ _insertSnippet: async function ($snippet) {
+ const jwEditor = this.wysiwyg.editor;
+ let result;
+ const insertSnippet = async (context) => {
+ const layout = jwEditor.plugins.get(this.JWEditorLib.Layout);
+ const domLayout = layout.engines.dom;
+ domLayout.markForRedraw(new Set($snippet.find('*').contents().addBack().add($snippet).get()));
+ const position = this._getRelativePosition($snippet[0]);
+ if (!position) {
+ throw new Error("Could not find a place to insert the snippet.");
+ }
+ result = await this.editorHelpers.insertHtml(context, $snippet[0].outerHTML, position[0], position[1]);
+ };
+ await jwEditor.execCommand(insertSnippet);
+ return result;
+ },
+ /**
+ * On click on save button.
+ */
+ _onSaveClick: function() {
+ this.wysiwyg.saveContent();
+ },
+ /**
+ * On click on discard button.
+ */
+ _onDiscardClick: function() {
+ this.wysiwyg.discardEditions();
+ },
+ /**
+ * On click on discard button.
+ */
+ _onMobilePreviewClick: function() {
+ this.wysiwyg.editor.execCommand('toggleDevicePreview', { device: 'mobile' });
+ },
+ /**
+ * Set the last snippet activated.
+ */
+ _setLastSnippet($snippet, preview = false) {
+ this._removeLastSnippetActivated();
+ $snippet[0].classList.add('o_we_last_snippet_activated');
+ if (preview) $snippet[0].classList.add('o_we_last_snippet_preview');
+ },
+ /**
+ * Remove the last snippet that has been activated.
+ */
+ async _removeLastSnippetActivated() {
+ const classesToRemove = ['o_we_last_snippet_activated', 'o_we_last_snippet_preview'];
+ const $lastSnippet = this.$editor.find('.o_we_last_snippet_activated');
+ $lastSnippet.removeClass(classesToRemove);
+ if ($lastSnippet.length) {
+ // loop in case the class has been duplicated (e.g. cloned)
+ const removeLastSnippetActivated = async (context) => {
+ for (const lastSnippet of $lastSnippet.toArray()) {
+ await this.editorHelpers.removeClass(context, lastSnippet, classesToRemove);
+ }
+ }
+ await this.wysiwyg.editor.execCommand(removeLastSnippetActivated);
+ }
+ },
});
return {
- Class: SnippetsMenu,
- Editor: SnippetEditor,
+ SnippetsMenu: SnippetsMenu,
globalSelector: globalSelector,
};
});
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 e9a2ce3e781..7e1b85885dd 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -152,16 +152,20 @@ function _buildCollapseElement(title, options) {
* Creates a proxy for an object where one property is replaced by a different
* value. This value is captured in the closure and can be read and written to.
*
+ * If the value is a function, return the result of calling it when reading
+ * the property.
+ *
* @param {Object} obj - the object for which to create a proxy
* @param {string} propertyName - the name/key of the property to replace
* @param {*} value - the initial value to give to the property's copy
+* If the value is a function, it will be called.
* @returns {Proxy} a proxy of the object with the property replaced
*/
function createPropertyProxy(obj, propertyName, value) {
return new Proxy(obj, {
get: function (obj, prop) {
if (prop === propertyName) {
- return value;
+ return typeof value === 'function' ? value() : value;
}
return obj[prop];
},
@@ -260,6 +264,17 @@ const UserValueWidget = Widget.extend({
// Public
//--------------------------------------------------------------------------
+ /**
+ * Set the target.
+ *
+ * @param {JQuery} $editorBlock
+ */
+ setTarget($editorBlock) {
+ this.$target = $editorBlock;
+ for (const widget of this._userValueWidgets) {
+ widget.setTarget(this.$target);
+ }
+ },
/**
* Closes the widget (only meaningful for widgets that can be closed).
*/
@@ -324,7 +339,7 @@ const UserValueWidget = Widget.extend({
* Returns the option parameters associated to the widget (for a given
* method name or not). Most are loaded with @see loadMethodsData.
*
- * @param {string} [methodName]
+ * @param {string | undefined} [methodName]
* @returns {Object}
*/
getMethodsParams: function (methodName) {
@@ -394,36 +409,36 @@ const UserValueWidget = Widget.extend({
* Loads option method names and option method parameters.
*
* @param {string[]} validMethodNames
- * @param {Object} extraParams
+ * @param {Object | undefined} parentMethodsParams
*/
- loadMethodsData: function (validMethodNames, extraParams) {
+ loadMethodsData: function (validMethodNames, parentMethodsParams) {
this._methodsNames = [];
- this._methodsParams = _.extend({}, extraParams);
+ this._methodsParams = _.extend({}, parentMethodsParams);
this._methodsParams.optionsPossibleValues = {};
this._dependencies = [];
this._triggerWidgetsNames = [];
this._triggerWidgetsValues = [];
- for (const key in this.el.dataset) {
- const dataValue = this.el.dataset[key].trim();
+ for (const dataKey in this.el.dataset) {
+ const dataValue = this.el.dataset[dataKey].trim();
- if (key === 'dependencies') {
+ if (dataKey === 'dependencies') {
this._dependencies.push(...dataValue.split(/\s*,\s*/g));
- } else if (key === 'trigger') {
+ } else if (dataKey === 'trigger') {
this._triggerWidgetsNames.push(...dataValue.split(/\s*,\s*/g));
- } else if (key === 'triggerValue') {
+ } else if (dataKey === 'triggerValue') {
this._triggerWidgetsValues.push(...dataValue.split(/\s*,\s*/g));
- } else if (validMethodNames.includes(key)) {
- this._methodsNames.push(key);
- this._methodsParams.optionsPossibleValues[key] = dataValue.split(/\s*\|\s*/g);
+ } else if (validMethodNames.includes(dataKey)) {
+ this._methodsNames.push(dataKey);
+ this._methodsParams.optionsPossibleValues[dataKey] = dataValue.split(/\s*\|\s*/g);
} else {
- this._methodsParams[key] = dataValue;
+ this._methodsParams[dataKey] = dataValue;
}
}
this._userValueWidgets.forEach(widget => {
- const inheritedParams = _.extend({}, this._methodsParams);
- inheritedParams.optionsPossibleValues = null;
- widget.loadMethodsData(validMethodNames, inheritedParams);
+ const inheritedMethodsParams = _.extend({}, this._methodsParams);
+ inheritedMethodsParams.optionsPossibleValues = null;
+ widget.loadMethodsData(validMethodNames, inheritedMethodsParams);
const subMethodsNames = widget.getMethodsNames();
const subMethodsParams = widget.getMethodsParams();
@@ -1822,23 +1837,37 @@ const SnippetOptionWidget = Widget.extend({
*
* @constructor
*/
- init: function (parent, $uiElements, $target, $overlay, data, options) {
+ init: function (parent, $uiElements, getTarget, $overlay, data, options) {
this._super.apply(this, arguments);
this.$originalUIElements = $uiElements;
- this.$target = $target;
this.$overlay = $overlay;
this.data = data;
this.options = options;
+ this.wysiwyg = this.options.wysiwyg;
+ this.editor = this.wysiwyg.editor;
+ this.editorHelpers = this.wysiwyg.editorHelpers;
this.className = 'snippet-option-' + this.data.optionName;
+ this._userValueWidgets = [];
+ this.getTarget = getTarget;
+ this.resetOptionTarget();
this.ownerDocument = this.$target[0].ownerDocument;
this._userValueWidgets = [];
this._actionQueues = new Map();
},
+ /**
+ * Reset the option target.
+ */
+ resetOptionTarget() {
+ this.setTarget(this.getTarget());
+ for (const subWidget of this._userValueWidgets) {
+ subWidget.setTarget(this.$target);
+ }
+ },
/**
* @override
*/
@@ -1946,7 +1975,7 @@ const SnippetOptionWidget = Widget.extend({
* @param {Object} params
* @returns {Promise|undefined}
*/
- selectClass: function (previewMode, widgetValue, params) {
+ selectClass: async function (previewMode, widgetValue, params) {
for (const classNames of params.possibleValues) {
if (classNames) {
this.$target[0].classList.remove(...classNames.trim().split(/\s+/g));
@@ -1955,6 +1984,8 @@ const SnippetOptionWidget = Widget.extend({
if (widgetValue) {
this.$target[0].classList.add(...widgetValue.trim().split(/\s+/g));
}
+
+ if (previewMode === false) await this.updateChangesInWysiwyg();
},
/**
* Default option method which allows to select a value and set it on the
@@ -1966,9 +1997,16 @@ const SnippetOptionWidget = Widget.extend({
* @param {Object} params
* @returns {Promise|undefined}
*/
- selectDataAttribute: function (previewMode, widgetValue, params) {
- const value = this._selectAttributeHelper(widgetValue, params);
- this.$target[0].dataset[params.attributeName] = value;
+ selectDataAttribute: async function (previewMode, widgetValue, params) {
+ const selectDataAttribute = async (context) => {
+ const value = await this._selectAttributeHelper(widgetValue, params, context);
+ this.$target[0].dataset[params.attributeName] = value;
+ if (!previewMode) {
+ const attributeName = params.attributeName.replace(/[A-Z]/g, m => "-" + m.toLowerCase());
+ await this.editorHelpers.setAttribute(context, this.$target[0], `data-${attributeName}`, value);
+ }
+ };
+ return this.wysiwyg.editor.execCommand(selectDataAttribute);
},
/**
* Default option method which allows to select a value and set it on the
@@ -1980,9 +2018,13 @@ const SnippetOptionWidget = Widget.extend({
* @param {Object} params
* @returns {Promise|undefined}
*/
- selectAttribute: function (previewMode, widgetValue, params) {
- const value = this._selectAttributeHelper(widgetValue, params);
- this.$target[0].setAttribute(params.attributeName, value);
+ selectAttribute: async function (previewMode, widgetValue, params) {
+ const value = await this._selectAttributeHelper(widgetValue, params);
+ if (previewMode) {
+ this.$target.attr(params.attributeName, value);
+ } else {
+ await this.editorHelpers.setAttribute(this.wysiwyg.editor, this.$target[0], params.attributeName, value);
+ }
},
/**
* Default option method which allows to select a value and set it on the
@@ -1994,7 +2036,7 @@ const SnippetOptionWidget = Widget.extend({
* @param {Object} params
* @returns {Promise|undefined}
*/
- selectStyle: function (previewMode, widgetValue, params) {
+ selectStyle: async function (previewMode, widgetValue, params) {
if (params.cssProperty === 'background-color') {
this.$target.trigger('background-color-event', previewMode);
}
@@ -2020,6 +2062,7 @@ const SnippetOptionWidget = Widget.extend({
// Those are the special color combinations classes. Just have
// to add it (and adding the potential extra class) then leave.
this.$target[0].classList.add('o_cc', `o_cc${widgetValue}`, params.extraClass);
+ if (previewMode === false) await this.updateChangesInWysiwyg();
return;
}
if (params.colorNames.includes(widgetValue)) {
@@ -2031,6 +2074,7 @@ const SnippetOptionWidget = Widget.extend({
// property we are editing, nothing more has to be done.
// (except adding the extra class)
this.$target.addClass(params.extraClass);
+ if (previewMode === false) await this.updateChangesInWysiwyg();
return;
}
// Otherwise, it means that class probably does not exist,
@@ -2086,6 +2130,8 @@ const SnippetOptionWidget = Widget.extend({
if (params.extraClass) {
this.$target.toggleClass(params.extraClass, hasUserValue);
}
+
+ if (previewMode === false) await this.updateChangesInWysiwyg();
},
//--------------------------------------------------------------------------
@@ -2133,8 +2179,9 @@ const SnippetOptionWidget = Widget.extend({
* @returns {Promise}
*/
notify: function (name, data) {
- if (name === 'target') {
- this.setTarget(data);
+ if (name === 'setTargetDependency') {
+ this._targetDependency = data;
+ this.setTarget();
}
},
/**
@@ -2149,7 +2196,11 @@ const SnippetOptionWidget = Widget.extend({
* @returns {Promise}
*/
setTarget: function ($target) {
- this.$target = $target;
+ if (this._targetDependency) {
+ this.$target = this._targetDependency();
+ } else {
+ this.$target = $target;
+ }
},
/**
* Updates the UI. For widget update, @see _computeWidgetState.
@@ -2171,11 +2222,13 @@ const SnippetOptionWidget = Widget.extend({
let obj = this;
if (params.applyTo) {
- const $firstSubTarget = this.$(params.applyTo).eq(0);
- if (!$firstSubTarget.length) {
+ const $firstSubTargetFn = () => this.$(params.applyTo).eq(0);
+ if (!$firstSubTargetFn().length) {
continue;
}
- obj = createPropertyProxy(this, '$target', $firstSubTarget);
+ // We use a function to retrive the value as the """original" $firstSubTarget might
+ // have been replaced by the jabberwock editor.
+ obj = createPropertyProxy(this, '$target', $firstSubTargetFn);
}
const value = await this._computeWidgetState.call(obj, methodName, params);
@@ -2204,12 +2257,14 @@ const SnippetOptionWidget = Widget.extend({
let obj = this;
if (params.applyTo) {
- const $firstSubTarget = this.$(params.applyTo).eq(0);
- if (!$firstSubTarget.length) {
+ const $firstSubTargetFn = () => this.$(params.applyTo).eq(0);
+ if (!$firstSubTargetFn().length) {
widget.toggleVisibility(false);
return;
}
- obj = createPropertyProxy(this, '$target', $firstSubTarget);
+ // We use a function to retrive the value as the "original" $firstSubTarget might have
+ // been replaced by the jabberwock editor.
+ obj = createPropertyProxy(this, '$target', $firstSubTargetFn);
}
// Make sure to check the visibility of all sub-widgets. For
@@ -2274,6 +2329,12 @@ const SnippetOptionWidget = Widget.extend({
el.querySelector('.o_we_collapse_toggler').classList.toggle('d-none', hasNoVisibleElInCollapseMenu);
}
},
+ /**
+ * Refresh the target in the wysiwyg.
+ */
+ async updateChangesInWysiwyg($target = this.$target) {
+ return this.wysiwyg.updateChanges($target);
+ },
//--------------------------------------------------------------------------
// Private
@@ -2527,7 +2588,7 @@ const SnippetOptionWidget = Widget.extend({
// widget start.
parentEl.removeChild(el);
- if (widget.isContainer()) {
+ if (widget.isContainer() && widget.el) {
return this._renderXMLWidgets(widget.el, widget);
}
});
@@ -2606,6 +2667,10 @@ const SnippetOptionWidget = Widget.extend({
}
}
+ if (previewMode === false) {
+ this.wysiwyg.snippetsMenu.activateLastSnippetBlock();
+ }
+
// We trigger the event on elements targeted by apply-to if any as
// this.$target could not be in an editable element while the elements
// targeted by apply-to are.
@@ -2618,7 +2683,7 @@ const SnippetOptionWidget = Widget.extend({
* @param {Object} params
* @returns {string|undefined}
*/
- _selectAttributeHelper(value, params) {
+ async _selectAttributeHelper(value, params, context) {
if (!params.attributeName) {
throw new Error('Attribute name missing');
}
@@ -2628,7 +2693,11 @@ const SnippetOptionWidget = Widget.extend({
value = value.split(params.saveUnit).join('');
}
if (params.extraClass) {
- this.$target.toggleClass(params.extraClass, params.defaultValue !== value);
+ if (params.defaultValue === value) {
+ await this.editorHelpers.removeClass(context, this.$target[0], params.extraClass, context);
+ } else {
+ await this.editorHelpers.addClass(context, this.$target[0], params.extraClass, context);
+ }
}
return value;
},
@@ -2751,12 +2820,6 @@ const SnippetOptionWidget = Widget.extend({
return;
}
- // If it is not preview mode, the user selected the option for good
- // (so record the action)
- if (shouldRecordUndo) {
- this.trigger_up('request_history_undo_record', {$target: this.$target});
- }
-
// Call widget option methods and update $target
await this._select(previewMode, widget);
if (previewMode) {
@@ -2889,7 +2952,7 @@ registry.sizing = SnippetOptionWidget.extend({
$body.addClass(cursor);
var xy = ev['page' + XY];
- var bodyMouseMove = function (ev) {
+ var bodyMouseMove = async function (ev) {
ev.preventDefault();
var dd = ev['page' + XY] - xy + resize[1][begin];
@@ -2897,14 +2960,15 @@ registry.sizing = SnippetOptionWidget.extend({
var prev = current ? (current - 1) : 0;
var change = false;
+ const cleanedClass = (self.$target.attr('class') || '').replace(regClass, '');
if (dd > (2 * resize[1][next] + resize[1][current]) / 3) {
- self.$target.attr('class', (self.$target.attr('class') || '').replace(regClass, ''));
+ self.$target.attr('class', cleanedClass);
self.$target.addClass(resize[0][next]);
current = next;
change = true;
}
if (prev !== current && dd < (2 * resize[1][prev] + resize[1][current]) / 3) {
- self.$target.attr('class', (self.$target.attr('class') || '').replace(regClass, ''));
+ self.$target.attr('class', cleanedClass);
self.$target.addClass(resize[0][prev]);
current = prev;
change = true;
@@ -2916,12 +2980,14 @@ registry.sizing = SnippetOptionWidget.extend({
$handle.addClass('o_active');
}
};
- var bodyMouseUp = function () {
+ var bodyMouseUp = async function () {
$body.off('mousemove', bodyMouseMove);
$body.off('mouseup', bodyMouseUp);
$body.removeClass(cursor);
$handle.removeClass('o_active');
+ await self.editorHelpers.setAttribute(self.wysiwyg.editor, self.$target[0], 'class', self.$target.attr('class'));
+
// Highlights the previews for a while
var $handlers = self.$overlay.find('.o_handle');
$handlers.addClass('o_active').delay(300).queue(function () {
@@ -2931,12 +2997,6 @@ registry.sizing = SnippetOptionWidget.extend({
if (begin === current) {
return;
}
- setTimeout(function () {
- self.trigger_up('request_history_undo_record', {
- $target: self.$target,
- event: 'resize_' + XY,
- });
- }, 0);
};
$body.on('mousemove', bodyMouseMove);
$body.on('mouseup', bodyMouseUp);
@@ -3417,6 +3477,7 @@ registry.BackgroundOptimize = ImageHandlerOption.extend({
this.$target[0].dataset[key] = value;
});
this.$target[0].dataset.bgSrc = img.getAttribute('src');
+ await this.updateChangesInWysiwyg();
}
},
@@ -3530,7 +3591,7 @@ registry.BackgroundToggler = SnippetOptionWidget.extend({
*
* @see this.selectClass for parameters
*/
- toggleBgFilter(previewMode, widgetValue, params) {
+ async toggleBgFilter(previewMode, widgetValue, params) {
if (widgetValue) {
const bgFilterEl = document.createElement('div');
bgFilterEl.classList.add('o_we_bg_filter', 'bg-black-50');
@@ -3543,6 +3604,7 @@ registry.BackgroundToggler = SnippetOptionWidget.extend({
} else {
this.$target.find('.o_we_bg_filter').remove();
}
+ if (previewMode === false) await this.updateChangesInWysiwyg();
},
//--------------------------------------------------------------------------
@@ -3634,12 +3696,14 @@ registry.BackgroundImage = SnippetOptionWidget.extend({
this.__customImageSrc = widgetValue;
}
- this._setBackground(widgetValue);
+ this._setBackground(this.$target, widgetValue);
if (previewMode !== 'reset') {
removeOnImageChangeAttrs.forEach(attr => delete this.$target[0].dataset[attr]);
this.$target.trigger('background_changed', [previewMode]);
}
+
+ if (previewMode === false) await this.updateChangesInWysiwyg();
},
/**
* Changes the main color of dynamic SVGs.
@@ -3660,7 +3724,7 @@ registry.BackgroundImage = SnippetOptionWidget.extend({
newURL.searchParams.set('c1', normalizeColor(widgetValue));
const src = newURL.pathname + newURL.search;
await loadImage(src);
- this.$target.css('background-image', `url('${src}')`);
+ await this.editorHelpers.setStyle(this.editor, this.$target.get(), 'background-image', `url('${src}')`);
if (!previewMode) {
this.previousSrc = src;
}
@@ -3676,11 +3740,17 @@ registry.BackgroundImage = SnippetOptionWidget.extend({
setTarget: function () {
// When we change the target of this option we need to transfer the
// background-image from the old target to the new one.
- const oldBgURL = getBgImageURL(this.$target);
- this._setBackground('');
+ let oldBgURL;
+ const $previousTarget = this.$target;
+ if (this.$target) {
+ oldBgURL = getBgImageURL(this.$target);
+ }
this._super(...arguments);
if (oldBgURL) {
- this._setBackground(oldBgURL);
+ this._setBackground(this.$target, oldBgURL);
+ }
+ if ($previousTarget && $previousTarget[0] !== this.$target[0]) {
+ this._setBackground($previousTarget, '');
}
// TODO should be automatic for all options as equal to the start method
@@ -3717,13 +3787,13 @@ registry.BackgroundImage = SnippetOptionWidget.extend({
* @private
* @param {string} backgroundURL
*/
- _setBackground(backgroundURL) {
+ _setBackground($target, backgroundURL) {
if (backgroundURL) {
- this.$target.css('background-image', `url('${backgroundURL}')`);
- this.$target.addClass('oe_img_bg');
+ $target.css('background-image', `url('${backgroundURL}')`);
+ $target.addClass('oe_img_bg');
} else {
- this.$target.css('background-image', '');
- this.$target.removeClass('oe_img_bg');
+ $target.css('background-image', '');
+ $target.removeClass('oe_img_bg');
}
},
});
@@ -3753,7 +3823,7 @@ registry.BackgroundShape = SnippetOptionWidget.extend({
* @see this.selectClass for params
*/
shape(previewMode, widgetValue, params) {
- this._handlePreviewState(previewMode, () => {
+ return this._handlePreviewState(previewMode, () => {
return {shape: widgetValue, colors: this._getDefaultColors(), flip: []};
});
},
@@ -3763,7 +3833,7 @@ registry.BackgroundShape = SnippetOptionWidget.extend({
* @see this.selectClass for params
*/
color(previewMode, widgetValue, params) {
- this._handlePreviewState(previewMode, () => {
+ return this._handlePreviewState(previewMode, () => {
const {colorName} = params;
const {colors: previousColors} = this._getShapeData();
const newColor = normalizeColor(widgetValue) || this._getDefaultColors()[colorName];
@@ -3777,7 +3847,7 @@ registry.BackgroundShape = SnippetOptionWidget.extend({
* @see this.selectClass for params
*/
flipX(previewMode, widgetValue, params) {
- this._flipShape(previewMode, 'x');
+ return this._flipShape(previewMode, 'x');
},
/**
* Flips the shape on its y axis.
@@ -3785,7 +3855,7 @@ registry.BackgroundShape = SnippetOptionWidget.extend({
* @see this.selectClass for params
*/
flipY(previewMode, widgetValue, params) {
- this._flipShape(previewMode, 'y');
+ return this._flipShape(previewMode, 'y');
},
//--------------------------------------------------------------------------
@@ -3854,7 +3924,7 @@ registry.BackgroundShape = SnippetOptionWidget.extend({
* @param {'x'|'y'} axis the axis of the shape that should be flipped.
*/
_flipShape(previewMode, axis) {
- this._handlePreviewState(previewMode, () => {
+ return this._handlePreviewState(previewMode, () => {
const flip = new Set(this._getShapeData().flip);
if (flip.has(axis)) {
flip.delete(axis);
@@ -3871,7 +3941,7 @@ registry.BackgroundShape = SnippetOptionWidget.extend({
* @param {boolean} previewMode
* @param {function} computeShapeData function to compute the new shape data.
*/
- _handlePreviewState(previewMode, computeShapeData) {
+ async _handlePreviewState(previewMode, computeShapeData) {
if (previewMode === 'reset') {
if (this.prevShape) {
this.$target[0].dataset.oeShapeData = this.prevShape;
@@ -3921,6 +3991,8 @@ registry.BackgroundShape = SnippetOptionWidget.extend({
});
// Custom colors, overwrite shape that is set by the class
$(shapeContainer).css('background-image', colors ? `url("${this._getShapeSrc()}")` : '');
+
+ if (previewMode === false) await this.updateChangesInWysiwyg();
},
/**
* Overwrites shape properties with the specified data.
@@ -4071,10 +4143,17 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
*
* @see this.selectClass for params
*/
- backgroundType: function (previewMode, widgetValue, params) {
- this.$target.toggleClass('o_bg_img_opt_repeat', widgetValue === 'repeat-pattern');
- this.$target.css('background-position', '');
- this.$target.css('background-size', '');
+ backgroundType: async function (previewMode, widgetValue, params) {
+ const backgroundType = async (context)=> {
+ if (widgetValue === 'repeat-pattern') {
+ await this.editorHelpers.addClass(context, this.$target[0], 'o_bg_img_opt_repeat');
+ } else {
+ await this.editorHelpers.removeClass(context, this.$target[0], 'o_bg_img_opt_repeat');
+ }
+ await this.editorHelpers.setStyle(context, this.$target[0], 'background-position', '');
+ await this.editorHelpers.setStyle(context, this.$target[0], 'background-size', '');
+ }
+ await this.wysiwyg.editor.execCommand(backgroundType);
},
/**
* Saves current background position and enables overlay.
@@ -4143,8 +4222,8 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
this.$overlayContent = this.$backgroundOverlay.find('.o_we_overlay_content');
this.$overlayBackground = this.$overlayContent.find('.o_overlay_background');
- this.$backgroundOverlay.on('click', '.o_btn_apply', () => {
- this.$target.css('background-position', this.$bgDragger.css('background-position'));
+ this.$backgroundOverlay.on('click', '.o_btn_apply', async () => {
+ await this.editorHelpers.setStyle(this.wysiwyg.editor, this.$target[0], 'background-position', this.$bgDragger.css('background-position'));
this._toggleBgOverlay(false);
});
this.$backgroundOverlay.on('click', '.o_btn_discard', () => {
@@ -4164,7 +4243,8 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
return;
}
// TODO: change #wrapwrap after web_editor rework.
- const $wrapwrap = $('#wrapwrap');
+ const content = document.querySelector('#wrapwrap *, jw-shadow::shadow /deep/ *').parentNode;
+ const $wrapwrap = $(content);
const targetOffset = this.$target.offset();
this.$backgroundOverlay.css({
@@ -4196,7 +4276,7 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
if (!activate) {
this.$backgroundOverlay.removeClass('oe_active');
this.trigger_up('unblock_preview_overlays');
- this.trigger_up('activate_snippet', {$snippet: this.$target});
+ this.trigger_up('activate_snippet', {$element: this.$target});
$(document).off('click.bgposition');
return;
@@ -4204,8 +4284,10 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
this.trigger_up('hide_overlay');
this.trigger_up('activate_snippet', {
- $snippet: this.$target,
+ $element: this.$target,
previewMode: true,
+ saveTarget: true,
+ savePreview: true,
});
this.trigger_up('block_preview_overlays');
@@ -4495,7 +4577,6 @@ registry.many2one = SnippetOptionWidget.extend({
this.ID = +$li.data('id');
this.$target.attr('data-oe-many2one-id', this.ID).data('oe-many2one-id', this.ID);
- this.trigger_up('request_history_undo_record', {$target: this.$target});
this.$target.trigger('content_changed');
if (self.$target.data('oe-type') === 'contact') {
@@ -4653,7 +4734,7 @@ registry.DynamicSvg = SnippetOptionWidget.extend({
newURL.searchParams.set('c1', normalizeColor(widgetValue));
const src = newURL.pathname + newURL.search;
await loadImage(src);
- target.src = src;
+ await this.editorHelpers.setAttribute(this.editor, target, 'src', src);
if (!previewMode) {
this.previousSrc = src;
}
@@ -4694,7 +4775,6 @@ registry.DynamicSvg = SnippetOptionWidget.extend({
return {
SnippetOptionWidget: SnippetOptionWidget,
- snippetOptionRegistry: registry,
NULL_ID: NULL_ID,
UserValueWidget: UserValueWidget,
diff --git a/addons/web_editor/static/src/js/frontend/loader.js b/addons/web_editor/static/src/js/frontend/loader.js
index 9c6cf0ef984..6ca492fc084 100644
--- a/addons/web_editor/static/src/js/frontend/loader.js
+++ b/addons/web_editor/static/src/js/frontend/loader.js
@@ -1,28 +1,86 @@
odoo.define('web_editor.loader', function (require) {
'use strict';
-var Wysiwyg = require('web_editor.wysiwyg.root');
+var ajax = require('web.ajax');
-function load(parent, textarea, options) {
- var loading = textarea.nextElementSibling;
- if (loading && !loading.classList.contains('o_wysiwyg_loading')) {
- loading = null;
+let wysiwygPromise;
+
+/**
+ * Load the assets and create a wysiwyg.
+ *
+ * @param {Widget} parent The wysiwyg parent
+ * @param {object} options The wysiwyg options
+ */
+async function createWysiwyg(parent, options, additionnalAssets = []) {
+ if (!wysiwygPromise) {
+ wysiwygPromise = new Promise(async (resolve) => {
+ await ajax.loadLibs({assetLibs: ['web_editor.compiled_assets_wysiwyg', ...additionnalAssets]});
+ // Wait the loading of the service and his dependencies (use string to
+ // avoid parsing of require function).
+ const stringFunction = `return new Promise(resolve => {
+ odoo.define('web_editor.wysiwig.loaded', require => {
+ ` + 'require' + `('web_editor.wysiwyg');
+ resolve();
+ });
+ });`;
+ await new Function(stringFunction)();
+ resolve();
+ });
}
+ await wysiwygPromise;
+ const Wysiwyg = odoo.__DEBUG__.services['web_editor.wysiwyg'];
+ return new Wysiwyg(parent, options);
+}
- if (!textarea.value.match(/\S/)) {
- textarea.value = '
';
+async function loadFromTextarea(parent, textarea, options) {
+ const $textarea = $(textarea);
+ const currentOptions = Object.assign({}, options);
+ if (!currentOptions.value || !currentOptions.value.trim()) {
+ currentOptions.value = '
';
}
+ const wysiwyg = await createWysiwyg(parent, Object.assign({
+ template: `
+
`
+ }, currentOptions));
- var wysiwyg = new Wysiwyg(parent, options);
- return wysiwyg.attachTo(textarea).then(() => {
- if (loading) {
- loading.parentNode.removeChild(loading);
- }
- return wysiwyg;
+ const $wysiwygWrapper = $textarea.closest('.o_wysiwyg_wrapper');
+ const $form = $textarea.closest('form');
+ $wysiwygWrapper.css({
+ 'display': 'flex',
+ 'flex-direction': 'column',
+ 'flex-grow': '1 1 auto',
+ 'height': 200,
});
+
+ // hide and append the $textarea in $form so it's value will be send
+ // through the form.
+ $textarea.hide();
+ $form.append($textarea);
+
+ await wysiwyg.attachTo($wysiwygWrapper);
+ $form.find('.note-editable').data('wysiwyg', wysiwyg);
+
+ $form.on('click', 'button[type=submit]', async (e) => {
+ // float-left class messes up the post layout OPW 769721
+ $form.find('.note-editable').find('img.float-left').removeClass('float-left');
+ $textarea.val(await wysiwyg.getValue());
+ });
+
+ return wysiwyg;
}
return {
- load: load,
+ loadFromTextarea: loadFromTextarea,
+ createWysiwyg: createWysiwyg,
};
});
diff --git a/addons/web_editor/static/src/js/wysiwyg/root.js b/addons/web_editor/static/src/js/wysiwyg/root.js
index dc87ff9e0be..e11ca2f2907 100644
--- a/addons/web_editor/static/src/js/wysiwyg/root.js
+++ b/addons/web_editor/static/src/js/wysiwyg/root.js
@@ -1,88 +1,3 @@
odoo.define('web_editor.wysiwyg.root', function (require) {
-'use strict';
-
-var Widget = require('web.Widget');
-
-var assetsLoaded = false;
-
-var WysiwygRoot = Widget.extend({
- assetLibs: ['web_editor.compiled_assets_wysiwyg'],
- _loadLibsTplRoute: '/web_editor/public_render_template',
-
- publicMethods: ['isDirty', 'save', 'getValue', 'setValue', 'getEditable', 'on', 'trigger', 'focus', 'saveModifiedImages'],
-
- /**
- * @see 'web_editor.wysiwyg' module
- **/
- init: function (parent, params) {
- this._super.apply(this, arguments);
- this._params = params;
- this.$editor = null;
- },
- /**
- * Load assets
- *
- * @override
- **/
- willStart: function () {
- var self = this;
-
- var $target = this.$el;
- this.$el = null;
-
- return this._super().then(function () {
- // FIXME: this code works by pure luck. If the web_editor.wysiwyg
- // JS module was requiring a delayed module, using it here right
- // away would lead to a crash.
- if (!assetsLoaded) {
- var Wysiwyg = odoo.__DEBUG__.services['web_editor.wysiwyg'];
- _.each(['getRange', 'setRange', 'setRangeFromNode'], function (methodName) {
- WysiwygRoot[methodName] = Wysiwyg[methodName].bind(Wysiwyg);
- });
- assetsLoaded = true;
- }
-
- var Wysiwyg = self._getWysiwygContructor();
- var instance = new Wysiwyg(self, self._params);
- self._params = null;
-
- _.each(self.publicMethods, function (methodName) {
- self[methodName] = instance[methodName].bind(instance);
- });
-
- return instance.attachTo($target).then(function () {
- self.$editor = instance.$editor || instance.$el;
- });
- });
- },
-
- _getWysiwygContructor: function () {
- return odoo.__DEBUG__.services['web_editor.wysiwyg'];
- }
-});
-
-return WysiwygRoot;
-
-});
-
-odoo.define('web_editor.wysiwyg.default_options', function (require) {
-'use strict';
-
-/**
- * TODO this should be refactored to be done another way, same as the 'root'
- * module that should be done another way.
- *
- * This allows to have access to default options that are used in the summernote
- * editor so that they can be tweaked (instead of entirely replaced) when using
- * the editor on an editable content.
- */
-
-var core = require('web.core');
-
-var _lt = core._lt;
-
-return {
- styleTags: ['p', 'pre', 'small', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'],
- fontSizes: [_lt('Default'), 8, 9, 10, 11, 12, 14, 18, 24, 36, 48, 62],
-};
+ 'use strict';
});
diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js
index 25e8dfa2835..6cdb38bfeda 100644
--- a/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js
+++ b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js
@@ -6,7 +6,7 @@ const core = require('web.core');
const session = require('web.session');
const {ColorpickerWidget} = require('web.Colorpicker');
const Widget = require('web.Widget');
-const summernoteCustomColors = require('web_editor.rte.summernote_custom_colors');
+const summernoteCustomColors = require('web_editor.custom_colors');
const weUtils = require('web_editor.utils');
const qweb = core.qweb;
diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/link_dialog.js b/addons/web_editor/static/src/js/wysiwyg/widgets/link_dialog.js
index ed17127c55a..0ad347ebfdf 100644
--- a/addons/web_editor/static/src/js/wysiwyg/widgets/link_dialog.js
+++ b/addons/web_editor/static/src/js/wysiwyg/widgets/link_dialog.js
@@ -4,8 +4,6 @@ odoo.define('wysiwyg.widgets.LinkDialog', function (require) {
var core = require('web.core');
var Dialog = require('wysiwyg.widgets.Dialog');
-var dom = $.summernote.core.dom;
-var range = $.summernote.core.range;
var _t = core._t;
@@ -26,20 +24,15 @@ var LinkDialog = Dialog.extend({
/**
* @constructor
+ * @param {} [options.props.text]
+ * @param {} [options.props.className]
*/
- init: function (parent, options, editable, linkInfo) {
- this.options = options || {};
+ init: function (parent, options) {
+ const self = this;
this._super(parent, _.extend({
title: _t("Link to"),
}, this.options));
- this.trigger_up('getRecordInfo', {
- recordInfo: this.options,
- callback: recordInfo => {
- _.defaults(this.options, recordInfo);
- },
- });
-
this.colorsData = [
{type: '', label: _t("Link"), btnPreview: 'link'},
{type: 'primary', label: _t("Primary"), btnPreview: 'primary'},
@@ -50,125 +43,46 @@ var LinkDialog = Dialog.extend({
// all btn-* classes anyway.
];
- this.editable = editable;
- this.data = linkInfo || {};
+ this.trigger_up('getRecordInfo', {
+ recordInfo: this.options,
+ callback: function (recordInfo) {
+ _.defaults(self.options, recordInfo);
+ },
+ });
- this.data.className = "";
- this.data.iniClassName = "";
+ // data is used in the dialog template.
+ this.props = options.props || {};
- var r = this.data.range;
- this.needLabel = !r || (r.sc === r.ec && r.so === r.eo);
-
- if (this.data.range) {
- const $link = $(this.data.range.sc).filter("a");
- this.data.iniClassName = $link.attr("class") || "";
- this.colorCombinationClass = false;
- let $node = $link;
- while ($node.length && !$node.is('body')) {
- const className = $node.attr('class') || '';
- const m = className.match(/\b(o_cc\d+)\b/g);
- if (m) {
- this.colorCombinationClass = m[0];
- break;
- }
- $node = $node.parent();
- }
- this.data.className = this.data.iniClassName.replace(/(^|\s+)btn(-[a-z0-9_-]*)?/gi, ' ');
-
- var is_link = this.data.range.isOnAnchor();
-
- 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);
- $(editable).data("range", this.data.range);
- 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 option, select[name="link_style_shape"] > option'), el => {
- var $option = $(el);
- if ($option.val() && this.data.iniClassName.match(new RegExp('(^|btn-| |btn-outline-)' + $option.val()))) {
+ this.$('input.link-style').prop('checked', false).first().prop('checked', true);
+ if (this.props.initialClassNames) {
+ this.$('input[name="link_style_color"], select[name="link_style_size"] > option, select[name="link_style_shape"] > option').each(function () {
+ var $option = $(this);
+ if ($option.val() && self.props.initialClassNames.match(new RegExp('(^|btn-| |btn-outline-)' + $option.val()))) {
if ($option.is("input")) {
$option.prop("checked", true);
} else {
@@ -179,16 +93,19 @@ var LinkDialog = Dialog.extend({
}
});
}
- if (this.data.url) {
- var match = /mailto:(.+)/.exec(this.data.url);
- this.$('input[name="url"]').val(match ? match[1] : this.data.url);
+ if (this.state.url) {
+ var match = /mailto:(.+)/.exec(this.state.url);
+ this.$('input[name="url"]').val(match ? match[1] : this.state.url);
this._onURLInput();
}
this._updateOptionsUI();
this._adaptPreview();
- this.$('input:visible:first').focus();
+ this.opened(() => {
+ // We need to wait to be sure to trigger focus() the tick after the Event normaliser mess with events
+ setTimeout(() => self.$('input:visible:first').focus(), 0);
+ });
return this._super.apply(this, arguments);
},
@@ -201,27 +118,21 @@ var LinkDialog = Dialog.extend({
* @override
*/
save: function () {
- var data = this._getData();
+ const data = this._getData();
if (data === null) {
var $url = this.$('input[name="url"]');
$url.closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
$url.focus();
return Promise.reject();
}
- this.data.text = data.label;
- this.data.url = data.url;
- var allWhitespace = /\s+/gi;
- var allStartAndEndSpace = /^\s+|\s+$/gi;
- var allBtnTypes = /(^|[ ])(btn-secondary|btn-success|btn-primary|btn-info|btn-warning|btn-danger)([ ]|$)/gi;
- this.data.className = data.classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, '');
- if (data.classes.replace(allBtnTypes, ' ')) {
- this.data.style = {
- 'background-color': '',
- 'color': '',
- };
- }
- this.data.isNewWindow = data.isNewWindow;
- this.final_data = this.data;
+
+ this.final_data = {
+ needLabel: this.state.needLabel,
+ text: data.label,
+ url: data.url,
+ isNewWindow: data.isNewWindow,
+ classes: data.classes,
+ };
return this._super.apply(this, arguments);
},
@@ -244,7 +155,15 @@ var LinkDialog = Dialog.extend({
href: data.url && data.url.length ? data.url : '#',
class: `${data.classes.replace(/float-\w+/, '')} o_btn_preview`,
};
- this.$("#link-preview").attr(attrs).html((data.label && data.label.length) ? data.label : data.url);
+ const $preview = this.$("#link-preview");
+ const $a = $preview.find('a:first');
+ $a.siblings().remove();
+
+ const labels = (data.label && data.label.length) ? data.label.split('\n') : [data.url];
+
+ for (const label of labels) {
+ $preview.append($a.clone().attr(attrs).html(label));
+ }
},
/**
* Get the link's data (url, label and styles).
@@ -255,11 +174,14 @@ var LinkDialog = Dialog.extend({
_getData: function () {
var $url = this.$('input[name="url"]');
var url = $url.val();
- var label = this.$('input[name="label"]').val() || url;
-
- if (label && this.data.images) {
- for (var i = 0; i < this.data.images.length; i++) {
- label = label.replace('<', "<").replace('>', ">").replace(/\[IMG\]/, this.data.images[i].outerHTML);
+ var label = this.state.text;
+ if (this.needLabel) {
+ label = this.$('input[name="label"]').val() || url || '';
+ label = label.replace('<', "<").replace('>', ">");
+ }
+ if (this.state.images) {
+ for (var i = 0; i < this.state.images.length; i++) {
+ label = label.replace(/\[IMG\]/, this.state.images[i]);
}
}
@@ -273,7 +195,7 @@ var LinkDialog = Dialog.extend({
const shapes = shape ? shape.split(',') : [];
const style = ['outline', 'fill'].includes(shapes[0]) ? `${shapes[0]}-` : '';
const shapeClasses = shapes.slice(style ? 1 : 0).join(' ');
- const classes = (this.data.className || '') +
+ const classes = (this.state.className || '') +
(type ? (` btn btn-${style}${type}`) : '') +
(shapeClasses ? (` ${shapeClasses}`) : '') +
(size ? (' btn-' + size) : '');
diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
index 7127a3ae23d..ff223e47bde 100644
--- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
+++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
@@ -448,7 +448,8 @@ var FileWidget = SearchableMediaWidget.extend({
}
href += 'unique=' + img.checksum + '&download=true';
this.$media.attr('href', href);
- this.$media.addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype);
+ this.$media.addClass('o_image').attr('title', img.name);
+ this.$media.append($(`
`))
}
this.$media.attr('alt', img.alt || img.description || '');
@@ -457,6 +458,11 @@ var FileWidget = SearchableMediaWidget.extend({
this.$media.css(style);
}
+ if (this.options.onUpload) {
+ // We consider that when selecting an image it is as if we upload it in the html content.
+ this.options.onUpload(img);
+ }
+
// Remove image modification attributes
removeOnImageChangeAttrs.forEach(attr => {
delete this.media.dataset[attr];
@@ -516,6 +522,142 @@ var FileWidget = SearchableMediaWidget.extend({
this.$urlSuccess.toggleClass('d-none', !isURL);
this.$urlError.toggleClass('d-none', emptyValue || isURL);
},
+ /**
+ * Returns the static url of an image representing the given mimetype.
+ *
+ * @see /web/static/src/scss/mimetypes.scss
+ *
+ * @param {string} mimetype
+ * @param {string} [title]
+ * @param {string} [ext]
+ * @returns {string}
+ */
+ _getFileImageUrl: function(mimetype, title, ext) {
+ title = title || '';
+ ext = ext || '';
+ const prefix = '/web/static/src/img/mimetypes';
+ if (mimetype.startsWith('image')) {
+ return `${prefix}/image.svg`;
+ } else if (mimetype.startsWith('audio')) {
+ return `${prefix}/audio.svg`;
+ } else if (
+ mimetype.startsWith('text') ||
+ mimetype.endsWith('rtf')
+ ) {
+ return `${prefix}/text.svg`;
+ } else if (
+ mimetype.includes('octet-stream') ||
+ mimetype.includes('download') ||
+ mimetype.includes('python')
+ ) {
+ return `${prefix}/binary.svg`;
+ } else if (
+ mimetype.startsWith('video') ||
+ title.endsWith('.mp4') ||
+ title.endsWith('.avi')
+ ) {
+ return `${prefix}/video.svg`;
+ } else if (
+ mimetype.endsWith('archive') ||
+ mimetype.endsWith('compressed') ||
+ mimetype.includes('zip') ||
+ mimetype.endsWith('tar') ||
+ mimetype.includes('package')
+ ) {
+ return `${prefix}/archive.svg`;
+ } else if (mimetype === 'application/pdf') {
+ return `${prefix}/pdf.svg`;
+ } else if (
+ mimetype.startsWith('text-master') ||
+ mimetype.includes('document') ||
+ mimetype.includes('msword') ||
+ mimetype.includes('wordprocessing')
+ ) {
+ return `${prefix}/document.svg`;
+ } else if (
+ mimetype.includes('application/xml') ||
+ mimetype.endsWith('html')
+ ) {
+ return `${prefix}/web_code.svg`;
+ } else if (
+ mimetype.endsWith('css') ||
+ mimetype.endsWith('less') ||
+ ext.endsWith('less')
+ ) {
+ return `${prefix}/web_style.svg`;
+ } else if (
+ mimetype.includes('-image') ||
+ mimetype.includes('diskimage') ||
+ ext.endsWith('dmg')
+ ) {
+ return `${prefix}/disk.svg`;
+ } else if (
+ mimetype.endsWith('csv') ||
+ mimetype.includes('vc') ||
+ mimetype.includes('excel') ||
+ mimetype.endsWith('numbers') ||
+ mimetype.endsWith('calc') ||
+ mimetype.includes('mods') ||
+ mimetype.includes('spreadsheet')
+ ) {
+ return `${prefix}/spreadsheet.svg`;
+ } else if (mimetype.startsWith('key')) {
+ return `${prefix}/certificate.svg`;
+ } else if (
+ mimetype.includes('presentation') ||
+ mimetype.includes('keynote') ||
+ mimetype.includes('teacher') ||
+ mimetype.includes('slideshow') ||
+ mimetype.includes('powerpoint')) {
+ return `${prefix}/presentation.svg`;
+ } else if (
+ mimetype.includes('cert') ||
+ mimetype.includes('rules') ||
+ mimetype.includes('pkcs') ||
+ mimetype.endsWith('stl') ||
+ mimetype.endsWith('crl')
+ ) {
+ return `${prefix}/certificate.svg`;
+ } else if (
+ mimetype.includes('-font') ||
+ mimetype.includes('font-') ||
+ ext.endsWith('ttf')
+ ) {
+ return `${prefix}/font.svg`;
+ } else if (mimetype.includes('-dvi')) {
+ return `${prefix}/print.svg`;
+ } else if (
+ mimetype.includes('script') ||
+ mimetype.includes('x-sh') ||
+ ext.includes('bat') ||
+ mimetype.endsWith('bat') ||
+ mimetype.endsWith('cgi') ||
+ mimetype.endsWith('-c') ||
+ mimetype.includes('java') ||
+ mimetype.includes('ruby')
+ ) {
+ return `${prefix}/script.svg`;
+ } else if (mimetype.includes('javascript')) {
+ return `${prefix}/javascript.svg`;
+ } else if (
+ mimetype.includes('calendar') ||
+ mimetype.endsWith('ldif')
+ ) {
+ return `${prefix}/calendar.svg`;
+ } else if (
+ mimetype.endsWith('postscript') ||
+ mimetype.endsWith('cdr') ||
+ mimetype.endsWith('xara') ||
+ mimetype.endsWith('cgm') ||
+ mimetype.endsWith('graphics') ||
+ mimetype.endsWith('draw') ||
+ mimetype.includes('svg')
+ ) {
+ return `${prefix}/vector.svg`;
+ } else {
+ return `${prefix}/unknown.svg`;
+ }
+ },
//--------------------------------------------------------------------------
// Handlers
diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js
index 0ba27dcf172..d1b9dbb3449 100644
--- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js
+++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js
@@ -1,35 +1,27 @@
odoo.define('web_editor.wysiwyg', function (require) {
'use strict';
-var Widget = require('web.Widget');
-var SummernoteManager = require('web_editor.rte.summernote');
-var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors');
-var id = 0;
-// core.bus
-// media_dialog_demand
+var Dialog = require('web.Dialog');
+var Widget = require('web.Widget');
+var JWEditorLib = require('web_editor.jabberwock');
+var SnippetsMenu = require('web_editor.snippet.editor').SnippetsMenu;
+var weWidgets = require('wysiwyg.widgets');
+var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget;
+var AttributeTranslateDialog = require('web_editor.wysiwyg.translate_attributes');
+var config = require('web.config');
+
+var core = require('web.core');
+var _t = core._t;
+
+const faZoomClassRegex = RegExp('fa-[0-9]x');
+
var Wysiwyg = Widget.extend({
- xmlDependencies: [
- ],
defaultOptions: {
- 'focus': false,
- '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',
- 'colors': summernoteCustomColors,
- recordInfo: {
- context: {},
+ 'recordInfo': {
+ 'context': {},
},
},
+
/**
* @options {Object} options
* @options {Object} options.recordInfo
@@ -49,8 +41,187 @@ var Wysiwyg = Widget.extend({
**/
init: function (parent, options) {
this._super.apply(this, arguments);
- this.id = ++id;
+ this.value = options.value || '';
this.options = options;
+ this.colorPickers = [];
+ this.JWEditorLib = JWEditorLib;
+ if (this.options.enableTranslation) {
+ this._modeConfig = {
+ id: 'translate',
+ rules: [
+ {
+ selector: [],
+ properties: {
+ editable: {
+ value: false,
+ cascading: true,
+ },
+ },
+ },
+ {
+ selector: [this.JWEditorLib.ContainerNode],
+ properties: {
+ breakable: { value: false },
+ },
+ },
+ {
+ selector: [node => !!node.modifiers.find(this.JWEditorLib.OdooTranslationFormat)],
+ properties: {
+ editable: {
+ value: true,
+ cascading: true,
+ },
+ },
+ },
+ {
+ selector: [node => {
+ const attributes = node.modifiers.find(this.JWEditorLib.Attributes);
+ return attributes &&
+ (
+ attributes.classList.has('o_not_editable') ||
+ attributes.has('data-oe-readonly')
+ );
+ }, () => true],
+ properties: {
+ editable: {
+ value: false,
+ cascading: true,
+ },
+ },
+ },
+ ],
+ };
+ } else if (this.options.enableWebsite) {
+ this._modeConfig = {
+ id: 'edit',
+ rules: [
+ {
+ selector: [
+ (node) => {
+ const attributes = node.modifiers.find(this.JWEditorLib.Attributes);
+ const isWrapper = attributes && attributes.classList.has('oe_structure') ;
+ return isWrapper;
+ },
+ ],
+ properties: {
+ breakable: { value: false },
+ allowEmpty: { value: true },
+ },
+ },
+ {
+ selector: [this.JWEditorLib.DividerNode],
+ properties: {
+ breakable: { value: false },
+ allowEmpty: { value: false },
+ },
+ },
+ {
+ selector: [
+ (node) => {
+ const attributes = node.modifiers.find(this.JWEditorLib.Attributes);
+
+ const isCountdown = attributes && attributes.classList.has('s_countdown');
+ const isNewsletterPopup = attributes && attributes.classList.has('o_newsletter_popup');
+ const isPopup = attributes && attributes.classList.has('s_popup');
+
+ return isCountdown || isPopup || isNewsletterPopup;
+ },
+ this.JWEditorLib.ContainerNode],
+ properties: {
+ allowEmpty: { value: true },
+ },
+ },
+ {
+ selector: [this.JWEditorLib.OdooFieldNode],
+ properties: {
+ allowEmpty: { value: true },
+ },
+ },
+
+ // o_header_standard and its descendants.
+ {
+ selector: [
+ (node) => {
+ const attributes = node.modifiers.find(this.JWEditorLib.Attributes);
+ return attributes && attributes.classList.has('o_header_standard');
+ },
+ ],
+ properties: {
+ editable: {
+ value: false,
+ cascading: true,
+ },
+ },
+ },
+ {
+ selector: [
+ (node) => {
+ const attributes = node.modifiers.find(this.JWEditorLib.Attributes);
+ return attributes && attributes.classList.has('o_header_standard');
+ },
+ (node) => {
+ const attributes = node.modifiers.find(this.JWEditorLib.Attributes);
+ const linkFormat = node.modifiers.find(this.JWEditorLib.LinkFormat);
+ const linkAttributes = linkFormat && linkFormat.modifiers.find(this.JWEditorLib.Attributes);
+
+ const hasNavLink = linkAttributes && (linkAttributes.classList.has('nav-link'));
+ const hasContainer = attributes && attributes.classList.has('container');
+
+ return hasNavLink || hasContainer;
+ },
+ ],
+ properties: {
+ editable: {
+ value: true,
+ cascading: true,
+ },
+ },
+ },
+
+ // blockquote and alert snippets and their descendants:
+ {
+ selector: [node => {
+ const attributes = node.modifiers.find(this.JWEditorLib.Attributes);
+ return attributes && (
+ attributes.classList.has('s_blockquote') ||
+ attributes.classList.has('s_alert')
+ );
+ }],
+ properties: {
+ editable: {
+ value: false,
+ cascading: true,
+ },
+ },
+ },
+ {
+ selector: [node => {
+ // Blockquote and alert snippets's children are not
+ // editable...
+ const attributes = node.modifiers.find(this.JWEditorLib.Attributes);
+ return attributes && (
+ attributes.classList.has('s_blockquote') ||
+ attributes.classList.has('s_alert')
+ );
+ }, node => {
+ // ...except for their _content child and its
+ // descendants.
+ const attributes = node.modifiers.find(this.JWEditorLib.Attributes);
+ return attributes && (
+ attributes.classList.has('s_blockquote_content') ||
+ attributes.classList.has('s_alert_content')
+ );
+ }],
+ properties: {
+ editable: {
+ value: true,
+ cascading: true,
+ },
+ },
+ },
+ ],
+ };
+ }
},
/**
* Load assets and color picker template then call summernote API
@@ -58,8 +229,7 @@ var Wysiwyg = Widget.extend({
*
* @override
**/
- willStart: function () {
- this._summernoteManager = new SummernoteManager(this);
+ willStart: async function () {
this.$target = this.$el;
return this._super();
},
@@ -67,32 +237,271 @@ var Wysiwyg = Widget.extend({
*
* @override
*/
- start: function () {
- this.$target.wrap('');
- this.$el = this.$target.parent();
- var options = this._editorOptions();
- this.$target.summernote(options);
- this.$editor = this.$('.note-editable:first');
- this.$editor.data('wysiwyg', this);
- this.$editor.data('oe-model', options.recordInfo.res_model);
- this.$editor.data('oe-id', options.recordInfo.res_id);
- $(document).on('mousedown', this._blur);
- this._value = this.$target.html() || this.$target.val();
- return this._super.apply(this, arguments);
+ start: async function () {
+ const self = this;
+ const _super = this._super;
+
+ if (this.options.enableWebsite) {
+ $(document.body).addClass('o_connected_user editor_enable');
+ }
+
+ const $mainSidebar = $('');
+ const $snippetManipulators = $('
');
+
+ const customCommands = {
+ openTextColorPicker: { handler: this.toggleTextColorPicker.bind(this) },
+ openBackgroundColorPicker: { handler: this.toggleBackgroundColorPicker.bind(this) },
+ discardOdoo: { handler: this.discardEditions.bind(this) },
+ saveOdoo: { handler: this.saveContent.bind(this) },
+ };
+ if (!this.options.enableTranslation) {
+ Object.assign(customCommands, {
+ openMedia: { handler: this.openMediaDialog.bind(this) },
+ openLinkDialog: { handler: this.openLinkDialog.bind(this) },
+ cropImage: { handler: this.cropImage.bind(this) },
+ transformImage: { handler: this.transformImage.bind(this) },
+ describeImage: { handler: this.describeImage.bind(this) },
+ });
+ }
+ this.editor = new JWEditorLib.OdooWebsiteEditor(Object.assign({}, this.options, {
+ snippetMenuElement: $mainSidebar[0],
+ snippetManipulators: $snippetManipulators[0],
+ customCommands: Object.assign(customCommands, this.options.customCommands),
+ plugins: this.options.enableWebsite && !this.options.enableTranslation ? [[this.JWEditorLib.OdooField]] : [],
+ source: this.value,
+ location: this.options.location || [this.el, 'replace'],
+ mode: this._modeConfig,
+ }));
+
+ if (typeof odoo.debug === 'string' && config.isDebug('assets') && JWEditorLib.DevTools) {
+ this.editor.load(JWEditorLib.DevTools);
+ }
+ await this.editor.start();
+ this._bindAfterStart();
+
+
+ this.editorHelpers = this.editor.plugins.get(JWEditorLib.DomHelpers);
+ const domLayout = this.editor.plugins.get(JWEditorLib.Layout).engines.dom;
+ this.zoneMain = domLayout.root.firstDescendant(node => node.managedZones && node.managedZones.includes('main'));
+ const rootElement = this.editorHelpers.getDomNodes(domLayout.root.firstDescendant(JWEditorLib.ContainerNode))[0];
+ this.editorEditable = this.editorHelpers.getDomNodes(this.zoneMain)[0] || this.editorHelpers.getDomNodes(this.zoneMain.parent)[0];
+
+ this.$toolbar = $(rootElement).find('jw-toolbar').detach();
+
+ if (this.options.enableWebsite) {
+ const $wrapwrap = $('#wrapwrap');
+ $wrapwrap.removeClass('o_editable'); // clean the dom before edition
+ this._getEditable($wrapwrap).addClass('o_editable');
+ $wrapwrap.data('wysiwyg', this);
+
+ // add class when page content is empty to show the "DRAG BUILDING BLOCKS HERE" block
+ const $targetNode = $(this.editorEditable).find(".oe_structure");
+ if ($targetNode.length) {
+ $targetNode.attr('data-editor-message', _t('DRAG BUILDING BLOCKS HERE'));
+ }
+ }
+
+ if (this.options.enableTranslation) {
+ this._setupTranslation();
+ }
+
+ // Make sure to warn the user if they're about to leave the page and
+ // they have changes that would be lost if they did.
+ let flag = false;
+ window.onbeforeunload = () => {
+ if (!flag) {
+ flag = true;
+ _.defer(() => (flag = false));
+ return _t('This document is not saved!');
+ }
+ };
+
+ if (this.options.snippets) {
+ document.body.classList.add('editor_has_snippets');
+ this.$webEditorToolbar = $('
');
+
+ var $toolbarHandler = $('#web_editor-top-edit');
+ $toolbarHandler.append(this.$webEditorToolbar);
+
+ this.snippetsMenu = new SnippetsMenu(this, Object.assign({
+ $el: $(this.editorEditable),
+ snippets: this.options.snippets,
+ selectorEditableArea: '.o_editable',
+ $snippetEditorArea: $snippetManipulators,
+ wysiwyg: this,
+ JWEditorLib: JWEditorLib,
+ onlyStyleTab: this.options.enableTranslation,
+ }, this.options));
+ await this.snippetsMenu.appendTo($mainSidebar);
+
+ // Place the history buttons in their right location.
+ const $undoButton = $('
');
+ const $redoButton = $('
');
+ $undoButton.on('click', () => this.editor.execCommand('undo'));
+ $redoButton.on('click', () => this.editor.execCommand('redo'));
+ const reactiveEditorInfo = this.editor.plugins.get(JWEditorLib.ReactiveEditorInfo);
+ const updateButtons = () => {
+ const state = reactiveEditorInfo.editorInfo.get();
+ state.canUndo ? $undoButton.removeAttr('disabled') : $undoButton.attr('disabled', true);
+ state.canRedo ? $redoButton.removeAttr('disabled') : $redoButton.attr('disabled', true);
+ };
+ reactiveEditorInfo.editorInfo.on('set', updateButtons);
+ updateButtons();
+
+ $('.o_we_website_top_actions .o_we_external_history_buttons').append($undoButton, $redoButton);
+
+ this.$el.on('content_changed', function (e) {
+ self.trigger_up('wysiwyg_change');
+ });
+
+ const onCommitCheckSnippets = (params) => {
+ const range = this.editor.selection.range;
+ let currentSelectionType = "text";
+ if(!range.isCollapsed()) {
+ const nextStartSibling = range.start.nextSibling();
+ const prevEndSibling = range.end.previousSibling();
+ if (nextStartSibling && prevEndSibling && nextStartSibling.id === prevEndSibling.id) {
+ switch (nextStartSibling.name) {
+ case "ImageNode":
+ currentSelectionType = "image";
+ break;
+ case "OdooVideoNode":
+ currentSelectionType = "video";
+ break;
+ case "FontAwesomeNode":
+ currentSelectionType = "picto";
+ break;
+ default:
+ currentSelectionType = "text";
+ break;
+ }
+
+ }
+ }
+ this.snippetsMenu.updateJabberwockToolbarContainer(currentSelectionType);//.bind(this.snippetsMenu);
+
+ if (params.commandNames.includes('undo') || params.commandNames.includes('redo')) {
+ setTimeout(() => {
+ // use setTimeout to reload snippets after the redraw
+ this.snippetsMenu.trigger('reload_snippet_dropzones');
+ });
+ }
+ };
+ const onSelectionUpdateColorPreview = (param) => {
+ let color, bgColor = undefined;
+
+ const findColorsIn = (object) => {
+ const attributes = object.modifiers.find(JWEditorLib.Attributes);
+ const curentTextStyle = attributes ? attributes.style : false;
+ if (curentTextStyle && !color) {
+ color = curentTextStyle.get('color') ;
+ }
+ if (curentTextStyle && !bgColor) {
+ bgColor = curentTextStyle.get('background-color');
+ }
+ }
+
+ const rangeStart = param.context.range.start
+ const node = rangeStart.nextSibling(node => !(node instanceof JWEditorLib.SeparatorNode) || rangeStart.nextSibling())
+ if(node) {
+ findColorsIn(node);
+ if (!color || !bgColor) {
+ const formats = node.modifiers.filter(JWEditorLib.Format);
+ for (const format of formats) {
+ findColorsIn(format)
+ }
+ }
+ }
+
+ if(!color) color = "#000";
+ if(!bgColor) bgColor = "rgba(255,255,255,0)";
+
+ this.$toolbar.find(".jw-dropdown-textcolor>jw-button").css("background-color", color);
+ this.$toolbar.find(".jw-dropdown-backgroundcolor>jw-button").css("background-color", bgColor);
+ }
+ this.editor.dispatcher.registerCommandHook('setSelection', onSelectionUpdateColorPreview);
+ this.editor.dispatcher.registerCommandHook('@commit', onCommitCheckSnippets);
+
+ } else {
+ return _super.apply(this, arguments);
+ }
},
/**
* @override
*/
destroy: function () {
- $(document).off('mousedown', this._blur);
- if (this.$target && this.$target.is('textarea') && this.$target.next('.note-editor').length) {
- this.$target.summernote('destroy');
- }
+ this.editor.stop();
this._super();
},
+
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
+
+ cropImage: async function (params) {
+ const imageNodes = params.context.range.targetedNodes(JWEditorLib.ImageNode);
+ const imageNode = imageNodes.length === 1 && imageNodes[0];
+ if (imageNode) {
+ const domEngine = this.editor.plugins.get(JWEditorLib.Layout).engines.dom;
+ const $node = $(domEngine.getDomNodes(imageNode)[0]);
+ $node.off('image_cropped');
+ $node.on('image_cropped', () => this._updateAttributes($node[0]));
+ new weWidgets.ImageCropWidget(this, $node[0]).appendTo($('#wrap'));
+ }
+ },
+ describeImage: async function (params) {
+ const imageNodes = params.context.range.targetedNodes(JWEditorLib.ImageNode);
+ const imageNode = imageNodes.length === 1 && imageNodes[0];
+ if (imageNode) {
+ const domEngine = this.editor.plugins.get(JWEditorLib.Layout).engines.dom;
+ const node = domEngine.getDomNodes(imageNode)[0];
+ var altDialog = new weWidgets.AltDialog(this, {}, node);
+ altDialog.on('save', this, () => this._updateAttributes(node));
+ altDialog.open();
+ }
+ },
+ discardEditions: async function () {
+ let self = this;
+ return new Promise(function (resolve, reject) {
+ // Only show an alert if there is a risk that changes would be lost.
+ if (self.isDirty()) {
+ const confirmMessage = `If you discard the current edits, all
+ unsaved changes will be lost. You can cancel to return to
+ edit mode.`.replace(/[\n\s]+/g, ' ');
+ const confirm = Dialog.confirm(this, _t(confirmMessage), {
+ confirm_callback: resolve,
+ });
+ confirm.on('closed', self, reject);
+ } else {
+ resolve();
+ }
+ }).then(function () {
+ window.onbeforeunload = null;
+ window.location.reload();
+ });
+ },
+ /**
+ * Set the focus on the element.
+ */
+ focus: function () {
+ // todo: handle tab that need to go to next field if the editor does not
+ // catch it.
+ this.$el.find('[contenteditable="true"]').focus();
+ },
+ /**
+ * Return dirty Odoo structure nodes and Odoo field nodes.
+ *
+ * @returns {Array
}
+ */
+ getDirtyNodes: function () {
+ return this.zoneMain.descendants(node => {
+ if (node instanceof JWEditorLib.OdooStructureNode) {
+ return node.dirty;
+ } else if (node instanceof JWEditorLib.OdooFieldNode) {
+ return node.fieldInfo.originalValue !== node.fieldInfo.value.get();
+ }
+ });
+ },
/**
* Return the editable area.
*
@@ -101,19 +510,8 @@ var Wysiwyg = Widget.extend({
getEditable: function () {
return this.$editor;
},
- /**
- * Return true if the content has changed.
- *
- * @returns {Boolean}
- */
- isDirty: function () {
- return this._value !== (this.$editor.html() || this.$editor.val());
- },
- /**
- * Set the focus on the element.
- */
- focus: function () {
- console.log('focus');
+ getFormatInfo: function() {
+ return this.editor.plugins.get(JWEditorLib.Odoo).formatInfo;
},
/**
* Get the value of the editable element.
@@ -122,44 +520,220 @@ var Wysiwyg = Widget.extend({
* @param {jQueryElement} [options.$layout]
* @returns {String}
*/
- getValue: function (options) {
- var $editable = options && options.$layout || this.$editor.clone();
- $editable.find('[contenteditable]').removeAttr('contenteditable');
- $editable.find('[class=""]').removeAttr('class');
- $editable.find('[style=""]').removeAttr('style');
- $editable.find('[title=""]').removeAttr('title');
- $editable.find('[alt=""]').removeAttr('alt');
- $editable.find('[data-original-title=""]').removeAttr('data-original-title');
- $editable.find('a.o_image, span.fa, i.fa').html('');
- $editable.find('[aria-describedby]').removeAttr('aria-describedby').removeAttr('data-original-title');
- return $editable.html();
+ getValue: async function (format) {
+ return this.editor.getValue(format || 'text/html');
},
- /**
- * Save the content in the target
- * - in init option beforeSave
- * - receive editable jQuery DOM as attribute
- * - called after deactivate codeview if needed
- * @returns {Promise}
- * - resolve with true if the content was dirty
- */
- save: function () {
- var isDirty = this.isDirty();
- var html = this.getValue();
- if (this.$target.is('textarea')) {
- this.$target.val(html);
- } else {
- this.$target.html(html);
+ async initColorPicker($dropdownNode, setCommandId, unsetCommandId) {
+ if (!$dropdownNode.hasClass("colorpicker-initalized")) {
+ $dropdownNode.addClass("colorpicker-initalized");
+ // Init the colorPalete for this color picker Dropdown.
+ const colorpicker = new ColorPaletteWidget(this, {});
+
+ // Prevent the dropdown to be closed when click inside it
+ $dropdownNode.on('click', (e)=> {
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ })
+ $dropdownNode.find('.dropdown-menu').empty();
+ await colorpicker.appendTo($dropdownNode.find('.dropdown-menu'));
+ // Events listeners to trigger color changes
+ colorpicker.on('custom_color_picked', this, (e) => {
+ this._setColor(colorpicker, setCommandId, unsetCommandId, e.data.color, $dropdownNode);
+ });
+ colorpicker.on('color_picked', this, (e) => {
+ this._setColor(colorpicker, setCommandId, unsetCommandId, e.data.color, $dropdownNode, true);
+ });
}
- return Promise.resolve({isDirty:isDirty, html:html});
},
/**
- * Create/Update cropped attachments.
+ * Return true if the content has changed.
*
- * @param {jQuery} $editable
- * @returns {Promise}
+ * @returns {boolean}
*/
- saveModifiedImages: function ($editable) {
- return this._summernoteManager.saveModifiedImages($editable);
+ isDirty: function () {
+ return !!this.getDirtyNodes().length;
+ },
+ async openLinkDialog(params) {
+ const range = this.editor.selection.range;
+ const Link = JWEditorLib.Link;
+
+ const previousNode = range.start.previousSibling();
+ const nextNode = range.start.nextSibling();
+ const node = previousNode && (Link.isLink(previousNode) || !nextNode) ? previousNode : nextNode;
+ const currentLink = node && Link.isLink(node) && node.modifiers.find(JWEditorLib.LinkFormat);
+
+ let text = '';
+ const images = [];
+ const domSelection = this.editorEditable.ownerDocument.getSelection();
+ if (domSelection.rangeCount) {
+ let domRange = domSelection.getRangeAt(0);
+ const ancestor = domRange.commonAncestorContainer.nodeType === Node.ELEMENT_NODE ?
+ domRange.commonAncestorContainer : domRange.commonAncestorContainer.parentNode;
+
+ let container;
+ if (domSelection.isCollapsed && currentLink) {
+ // If colapse in a link, select all the link.
+ container = ancestor.closest('a');
+ const nodes = this.editorHelpers.getNodes(container);
+ await params.context.execCommand('setSelection', {
+ vSelection: {
+ anchorNode: nodes[0],
+ anchorPosition: 'BEFORE',
+ focusNode: nodes[nodes.length - 1],
+ focusPosition: 'AFTER',
+ direction: 'FORWARD',
+ },
+ });
+ } else {
+ // If a selection exists, create a text value with \n to split each block (for the preview)
+ domRange = domSelection.getRangeAt(0);
+ container = document.createElement('w-clone');
+ container.appendChild(domRange.cloneContents());
+ ancestor.appendChild(container);
+ }
+
+ const blocks = [''];
+ let domNode = container && container.firstChild;
+ if (domNode) {
+ while (domNode.childNodes && domNode.childNodes.length) {
+ domNode = domNode.childNodes[0];
+ }
+ while (domNode && domNode !== container) {
+ if (domNode.nodeName === 'IMG' || domNode.classList && domNode.classList.contains('fa')) {
+ images.push(domNode.outerHTML);
+ blocks[blocks.length - 1] += '[IMG]';
+ } else if (domNode.classList && domNode.classList.contains('media_iframe_video')) {
+ blocks.push('');
+ } else if (domNode.nodeType === Node.ELEMENT_NODE && window.getComputedStyle(domNode).display === 'block') {
+ blocks.push('');
+ } else if (domNode.nodeType === Node.TEXT_NODE) {
+ blocks[blocks.length - 1] += domNode.textContent;
+ }
+
+ if (domNode.childNodes && domNode.childNodes.length) {
+ domNode = domNode.childNodes[0];
+ } else if (domNode.nextSibling) {
+ domNode = domNode.nextSibling;
+ } else {
+ while (domNode && !domNode.nextSibling && domNode !== container) {
+ domNode = domNode.parentNode;
+ }
+ domNode = domNode && domNode !== container && domNode.nextSibling;
+ }
+ }
+ text = blocks.filter(str => str.length).join('\n');
+ }
+
+ if (!domSelection.isCollapsed || !currentLink) {
+ container.remove();
+ }
+ }
+
+ const modifiers = range.modifiers;
+ const linkFormat = modifiers && modifiers.find(JWEditorLib.LinkFormat);
+ let classes = '';
+ let target;
+ if (currentLink) {
+ const linkAttributes = currentLink.modifiers.find(JWEditorLib.Attributes);
+ classes = (linkAttributes && linkAttributes.get('class')) || '';
+ target = linkAttributes && linkAttributes.get('target');
+ }
+
+ const coloredLevelParent = node ? node.ancestor(cNode => {
+ const attributes = cNode.modifiers.find(JWEditorLib.Attributes);
+ return attributes && attributes.get('class') && attributes.get('class').includes('o_cc')
+ }) : false;
+
+ let colorCombinationClass = "";
+ if (coloredLevelParent) {
+ let allClasses = coloredLevelParent.modifiers.find(JWEditorLib.Attributes).get('class').split(" ");
+ colorCombinationClass = allClasses.find(classStr => classStr.match(/o_cc[0-9]+/));
+ }
+
+ const linkDialog = new weWidgets.LinkDialog(this,
+ {
+ props: {
+ text: text,
+ images: images,
+ url: linkFormat && linkFormat.url || '',
+ initialClassNames: classes,
+ colorCombinationClass: colorCombinationClass,
+ target: target,
+ },
+ },
+ );
+ linkDialog.open();
+ linkDialog.on('save', this, async (params)=> {
+ const onSaveLinkDialog = async (context) => {
+ const linkParams = {
+ url: params.url,
+ label: range.isCollapsed() && params.text,
+ target: params.isNewWindow ? '_blank' : '',
+ };
+ const nodes = range.targetedNodes(JWEditorLib.InlineNode);
+ await context.execCommand('link', linkParams);
+ const links = nodes.map(node => node.modifiers.find(JWEditorLib.LinkFormat)).filter(f => f);
+ for (const link of links) {
+ link.modifiers.get(JWEditorLib.Attributes).set('class', params.classes);
+ }
+ };
+ await this.editor.execCommand(onSaveLinkDialog);
+ });
+ },
+ openMediaDialog(params) {
+ const nodes = params.context.range.selectedNodes();
+ const node = nodes[0];
+ let $baseNode;
+
+ if (nodes.length === 1){
+ if (node instanceof JWEditorLib.FontAwesomeNode || node instanceof JWEditorLib.OdooVideoNode) {
+ const $originalDomNode = this.editorHelpers.getDomNodes(node).filter(dom => dom.nodeType === Node.ELEMENT_NODE);
+ $baseNode = $($originalDomNode).clone();
+ }
+ if (node instanceof JWEditorLib.FontAwesomeNode) {
+ params.htmlClass = [...$baseNode[0].classList].filter((className) => {
+ return !className.startsWith('fa') || faZoomClassRegex.test(className);
+ }).join(' ');
+ }
+ }
+
+ // avoid circular reference
+ delete params.context;
+
+ let mediaDialog = new weWidgets.MediaDialog(this, params, $baseNode);
+ mediaDialog.open();
+ mediaDialog.on('save', this, async (element) => {
+ if (params.htmlClass) element.className += " " + params.htmlClass;
+ return this.editor.execCommand('insertMedia', { element: element });
+ });
+ },
+ async saveContent(context = this.editor) {
+ if (this.options.enableTranslation) {
+ await this._onSaveTranslation(context);
+ } else {
+ await this.saveToServer(context);
+ }
+ },
+ saveToServer: async function (context = this.editor, reload = true) {
+ const defs = [];
+ this.trigger_up('edition_will_stopped');
+ this.trigger_up('ready_to_save', {defs: defs});
+ await Promise.all(defs);
+
+ if (this.snippetsMenu) {
+ await this.snippetsMenu.cleanForSave();
+ }
+
+ return this._saveWebsiteContent(context)
+ .then(() => {
+ this.trigger_up('edition_was_stopped');
+ if (reload) {
+ window.onbeforeunload = null;
+ window.location.reload();
+ }
+ }).catch(error => {
+ console.error('Impossible to save.', error);
+ });
},
/**
* @param {String} value
@@ -168,39 +742,631 @@ var Wysiwyg = Widget.extend({
* @returns {String}
*/
setValue: function (value, options) {
- if (this.$editor.is('textarea')) {
- this.$target.val(value);
- } else {
- this.$target.html(value);
- }
- this.$editor.html(value);
+ this._value = value;
},
+ async toggleBackgroundColorPicker() {
+ // event.target equal the current toggle button clicked.
+ // window.event can be undefined during Qunit test
+ const $backgroundColorDropdown = event ? $(event.target).parent() : $(".jw-dropdown-backgroundcolor");
+ await this.initColorPicker(
+ $backgroundColorDropdown,
+ "colorBackground",
+ "uncolorBackground");
+
+ $backgroundColorDropdown.find(".dropdown-toggle").dropdown("toggle");
+ },
+ async toggleTextColorPicker() {
+ // event.target equal the current toggle button clicked.
+ // window.event can be undefined during Qunit test
+ const $textColorDropdown = event ? $(event.target).parent() : $(".jw-dropdown-textcolor");
+ await this.initColorPicker(
+ $textColorDropdown,
+ "colorText",
+ "uncolorText");
+
+ $textColorDropdown.find(".dropdown-toggle").dropdown("toggle");
+ },
+ transformImage: async function (params) {
+ const imageNodes = params.context.range.targetedNodes(JWEditorLib.ImageNode);
+ const imageNode = imageNodes.length === 1 && imageNodes[0];
+ if (imageNode) {
+ const domEngine = this.editor.plugins.get(JWEditorLib.Layout).engines.dom;
+ const $node = $(domEngine.getDomNodes(imageNode)[0]);
+ this._transform($node);
+ }
+ },
+ async updateChanges($target) {
+ const updateChanges = async (context) => {
+ const html = $target.html();
+ $target.html('');
+ const attributes = [...$target[0].attributes].reduce( (acc, attribute) => {
+ acc[attribute.name] = attribute.value;
+ return acc
+ }, {})
+ await this.editorHelpers.updateAttributes(context, $target[0], attributes);
+ await this.editorHelpers.empty(context, $target[0]);
+ await this.editorHelpers.insertHtml(context, html, $target[0], 'INSIDE');
+ };
+ await this.editor.execCommand(updateChanges);
+ },
+ withDomMutationsObserver ($target, callback) {
+ callback();
+ this.updateChanges($target);
+ },
+
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
- _editorOptions: function () {
- var self = this;
- var options = Object.assign({}, $.summernote.options, this.defaultOptions, this.options);
- if (this.options.generateOptions) {
- options = this.options.generateOptions(options);
+
+ /**
+ * Additional binding after start.
+ *
+ * Meant to be overridden
+ */
+ _bindAfterStart() {},
+ /**
+ * Returns the editable areas on the page.
+ *
+ * @param {JQuery} $element
+ * @returns {JQuery}
+ */
+ _getEditable($element) {
+ const $editable = $element.find('[data-oe-model]')
+ .not('.o_not_editable')
+ .filter(function () {
+ var $parent = $(this).closest('.o_editable, .o_not_editable');
+ return !$parent.length || $parent.hasClass('o_editable');
+ })
+ .not('link, script')
+ .not('img[data-oe-field="arch"], br[data-oe-field="arch"], input[data-oe-field="arch"]')
+ .not('.oe_snippet_editor')
+ .not('hr, br, input, textarea')
+ .add('.o_editable');
+ if (this.options.enableTranslation) {
+ const selector = '[data-oe-translation-id], '+
+ '[data-oe-model][data-oe-id][data-oe-field], ' +
+ '[placeholder*="data-oe-translation-id="], ' +
+ '[title*="data-oe-translation-id="], ' +
+ '[alt*="data-oe-translation-id="]';
+ $editable.filter(':has(' + selector + ')').attr('data-oe-readonly', true);
}
- options.airPopover = options.toolbar;
- options.onChange = function (html, $editable) {
- $editable.trigger('content_changed');
- self.trigger_up('wysiwyg_change');
+ return $editable.not('[data-oe-readonly]')
+ },
+ /**
+ * Gets jQuery cloned element with internal text nodes escaped for XML
+ * storage.
+ *
+ * @private
+ * @param {jQuery} $el
+ * @return {jQuery}
+ */
+ _getEscapedElement: function ($el) {
+ var escaped_el = $el.clone();
+ var to_escape = escaped_el.find('*').addBack();
+ to_escape = to_escape.not(to_escape.filter('object,iframe,script,style,[data-oe-model][data-oe-model!="ir.ui.view"]').find('*').addBack());
+ to_escape.contents().each(function () {
+ if (this.nodeType === 3) {
+ this.nodeValue = $('').text(this.nodeValue).html();
+ }
+ });
+ return escaped_el;
+ },
+ /**
+ * Returns a translation object.
+ *
+ * @private
+ * @param {Node} node
+ * @returns {Object}
+ */
+ _getTranslationObject: 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 translation = _.find(this.translations, function (translation) {
+ return translation.id === id;
+ });
+ if (!translation) {
+ this.translations.push(translation = {'id': id});
+ }
+ return translation;
+ },
+ /**
+ * @private
+ */
+ _markTranslatableNodes: function () {
+ const self = this;
+ const $editable = $(this.editorEditable);
+ $editable.prependEvent('click.translator', function (ev) {
+ if (ev.ctrlKey || !$(ev.target).is(':o_editable')) {
+ return;
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ });
+
+ // attributes
+
+ this.$nodesToTranslateAttributes.each(function () {
+ var $node = $(this);
+ var translation = $node.data('translation');
+ _.each(translation, function (node) {
+ if (node) {
+ var translation = self._getTranslationObject(node);
+ translation.value = (translation.value ? translation.value : $node.html()).replace(/[ \t\n\r]+/, ' ');
+ $node.attr('data-oe-translation-state', (translation.state || 'to_translate'));
+ }
+ });
+ });
+
+ this.$nodesToTranslateAttributes.prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) {
+ if (ev.ctrlKey) {
+ return;
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ if (ev.type !== 'mousedown') {
+ return;
+ }
+
+ new AttributeTranslateDialog(self, {
+ editor: self.editor,
+ editorHelpers: self.editorHelpers,
+ }, ev.target).open();
+ });
+ },
+ /**
+ * Save all translation blocks.
+ *
+ * @private
+ */
+ _onSaveTranslation: async function (context) {
+ const defs = [];
+ this.trigger_up('edition_will_stopped');
+ this.trigger_up('ready_to_save', {defs: defs});
+ await Promise.all(defs);
+
+ const promises = [];
+ const translationContainers = {};
+ const getTranslationNodes = () => {
+ let previousTranslationId;
+ this.zoneMain.descendants(descendant => {
+ const format = descendant.modifiers.find(JWEditorLib.OdooTranslationFormat);
+ const formatAttributes = format && format.modifiers.find(JWEditorLib.Attributes);
+ const translationId = format && (format.translationId || +formatAttributes.get('data-oe-id'));
+ if (this.editor.mode.is(descendant, 'editable')) {
+ translationContainers[translationId] = translationContainers[translationId] || [];
+ const containers = translationContainers[translationId];
+ if (previousTranslationId !== translationId) {
+ containers.push(new JWEditorLib.ContainerNode());
+ }
+ const lastContainer = containers[containers.length-1];
+ lastContainer.append(descendant.clone());
+ }
+ previousTranslationId = translationId;
+ });
+ }
+ await context.execCommand(getTranslationNodes);
+
+ // Save the odoo translation formats.
+ for (const id of Object.keys(translationContainers)) {
+ const containers = translationContainers[id];
+ // todo: check that all container render the same way otherwise
+ // inform the user that there is conflict between the same
+ // traduction.
+ const lastContainer = containers[containers.length-1];
+ const translationNode = lastContainer.children()[0];
+ const renderer = this.editor.plugins.get(JWEditorLib.Renderer);
+
+ const renderedNode = (await renderer.render('dom/html', lastContainer))[0].firstChild;
+ const translationFormat = translationNode.modifiers.find(JWEditorLib.OdooTranslationFormat);
+
+ let $renderedTranslation = $(renderedNode);
+ if (!$renderedTranslation.data('oe-translation-state')) {
+ $renderedTranslation = $renderedTranslation.find('[data-oe-translation-state]');
+ }
+
+ if (translationFormat.translationId) {
+ promises.push(this._saveTranslationTo($renderedTranslation, +translationFormat.translationId));
+ } else {
+ const attributes = translationFormat.modifiers.find(JWEditorLib.Attributes);
+ promises.push(this._saveViewTo(
+ $renderedTranslation,
+ attributes.get('data-oe-id'),
+ attributes.get('data-oe-xpath')
+ ));
+ }
+ }
+
+ // Save attributes
+ for (const attribute_translation of this.$attribute_translations) {
+ const $attribute_translations = $(attribute_translation);
+ promises.push(this._saveTranslationTo(
+ $attribute_translations,
+ +$attribute_translations.data('oe-translation-id')
+ ));
+ }
+
+ await Promise.all(promises);
+ this.trigger_up('edition_was_stopped');
+ window.location.reload();
+ },
+ /**
+ * Save all "cover properties" blocks.
+ *
+ * @private
+ */
+ _saveCoverPropertiesBlocks: async function (context) {
+ let rpcResult;
+ const wysiwygSaveCoverPropertiesBlocks = async () => {
+ const covers = this.zoneMain.descendants(node => {
+ const attributes = node.modifiers.find(JWEditorLib.Attributes);
+
+ if (attributes && attributes.length && typeof attributes.get('class') === 'string') {
+ return attributes.classList.has('o_record_cover_container');
+ }
+ });
+ const el = covers && covers[0] && this.editorHelpers.getDomNodes(covers[0])[0];
+ if (!el) {
+ console.warn('No cover found.');
+ return;
+ }
+
+ var resModel = el.dataset.resModel;
+ var resID = parseInt(el.dataset.resId);
+ if (!resModel || !resID) {
+ throw new Error('There should be a model and id associated to the cover.');
+ }
+
+ this.__savedCovers = this.__savedCovers || {};
+ this.__savedCovers[resModel] = this.__savedCovers[resModel] || [];
+
+ if (this.__savedCovers[resModel].includes(resID)) {
+ return;
+ }
+ this.__savedCovers[resModel].push(resID);
+
+ var cssBgImage = $(el.querySelector('.o_record_cover_image')).css('background-image');
+ var coverProps = {
+ 'background-image': cssBgImage.replace(/"/g, '').replace(window.location.protocol + "//" + window.location.host, ''),
+ 'background_color_class': el.dataset.bgColorClass,
+ 'background_color_style': el.dataset.bgColorStyle,
+ 'opacity': el.dataset.filterValue,
+ 'resize_class': el.dataset.coverClass,
+ 'text_align_class': el.dataset.textAlignClass,
+ };
+
+ rpcResult = this._rpc({
+ model: resModel,
+ method: 'write',
+ args: [
+ resID,
+ {'cover_properties': JSON.stringify(coverProps)}
+ ],
+ });
};
- options.onUpload = function (attachments) {
- self.trigger_up('wysiwyg_attachment', attachments);
+ await context.execCommand(wysiwygSaveCoverPropertiesBlocks);
+ return rpcResult;
+ },
+ /**
+ * Save all "mega menu" classes.
+ *
+ * @private
+ */
+ _saveMegaMenuClasses: async function () {
+ const structureNodes = this.zoneMain.descendants((node) => {
+ return node.modifiers.get(JWEditorLib.Attributes).get('data-oe-field') === 'mega_menu_content';
+ });
+ const promises = [];
+ for (const node of structureNodes) {
+ // On top of saving the mega menu content like any other field
+ // content, we must save the custom classes that were set on the
+ // menu itself.
+ // FIXME: normally removing the 'show' class should not be necessary here
+ // TODO: check that editor classes are removed here as well
+ let promises = [];
+ const items = node.modifiers.get(JWEditorLib.Attributes).classList.items();
+ var classes = _.without(items, 'dropdown-menu', 'o_mega_menu', 'show');
+
+ const itemId = node.modifiers.get(JWEditorLib.Attributes).get('data-oe-id');
+
+ promises.push(this._rpc({
+ model: 'website.menu',
+ method: 'write',
+ args: [
+ [parseInt(itemId)],
+ {
+ 'mega_menu_classes': classes.join(' '),
+ },
+ ],
+ }));
+ }
+
+ await Promise.all(promises);
+ },
+ /**
+ * Save all modified images.
+ *
+ * @private
+ */
+ _saveModifiedImages: async function (context) {
+ const wysiwygSaveModifiedImages = async (context) => {
+ const defs = _.map(this._getEditable($('#wrapwrap')), async editableEl => {
+ const {oeModel: resModel, oeId: resId} = editableEl.dataset;
+ const proms = [...editableEl.querySelectorAll('.o_modified_image_to_save')].map(async el => {
+ const isBackground = !el.matches('img');
+ el.classList.remove('o_modified_image_to_save');
+
+ await this.editorHelpers.removeClass(context, el, 'o_modified_image_to_save');
+ // Modifying an image always creates a copy of the original, even if
+ // it was modified previously, as the other modified image may be used
+ // elsewhere if the snippet was duplicated or was saved as a custom one.
+ const newAttachmentSrc = await this._rpc({
+ route: `/web_editor/modify_image/${el.dataset.originalId}`,
+ params: {
+ res_model: resModel,
+ res_id: parseInt(resId),
+ data: (isBackground ? el.dataset.bgSrc : el.getAttribute('src')).split(',')[1],
+ },
+ });
+ if (isBackground) {
+ await this.editorHelpers.setStyle(context, el, 'background-image', `url('${newAttachmentSrc}')`);
+ await this.editorHelpers.setAttribute(context, el, 'data-bgSrc', '');
+ } else {
+ await this.editorHelpers.setAttribute(context, el, 'src', newAttachmentSrc);
+ }
+ });
+ return Promise.all(proms);
+ });
+ await Promise.all(defs);
};
- options.onFocus = function () {
- self.trigger_up('wysiwyg_focus');
+ return context.execCommand(wysiwygSaveModifiedImages);
+ },
+ /**
+ * Save all "newsletter" blocks.
+ *
+ * @private
+ */
+ _saveNewsletterBlocks: async function () {
+ const defs = [];
+ const wysiwygSaveNewsletterBlocks = async () => {
+ defs.push(this._super.apply(this, arguments));
+ const $popups = $(this.editorEditable).find('.o_newsletter_popup');
+ for (const popup of $popups) {
+ const $popup = $(popup);
+ const content = $popup.data('content');
+ if (content) {
+ defs.push(this._rpc({
+ route: '/website_mass_mailing/set_content',
+ params: {
+ 'newsletter_id': parseInt($popup.attr('data-list-id')),
+ 'content': content,
+ },
+ }));
+ }
+ }
};
- options.onBlur = function () {
- self.trigger_up('wysiwyg_blur');
+ await this.editor.execCommand(wysiwygSaveNewsletterBlocks);
+ return Promise.all(defs);
+ },
+ /**
+ * Return a promise resulting from a rpc to 'ir.translation' to save the
+ * given view to the given translationId.
+ *
+ * @param {JQuery} $elem
+ * @param {number} translationId
+ * @param {string} [xpath]
+ */
+ _saveTranslationTo($elem, translationId) {
+ const $escapedElement = this._getEscapedElement($elem);
+ return this._rpc({
+ model: 'ir.translation',
+ method: 'save_html',
+ args: [
+ [translationId],
+ $escapedElement.html() || $escapedElement.text() || '',
+ ],
+ context: this.options.recordInfo.context,
+ });
+ },
+ /**
+ * Save all "view" blocks.
+ *
+ * @private
+ */
+ _saveViewBlocks: async function () {
+ const promises = [];
+ const nodes = this.zoneMain.descendants(node => {
+ return (
+ node instanceof JWEditorLib.OdooStructureNode ||
+ node instanceof JWEditorLib.OdooFieldNode
+ );
+ });
+ for (const node of nodes) {
+ const renderer = this.editor.plugins.get(JWEditorLib.Renderer);
+ const renderedNode = (await renderer.render('dom/html', node))[0];
+ $(renderedNode).find('.o_snippet_editor_updated').addBack().removeClass('o_snippet_editor_updated');
+ let $saveNode = $(renderedNode).find('[data-oe-expression][data-oe-id]');
+ if ($saveNode.length === 0) {
+ $saveNode = $(renderedNode)
+ }
+ const isStructureDirty = node instanceof JWEditorLib.OdooStructureNode && node.dirty;
+ const isFieldDirty = node instanceof JWEditorLib.OdooFieldNode && node.fieldInfo.originalValue !== node.fieldInfo.value.get();
+ if (isStructureDirty || isFieldDirty) {
+ const promise = this._saveViewTo($saveNode, +$saveNode[0].dataset.oeId, node.xpath);
+ promise.catch(() => { console.error('Fail to save:', $saveNode[0]); });
+ promises.push(promise);
+ }
+ }
+ return Promise.all(promises);
+ },
+ /**
+ * Return a promise resulting from a rpc to 'ir.ui.view' to save the given
+ * view to the given viewId.
+ *
+ * @param {JQuery} $elem
+ * @param {number} viewId
+ * @param {string} [xpath]
+ */
+ _saveViewTo($elem, viewId, xpath = null) {
+ const $escapedElement = this._getEscapedElement($elem);
+ return this._rpc({
+ model: 'ir.ui.view',
+ method: 'save',
+ args: [
+ viewId,
+ $escapedElement.prop('outerHTML'),
+ xpath,
+ ],
+ context: this.options.recordInfo.context,
+ });
+ },
+ /**
+ * Save after any cleaning has been done and before reloading
+ * the page.
+ */
+ async _saveWebsiteContent(context = this.editor) {
+ return new Promise((resolve, reject) => {
+ const wysiwygSaveContent = async (context)=> {
+ await this._saveModifiedImages(context);
+ await this._saveViewBlocks();
+ await this._saveCoverPropertiesBlocks(context);
+ await this._saveMegaMenuClasses();
+ };
+ context.execCommand(wysiwygSaveContent).then(params => {
+ if (params && params.error) {
+ reject(params.error.message);
+ } else {
+ resolve();
+ }
+ });
+ });
+ },
+ _setColor(colorpicker, setCommandId, unsetCommandId, color, $dropDownToToggle, closeColorPicker = false) {
+ if(color === "") {
+ this.editor.execCommand(unsetCommandId);
+ } else {
+ if (colorpicker.colorNames.indexOf(color) !== -1) {
+ // todo : find a better way to detect and send css variable
+ color = "var(--" + color + ")";
+ }
+ this.editor.execCommand(setCommandId, {color: color});
+ }
+ const $jwButton = $dropDownToToggle.find(".dropdown-toggle")
+ $jwButton.css("background-color", color);
+ if(closeColorPicker) {
+ $jwButton.dropdown("toggle");
+ colorpicker.selectedColor = '';
+ }
+ },
+ /**
+ * Initialize the editor for a translation.
+ *
+ * @private
+ */
+ _setupTranslation: function () {
+ const attributeNames = ['placeholder', 'title', 'alt'];
+ const nodesToTranslateAttributes = this.zoneMain.descendants(node => {
+ const attributes = node.modifiers.find(JWEditorLib.Attributes);
+ return attributes && attributes.keys().some(key => {
+ if (attributeNames.includes(key)) {
+ const div = document.createElement('div');
+ div.innerHTML = attributes.get(key);
+ // The attribute is translatable if its value is the HTML
+ // of a node with attributes that include
+ // data-oe-translation-state.
+ return div.querySelector('[data-oe-translation-state]');
+ }
+ });
+ });
+ const domNodesToTranslateAttributes = nodesToTranslateAttributes.flatMap(nodeToTranslateAttributes => {
+ return this.editorHelpers.getDomNodes(nodeToTranslateAttributes)[0];
+ });
+ this.$nodesToTranslateAttributes = $(domNodesToTranslateAttributes);
+ for (const attr of attributeNames) {
+ this.$nodesToTranslateAttributes.each(function () {
+ var $node = $(this);
+ var translation = $node.data('translation') || {};
+ var attributeTranslation = $node.attr(attr);
+ if (attributeTranslation) {
+ var match = attributeTranslation.match(/]*data-oe-translation-id="([0-9]+)"[^>]*>(.*)<\/span>/);
+ if (match) {
+ var $translatedAttributeNode = $(attributeTranslation).addClass('d-none o_editable o_editable_translatable_attribute').appendTo('body');
+ $translatedAttributeNode.data('$node', $node).data('attribute', attr);
+ translation[attr] = $translatedAttributeNode[0];
+ $node.attr(attr, match[2]);
+ }
+ }
+ var select2 = $node.data('select2');
+ if (select2) {
+ select2.blur();
+ $node.on('translate', function () {
+ select2.blur();
+ });
+ $node = select2.container.find('input');
+ }
+ $node.addClass('o_translatable_attribute').data('translation', translation);
+ });
+ }
+ this.$attribute_translations = $('.o_editable_translatable_attribute');
+ this.translations = [];
+ this._markTranslatableNodes();
+
+ // We don't want the BS dropdown to close
+ // when clicking in a element to translate
+ $('.dropdown-menu').on('click', '.o_editable', function (ev) {
+ ev.stopPropagation();
+ });
+
+ // Add a tooltip for not-editable areas
+ $(document.body)
+ .tooltip({
+ selector: '[data-oe-readonly], .o_not_editable',
+ container: 'body',
+ trigger: 'hover',
+ delay: { 'show': 1000, 'hide': 100 },
+ placement: 'bottom',
+ title: _t("Readonly field")
+ })
+ .on('click', function () {
+ $(this).tooltip('hide');
+ });
+ },
+ _transform($image) {
+ if ($image.data('transfo-destroy')) {
+ $image.removeData('transfo-destroy');
+ return;
+ }
+
+ $image.transfo();
+
+ const mouseup = (event) => {
+ $('.note-popover button[data-event="transform"]').toggleClass('active', $image.is('[style*="transform"]'));
};
- return options;
+ $(document).on('mouseup', mouseup);
+
+ const mousedown = (event) => {
+ if (!$(event.target).closest('.transfo-container').length) {
+ $image.transfo('destroy');
+ $(document).off('mousedown', mousedown).off('mouseup', mouseup);
+ }
+ if ($(event.target).closest('.note-popover').length) {
+ $image.data('transfo-destroy', true).attr('style', ($image.attr('style') || '').replace(/[^;]*transform[\w:]*;?/g, ''));
+ }
+ this._updateAttributes($image[0])
+ };
+ $(document).on('mousedown', mousedown);
+ },
+ _updateAttributes(node) {
+ const attributes = {}
+ for (const attr of node.attributes){
+ attributes[attr.name] = attr.value;
+ }
+ this.editorHelpers.updateAttributes(this.editor, node, attributes);
},
});
+
//--------------------------------------------------------------------------
// Public helper
//--------------------------------------------------------------------------
@@ -213,13 +1379,15 @@ var Wysiwyg = Widget.extend({
* @returns {Number} eo - end offset
*/
Wysiwyg.getRange = function (node) {
- var range = $.summernote.core.range.create();
- return range && {
- sc: range.sc,
- so: range.so,
- ec: range.ec,
- eo: range.eo,
+ const selection = window.getSelection();
+ const range = selection.getRangeAt(0);
+ var result = range && {
+ sc: range.startContainer,
+ so: range.startOffset,
+ ec: range.endContainer,
+ eo: range.endOffset,
};
+ return result;
};
/**
* @param {Node} startNode
@@ -227,46 +1395,16 @@ Wysiwyg.getRange = function (node) {
* @param {Node} endNode
* @param {Number} endOffset
*/
-Wysiwyg.setRange = function (startNode, startOffset, endNode, endOffset) {
- $(startNode).focus();
- if (endNode) {
- $.summernote.core.range.create(startNode, startOffset, endNode, endOffset).select();
- } else {
- $.summernote.core.range.create(startNode, startOffset).select();
- }
- // trigger for Unbreakable
- $(startNode.tagName ? startNode : startNode.parentNode).trigger('wysiwyg.range');
-};
-/**
- * @param {Node} node - dom node
- * @param {Object} [options]
- * @param {Boolean} options.begin move the range to the beginning of the first node.
- * @param {Boolean} options.end move the range to the end of the last node.
- */
-Wysiwyg.setRangeFromNode = function (node, options) {
- var last = node;
- while (last.lastChild) {
- last = last.lastChild;
- }
- var first = node;
- while (first.firstChild) {
- first = first.firstChild;
- }
- if (options && options.begin && !options.end) {
- Wysiwyg.setRange(first, 0);
- } else if (options && !options.begin && options.end) {
- Wysiwyg.setRange(last, last.textContent.length);
- } else {
- Wysiwyg.setRange(first, 0, last, last.tagName ? last.childNodes.length : last.textContent.length);
- }
+Wysiwyg.setRange = async function (wysiwyg, startNode, startOffset, endNode, endOffset) {
+ endNode = endNode || startNode;
+ endOffset = endOffset || startOffset-1;
+ const wysiwygSetRange = async () => {
+ const startVNode = wysiwyg.editorHelpers.getNodes(startNode);
+ const endVNode = wysiwyg.editorHelpers.getNodes(endNode);
+ wysiwyg.editor.selection.select(startVNode[startOffset], endVNode[endOffset]);
+ };
+ await wysiwyg.editor.execCommand(wysiwygSetRange);
};
+
return Wysiwyg;
});
-odoo.define('web_editor.widget', function (require) {
-'use strict';
- return {
- Dialog: require('wysiwyg.widgets.Dialog'),
- MediaDialog: require('wysiwyg.widgets.MediaDialog'),
- LinkDialog: require('wysiwyg.widgets.LinkDialog'),
- };
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js
index 9450e96df93..e69de29bb2d 100644
--- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js
+++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js
@@ -1,131 +0,0 @@
-odoo.define('web_editor.wysiwyg.iframe', function (require) {
-'use strict';
-
-var Wysiwyg = require('web_editor.wysiwyg');
-var ajax = require('web.ajax');
-var core = require('web.core');
-var config = require('web.config');
-
-var qweb = core.qweb;
-var promiseCommon;
-var promiseWysiwyg;
-
-
-/**
- * Add option (inIframe) to load Wysiwyg in an iframe.
- **/
-Wysiwyg.include({
- /**
- * Add options to load Wysiwyg in an iframe.
- *
- * @override
- * @param {boolean} options.inIframe
- **/
- init: function (parent, options) {
- this._super.apply(this, arguments);
- if (this.options.inIframe) {
- this._onUpdateIframeId = 'onLoad_' + this.id;
- }
- },
- /**
- * Load assets to inject into iframe.
- *
- * @override
- **/
- willStart: function () {
- if (!this.options.inIframe) {
- return this._super();
- }
-
- var defAsset;
- if (this.options.iframeCssAssets) {
- defAsset = ajax.loadAsset(this.options.iframeCssAssets);
- } else {
- defAsset = Promise.resolve({
- cssLibs: [],
- cssContents: []
- });
- }
-
- promiseWysiwyg = promiseWysiwyg || ajax.loadAsset('web_editor.wysiwyg_iframe_editor_assets');
- this.defAsset = Promise.all([promiseWysiwyg, defAsset]);
-
- this.$target = this.$el;
- return this.defAsset
- .then(this._loadIframe.bind(this))
- .then(this._super.bind(this));
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Create iframe, inject css and create a link with the content,
- * then inject the target inside.
- *
- * @private
- * @returns {Promise}
- */
- _loadIframe: function () {
- var self = this;
- this.$iframe = $('