summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src/js/backend
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web_editor/static/src/js/backend
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web_editor/static/src/js/backend')
-rw-r--r--addons/web_editor/static/src/js/backend/convert_inline.js485
-rw-r--r--addons/web_editor/static/src/js/backend/field_html.js536
2 files changed, 1021 insertions, 0 deletions
diff --git a/addons/web_editor/static/src/js/backend/convert_inline.js b/addons/web_editor/static/src/js/backend/convert_inline.js
new file mode 100644
index 00000000..071caa3f
--- /dev/null
+++ b/addons/web_editor/static/src/js/backend/convert_inline.js
@@ -0,0 +1,485 @@
+odoo.define('web_editor.convertInline', function (require) {
+'use strict';
+
+var FieldHtml = require('web_editor.field.html');
+
+/**
+ * Returns the css rules which applies on an element, tweaked so that they are
+ * browser/mail client ok.
+ *
+ * @param {DOMElement} a
+ * @returns {Object} css property name -> css property value
+ */
+function getMatchedCSSRules(a) {
+ var i, r, k;
+ var doc = a.ownerDocument;
+ var rulesCache = a.ownerDocument._rulesCache || (a.ownerDocument._rulesCache = []);
+
+ if (!rulesCache.length) {
+ var sheets = doc.styleSheets;
+ for (i = sheets.length-1 ; i >= 0 ; i--) {
+ var rules;
+ // try...catch because browser may not able to enumerate rules for cross-domain sheets
+ try {
+ rules = sheets[i].rules || sheets[i].cssRules;
+ } catch (e) {
+ console.warn("Can't read the css rules of: " + sheets[i].href, e);
+ continue;
+ }
+ if (rules) {
+ for (r = rules.length-1; r >= 0; r--) {
+ var selectorText = rules[r].selectorText;
+ if (selectorText &&
+ rules[r].cssText &&
+ selectorText !== '*' &&
+ selectorText.indexOf(':hover') === -1 &&
+ selectorText.indexOf(':before') === -1 &&
+ selectorText.indexOf(':after') === -1 &&
+ selectorText.indexOf(':active') === -1 &&
+ selectorText.indexOf(':link') === -1 &&
+ selectorText.indexOf('::') === -1 &&
+ selectorText.indexOf("'") === -1) {
+ var st = selectorText.split(/\s*,\s*/);
+ for (k = 0 ; k < st.length ; k++) {
+ rulesCache.push({ 'selector': st[k], 'style': rules[r].style });
+ }
+ }
+ }
+ }
+ }
+ rulesCache.reverse();
+ }
+
+ var css = [];
+ var style;
+ a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector;
+ for (r = 0; r < rulesCache.length; r++) {
+ if (a.matches(rulesCache[r].selector)) {
+ style = rulesCache[r].style;
+ if (style.parentRule) {
+ var style_obj = {};
+ var len;
+ for (k = 0, len = style.length ; k < len ; k++) {
+ if (style[k].indexOf('animation') !== -1) {
+ continue;
+ }
+ style_obj[style[k]] = style[style[k].replace(/-(.)/g, function (a, b) { return b.toUpperCase(); })];
+ if (new RegExp(style[k] + '\s*:[^:;]+!important' ).test(style.cssText)) {
+ style_obj[style[k]] += ' !important';
+ }
+ }
+ rulesCache[r].style = style = style_obj;
+ }
+ css.push([rulesCache[r].selector, style]);
+ }
+ }
+
+ function specificity(selector) {
+ // http://www.w3.org/TR/css3-selectors/#specificity
+ var a = 0;
+ selector = selector.replace(/#[a-z0-9_-]+/gi, function () { a++; return ''; });
+ var b = 0;
+ selector = selector.replace(/(\.[a-z0-9_-]+)|(\[.*?\])/gi, function () { b++; return ''; });
+ var c = 0;
+ selector = selector.replace(/(^|\s+|:+)[a-z0-9_-]+/gi, function (a) { if (a.indexOf(':not(')===-1) c++; return ''; });
+ return a*100 + b*10 + c;
+ }
+ css.sort(function (a, b) { return specificity(a[0]) - specificity(b[0]); });
+
+ style = {};
+ _.each(css, function (v,k) {
+ _.each(v[1], function (v,k) {
+ if (v && _.isString(v) && k.indexOf('-webkit') === -1 && (!style[k] || style[k].indexOf('important') === -1 || v.indexOf('important') !== -1)) {
+ style[k] = v;
+ }
+ });
+ });
+
+ _.each(style, function (v,k) {
+ if (v.indexOf('important') !== -1) {
+ style[k] = v.slice(0, v.length-11);
+ }
+ });
+
+ if (style.display === 'block') {
+ delete style.display;
+ }
+
+ // The css generates all the attributes separately and not in simplified form.
+ // In order to have a better compatibility (outlook for example) we simplify the css tags.
+ // e.g. border-left-style: none; border-bottom-s .... will be simplified in border-style = none
+ _.each([
+ {property: 'margin'},
+ {property: 'padding'},
+ {property: 'border', propertyEnd: '-style', defaultValue: 'none'},
+ ], function (propertyInfo) {
+ var p = propertyInfo.property;
+ var e = propertyInfo.propertyEnd || '';
+ var defVal = propertyInfo.defaultValue || 0;
+
+ if (style[p+'-top'+e] || style[p+'-right'+e] || style[p+'-bottom'+e] || style[p+'-left'+e]) {
+ if (style[p+'-top'+e] === style[p+'-right'+e] && style[p+'-top'+e] === style[p+'-bottom'+e] && style[p+'-top'+e] === style[p+'-left'+e]) {
+ // keep => property: [top/right/bottom/left value];
+ style[p+e] = style[p+'-top'+e];
+ }
+ else {
+ // keep => property: [top value] [right value] [bottom value] [left value];
+ style[p+e] = (style[p+'-top'+e] || defVal) + ' ' + (style[p+'-right'+e] || defVal) + ' ' + (style[p+'-bottom'+e] || defVal) + ' ' + (style[p+'-left'+e] || defVal);
+ if (style[p+e].indexOf('inherit') !== -1 || style[p+e].indexOf('initial') !== -1) {
+ // keep => property-top: [top value]; property-right: [right value]; property-bottom: [bottom value]; property-left: [left value];
+ delete style[p+e];
+ return;
+ }
+ }
+ delete style[p+'-top'+e];
+ delete style[p+'-right'+e];
+ delete style[p+'-bottom'+e];
+ delete style[p+'-left'+e];
+ }
+ });
+
+ if (style['border-bottom-left-radius']) {
+ style['border-radius'] = style['border-bottom-left-radius'];
+ delete style['border-bottom-left-radius'];
+ delete style['border-bottom-right-radius'];
+ delete style['border-top-left-radius'];
+ delete style['border-top-right-radius'];
+ }
+
+ // if the border styling is initial we remove it to simplify the css tags for compatibility.
+ // Also, since we do not send a css style tag, the initial value of the border is useless.
+ _.each(_.keys(style), function (k) {
+ if (k.indexOf('border') !== -1 && style[k] === 'initial') {
+ delete style[k];
+ }
+ });
+
+ // text-decoration rule is decomposed in -line, -color and -style. This is
+ // however not supported by many browser/mail clients and the editor does
+ // not allow to change -color and -style rule anyway
+ if (style['text-decoration-line']) {
+ style['text-decoration'] = style['text-decoration-line'];
+ delete style['text-decoration-line'];
+ delete style['text-decoration-color'];
+ delete style['text-decoration-style'];
+ delete style['text-decoration-thickness'];
+ }
+
+ // text-align inheritance does not seem to get past <td> elements on some
+ // mail clients
+ if (style['text-align'] === 'inherit') {
+ var $el = $(a).parent();
+ do {
+ var align = $el.css('text-align');
+ if (_.indexOf(['left', 'right', 'center', 'justify'], align) >= 0) {
+ style['text-align'] = align;
+ break;
+ }
+ $el = $el.parent();
+ } while ($el.length && !$el.is('html'));
+ }
+
+ return style;
+}
+
+/**
+ * Converts font icons to images.
+ *
+ * @param {jQuery} $editable - the element in which the font icons have to be
+ * converted to images
+ */
+function fontToImg($editable) {
+ var fonts = odoo.__DEBUG__.services["wysiwyg.fonts"];
+
+ $editable.find('.fa').each(function () {
+ var $font = $(this);
+ var icon, content;
+ _.find(fonts.fontIcons, function (font) {
+ return _.find(fonts.getCssSelectors(font.parser), function (data) {
+ if ($font.is(data.selector.replace(/::?before/g, ''))) {
+ icon = data.names[0].split('-').shift();
+ content = data.css.match(/content:\s*['"]?(.)['"]?/)[1];
+ return true;
+ }
+ });
+ });
+ if (content) {
+ var color = $font.css('color').replace(/\s/g, '');
+ $font.replaceWith($('<img/>', {
+ src: _.str.sprintf('/web_editor/font_to_img/%s/%s/%s', content.charCodeAt(0), window.encodeURI(color), Math.max(1, Math.round($font.height()))),
+ 'data-class': $font.attr('class'),
+ 'data-style': $font.attr('style'),
+ class: $font.attr('class').replace(new RegExp('(^|\\s+)' + icon + '(-[^\\s]+)?', 'gi'), ''), // remove inline font-awsome style
+ style: $font.attr('style'),
+ }).css({height: 'auto', width: 'auto'}));
+ } else {
+ $font.remove();
+ }
+ });
+}
+
+/**
+ * Converts images which were the result of a font icon convertion to a font
+ * icon again.
+ *
+ * @param {jQuery} $editable - the element in which the images will be converted
+ * back to font icons
+ */
+function imgToFont($editable) {
+ $editable.find('img[src*="/web_editor/font_to_img/"]').each(function () {
+ var $img = $(this);
+ $img.replaceWith($('<span/>', {
+ class: $img.data('class'),
+ style: $img.data('style')
+ }));
+ });
+}
+
+/*
+ * Utility function to apply function over descendants elements
+ *
+ * This is needed until the following issue of jQuery is solved:
+ * https://github.com./jquery/sizzle/issues/403
+ *
+ * @param {Element} node The root Element node
+ * @param {Function} func The function applied over descendants
+ */
+function applyOverDescendants(node, func) {
+ node = node.firstChild;
+ while (node) {
+ if (node.nodeType === 1) {
+ func(node);
+ applyOverDescendants(node, func);
+ }
+ var $node = $(node);
+ if (node.nodeName === 'A' && $node.hasClass('btn') && !$node.children().length && $(node).parents('.o_outlook_hack').length) {
+ node = $(node).parents('.o_outlook_hack')[0];
+ }
+ else if (node.nodeName === 'IMG' && $node.parent('p').hasClass('o_outlook_hack')) {
+ node = $node.parent()[0];
+ }
+ node = node.nextSibling;
+ }
+}
+
+/**
+ * Converts css style to inline style (leave the classes on elements but forces
+ * the style they give as inline style).
+ *
+ * @param {jQuery} $editable
+ */
+function classToStyle($editable) {
+ applyOverDescendants($editable[0], function (node) {
+ var $target = $(node);
+ var css = getMatchedCSSRules(node);
+ var style = $target.attr('style') || '';
+ _.each(css, function (v,k) {
+ if (!(new RegExp('(^|;)\s*' + k).test(style))) {
+ style = k+':'+v+';'+style;
+ }
+ });
+ if (_.isEmpty(style)) {
+ $target.removeAttr('style');
+ } else {
+ $target.attr('style', style);
+ }
+ // Apple Mail
+ if (node.nodeName === 'TD' && !node.childNodes.length) {
+ node.innerHTML = '&nbsp;';
+ }
+
+ // Outlook
+ if (node.nodeName === 'A' && $target.hasClass('btn') && !$target.hasClass('btn-link') && !$target.children().length) {
+ var $hack = $('<table class="o_outlook_hack" style="display: inline-table;vertical-align:middle"><tr><td></td></tr></table>');
+ $hack.find('td')
+ .attr('height', $target.outerHeight())
+ .css({
+ 'text-align': $target.parent().css('text-align'),
+ 'margin': $target.css('padding'),
+ 'border-radius': $target.css('border-radius'),
+ 'background-color': $target.css('background-color'),
+ });
+ $target.after($hack);
+ $target.appendTo($hack.find('td'));
+ // the space add a line when it's a table but it's invisible when it's a link
+ node = $hack[0].previousSibling;
+ if (node && node.nodeType === Node.TEXT_NODE && !node.textContent.match(/\S/)) {
+ $(node).remove();
+ }
+ node = $hack[0].nextSibling;
+ if (node && node.nodeType === Node.TEXT_NODE && !node.textContent.match(/\S/)) {
+ $(node).remove();
+ }
+ }
+ else if (node.nodeName === 'IMG' && $target.is('.mx-auto.d-block')) {
+ $target.wrap('<p class="o_outlook_hack" style="text-align:center;margin:0"/>');
+ }
+ });
+}
+
+/**
+ * Removes the inline style which is not necessary (because, for example, a
+ * class on an element will induce the same style).
+ *
+ * @param {jQuery} $editable
+ */
+function styleToClass($editable) {
+ // Outlook revert
+ $editable.find('.o_outlook_hack').each(function () {
+ $(this).after($('a,img', this));
+ }).remove();
+
+ var $c = $('<span/>').appendTo($editable[0].ownerDocument.body);
+
+ applyOverDescendants($editable[0], function (node) {
+ var $target = $(node);
+ var css = getMatchedCSSRules(node);
+ var style = '';
+ _.each(css, function (v,k) {
+ if (!(new RegExp('(^|;)\s*' + k).test(style))) {
+ style = k+':'+v+';'+style;
+ }
+ });
+ css = ($c.attr('style', style).attr('style') || '').split(/\s*;\s*/);
+ style = ($target.attr('style') || '').replace(/\s*:\s*/, ':').replace(/\s*;\s*/, ';');
+ _.each(css, function (v) {
+ style = style.replace(v, '');
+ });
+ style = style.replace(/;+(\s;)*/g, ';').replace(/^;/g, '');
+ if (style !== '') {
+ $target.attr('style', style);
+ } else {
+ $target.removeAttr('style');
+ }
+ });
+ $c.remove();
+}
+
+/**
+ * Converts css display for attachment link to real image.
+ * Without this post process, the display depends on the css and the picture
+ * does not appear when we use the html without css (to send by email for e.g.)
+ *
+ * @param {jQuery} $editable
+ */
+function attachmentThumbnailToLinkImg($editable) {
+ $editable.find('a[href*="/web/content/"][data-mimetype]').filter(':empty, :containsExact( )').each(function () {
+ var $link = $(this);
+ var $img = $('<img/>')
+ .attr('src', $link.css('background-image').replace(/(^url\(['"])|(['"]\)$)/g, ''))
+ .css('height', Math.max(1, $link.height()) + 'px')
+ .css('width', Math.max(1, $link.width()) + 'px');
+ $link.prepend($img);
+ });
+}
+
+/**
+ * Revert attachmentThumbnailToLinkImg changes
+ *
+ * @see attachmentThumbnailToLinkImg
+ * @param {jQuery} $editable
+ */
+function linkImgToAttachmentThumbnail($editable) {
+ $editable.find('a[href*="/web/content/"][data-mimetype] > img').remove();
+}
+
+
+//--------------------------------------------------------------------------
+//--------------------------------------------------------------------------
+
+
+FieldHtml.include({
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ commitChanges: function () {
+ if (this.nodeOptions['style-inline'] && this.mode === "edit") {
+ this._toInline();
+ }
+ return this._super();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Converts CSS dependencies to CSS-independent HTML.
+ * - CSS display for attachment link -> real image
+ * - Font icons -> images
+ * - CSS styles -> inline styles
+ *
+ * @private
+ */
+ _toInline: function () {
+ var $editable = this.wysiwyg.getEditable();
+ var html = this.wysiwyg.getValue({'style-inline': true});
+ $editable.html(html);
+
+ attachmentThumbnailToLinkImg($editable);
+ fontToImg($editable);
+ classToStyle($editable);
+
+ // fix outlook image rendering bug
+ _.each(['width', 'height'], function(attribute) {
+ $editable.find('img[style*="width"], img[style*="height"]').attr(attribute, function(){
+ return $(this)[attribute]();
+ }).css(attribute, function(){
+ return $(this).get(0).style[attribute] || 'auto';
+ });
+ });
+
+ this.wysiwyg.setValue($editable.html(), {
+ notifyChange: false,
+ });
+ },
+ /**
+ * Revert _toInline changes.
+ *
+ * @private
+ */
+ _fromInline: function () {
+ var $editable = this.wysiwyg.getEditable();
+ var html = this.wysiwyg.getValue();
+ $editable.html(html);
+
+ styleToClass($editable);
+ imgToFont($editable);
+ linkImgToAttachmentThumbnail($editable);
+
+ // fix outlook image rendering bug
+ $editable.find('img[style*="width"], img[style*="height"]').removeAttr('height width');
+
+ this.wysiwyg.setValue($editable.html(), {
+ notifyChange: false,
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handler
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _onLoadWysiwyg: function () {
+ if (this.nodeOptions['style-inline'] && this.mode === "edit") {
+ this._fromInline();
+ }
+ this._super();
+ },
+});
+
+return {
+ fontToImg: fontToImg,
+ imgToFont: imgToFont,
+ classToStyle: classToStyle,
+ styleToClass: styleToClass,
+ attachmentThumbnailToLinkImg: attachmentThumbnailToLinkImg,
+ linkImgToAttachmentThumbnail: linkImgToAttachmentThumbnail,
+};
+}); \ No newline at end of file
diff --git a/addons/web_editor/static/src/js/backend/field_html.js b/addons/web_editor/static/src/js/backend/field_html.js
new file mode 100644
index 00000000..37f2a8ba
--- /dev/null
+++ b/addons/web_editor/static/src/js/backend/field_html.js
@@ -0,0 +1,536 @@
+odoo.define('web_editor.field.html', function (require) {
+'use strict';
+
+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 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');
+
+var _lt = core._lt;
+var TranslatableFieldMixin = basic_fields.TranslatableFieldMixin;
+var QWeb = core.qweb;
+var assetsLoaded;
+
+var jinjaRegex = /(^|\n)\s*%\s(end|set\s)/;
+
+/**
+ * FieldHtml Widget
+ * Intended to display HTML content. This widget uses the wysiwyg editor
+ * improved by odoo.
+ *
+ * nodeOptions:
+ * - style-inline => convert class to inline style (no re-edition) => for sending by email
+ * - no-attachment
+ * - cssEdit
+ * - cssReadonly
+ * - snippets
+ * - wrapper
+ */
+var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
+ description: _lt("Html"),
+ className: 'oe_form_field oe_form_field_html',
+ supportedFieldTypes: ['html'],
+
+ custom_events: {
+ wysiwyg_focus: '_onWysiwygFocus',
+ wysiwyg_blur: '_onWysiwygBlur',
+ wysiwyg_change: '_onChange',
+ wysiwyg_attachment: '_onAttachmentChange',
+ },
+
+ /**
+ * @override
+ */
+ willStart: function () {
+ var self = this;
+ this.isRendered = false;
+ this._onUpdateIframeId = 'onLoad_' + _.uniqueId('FieldHtml');
+ var defAsset;
+ if (this.nodeOptions.cssReadonly) {
+ defAsset = 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($('<textarea>')).then(function () {
+ wysiwyg.destroy();
+ resolve();
+ });
+ });
+ }
+
+ return Promise.all([this._super(), assetsLoaded, defAsset]);
+ },
+ /**
+ * @override
+ */
+ destroy: function () {
+ delete window.top[this._onUpdateIframeId];
+ if (this.$iframe) {
+ this.$iframe.remove();
+ }
+ this._super();
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ activate: function (options) {
+ if (this.wysiwyg) {
+ this.wysiwyg.focus();
+ return true;
+ }
+ },
+ /**
+ * Wysiwyg doesn't notify for changes done in code mode. We override
+ * commitChanges to manually switch back to normal mode before committing
+ * changes, so that the widget is aware of the changes done in code mode.
+ *
+ * @override
+ */
+ commitChanges: function () {
+ var self = this;
+ if (config.isDebug() && this.mode === 'edit') {
+ var layoutInfo = $.summernote.core.dom.makeLayoutInfo(this.wysiwyg.$editor);
+ $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
+ }
+ if (this.mode == "readonly" || !this.isRendered) {
+ return this._super();
+ }
+ var _super = this._super.bind(this);
+ return this.wysiwyg.saveModifiedImages(this.$content).then(function () {
+ return self.wysiwyg.save(self.nodeOptions).then(function (result) {
+ self._isDirty = result.isDirty;
+ _super();
+ });
+ });
+ },
+ /**
+ * @override
+ */
+ isSet: function () {
+ var value = this.value && this.value.split('&nbsp;').join('').replace(/\s/g, ''); // Removing spaces & html spaces
+ return value && value !== "<p></p>" && value !== "<p><br></p>" && value.match(/\S/);
+ },
+ /**
+ * @override
+ */
+ getFocusableElement: function () {
+ return this.$target || $();
+ },
+ /**
+ * Do not re-render this field if it was the origin of the onchange call.
+ *
+ * @override
+ */
+ reset: function (record, event) {
+ this._reset(record, event);
+ var value = this.value;
+ if (this.nodeOptions.wrapper) {
+ value = this._wrap(value);
+ }
+ value = this._textToHtml(value);
+ if (!event || event.target !== this) {
+ if (this.mode === 'edit') {
+ this.wysiwyg.setValue(value);
+ } else {
+ this.$content.html(value);
+ }
+ }
+ return Promise.resolve();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _getValue: function () {
+ var value = this.$target.val();
+ if (this.nodeOptions.wrapper) {
+ return this._unWrap(value);
+ }
+ return value;
+ },
+ /**
+ * Create the wysiwyg instance with the target (this.$target)
+ * then add the editable content (this.$content).
+ *
+ * @private
+ * @returns {$.Promise}
+ */
+ _createWysiwygIntance: function () {
+ var self = this;
+ this.wysiwyg = new Wysiwyg(this, this._getWysiwygOptions());
+ this.wysiwyg.__extraAssetsForIframe = this.__extraAssetsForIframe || [];
+
+ // by default this is synchronous because the assets are already loaded in willStart
+ // but it can be async in the case of options such as iframe, snippets...
+ return this.wysiwyg.attachTo(this.$target).then(function () {
+ self.$content = self.wysiwyg.$editor.closest('body, odoo-wysiwyg-container');
+ self._onLoadWysiwyg();
+ self.isRendered = true;
+ });
+ },
+ /**
+ * Get wysiwyg options to create wysiwyg instance.
+ *
+ * @private
+ * @returns {Object}
+ */
+ _getWysiwygOptions: function () {
+ var self = this;
+ return Object.assign({}, this.nodeOptions, {
+ recordInfo: {
+ context: this.record.getContext(this.recordParams),
+ res_model: this.model,
+ res_id: this.res_id,
+ },
+ noAttachment: this.nodeOptions['no-attachment'],
+ inIframe: !!this.nodeOptions.cssEdit,
+ iframeCssAssets: this.nodeOptions.cssEdit,
+ snippets: this.nodeOptions.snippets,
+
+ tabsize: 0,
+ height: 180,
+ generateOptions: function (options) {
+ var toolbar = options.toolbar || options.airPopover || {};
+ var para = _.find(toolbar, function (item) {
+ return item[0] === 'para';
+ });
+ if (para && para[1] && para[1].indexOf('checklist') === -1) {
+ para[1].splice(2, 0, 'checklist');
+ }
+ if (config.isDebug()) {
+ options.codeview = true;
+ var view = _.find(toolbar, function (item) {
+ return item[0] === 'view';
+ });
+ if (view) {
+ if (!view[1].includes('codeview')) {
+ view[1].splice(-1, 0, 'codeview');
+ }
+ } else {
+ toolbar.splice(-1, 0, ['view', ['codeview']]);
+ }
+ }
+ if (self.model === "mail.compose.message" || self.model === "mailing.mailing") {
+ options.noVideos = true;
+ }
+ options.prettifyHtml = false;
+ return options;
+ },
+ });
+ },
+ /**
+ * trigger_up 'field_changed' add record into the "ir.attachment" field found in the view.
+ * This method is called when an image is uploaded via the media dialog.
+ *
+ * For e.g. when sending email, this allows people to add attachments with the content
+ * editor interface and that they appear in the attachment list.
+ * The new documents being attached to the email, they will not be erased by the CRON
+ * when closing the wizard.
+ *
+ * @private
+ * @param {Object} attachments
+ */
+ _onAttachmentChange: function (attachments) {
+ if (!this.fieldNameAttachment) {
+ return;
+ }
+ this.trigger_up('field_changed', {
+ dataPointID: this.dataPointID,
+ changes: _.object([this.fieldNameAttachment], [{
+ operation: 'ADD_M2M',
+ ids: attachments
+ }])
+ });
+ },
+ /**
+ * @override
+ */
+ _renderEdit: function () {
+ var value = this._textToHtml(this.value);
+ if (this.nodeOptions.wrapper) {
+ value = this._wrap(value);
+ }
+ this.$target = $('<textarea>').val(value).hide();
+ this.$target.appendTo(this.$el);
+
+ var fieldNameAttachment = _.chain(this.recordData)
+ .pairs()
+ .find(function (value) {
+ return _.isObject(value[1]) && value[1].model === "ir.attachment";
+ })
+ .first()
+ .value();
+ if (fieldNameAttachment) {
+ this.fieldNameAttachment = fieldNameAttachment;
+ }
+
+ if (this.nodeOptions.cssEdit) {
+ // must be async because the target must be append in the DOM
+ this._createWysiwygIntance();
+ } else {
+ return this._createWysiwygIntance();
+ }
+ },
+ /**
+ * @override
+ */
+ _renderReadonly: function () {
+ var self = this;
+ var value = this._textToHtml(this.value);
+ if (this.nodeOptions.wrapper) {
+ value = this._wrap(value);
+ }
+
+ this.$el.empty();
+ var resolver;
+ var def = new Promise(function (resolve) {
+ resolver = resolve;
+ });
+ if (this.nodeOptions.cssReadonly) {
+ this.$iframe = $('<iframe class="o_readonly"/>');
+ this.$iframe.appendTo(this.$el);
+
+ var avoidDoubleLoad = 0; // this bug only appears on some computers with some chrome version.
+
+ // inject content in iframe
+
+ this.$iframe.data('loadDef', def); // for unit test
+ window.top[this._onUpdateIframeId] = function (_avoidDoubleLoad) {
+ if (_avoidDoubleLoad !== avoidDoubleLoad) {
+ console.warn('Wysiwyg iframe double load detected');
+ return;
+ }
+ self.$content = $('#iframe_target', self.$iframe[0].contentWindow.document.body);
+ resolver();
+ };
+
+ this.$iframe.on('load', function onLoad() {
+ var _avoidDoubleLoad = ++avoidDoubleLoad;
+ ajax.loadAsset(self.nodeOptions.cssReadonly).then(function (asset) {
+ if (_avoidDoubleLoad !== avoidDoubleLoad) {
+ console.warn('Wysiwyg immediate iframe double load detected');
+ return;
+ }
+ var cwindow = self.$iframe[0].contentWindow;
+ cwindow.document
+ .open("text/html", "replace")
+ .write(
+ '<head>' +
+ '<meta charset="utf-8"/>' +
+ '<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>\n' +
+ '<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>\n' +
+ _.map(asset.cssLibs, function (cssLib) {
+ return '<link type="text/css" rel="stylesheet" href="' + cssLib + '"/>';
+ }).join('\n') + '\n' +
+ _.map(asset.cssContents, function (cssContent) {
+ return '<style type="text/css">' + cssContent + '</style>';
+ }).join('\n') + '\n' +
+ '</head>\n' +
+ '<body class="o_in_iframe o_readonly">\n' +
+ '<div id="iframe_target">' + value + '</div>\n' +
+ '<script type="text/javascript">' +
+ 'if (window.top.' + self._onUpdateIframeId + ') {' +
+ 'window.top.' + self._onUpdateIframeId + '(' + _avoidDoubleLoad + ')' +
+ '}' +
+ '</script>\n' +
+ '</body>');
+
+ var height = cwindow.document.body.scrollHeight;
+ self.$iframe.css('height', Math.max(30, Math.min(height, 500)) + 'px');
+ });
+ });
+ } else {
+ this.$content = $('<div class="o_readonly"/>').html(value);
+ this.$content.appendTo(this.$el);
+ resolver();
+ }
+
+ def.then(function () {
+ self.$content.on('click', 'ul.o_checklist > li', self._onReadonlyClickChecklist.bind(self));
+ });
+ },
+ /**
+ * @private
+ * @param {string} text
+ * @returns {string} the text converted to html
+ */
+ _textToHtml: function (text) {
+ var value = text || "";
+ if (jinjaRegex.test(value)) { // is jinja
+ return value;
+ }
+ try {
+ $(text)[0].innerHTML; // crashes if text isn't html
+ } catch (e) {
+ if (value.match(/^\s*$/)) {
+ value = '<p><br/></p>';
+ } else {
+ value = "<p>" + value.split(/<br\/?>/).join("<br/></p><p>") + "</p>";
+ value = value
+ .replace(/<p><\/p>/g, '')
+ .replace('<p><p>', '<p>')
+ .replace('<p><p ', '<p ')
+ .replace('</p></p>', '</p>');
+ }
+ }
+ return value;
+ },
+ /**
+ * Move HTML contents out of their wrapper.
+ *
+ * @private
+ * @param {string} html content
+ * @returns {string} html content
+ */
+ _unWrap: function (html) {
+ var $wrapper = $(html).find('#wrapper');
+ return $wrapper.length ? $wrapper.html() : html;
+ },
+ /**
+ * Wrap HTML in order to create a custom display.
+ *
+ * The wrapper (this.nodeOptions.wrapper) must be a static
+ * XML template with content id="wrapper".
+ *
+ * @private
+ * @param {string} html content
+ * @returns {string} html content
+ */
+ _wrap: function (html) {
+ return $(QWeb.render(this.nodeOptions.wrapper))
+ .find('#wrapper').html(html)
+ .end().prop('outerHTML');
+ },
+
+ //--------------------------------------------------------------------------
+ // Handler
+ //--------------------------------------------------------------------------
+
+ /**
+ * Method called when wysiwyg triggers a change.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onChange: function (ev) {
+ this._doDebouncedAction.apply(this, arguments);
+
+ var $lis = this.$content.find('.note-editable ul.o_checklist > li:not(:has(> ul.o_checklist))');
+ if (!$lis.length) {
+ return;
+ }
+ var max = 0;
+ var ids = [];
+ $lis.map(function () {
+ var checklistId = parseInt(($(this).attr('id') || '0').replace(/^checklist-id-/, ''));
+ if (ids.indexOf(checklistId) === -1) {
+ if (checklistId > max) {
+ max = checklistId;
+ }
+ ids.push(checklistId);
+ } else {
+ $(this).removeAttr('id');
+ }
+ });
+ $lis.not('[id]').each(function () {
+ $(this).attr('id', 'checklist-id-' + (++max));
+ });
+ },
+ /**
+ * Allows Enter keypress in a textarea (source mode)
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onKeydown: function (ev) {
+ if (ev.which === $.ui.keyCode.ENTER) {
+ ev.stopPropagation();
+ return;
+ }
+ this._super.apply(this, arguments);
+ },
+ /**
+ * Method called when wysiwyg triggers a change.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onReadonlyClickChecklist: function (ev) {
+ var self = this;
+ if (ev.offsetX > 0) {
+ return;
+ }
+ ev.stopPropagation();
+ ev.preventDefault();
+ var checked = $(ev.target).hasClass('o_checked');
+ var checklistId = parseInt(($(ev.target).attr('id') || '0').replace(/^checklist-id-/, ''));
+
+ this._rpc({
+ route: '/web_editor/checklist',
+ params: {
+ res_model: this.model,
+ res_id: this.res_id,
+ filename: this.name,
+ checklistId: checklistId,
+ checked: !checked,
+ },
+ }).then(function (value) {
+ self._setValue(value);
+ });
+ },
+ /**
+ * Method called when the wysiwyg instance is loaded.
+ *
+ * @private
+ */
+ _onLoadWysiwyg: function () {
+ var $button = this._renderTranslateButton();
+ $button.css({
+ 'font-size': '15px',
+ position: 'absolute',
+ right: '+5px',
+ top: '+5px',
+ });
+ this.$el.append($button);
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onWysiwygBlur: function (ev) {
+ ev.stopPropagation();
+ this._doAction();
+ if (ev.data.key === 'TAB') {
+ this.trigger_up('navigation_move', {
+ direction: ev.data.shiftKey ? 'left' : 'right',
+ });
+ }
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onWysiwygFocus: function (ev) {},
+});
+
+
+field_registry.add('html', FieldHtml);
+
+
+return FieldHtml;
+});