diff --git a/addons/mass_mailing/static/src/js/mass_mailing_widget.js b/addons/mass_mailing/static/src/js/mass_mailing_widget.js index 812aec0e909..a9705c7f1a1 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing_widget.js +++ b/addons/mass_mailing/static/src/js/mass_mailing_widget.js @@ -92,35 +92,6 @@ var MassMailingFieldHtml = FieldHtml.extend({ // Private //-------------------------------------------------------------------------- - /** - * Add generateOptions options to change wysiwyg configuration: - * - Remove table menu in toolbar - * - Remove table popover - * - Add rules for isEditableNode, the TD must content a not TABLE node as - * children to be editable. - * - * @override - */ - _getWysiwygOptions: function () { - var options = this._super(); - options.generateOptions = function (options) { - options.toolbar = _.filter(options.toolbar, function (item) { - return item[0] !== 'table'; - }); - delete options.popover.table; - - var isEditableNode = options.isEditableNode; - options.isEditableNode = function (node) { - if (node.tagName === "TD" && !$(node).children('*:not(table):first').length) { - return false; - } - return isEditableNode.call(this, node); - }; - - return options; - }; - return options; - }, /** * Returns true if must force the user to choose a theme. * @@ -293,12 +264,12 @@ var MassMailingFieldHtml = FieldHtml.extend({ } else if ($old_layout.length) { $contents = ($old_layout.hasClass('oe_structure') ? $old_layout : $old_layout.find('.oe_structure').first()).contents(); } else { - $contents = this.$content.find('.note-editable').contents(); + $contents = this.$content.find('.o_editable').contents(); } $newWrapperContent.append($contents); this._switchImages(themeParams, $newWrapperContent); - this.$content.find('.note-editable').empty().append($newLayout); + this.$content.find('.o_editable').empty().append($newLayout); $old_layout.remove(); if (firstChoice) { @@ -326,6 +297,9 @@ var MassMailingFieldHtml = FieldHtml.extend({ if (this._isFromInline) { this._fromInline(); } + if (this.snippetsLoaded) { + this._onSnippetsLoaded(this.snippetsLoaded); + } this._super(); }, /** @@ -334,6 +308,10 @@ var MassMailingFieldHtml = FieldHtml.extend({ */ _onSnippetsLoaded: function (ev) { var self = this; + if (!this.$content) { + this.snippetsLoaded = ev; + return; + } var $snippetsSideBar = ev.data; var $themes = $snippetsSideBar.find("#email_designer_themes").children(); var $snippets = $snippetsSideBar.find(".oe_snippet"); @@ -341,7 +319,6 @@ var MassMailingFieldHtml = FieldHtml.extend({ if (config.device.isMobile) { $snippetsSideBar.hide(); - console.log(this.$content[0]); this.$content.attr('style', 'padding-left: 0px !important'); } diff --git a/addons/mass_mailing/static/src/scss/mass_mailing.ui.scss b/addons/mass_mailing/static/src/scss/mass_mailing.ui.scss index 297b241070c..7aec2b165e9 100644 --- a/addons/mass_mailing/static/src/scss/mass_mailing.ui.scss +++ b/addons/mass_mailing/static/src/scss/mass_mailing.ui.scss @@ -35,17 +35,23 @@ .dropdown-menu { position: absolute; - bottom: -1px; + bottom: -2px; top: -3px !important; left: -5px !important; - right: 3px !important; + right: -2px !important; margin: 0; border-radius: 0; overflow: auto; background-color: $o-wysiwyg-bg-color; z-index: 1050; - .dropdown-item { + &.show { + bottom: 31px !important; + top: -2px !important; + right: 3px !important; + } + + .dropdown-item { padding: 10px 10px; &:first-child { display: none; @@ -182,6 +188,13 @@ body.o_force_mail_theme_choice { overflow: initial; } +#web_editor-toolbars .popover .note-color { + .note-back-color-preview .dropdown-toggle, + .note-fore-color-preview .dropdown-toggle { + height: $o-wysiwyg-toolbar-height - 1; + } +} + .oe_structure { width: 100%; } diff --git a/addons/mass_mailing/static/src/scss/themes/theme_default.scss b/addons/mass_mailing/static/src/scss/themes/theme_default.scss index 4d49a7ef22c..13bc9d56894 100644 --- a/addons/mass_mailing/static/src/scss/themes/theme_default.scss +++ b/addons/mass_mailing/static/src/scss/themes/theme_default.scss @@ -56,7 +56,6 @@ td:not([align]) { > .o_mail_wrapper { width: 100%; - margin: 24px 0; border-collapse: separate; // Allow the first and last td to have the // same width diff --git a/addons/mass_mailing/views/assets.xml b/addons/mass_mailing/views/assets.xml index 46a6cfcb5dc..d2645ef6c14 100644 --- a/addons/mass_mailing/views/assets.xml +++ b/addons/mass_mailing/views/assets.xml @@ -32,9 +32,6 @@ diff --git a/addons/web_editor/static/src/js/editor/transcoder.js b/addons/web_editor/static/src/js/backend/convert_inline.js similarity index 83% rename from addons/web_editor/static/src/js/editor/transcoder.js rename to addons/web_editor/static/src/js/backend/convert_inline.js index 745e8492b11..4482a8f67ba 100644 --- a/addons/web_editor/static/src/js/editor/transcoder.js +++ b/addons/web_editor/static/src/js/backend/convert_inline.js @@ -1,9 +1,7 @@ -odoo.define('web_editor.transcoder', function (require) { +odoo.define('web_editor.convertInline', function (require) { 'use strict'; -var base = require('web_editor.base'); - -var rulesCache = []; +var FieldHtml = require('web_editor.field.html'); /** * Returns the css rules which applies on an element, tweaked so that they are @@ -14,8 +12,11 @@ var rulesCache = []; */ function getMatchedCSSRules(a) { var i, r, k; + var doc = a.ownerDocument; + var rulesCache = a.ownerDocument._rulesCache || (a.ownerDocument._rulesCache = []); + if (!rulesCache.length) { - var sheets = document.styleSheets; + 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 @@ -187,11 +188,13 @@ function getMatchedCSSRules(a) { * 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(base.fontIcons, function (font) { - return _.find(base.getCssSelectors(font.parser), function (data) { + _.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]; @@ -265,9 +268,6 @@ function applyOverDescendants(node, func) { * @param {jQuery} $editable */ function classToStyle($editable) { - if (!rulesCache.length) { - getMatchedCSSRules($editable[0]); - } applyOverDescendants($editable[0], function (node) { var $target = $(node); var css = getMatchedCSSRules(node); @@ -328,9 +328,7 @@ function styleToClass($editable) { $(this).after($('a,img', this)); }).remove(); - getMatchedCSSRules($editable[0]); - - var $c = $('').appendTo(document.body); + var $c = $('').appendTo($editable[0].ownerDocument.body); applyOverDescendants($editable[0], function (node) { var $target = $(node); @@ -342,7 +340,7 @@ function styleToClass($editable) { } }); css = ($c.attr('style', style).attr('style') || '').split(/\s*;\s*/); - style = $target.attr('style') || ''; + style = ($target.attr('style') || '').replace(/\s*:\s*/, ':').replace(/\s*;\s*/, ';'); _.each(css, function (v) { style = style.replace(v, ''); }); @@ -364,13 +362,13 @@ function styleToClass($editable) { * @param {jQuery} $editable */ function attachmentThumbnailToLinkImg($editable) { - $editable.find('a[href*="/web/content/"][data-mimetype]:empty').each(function () { + $editable.find('a[href*="/web/content/"][data-mimetype]').filter(':empty, :containsExact( )').each(function () { var $link = $(this); var $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.append($img); + $link.prepend($img); }); } @@ -384,6 +382,83 @@ 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(); + $editable.html(html); + + attachmentThumbnailToLinkImg($editable); + fontToImg($editable); + classToStyle($editable); + 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); + 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, @@ -392,4 +467,4 @@ return { 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 index f6d86c9020c..0be20743132 100644 --- a/addons/web_editor/static/src/js/backend/field_html.js +++ b/addons/web_editor/static/src/js/backend/field_html.js @@ -179,8 +179,6 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { * @returns {Object} */ _getWysiwygOptions: function () { - var iPhone = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; - return Object.assign({}, this.nodeOptions, { recordInfo: { context: this.record.getContext(this.recordParams), @@ -193,6 +191,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { snippets: this.nodeOptions.snippets, tabsize: 0, + height: 180, generateOptions: function (options) { var para = _.find(options.toolbar, function (item) { return item[0] === 'para'; diff --git a/addons/web_editor/static/src/js/editor/editor.js b/addons/web_editor/static/src/js/editor/editor.js index dc0f53c8e57..07947db56e4 100644 --- a/addons/web_editor/static/src/js/editor/editor.js +++ b/addons/web_editor/static/src/js/editor/editor.js @@ -6,6 +6,7 @@ var Widget = require('web.Widget'); var core = require('web.core'); var rte = require('web_editor.rte'); var snippetsEditor = require('web_editor.snippet.editor'); +var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors'); var _t = core._t; @@ -26,7 +27,7 @@ var EditorMenuBar = Widget.extend({ * * @constructor */ - init: function (parent) { + init: function (parent, options) { var self = this; var res = this._super.apply(this, arguments); var Editor = options.Editor || rte.Class; @@ -48,8 +49,6 @@ var EditorMenuBar = Widget.extend({ var $editable = this.rte.editable(); window.__EditorMenuBar_$editable = $editable; // TODO remove this hack asap - - var options = this.getParent().params; if (options.snippets) { this.snippetsMenu = new snippetsEditor.Class(this, Object.assign({ $el: $editable, @@ -185,6 +184,32 @@ var EditorMenuBar = Widget.extend({ // Private //-------------------------------------------------------------------------- + /** + * @private + */ + _getDefaultConfig: function ($editable) { + return { + 'airMode' : true, + 'focus': false, + 'airPopover': [ + ['style', ['style']], + ['font', ['bold', 'italic', 'underline', 'clear']], + ['fontsize', ['fontsize']], + ['color', ['color']], + ['para', ['ul', 'ol', 'paragraph']], + ['table', ['table']], + ['insert', ['link', 'picture']], + ['history', ['undo', 'redo']], + ], + 'styleWithSpan': false, + 'inlinemedia' : ['p'], + 'lang': 'odoo', + 'onChange': function (html, $editable) { + $editable.trigger('content_changed'); + }, + 'colors': summernoteCustomColors, + }; + }, /** * Reloads the page in non-editable mode, with the right scrolling. * 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 2f2e55a0084..4fba95f2490 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -67,7 +67,7 @@ var SnippetEditor = Widget.extend({ defs.push(this._initializeOptions()); // Initialize move/clone/remove buttons - if (!this.options.isEditableNode(this.$target[0])) { + if (!this.$target.is(':o_editable')) { this.$el.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove(); } else { this.dropped = false; @@ -1249,6 +1249,7 @@ var SnippetsMenu = Widget.extend({ this.$el.addClass('o_loaded'); $('body.editor_enable').addClass('editor_has_snippets'); + this.trigger_up('snippets_loaded', self.$el); }, /** * Creates a snippet editor to associated to the given snippet. If the given diff --git a/addons/web_editor/static/src/js/wysiwyg/root.js b/addons/web_editor/static/src/js/wysiwyg/root.js index ab531d086f7..c2b3d703733 100644 --- a/addons/web_editor/static/src/js/wysiwyg/root.js +++ b/addons/web_editor/static/src/js/wysiwyg/root.js @@ -47,7 +47,7 @@ var WysiwygRoot = Widget.extend({ }); return instance.attachTo($target).then(function () { - self.$editor = instance.$el; + self.$editor = instance.$editor || instance.$el; }); }); }, diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index cb0db917e54..d33f5455da4 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -2,7 +2,6 @@ odoo.define('web_editor.wysiwyg', function (require) { 'use strict'; var Widget = require('web.Widget'); var SummernoteManager = require('web_editor.rte.summernote'); -var transcoder = require('web_editor.transcoder'); var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors'); var id = 0; @@ -32,28 +31,26 @@ var Wysiwyg = Widget.extend({ }, }, /** - * @params {Object} params - * @params {Object} params.recordInfo - * @params {Object} params.recordInfo.context - * @params {String} [params.recordInfo.context] - * @params {integer} [params.recordInfo.res_id] - * @params {String} [params.recordInfo.data_res_model] - * @params {integer} [params.recordInfo.data_res_id] + * @options {Object} options + * @options {Object} options.recordInfo + * @options {Object} options.recordInfo.context + * @options {String} [options.recordInfo.context] + * @options {integer} [options.recordInfo.res_id] + * @options {String} [options.recordInfo.data_res_model] + * @options {integer} [options.recordInfo.data_res_id] * @see _onGetRecordInfo * @see _getAttachmentsDomain in /wysiwyg/widgets/media.js - * @params {Object} params.attachments + * @options {Object} options.attachments * @see _onGetRecordInfo * @see _getAttachmentsDomain in /wysiwyg/widgets/media.js (for attachmentIDs) - * @params {function} params.generateOptions + * @options {function} options.generateOptions * called with the summernote configuration object used before sending to summernote * @see _editorOptions **/ - init: function (parent, params) { + init: function (parent, options) { this._super.apply(this, arguments); - this.params = params; - this.params.isEditableNode = function (node) { - return $(node).is(':o_editable'); - }; + this.id = ++id; + this.options = options; }, /** * Load assets and color picker template then call summernote API @@ -64,14 +61,13 @@ var Wysiwyg = Widget.extend({ willStart: function () { new SummernoteManager(this); this.$target = this.$el; - return this._super.apply(this, arguments); + return this._super(); }, /** * * @override */ start: function () { - var self = this; this.$target.wrap(''); this.$el = this.$target.parent(); var options = this._editorOptions(); @@ -80,7 +76,6 @@ var Wysiwyg = Widget.extend({ this.$editor.data('wysiwyg', this); this.$editor.data('oe-model', options.recordInfo.res_model); this.$editor.data('oe-id', options.recordInfo.res_id); - var $wysiwyg = this.$editor.closest('odoo-wysiwyg-container'); $(document).on('mousedown', this._blur); this._value = this.$target.html() || this.$target.val(); return this._super.apply(this, arguments).then(() => { @@ -106,7 +101,7 @@ var Wysiwyg = Widget.extend({ * @returns {jQuery} */ getEditable: function () { - console.log('getEditable'); + return this.$editor; }, /** * Return true if the content has changed. @@ -116,27 +111,6 @@ var Wysiwyg = Widget.extend({ isDirty: function () { return this._value !== (this.$editor.html() || this.$editor.val()); }, - /** - * Return true if the current node is unbreakable. - * An unbreakable node can be removed or added but can't by split into - * different nodes (for keypress and selection). - * An unbreakable node can contain nodes that can be edited. - * - * @param {Node} node - * @returns {Boolean} - */ - isUnbreakableNode: function (node) { - console.log('isUnbreakableNode'); - }, - /** - * Return true if the current node is editable (for keypress and selection). - * - * @param {Node} node - * @returns {Boolean} - */ - isEditableNode: function (node) { - console.log('isEditableNode'); - }, /** * Set the focus on the element. */ @@ -193,12 +167,6 @@ var Wysiwyg = Widget.extend({ this.$target.html(value); } this.$editor.html(value); - if (this.params['style-inline']) { - transcoder.styleToClass(this.$editor); - transcoder.imgToFont(this.$editor); - transcoder.linkImgToAttachmentThumbnail(this.$editor); - } - this._value = (this.$target.html() || this.$target.val()); }, //-------------------------------------------------------------------------- // Private @@ -246,8 +214,21 @@ Wysiwyg.getRange = function (node) { eo: range.eo, }; }; -Wysiwyg.setRange = function () { - console.log('setRange'); +/** + * @param {Node} startNode + * @param {Number} startOffset + * @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 diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js new file mode 100644 index 00000000000..64b7c6a8f8c --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js @@ -0,0 +1,136 @@ +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 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) { + if (!this.options.iframeCssAssets) { + this.options.iframeCssAssets = 'web_editor.wysiwyg_iframe_css_assets'; + } + this._onUpdateIframeId = 'onLoad_' + this.id; + } + }, + /** + * Load assets to inject into iframe. + * + * @override + **/ + willStart: function () { + if (!this.options.inIframe) { + return this._super(); + } + var debug = odoo.debug; + odoo.debug = false; + + 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'); + odoo.debug = debug; + + 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 = $('