From 912996fe902c41cafa12dcfe09713ce9857f95ab Mon Sep 17 00:00:00 2001 From: Gorash <chm@odoo.com> Date: Tue, 20 Aug 2019 07:41:18 +0000 Subject: [PATCH] [REF] web_editor, website: adapt wysiwyg to 12.0 editor The wysiwyg abstraction layer was introduced in saas-12.2 by commit f2969923. Its goal was to provide an external interface for other modules that wanted to use the editor feature such that the editor core itself could be changed without requiring extensive changes in the other modules. This commit is adapting the wysiwyg interface to instantiate the 12.0 editor below it rather than the saas-12.2 one, thus reverting the "new editor" part of f2969923 itself. Part of PR 35677. Co-authored-by: Nicolas Bayet <nby@odoo.com> Co-authored-by: Antoine Guenet <age@odoo.com> Co-authored-by: Christophe Matthieu <chm@odoo.com> Co-authored-by: David Monjoie <dmo@odoo.com> --- addons/web_editor/controllers/main.py | 2 + .../static/src/js/backend/field_html.js | 28 +- addons/web_editor/static/src/js/base.js | 11 +- addons/web_editor/static/src/js/common/ace.js | 270 ++++--- .../web_editor/static/src/js/editor/editor.js | 64 +- addons/web_editor/static/src/js/editor/rte.js | 9 +- .../static/src/js/editor/rte.summernote.js | 57 +- .../static/src/js/editor/snippets.editor.js | 334 ++++++--- .../static/src/js/editor/snippets.options.js | 35 +- .../static/src/js/editor/summernote.js | 68 +- .../static/src/js/wysiwyg/widgets/media.js | 7 +- .../src/js/wysiwyg/widgets/media_dialog.js | 47 +- .../static/src/js/wysiwyg/wysiwyg.js | 681 +++--------------- .../static/src/scss/web_editor.backend.scss | 26 + .../static/src/scss/web_editor.common.scss | 11 + .../web_editor/static/src/scss/wysiwyg.scss | 6 + addons/web_editor/static/src/xml/backend.xml | 8 + addons/web_editor/static/src/xml/editor.xml | 9 + .../static/tests/field_html_tests.js | 7 +- addons/web_editor/static/tests/test_utils.js | 34 - .../src/js/running_tour_action_helper.js | 2 +- addons/web_unsplash/models/ir_qweb.py | 2 +- .../static/src/js/editor/editor_menu.js | 8 +- .../static/src/js/editor/snippets.options.js | 2 +- .../static/src/js/editor/wysiwyg_multizone.js | 558 +------------- .../js/editor/wysiwyg_multizone_translate.js | 93 +-- addons/website/static/src/js/menu/edit.js | 37 +- .../static/src/scss/website.wysiwyg.scss | 78 +- addons/website/static/tests/tours/rte.js | 18 +- .../static/src/js/tours/website_blog.js | 2 +- .../static/src/js/website_forum.js | 2 +- .../views/website_mass_mailing_templates.xml | 2 +- .../static/src/js/tours/website_sale_shop.js | 2 +- addons/website_sale/views/templates.xml | 5 +- 34 files changed, 916 insertions(+), 1609 deletions(-) diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index bf169fac138d..bc053a743ba3 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -147,6 +147,8 @@ class Web_Editor(http.Controller): allSelected = False node = ul.getprevious() + if node is None: + node = ul.getparent().getprevious() if node is not None and node.tag == 'li': self._update_checklist_recursive(node, allSelected, ancestors=True) 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 5ba630edc89f..f6d86c9020c6 100644 --- a/addons/web_editor/static/src/js/backend/field_html.js +++ b/addons/web_editor/static/src/js/backend/field_html.js @@ -167,7 +167,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { // 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; + self.$content = self.wysiwyg.$editor.closest('body, odoo-wysiwyg-container'); self._onLoadWysiwyg(); self.isRendered = true; }); @@ -181,7 +181,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { _getWysiwygOptions: function () { var iPhone = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; - return { + return Object.assign({}, this.nodeOptions, { recordInfo: { context: this.record.getContext(this.recordParams), res_model: this.model, @@ -192,26 +192,17 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { iframeCssAssets: this.nodeOptions.cssEdit, snippets: this.nodeOptions.snippets, - tabSize: 0, - keyMap: { - pc: { - 'TAB': null, - 'SHIFT+TAB': null, - }, - mac: { - 'ENTER': iPhone ? null : 'insertParagraph', - 'TAB': null, - 'SHIFT+TAB': null, - }, - }, + tabsize: 0, generateOptions: function (options) { var para = _.find(options.toolbar, function (item) { return item[0] === 'para'; }); - para[1].splice(2, 0, 'checklist'); + if (para && para[1] && para[1].indexOf('checklist') === -1) { + para[1].splice(2, 0, 'checklist'); + } return options; }, - }; + }); }, /** * trigger_up 'field_changed' add record into the "ir.attachment" field found in the view. @@ -469,10 +460,9 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { 'font-size': '15px', position: 'absolute', right: '+5px', + top: '+5px', }); - var $toolbar = this.$content.find('.note-toolbar'); - $toolbar.css('position', 'relative'); - $toolbar.append($button); + this.$el.append($button); }, /** * @private diff --git a/addons/web_editor/static/src/js/base.js b/addons/web_editor/static/src/js/base.js index d69cace31005..4eb0da20fab5 100644 --- a/addons/web_editor/static/src/js/base.js +++ b/addons/web_editor/static/src/js/base.js @@ -4,8 +4,9 @@ odoo.define('web_editor.base', function (require) { var ajax = require('web.ajax'); var session = require('web.session'); -var domReady = $.Deferred(); -$(domReady.resolve.bind(domReady)); +var domReady = new Promise(function(resolve) { + $(resolve); +}); return { /** @@ -103,7 +104,7 @@ return { }), /** * If a widget needs to be instantiated on page loading, it needs to wait - * for appropriate resources to be loaded. This function returns a Deferred + * for appropriate resources to be loaded. This function returns a Promise * which is resolved when the dom is ready, the session is bound * (translations loaded) and the XML is loaded. This should however not be * necessary anymore as widgets should not be parentless and should then be @@ -112,10 +113,10 @@ return { * component is in charge of waiting for the session and the XML can be * lazy loaded thanks to the @see Widget.xmlDependencies key. * - * @returns {Deferred} + * @returns {Promise} */ ready: function () { - return $.when(domReady, session.is_bound, ajax.loadXML()); + return Promise.all([domReady, session.is_bound, ajax.loadXML()]); }, }; }); diff --git a/addons/web_editor/static/src/js/common/ace.js b/addons/web_editor/static/src/js/common/ace.js index 37c21f2aa369..dc2d78a53c7a 100644 --- a/addons/web_editor/static/src/js/common/ace.js +++ b/addons/web_editor/static/src/js/common/ace.js @@ -2,13 +2,12 @@ odoo.define('web_editor.ace', function (require) { 'use strict'; var ajax = require('web.ajax'); +var config = require('web.config'); var concurrency = require('web.concurrency'); var core = require('web.core'); var Dialog = require('web.Dialog'); var Widget = require('web.Widget'); -var weContext = require('web_editor.context'); var localStorage = require('web.local_storage'); -var session = require('web.session'); var _t = core._t; @@ -120,20 +119,22 @@ var ViewEditor = Widget.extend({ jsLibs: [ '/web/static/lib/ace/ace.js', [ + '/web/static/lib/ace/javascript_highlight_rules.js', '/web/static/lib/ace/mode-xml.js', '/web/static/lib/ace/mode-scss.js', + '/web/static/lib/ace/mode-js.js', '/web/static/lib/ace/theme-monokai.js' ] ], events: { 'click .o_ace_type_switcher_choice': '_onTypeChoice', 'change .o_res_list': '_onResChange', - 'click .js_include_bundles': '_onIncludeBundlesChange', - 'click .js_include_all_scss': '_onIncludeAllSCSSChange', + 'click .o_ace_filter': '_onFilterChange', 'click button[data-action=save]': '_onSaveClick', 'click button[data-action=reset]': '_onResetClick', 'click button[data-action=format]': '_onFormatClick', 'click button[data-action=close]': '_onCloseClick', + 'click #ace-view-id > .alert-warning .close': '_onCloseWarningClick' }, /** @@ -151,30 +152,35 @@ var ViewEditor = Widget.extend({ * @param {string} [options.position=right] * @param {boolean} [options.doNotLoadViews=false] * @param {boolean} [options.doNotLoadSCSS=false] + * @param {boolean} [options.doNotLoadJS=false] * @param {boolean} [options.includeBundles=false] - * @param {boolean} [options.includeAllSCSS=false] + * @param {string} [options.filesFilter=custom] * @param {string[]} [options.defaultBundlesRestriction] */ init: function (parent, viewKey, options) { this._super.apply(this, arguments); + this.context = options.context; + this.viewKey = viewKey; this.options = _.defaults({}, options, { position: 'right', doNotLoadViews: false, doNotLoadSCSS: false, + doNotLoadJS: false, includeBundles: false, - includeAllSCSS: false, + filesFilter: 'custom', defaultBundlesRestriction: [], }); - this.resources = {xml: {}, scss: {}}; - this.editingSessions = {xml: {}, scss: {}}; + this.resources = {xml: {}, scss: {}, js: {}}; + this.editingSessions = {xml: {}, scss: {}, js: {}}; this.currentType = 'xml'; // Alias this.views = this.resources.xml; this.scss = this.resources.scss; + this.js = this.resources.js; }, /** * Loads everything the ace library needs to work. @@ -183,11 +189,10 @@ var ViewEditor = Widget.extend({ * @override */ willStart: function () { - return $.when( + return Promise.all([ this._super.apply(this, arguments), - ajax.loadLibs(this), this._loadResources() - ); + ]); }, /** * Initializes the library and initial view once the DOM is ready. It also @@ -203,11 +208,13 @@ var ViewEditor = Widget.extend({ this.$lists = { xml: this.$('#ace-view-list'), - scss: this.$('#ace-scss-list') + scss: this.$('#ace-scss-list'), + js: this.$('#ace-js-list'), }; this.$includeBundlesArea = this.$('.oe_include_bundles'); this.$includeAllSCSSArea = this.$('.o_include_all_scss'); this.$viewID = this.$('#ace-view-id > span'); + this.$warningMessage = this.$('#ace-view-id > .alert-warning'); this.$formatButton = this.$('button[data-action=format]'); this.$resetButton = this.$('button[data-action=reset]'); @@ -227,12 +234,24 @@ var ViewEditor = Widget.extend({ var initType; if (this.options.initialResID) { initResID = this.options.initialResID; - initType = (_.isString(initResID) && initResID[0] === '/') ? 'scss' : 'xml'; + if (_.isString(initResID) && initResID[0] === '/') { + if (_.str.endsWith(initResID, '.scss')) { + initType = 'scss'; + } else { + initType = 'js'; + } + } else { + initType = 'xml'; + } } else { if (!this.options.doNotLoadSCSS) { initResID = this.sortedSCSS[0][1][0].url; // first bundle, scss files, first one initType = 'scss'; } + if (!this.options.doNotLoadJS) { + initResID = this.sortedJS[0][1][0].url; // first bundle, js files, first one + initType = 'js'; + } if (!this.options.doNotLoadViews) { if (typeof this.viewKey === "number") { initResID = this.viewKey; @@ -320,7 +339,7 @@ var ViewEditor = Widget.extend({ * Initializes a text editor for the specified resource. * * @private - * @param {integer|string} resID - the ID/URL of the view/scss file + * @param {integer|string} resID - the ID/URL of the view/scss/js file * @param {string} [type] (default to the currently selected one) * @returns {ace.EditSession} */ @@ -340,13 +359,13 @@ var ViewEditor = Widget.extend({ return editingSession; }, /** - * Forces the view/scss file identified by its ID/URL to be displayed in the + * Forces the view/scss/js file identified by its ID/URL to be displayed in the * editor. The method will update the resource select DOM element as well if * necessary. * * @private * @param {integer|string} resID - * @param {string} [type] - the type of resource (either 'xml' or 'scss') + * @param {string} [type] - the type of resource (either 'xml', 'scss' or 'js') */ _displayResource: function (resID, type) { if (type) { @@ -359,14 +378,29 @@ var ViewEditor = Widget.extend({ } this.aceEditor.setSession(editingSession); + var isCustomized = false; if (this.currentType === 'xml') { this.$viewID.text(_.str.sprintf(_t("Template ID: %s"), this.views[resID].key)); - } else { + } else if (this.currentType === 'scss') { + isCustomized = this.scss[resID].customized; this.$viewID.text(_.str.sprintf(_t("SCSS file: %s"), resID)); + } else { + isCustomized = this.js[resID].customized; + this.$viewID.text(_.str.sprintf(_t("JS file: %s"), resID)); } this.$lists[this.currentType].select2('val', resID); - this.$resetButton.toggleClass('d-none', this.currentType === 'xml' || !this.scss[resID].customized); + this.$resetButton.toggleClass('d-none', this.currentType === 'xml' || !isCustomized); + + // TODO the warning message is always shown for XML templates but: + // 1) We have to implement a way to be able to reset XML templates + // otherwise the warning message is not accurate + // 2) We should be able to detect if the XML template is customized to + // not show the warning in that case + this.$warningMessage.toggleClass('d-none', + this.currentType !== 'xml' && (resID.indexOf('/user_custom_') >= 0 || isCustomized)); + + this.aceEditor.resize(true); }, /** * Formats the current resource being vizualized. @@ -398,14 +432,15 @@ var ViewEditor = Widget.extend({ * is loading the activate views, index them and build their hierarchy. * * @private - * @returns {Deferred} + * @returns {Promise} */ _loadResources: function () { // Reset resources - this.resources = {xml: {}, scss: {}}; - this.editingSessions = {xml: {}, scss: {}}; + this.resources = {xml: {}, scss: {}, js: {}}; + this.editingSessions = {xml: {}, scss: {}, js: {}}; this.views = this.resources.xml; this.scss = this.resources.scss; + this.js = this.resources.js; // Load resources return this._rpc({ @@ -414,12 +449,15 @@ var ViewEditor = Widget.extend({ key: this.viewKey, get_views: !this.options.doNotLoadViews, get_scss: !this.options.doNotLoadSCSS, + get_js: !this.options.doNotLoadJS, bundles: this.options.includeBundles, - bundles_restriction: this.options.includeAllSCSS ? [] : this.options.defaultBundlesRestriction, + bundles_restriction: this.options.filesFilter === 'all' ? [] : this.options.defaultBundlesRestriction, + only_user_custom_files: this.options.filesFilter === 'custom', }, }).then((function (resources) { _processViews.call(this, resources.views || []); - _processSCSS.call(this, resources.scss || []); + _processJSorSCSS.call(this, resources.scss || [], 'scss'); + _processJSorSCSS.call(this, resources.js || [], 'js'); }).bind(this)); function _processViews(views) { @@ -460,20 +498,24 @@ var ViewEditor = Widget.extend({ }); } - function _processSCSS(scss) { - // The received scss data is already sorted by bundle and DOM order - this.sortedSCSS = scss; + function _processJSorSCSS(data, type) { + // The received scss or js data is already sorted by bundle and DOM order + if (type === 'scss') { + this.sortedSCSS = data; + } else { + this.sortedJS = data; + } // Store the URL ungrouped by bundle and use the URL as key (resource ID) - var self = this; - _.each(scss, function (bundleInfos) { + var resources = type === 'scss' ? this.scss : this.js; + _.each(data, function (bundleInfos) { _.each(bundleInfos[1], function (info) { info.bundle_xmlid = bundleInfos[0].xmlid; }); - _.extend(self.scss, _.indexBy(bundleInfos[1], 'url')); + _.extend(resources, _.indexBy(bundleInfos[1], 'url')); }); } }, /** - * Forces the view/scss file identified by its ID/URL to be reset to the way + * Forces the view/scss/js file identified by its ID/URL to be reset to the way * it was before the user started editing it. * * @todo views reset is not supported yet @@ -481,59 +523,57 @@ var ViewEditor = Widget.extend({ * @private * @param {integer|string} [resID] (default to the currently selected one) * @param {string} [type] (default to the currently selected one) - * @returns {Deferred} + * @returns {Promise} */ _resetResource: function (resID, type) { resID = resID || this._getSelectedResource(); type = type || this.currentType; if (this.currentType === 'xml') { - return $.Defered().reject(_t("Reseting views is not supported yet")); + return Promise.reject(_t("Reseting views is not supported yet")); } else { + var resource = type === 'scss' ? this.scss[resID] : this.js[resID]; return this._rpc({ - route: '/web_editor/reset_scss', + route: '/web_editor/reset_asset', params: { url: resID, - bundle_xmlid: this.scss[resID].bundle_xmlid, + bundle_xmlid: resource.bundle_xmlid, }, }); } }, /** - * Saves an unique SCSS file. + * Saves a unique SCSS or JS file. * * @private * @param {Object} session - contains the 'id' (url) and the 'text' of the - * SCSS file to save. - * @return {Deferred} status indicates if the save is finished or if an + * SCSS or JS file to save. + * @return {Promise} status indicates if the save is finished or if an * error occured. */ - _saveSCSS: function (session) { - var def = $.Deferred(); - + _saveSCSSorJS: function (session) { var self = this; - this._rpc({ - route: '/web_editor/save_scss', + var sessionIdEndsWithJS = _.string.endsWith(session.id, '.js'); + var bundleXmlID = sessionIdEndsWithJS ? this.js[session.id].bundle_xmlid : this.scss[session.id].bundle_xmlid; + var fileType = sessionIdEndsWithJS ? 'js' : 'scss'; + return self._rpc({ + route: '/web_editor/save_asset', params: { url: session.id, - bundle_xmlid: this.scss[session.id].bundle_xmlid, + bundle_xmlid: bundleXmlID, content: session.text, + file_type: fileType, }, }).then(function () { - self._toggleDirtyInfo(session.id, 'scss', false); - def.resolve(); - }, function (source, error) { - def.reject(session, error); + self._toggleDirtyInfo(session.id, fileType, false); }); - - return def; }, /** * Saves every resource that has been modified. If one cannot be saved, none * is saved and an error message is displayed. * * @private - * @return {Deferred} status indicates if the save is finished or if an + * @return {Promise} status indicates if the save is finished or if an * error occured. */ _saveResources: function () { @@ -562,7 +602,7 @@ var ViewEditor = Widget.extend({ } } }).bind(this)); - if (errorFound) return $.Deferred().reject(errorFound); + if (errorFound) return Promise.reject(errorFound); var defs = []; var mutex = new concurrency.Mutex(); @@ -571,13 +611,15 @@ var ViewEditor = Widget.extend({ _toSave = _.sortBy(_toSave, 'id').reverse(); _.each(_toSave, function (session) { defs.push(mutex.exec(function () { - return (type === 'xml' ? self._saveView(session) : self._saveSCSS(session)); + return (type === 'xml' ? self._saveView(session) : self._saveSCSSorJS(session)); })); }); }).bind(this)); - return $.when.apply($, defs).fail((function (session, error) { - Dialog.alert(this, '', { + var self = this; + return Promise.all(defs).guardedCatch(function (results) { + var error = results[1]; + Dialog.alert(self, '', { title: _t("Server error"), $content: $('<div/>').html( _t("A server error occured. Please check you correctly signed in and that the file you are saving is correctly formatted.") @@ -585,34 +627,32 @@ var ViewEditor = Widget.extend({ + error ) }); - }).bind(this)); + }); }, /** * Saves an unique XML view. * * @private * @param {Object} session - the 'id' and the 'text' of the view to save. - * @returns {Deferred} status indicates if the save is finished or if an + * @returns {Promise} status indicates if the save is finished or if an * error occured. */ _saveView: function (session) { - var def = $.Deferred(); - var self = this; - this._rpc({ - model: 'ir.ui.view', - method: 'write', - args: [[session.id], {arch: session.text}], - }, { - noContextKeys: 'lang', - }).then(function () { - self._toggleDirtyInfo(session.id, 'xml', false); - def.resolve(); - }, function (source, error) { - def.reject(session, error); + return new Promise(function (resolve, reject) { + self._rpc({ + model: 'ir.ui.view', + method: 'write', + args: [[session.id], {arch: session.text}], + }, { + noContextKeys: 'lang', + }).then(function () { + self._toggleDirtyInfo(session.id, 'xml', false); + resolve(); + }, function (source, error) { + reject(session, error); + }); }); - - return def; }, /** * Shows a line which produced an error. Red color is added to the editor, @@ -664,11 +704,11 @@ var ViewEditor = Widget.extend({ } }, /** - * Switches to the SCSS or XML edition. Calling this method will adapt all + * Switches to the SCSS, XML or JS edition. Calling this method will adapt all * DOM elements to keep the editor consistent. * * @private - * @param {string} type - either 'xml' or 'scss' + * @param {string} type - either 'xml', 'scss' or 'js' */ _switchType: function (type) { this.currentType = type; @@ -676,9 +716,10 @@ var ViewEditor = Widget.extend({ _.each(this.$lists, function ($list, _type) { $list.toggleClass('d-none', type !== _type); }); this.$lists[type].change(); - this.$includeBundlesArea.toggleClass('d-none', this.currentType === 'scss' || !session.debug); - this.$includeAllSCSSArea.toggleClass('d-none', this.currentType === 'xml' || !session.debug || this.options.defaultBundlesRestriction.length === 0); - this.$formatButton.toggleClass('d-none', this.currentType === 'scss'); + this.$includeBundlesArea.toggleClass('d-none', this.currentType !== 'xml' || !config.isDebug()); + this.$includeAllSCSSArea.toggleClass('d-none', this.currentType !== 'scss' || !config.isDebug()); + this.$includeAllSCSSArea.find('[data-value="restricted"]').toggleClass('d-none', this.options.defaultBundlesRestriction.length === 0); + this.$formatButton.toggleClass('d-none', this.currentType !== 'xml'); }, /** * Updates the select option DOM element associated with a particular resID @@ -723,21 +764,10 @@ var ViewEditor = Widget.extend({ }); this.$lists.scss.empty(); - _.each(this.sortedSCSS, function (bundleInfos) { - var $optgroup = $('<optgroup/>', { - label: bundleInfos[0].name, - }).appendTo(self.$lists.scss); - _.each(bundleInfos[1], function (scssInfo) { - var name = scssInfo.url.substring(_.lastIndexOf(scssInfo.url, '/') + 1, scssInfo.url.length - 5); - $optgroup.append($('<option/>', { - value: scssInfo.url, - text: name, - selected: currentId === scssInfo.url, - 'data-debug': scssInfo.url, - 'data-customized': scssInfo.customized - })); - }); - }); + _populateList(this.sortedSCSS, this.$lists.scss, 5); + + this.$lists.js.empty(); + _populateList(this.sortedJS, this.$lists.js, 3); this.$lists.xml.select2('destroy'); this.$lists.xml.select2({ @@ -751,6 +781,30 @@ var ViewEditor = Widget.extend({ formatSelection: _formatDisplay.bind(this, true), }); this.$lists.scss.data('select2').dropdown.addClass('o_ace_select2_dropdown'); + this.$lists.js.select2('destroy'); + this.$lists.js.select2({ + formatResult: _formatDisplay.bind(this, false), + formatSelection: _formatDisplay.bind(this, true), + }); + this.$lists.js.data('select2').dropdown.addClass('o_ace_select2_dropdown'); + + function _populateList(sortedData, $list, lettersToRemove) { + _.each(sortedData, function (bundleInfos) { + var $optgroup = $('<optgroup/>', { + label: bundleInfos[0].name, + }).appendTo($list); + _.each(bundleInfos[1], function (dataInfo) { + var name = dataInfo.url.substring(_.lastIndexOf(dataInfo.url, '/') + 1, dataInfo.url.length - lettersToRemove); + $optgroup.append($('<option/>', { + value: dataInfo.url, + text: name, + selected: currentId === dataInfo.url, + 'data-debug': dataInfo.url, + 'data-customized': dataInfo.customized + })); + }); + }); + } function _formatDisplay(isSelected, data) { var $elem = $(data.element); @@ -770,7 +824,7 @@ var ViewEditor = Widget.extend({ })); } - if (!isSelected && session.debug && $elem.data('debug')) { + if (!isSelected && config.isDebug() && $elem.data('debug')) { $div.append($('<span/>', { text: ' (' + $elem.data('debug') + ')', class: 'ml4 small text-muted', @@ -802,25 +856,20 @@ var ViewEditor = Widget.extend({ this._formatResource(); }, /** - * Called when the checkbox which indicates if all SCSS should be included - * is toggled -> reloads the resources accordingly. + * Called when a filter dropdown item is cliked. Reload the resources + * according to the new filter and make it visually active. * * @private * @param {Event} ev */ - _onIncludeAllSCSSChange: function (ev) { - this.options.includeAllSCSS = $(ev.target).prop('checked'); - this._loadResources().then(this._updateViewSelectDOM.bind(this)); - }, - /** - * Called when the checkbox which indicates if assets bundles should be - * included is toggled -> reloads the resources accordingly. - * - * @private - * @param {Event} ev - */ - _onIncludeBundlesChange: function (ev) { - this.options.includeBundles = $(ev.target).prop('checked'); + _onFilterChange: function (ev) { + var $item = $(ev.target); + $item.addClass('active').siblings().removeClass('active'); + if ($item.data('type') === 'xml') { + this.options.includeBundles = $(ev.target).data('value') === 'all'; + } else { + this.options.filesFilter = $item.data('value'); + } this._loadResources().then(this._updateViewSelectDOM.bind(this)); }, /** @@ -866,6 +915,13 @@ var ViewEditor = Widget.extend({ ev.preventDefault(); this._switchType($(ev.target).data('type')); }, + /** + * Allows to hide the warning message without removing it from the DOM + * -> by default Bootstrap removes alert from the DOM + */ + _onCloseWarningClick: function () { + this.$warningMessage.addClass('d-none'); + }, }); return ViewEditor; diff --git a/addons/web_editor/static/src/js/editor/editor.js b/addons/web_editor/static/src/js/editor/editor.js index 63fa8c1c6675..dc0f53c8e570 100644 --- a/addons/web_editor/static/src/js/editor/editor.js +++ b/addons/web_editor/static/src/js/editor/editor.js @@ -29,7 +29,17 @@ var EditorMenuBar = Widget.extend({ init: function (parent) { var self = this; var res = this._super.apply(this, arguments); - this.rte = new rte.Class(this); + var Editor = options.Editor || rte.Class; + this.rte = new Editor(this, { + getConfig: function ($editable) { + var param = self._getDefaultConfig($editable); + if (options.generateOptions) { + param = options.generateOptions(param); + } + return param; + }, + saveElement: options.saveElement, + }); this.rte.on('rte:start', this, function () { self.trigger('rte:start'); }); @@ -37,7 +47,15 @@ var EditorMenuBar = Widget.extend({ // Snippets edition var $editable = this.rte.editable(); window.__EditorMenuBar_$editable = $editable; // TODO remove this hack asap - this.snippetsMenu = new snippetsEditor.Class(this, $editable); + + + var options = this.getParent().params; + if (options.snippets) { + this.snippetsMenu = new snippetsEditor.Class(this, Object.assign({ + $el: $editable, + selectorEditableArea: '.o_editable', + }, options)); + } return res; }, @@ -88,10 +106,12 @@ var EditorMenuBar = Widget.extend({ }; // Snippets menu - defs.push(this.snippetsMenu.insertAfter(this.$el)); + if (self.snippetsMenu) { + defs.push(this.snippetsMenu.insertAfter(this.$el)); + } this.rte.editable().find('*').off('mousedown mouseup click'); - return $.when.apply($, defs).then(function () { + return Promise.all(defs).then(function () { self.trigger_up('edit_mode'); }); }, @@ -115,20 +135,20 @@ var EditorMenuBar = Widget.extend({ * @param {boolean} [reload=true] * true if the page has to be reloaded when the user answers yes * (do nothing otherwise but add this to allow class extension) - * @returns {Deferred} + * @returns {Promise} */ cancel: function (reload) { var self = this; - var def = $.Deferred(); - if (!rte.history.getEditableHasUndo().length) { - def.resolve(); - } else { - var confirm = Dialog.confirm(this, _t("If you discard the current edition, all unsaved changes will be lost. You can cancel to return to the edition mode."), { - confirm_callback: def.resolve.bind(def), - }); - confirm.on('closed', def, def.reject); - } - return def.then(function () { + return new Promise(function(resolve, reject) { + if (!rte.history.getEditableHasUndo().length) { + resolve(); + } else { + var confirm = Dialog.confirm(this, _t("If you discard the current edition, all unsaved changes will be lost. You can cancel to return to the edition mode."), { + confirm_callback: resolve, + }); + confirm.on('closed', self, reject); + } + }).then(function () { if (reload !== false) { window.onbeforeunload = null; return self._reload(); @@ -141,14 +161,16 @@ var EditorMenuBar = Widget.extend({ * * @param {boolean} [reload=true] * true if the page has to be reloaded after the save - * @returns {Deferred} + * @returns {Promise} */ save: function (reload) { var self = this; var defs = []; this.trigger_up('ready_to_save', {defs: defs}); - return $.when.apply($, defs).then(function () { - self.snippetsMenu.cleanForSave(); + return Promise.all(defs).then(function () { + if (self.snippetsMenu) { + self.snippetsMenu.cleanForSave(); + } return self._saveCroppedImages(); }).then(function () { return self.rte.save(); @@ -167,7 +189,7 @@ var EditorMenuBar = Widget.extend({ * Reloads the page in non-editable mode, with the right scrolling. * * @private - * @returns {Deferred} (never resolved, the page is reloading anyway) + * @returns {Promise} (never resolved, the page is reloading anyway) */ _reload: function () { window.location.hash = 'scrollTop=' + window.document.body.scrollTop; @@ -176,7 +198,7 @@ var EditorMenuBar = Widget.extend({ } else { window.location.reload(true); } - return $.Deferred(); + return new Promise(function(){}); }, /** * @private @@ -226,7 +248,7 @@ var EditorMenuBar = Widget.extend({ }); } }); - return $.when.apply($, defs); + return Promise.all(defs); }, //-------------------------------------------------------------------------- diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js index ba6e1fed7ecb..bfd7994b161d 100644 --- a/addons/web_editor/static/src/js/editor/rte.js +++ b/addons/web_editor/static/src/js/editor/rte.js @@ -246,7 +246,7 @@ var RTEWidget = Widget.extend({ /** * @constructor */ - init: function (parent, getConfig) { + init: function (parent, params) { var self = this; this._super.apply(this, arguments); @@ -259,7 +259,8 @@ var RTEWidget = Widget.extend({ return res; }; - this._getConfig = getConfig || this._getDefaultConfig; + this._getConfig = params && params.getConfig || this._getDefaultConfig; + this._saveElement = params && params.saveElement || this._saveElement; base.computeFonts(); }, @@ -440,7 +441,7 @@ var RTEWidget = Widget.extend({ * * @param {Object} [context] - the context to use for saving rpc, default to * the editor context found on the page - * @return {Deferred} rejected if the save cannot be done + * @return {Promise} rejected if the save cannot be done */ save: function (context) { var self = this; @@ -494,7 +495,7 @@ var RTEWidget = Widget.extend({ }); }); - return $.when.apply($, defs).then(function () { + return Promise.all(defs).then(function () { window.onbeforeunload = null; }, function (failed) { // If there were errors, re-enable edition diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js index 464c58ed4ee1..b5b74f644663 100644 --- a/addons/web_editor/static/src/js/editor/rte.summernote.js +++ b/addons/web_editor/static/src/js/editor/rte.summernote.js @@ -4,12 +4,12 @@ odoo.define('web_editor.rte.summernote', function (require) { var ajax = require('web.ajax'); var Class = require('web.Class'); var core = require('web.core'); -var ColorpickerDialog = require('web.colorpicker'); +var ColorpickerDialog = require('web.ColorpickerDialog'); var mixins = require('web.mixins'); -var base = require('web_editor.base'); +var fonts = require('wysiwyg.fonts'); var weContext = require('web_editor.context'); var rte = require('web_editor.rte'); -var weWidgets = require('web_editor.widget'); +var weWidgets = require('wysiwyg.widgets'); var QWeb = core.qweb; var _t = core._t; @@ -17,9 +17,12 @@ var _t = core._t; ajax.jsonRpc('/web/dataset/call', 'call', { 'model': 'ir.ui.view', 'method': 'read_template', - 'args': ['web_editor.colorpicker', weContext.get()] -}).done(function (data) { - QWeb.add_template(data); + 'args': ['web_editor.colorpicker'], + 'kwargs': { + 'context': weContext.get(), + } +}).then(function (data) { + QWeb.add_template('<templates>' + data + '</templates>'); }); // Summernote Lib (neek change to make accessible: method and object) @@ -40,7 +43,7 @@ function _rgbToHex(cssColor) { if (rgba[4]) { return cssColor; } - var hex = ColorpickerDialog.prototype.convertRgbToHex( + var hex = ColorpickerDialog.convertRgbToHex( parseInt(rgba[1]), parseInt(rgba[2]), parseInt(rgba[3]) @@ -107,13 +110,27 @@ renderer.createPalette = function ($container, options) { $palettes.push.apply($palettes, $customColorPalettes); - var $fore = $palettes.filter(":even").find("button:not(.note-color-btn)").addClass("note-color-btn"); - var $bg = $palettes.filter(":odd").find("button:not(.note-color-btn)").addClass("note-color-btn"); + var $forePalette = $palettes.filter(":even").closest('li').addClass('note-palette'); + var $bgPalette = $palettes.filter(":odd").closest('li').addClass('note-palette'); + + $forePalette.add($bgPalette).find('.note-color-reset').remove(); + $forePalette.children().each(function () { + var $reset = $('<div class="note-color-reset" data-event="foreColor" data-value="inherit"></div>').text(_t('Reset to default')); + $(this).prepend($reset); + }) + $bgPalette.children().each(function () { + var $reset = $('<div class="note-color-reset" data-event="backColor" data-value="inherit"></div>').text(_t('Reset to default')); + $(this).prepend($reset); + }) + + var $fore = $forePalette.find("button:not(.note-color-btn)").addClass("note-color-btn"); $fore.each(function () { var $el = $(this); var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'text-' + $el.data('color'); $el.attr('data-event', 'foreColor').attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : 'bg-' + $el.data('color')); }); + + var $bg = $bgPalette.find("button:not(.note-color-btn)").addClass("note-color-btn"); $bg.each(function () { var $el = $(this); var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'bg-' + $el.data('color'); @@ -325,6 +342,15 @@ eventHandler.modules.popover.button.update = function ($container, oStyle) { } }; +var fn_toolbar_boutton_update = eventHandler.modules.toolbar.button.update; +eventHandler.modules.toolbar.button.update = function ($container, oStyle) { + fn_toolbar_boutton_update.call(this, $container, oStyle); + + $container.find('button[data-event="insertUnorderedList"]').toggleClass("active", $(oStyle.ancestors).is('ul:not(.o_checklist)')); + $container.find('button[data-event="insertOrderedList"]').toggleClass("active", $(oStyle.ancestors).is('ol')); + $container.find('button[data-event="insertCheckList"]').toggleClass("active", $(oStyle.ancestors).is('ul.o_checklist')); +}; + var fn_popover_update = eventHandler.modules.popover.update; eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) { var $imagePopover = $popover.find('.note-image-popover'); @@ -457,6 +483,11 @@ eventHandler.modules.imageDialog.showImageDialog = function ($editable) { onUpload: $editable.data('callbacks').onUpload, noVideos: $editable.data('oe-model') === "mail.compose.message", }, + onSave: function (media) { + if(!document.body.contains(media)) { + r.insertNode(media); + }; + }, }); return new $.Deferred().reject(); }; @@ -520,8 +551,8 @@ dom.isImgFont = function (node) { var className = (node && node.className || ""); if (node && (nodeName === "SPAN" || nodeName === "I") && className.length) { var classNames = className.split(/\s+/); - for (var k=0; k<base.fontIcons.length; k++) { - if (_.intersection(base.fontIcons[k].alias, classNames).length) { + for (var k=0; k<fonts.fontIcons.length; k++) { + if (_.intersection(fonts.fontIcons[k].alias, classNames).length) { return true; } } @@ -1140,7 +1171,6 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, { data.__alreadyDone = true; var altDialog = new weWidgets.AltDialog(this, data.options || {}, - data.$editable, data.media ); if (data.onSave) { @@ -1188,7 +1218,6 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, { res_model: data.$editable.data('oe-model'), res_id: data.$editable.data('oe-id'), }, data.options || {}), - data.$editable, data.media ); if (data.onSave) { @@ -1212,7 +1241,6 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, { data.__alreadyDone = true; var linkDialog = new weWidgets.LinkDialog(this, data.options || {}, - data.$editable, data.linkInfo ); if (data.onSave) { @@ -1241,7 +1269,6 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, { res_id: data.$editable.data('oe-id'), domain: data.$editable.data('oe-media-domain'), }, data.options), - data.$editable, data.media ); if (data.onSave) { 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 776d9992bb72..2f2e55a00842 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -5,6 +5,7 @@ var core = require('web.core'); var dom = require('web.dom'); var Widget = require('web.Widget'); var options = require('web_editor.snippets.options'); +var Wysiwyg = require('web_editor.wysiwyg'); var _t = core._t; @@ -24,6 +25,7 @@ var SnippetEditor = Widget.extend({ 'click .oe_snippet_parent': '_onParentButtonClick', 'click .oe_snippet_clone': '_onCloneClick', 'click .oe_snippet_remove': '_onRemoveClick', + 'mousedown .oe_options [data-toggle="dropdown"]:first': '_onOpenCusomize', }, custom_events: { cover_update: '_onCoverUpdate', @@ -35,9 +37,15 @@ var SnippetEditor = Widget.extend({ * @param {Widget} parent * @param {Element} target * @param templateOptions + * @param {jQuery} $editable + * @param {Object} options */ - init: function (parent, target, templateOptions) { + init: function (parent, target, templateOptions, $editable, 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.templateOptions = templateOptions; @@ -59,13 +67,13 @@ var SnippetEditor = Widget.extend({ defs.push(this._initializeOptions()); // Initialize move/clone/remove buttons - if (!this.$target.parent().is(':o_editable')) { + if (!this.options.isEditableNode(this.$target[0])) { this.$el.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove(); } else { this.dropped = false; this.$el.draggable({ greedy: true, - appendTo: 'body', + appendTo: this.$body, cursor: 'move', handle: '.oe_snippet_move', cursorAt: { @@ -76,7 +84,7 @@ var SnippetEditor = Widget.extend({ var $clone = $(this).clone().css({width: '24px', height: '24px', border: 0}); $clone.find('.oe_overlay_options >:not(:contains(.oe_snippet_move)), .o_handle').remove(); $clone.find(':not(.glyphicon)').css({position: 'absolute', top: 0, left: 0}); - $clone.appendTo('body').removeClass('d-none'); + $clone.appendTo(self.$body).removeClass('d-none'); return $clone; }, start: _.bind(self._onDragAndDropStart, self), @@ -94,7 +102,7 @@ var SnippetEditor = Widget.extend({ } }); - return $.when.apply($, defs); + return Promise.all(defs); }, /** * @override @@ -150,7 +158,7 @@ var SnippetEditor = Widget.extend({ top: offset.top, }); this.$('.o_handles').css('height', this.$target.outerHeight()); - this.$el.toggleClass('o_top_cover', offset.top < 15); + this.$el.toggleClass('o_top_cover', offset.top < this.$editable.offset().top); }, /** * Removes the associated snippet from the DOM and destroys the associated @@ -169,13 +177,12 @@ var SnippetEditor = Widget.extend({ }); var $parent = this.$target.parent(); - this.$target.find('*').andSelf().tooltip('dispose'); + this.$target.find('*').addBack().tooltip('dispose'); this.$target.remove(); this.$el.remove(); var node = $parent[0]; if (node && node.firstChild) { - $.summernote.core.dom.removeSpace(node, node.firstChild, 0, node.lastChild, 1); if (!node.firstChild.tagName && node.firstChild.textContent === ' ') { node.removeChild(node.firstChild); } @@ -197,11 +204,12 @@ var SnippetEditor = Widget.extend({ } // clean editor if they are image or table in deleted content - $('.note-control-selection').hide(); - $('.o_table_handler').remove(); + this.$body.find('.note-control-selection').hide(); + this.$body.find('.o_table_handler').remove(); this.trigger_up('snippet_removed'); this.destroy(); + $parent.trigger('content_changed'); }, /** * Displays/Hides the editor overlay and notifies the associated snippet @@ -211,6 +219,9 @@ var SnippetEditor = Widget.extend({ * @param {boolean} focus - true to display, false to hide */ toggleFocus: function (focus) { + if (!this.$el) { + return; + } var do_action = (focus ? _do_action_focus : _do_action_blur); // Attach own and parent options on the current overlay @@ -238,6 +249,10 @@ var SnippetEditor = Widget.extend({ this.cover(); this.$el.toggleClass('oe_active', !!focus); + if (focus) { + this.trigger_up('snippet_focused'); + } + function _do_action_focus(style, $dest) { style.$el.insertAfter($dest); style.onFocus(); @@ -298,7 +313,8 @@ var SnippetEditor = Widget.extend({ self, val.base_target ? self.$target.find(val.base_target).eq(0) : self.$target, self.$el, - val.data + val.data, + self.options ); self.styles[optionName || _.uniqueId('option')] = option; option.__order = i++; @@ -325,7 +341,7 @@ var SnippetEditor = Widget.extend({ this.$el.find('[data-toggle="dropdown"]').dropdown(); - return $.when.apply($, defs); + return Promise.all(defs); }, //-------------------------------------------------------------------------- @@ -340,13 +356,13 @@ var SnippetEditor = Widget.extend({ */ _onCloneClick: function (ev) { ev.preventDefault(); + this.trigger_up('cover_will_change'); this.trigger_up('snippet_will_be_cloned', {$target: this.$target}); var $clone = this.$target.clone(false); this.trigger_up('request_history_undo_record', {$target: this.$target}); - this.$target.after($clone); this.trigger_up('call_for_each_child_snippet', { $snippet: $clone, @@ -359,6 +375,7 @@ var SnippetEditor = Widget.extend({ }, }); this.trigger_up('snippet_cloned', {$target: $clone, $origin: this.$target}); + $clone.trigger('content_changed'); }, /** * Called when the overlay dimensions/positions should be recomputed. @@ -366,6 +383,7 @@ var SnippetEditor = Widget.extend({ * @private */ _onCoverUpdate: function () { + this.trigger_up('cover_will_change'); this.cover(); }, /** @@ -402,11 +420,11 @@ var SnippetEditor = Widget.extend({ $selectorChildren: $selectorChildren, }); - $('body').addClass('move-important'); + this.$body.addClass('move-important'); - $('.oe_drop_zone').droppable({ + this.$editable.find('.oe_drop_zone').droppable({ over: function () { - $('.oe_drop_zone.hide').removeClass('hide'); + self.$editable.find('.oe_drop_zone.hide').removeClass('hide'); $(this).addClass('hide').first().after(self.$target); self.dropped = true; }, @@ -416,6 +434,8 @@ var SnippetEditor = Widget.extend({ self.dropped = false; }, }); + + this.trigger_up('cover_will_change'); }, /** * Called when the snippet is dropped after being dragged thanks to the @@ -426,33 +446,32 @@ var SnippetEditor = Widget.extend({ * @param {Object} ui */ _onDragAndDropStop: function (ev, ui) { - var self = this; + this.$editable.find('.oe_drop_zone').droppable('destroy').remove(); // 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').first(); + 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); this.dropped = true; } } - $('.oe_drop_zone').droppable('destroy').remove(); - var prev = this.$target.first()[0].previousSibling; var next = this.$target.last()[0].nextSibling; var $parent = this.$target.parent(); - var $clone = $('.oe_drop_clone'); + var $clone = this.$editable.find('.oe_drop_clone'); if (prev === $clone[0]) { prev = $clone[0].previousSibling; } else if (next === $clone[0]) { next = $clone[0].nextSibling; } $clone.after(this.$target); + var $from = $clone.parent(); this.$el.removeClass('d-none'); - $('body').removeClass('move-important'); + this.$body.removeClass('move-important'); $clone.remove(); if (this.dropped) { @@ -469,10 +488,13 @@ var SnippetEditor = Widget.extend({ for (var i in this.styles) { this.styles[i].onMove(); } + + this.$target.trigger('content_changed'); + $from.trigger('content_changed'); } - self.trigger_up('drag_and_drop_stop', { - $snippet: self.$target, + this.trigger_up('drag_and_drop_stop', { + $snippet: this.$target, }); }, /** @@ -483,6 +505,7 @@ var SnippetEditor = Widget.extend({ * @param {OdooEvent} ev */ _onOptionUpdate: function (ev) { + this.trigger_up('cover_will_change'); // If multiple option names are given, we suppose it should not be // propagated to parent editor if (ev.data.optionNames) { @@ -505,6 +528,16 @@ var SnippetEditor = Widget.extend({ } } }, + /** + * Called when the user opens the customize menu. + * + * @private + * @param {OdooEvent} ev + */ + _onOpenCusomize: function (ev) { + this.trigger_up('request_history_undo_record', {$target: this.$target}); + }, + /** * Called when the 'parent' button is clicked. * @@ -525,6 +558,7 @@ var SnippetEditor = Widget.extend({ */ _onRemoveClick: function (ev) { ev.preventDefault(); + this.trigger_up('cover_will_change'); this.trigger_up('request_history_undo_record', {$target: this.$target}); this.removeSnippet(); }, @@ -535,6 +569,7 @@ var SnippetEditor = Widget.extend({ */ var SnippetsMenu = Widget.extend({ id: 'oe_snippets', + cacheSnippetTemplate: {}, activeSnippets: [], custom_events: { activate_insertion_zones: '_onActivateInsertionZones', @@ -544,17 +579,38 @@ var SnippetsMenu = Widget.extend({ go_to_parent: '_onGoToParent', remove_snippet: '_onRemoveSnippet', snippet_removed: '_onSnippetRemoved', + reload_snippet_dropzones: '_disableUndroppableSnippets', }, /** + * @param {Widget} parent + * @param {Object} [options] + * @param {string} [options.snippets] + * URL of the snippets template. This URL might have been set + * in the global 'snippets' variable, otherwise this function + * assigns a default one. + * default: 'web_editor.snippets' + * * @constructor */ - init: function (parent, $editable) { + init: function (parent, options) { this._super.apply(this, arguments); + options = options || {}; + this.trigger_up('getRecordInfo', { + recordInfo: options, + callback: function (recordInfo) { + _.defaults(options, recordInfo); + }, + }); - this.$editable = $editable; + this.options = options; + if (!this.options.snippets) { + this.options.snippets = 'web_editor.snippets'; + } this.$activeSnippet = false; this.snippetEditors = []; + + this.setSelectorEditableArea(options.$el, options.selectorEditableArea); }, /** * @override @@ -562,12 +618,14 @@ var SnippetsMenu = Widget.extend({ start: function () { var self = this; var defs = [this._super.apply(this, arguments)]; - var $document = $(document); - var $window = $(window); + this.ownerDocument = this.$el[0].ownerDocument; + this.$document = $(this.ownerDocument); + this.window = this.ownerDocument.defaultView; + this.$window = $(this.window); // Fetch snippet templates and compute it - var url = this._getSnippetURL(); - defs.push(this._rpc({route: url}).then(function (html) { + + defs.push(this.loadSnippets().then(function (html) { return self._computeSnippetTemplates(html); })); @@ -578,8 +636,8 @@ var SnippetsMenu = Widget.extend({ // Active snippet editor on click in the page var lastClickedElement; - $document.on('click.snippets_menu', '*', function (ev) { - var srcElement = ev.srcElement || (ev.originalEvent && (ev.originalEvent.originalTarget || ev.originalEvent.target) || ev.target); + this.$document.on('click.snippets_menu', '*', function (ev) { + var srcElement = ev.target || (ev.originalEvent && (ev.originalEvent.target || ev.originalEvent.originalTarget)) || ev.srcElement; if (lastClickedElement === srcElement || !srcElement) { return; } @@ -598,27 +656,21 @@ var SnippetsMenu = Widget.extend({ core.bus.on('deactivate_snippet', this, this._onDeactivateSnippet); core.bus.on('snippet_editor_clean_for_save', this, this._onCleanForSaveDemand); - // Some summernote customization - var _isNotBreakable = $.summernote.core.dom.isNotBreakable; - $.summernote.core.dom.isNotBreakable = function (node) { - return _isNotBreakable(node) || $(node).is('div') || globalSelector.is($(node)); - }; - // Adapt overlay covering when the window is resized / content changes var debouncedCoverUpdate = _.debounce(function () { - self._updateCurrentSnippetEditorOverlay(); + self.updateCurrentSnippetEditorOverlay(); }, 200); - $window.on('resize.snippets_menu', debouncedCoverUpdate); - $window.on('content_changed.snippets_menu', debouncedCoverUpdate); + this.$window.on('resize.snippets_menu', debouncedCoverUpdate); + this.$window.on('content_changed.snippets_menu', debouncedCoverUpdate); // On keydown add a class on the active overlay to hide it and show it // again when the mouse moves - $document.on('keydown.snippets_menu', function () { + this.$document.on('keydown.snippets_menu', function () { if (self.$activeSnippet && self.$activeSnippet.data('snippet-editor')) { self.$activeSnippet.data('snippet-editor').$el.addClass('o_keypress'); } }); - $document.on('mousemove.snippets_menu', function () { + this.$document.on('mousemove.snippets_menu, mousedown.snippets_menu', function () { if (self.$activeSnippet && self.$activeSnippet.data('snippet-editor')) { self.$activeSnippet.data('snippet-editor').$el.removeClass('o_keypress'); } @@ -626,20 +678,22 @@ var SnippetsMenu = Widget.extend({ // Auto-selects text elements with a specific class and remove this // on text changes - $document.on('click.snippets_menu', '.o_default_snippet_text', function (ev) { + 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'); }); - $document.on('keyup.snippets_menu', function () { - var r = $.summernote.core.range.create(); - $(r && r.sc).closest('.o_default_snippet_text').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'); }); - return $.when.apply($, defs).then(function () { + return Promise.all(defs).then(function () { // 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(function () { - $window.trigger('resize'); + self.$window.trigger('resize'); }, 1000); }); }, @@ -648,11 +702,14 @@ var SnippetsMenu = Widget.extend({ */ destroy: function () { this._super.apply(this, arguments); - this.$snippetEditorArea.remove(); - $(window).off('.snippets_menu'); - $(document).off('.snippets_menu'); + if (this.$window) { + this.$snippetEditorArea.remove(); + this.$window.off('.snippets_menu'); + this.$document.off('.snippets_menu'); + } core.bus.off('deactivate_snippet', this, this._onDeactivateSnippet); core.bus.off('snippet_editor_clean_for_save', this, this._onCleanForSaveDemand); + delete this.cacheSnippetTemplate[this.options.snippets]; }, //-------------------------------------------------------------------------- @@ -665,16 +722,70 @@ var SnippetsMenu = Widget.extend({ * - Remove the 'contentEditable' attributes */ cleanForSave: function () { + this._activateSnippet(false); this.trigger_up('ready_to_clean_for_save'); this._destroyEditors(); - this.$editable.find('[contentEditable]') + this.getEditableArea().find('[contentEditable]') .removeAttr('contentEditable') .removeProp('contentEditable'); - this.$editable.find('.o_we_selected_image') + this.getEditableArea().find('.o_we_selected_image') .removeClass('o_we_selected_image'); }, + /** + * Load snippets. + */ + loadSnippets: function () { + if (this.cacheSnippetTemplate[this.options.snippets]) { + this._defLoadSnippets = this.cacheSnippetTemplate[this.options.snippets]; + return this._defLoadSnippets; + } + this._defLoadSnippets = this._rpc({ + model: 'ir.ui.view', + method: 'render_template', + args: [this.options.snippets, {}], + kwargs: { + context: this.options.context, + }, + }); + 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. + * + * @returns {JQuery} + */ + getEditableArea: function () { + return this.$editor.find(this.selectorEditableArea) + .add(this.$editor.filter(this.selectorEditableArea)); + }, + /** + * Updates the cover dimensions of the current snippet editor. + */ + updateCurrentSnippetEditorOverlay: function () { + if (this.$activeSnippet && this.$activeSnippet.data('snippet-editor')) { + this.$activeSnippet.data('snippet-editor').cover(); + } + this.snippetEditors = _.filter(this.snippetEditors, function (snippetEditor) { + if (snippetEditor.$target.closest('body').length) { + return true; + } + snippetEditor.destroy(); + }); + }, //-------------------------------------------------------------------------- // Private @@ -692,6 +803,7 @@ var SnippetsMenu = Widget.extend({ * child */ _activateInsertionZones: function ($selectorSiblings, $selectorChildren) { + var self = this; var zone_template = $('<div/>', { class: 'oe_drop_zone oe_insert', }); @@ -703,8 +815,8 @@ var SnippetsMenu = Widget.extend({ if ($selectorChildren) { $selectorChildren.each(function () { var $zone = $(this); - var css = window.getComputedStyle(this); - var parentCss = window.getComputedStyle($zone.parent()[0]); + var css = self.window.getComputedStyle(this); + var parentCss = self.window.getComputedStyle($zone.parent()[0]); var float = css.float || css.cssFloat; var parentDisplay = parentCss.display; var parentFlex = parentCss.flexDirection; @@ -715,7 +827,7 @@ var SnippetsMenu = Widget.extend({ var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR')); if (test) { $drop.addClass('oe_vertical').css({ - height: parseInt(window.getComputedStyle($zone[0]).lineHeight), + height: parseInt(self.window.getComputedStyle($zone[0]).lineHeight), float: 'none', display: 'inline-block', }); @@ -733,7 +845,7 @@ var SnippetsMenu = Widget.extend({ test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR')); if (test) { $drop.addClass('oe_vertical').css({ - height: parseInt(window.getComputedStyle($zone[0]).lineHeight), + height: parseInt(self.window.getComputedStyle($zone[0]).lineHeight), float: 'none', display: 'inline-block' }); @@ -756,8 +868,8 @@ var SnippetsMenu = Widget.extend({ $selectorSiblings.filter(':not(.oe_drop_zone):not(.oe_drop_clone)').each(function () { var $zone = $(this); var $drop; - var css = window.getComputedStyle(this); - var parentCss = window.getComputedStyle($zone.parent()[0]); + var css = self.window.getComputedStyle(this); + var parentCss = self.window.getComputedStyle($zone.parent()[0]); var float = css.float || css.cssFloat; var parentDisplay = parentCss.display; var parentFlex = parentCss.flexDirection; @@ -789,14 +901,14 @@ var SnippetsMenu = Widget.extend({ var $zones; do { count = 0; - $zones = this.$editable.find('.oe_drop_zone > .oe_drop_zone').remove(); // no recursive zones + $zones = this.getEditableArea().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.$editable.find('.oe_drop_zone:not(.oe_vertical)'); + $zones = this.getEditableArea().find('.oe_drop_zone:not(.oe_vertical)'); $zones.each(function () { var zone = $(this); var prev = zone.prev(); @@ -830,7 +942,8 @@ var SnippetsMenu = Widget.extend({ * @param {jQuery|false} $snippet * The DOM element whose editor need to be enabled. Only disable the * current one if false is given. - * @returns {Deferred} (might be async when an editor must be created) + * @returns {Promise<SnippetEditor>} + * (might be async when an editor must be created) */ _activateSnippet: function ($snippet) { if ($snippet) { @@ -838,25 +951,27 @@ var SnippetsMenu = Widget.extend({ $snippet = globalSelector.closest($snippet); } if (this.$activeSnippet && this.$activeSnippet[0] === $snippet[0]) { - return $.when(); + return Promise.resolve($snippet.data('snippet-editor')); } } + var editor = null; if (this.$activeSnippet) { - if (this.$activeSnippet.data('snippet-editor')) { - this.$activeSnippet.data('snippet-editor').toggleFocus(false); + editor = this.$activeSnippet.data('snippet-editor'); + if (editor) { + editor.toggleFocus(false); } this.$activeSnippet = false; } if ($snippet && $snippet.length) { var self = this; - return this._createSnippetEditor($snippet).then(function () { + this.trigger_up('activate_snippet', $snippet); + return this._createSnippetEditor($snippet).then(function (editor) { self.$activeSnippet = $snippet; - if (self.$activeSnippet.data('snippet-editor')) { - self.$activeSnippet.data('snippet-editor').toggleFocus(true); - } + editor.toggleFocus(true); + return editor; }); } - return $.when(); + return Promise.resolve(editor); }, /** * @private @@ -866,16 +981,6 @@ var SnippetsMenu = Widget.extend({ snippetEditor.destroy(); }); }, - /** - * Updates the cover dimensions of the current snippet editor. - * - * @private - */ - _updateCurrentSnippetEditorOverlay: function () { - if (this.$activeSnippet && this.$activeSnippet.data('snippet-editor')) { - this.$activeSnippet.data('snippet-editor').cover(); - } - }, /** * Calls a given callback 'on' the given snippet and all its child ones if * any (DOM element with options). @@ -887,7 +992,7 @@ var SnippetsMenu = Widget.extend({ * @param {function} callback * Given two arguments: the snippet editor associated to the snippet * being managed and the DOM element of this snippet. - * @returns {Deferred} (might be async if snippet editors need to be created + * @returns {Promise} (might be async if snippet editors need to be created * and/or the callback is async) */ _callForEachChildSnippet: function ($snippet, callback) { @@ -900,7 +1005,7 @@ var SnippetsMenu = Widget.extend({ } }); }); - return $.when.apply($, defs); + return Promise.all(defs); }, /** * Creates and returns a set of helper functions which can help finding @@ -955,7 +1060,7 @@ var SnippetsMenu = Widget.extend({ selectorConditions += ':has(' + target + ')'; } if (!noCheck) { - selectorConditions = ':o_editable' + selectorConditions; + selectorConditions = (this.options.addDropSelector || '') + selectorConditions; } // (Re)join the subselectors @@ -978,7 +1083,7 @@ var SnippetsMenu = Widget.extend({ }; } else { functions.closest = function ($from, parentNode) { - var parents = self.$editable.get(); + var parents = self.getEditableArea().get(); return $from.closest(selector, parentNode).filter(function () { var node = this; while (node.parentNode) { @@ -991,9 +1096,9 @@ var SnippetsMenu = Widget.extend({ }); }; functions.all = isChildren ? function ($from) { - return dom.cssFind($from || self.$editable, selector); + return dom.cssFind($from || self.getEditableArea(), selector); } : function ($from) { - $from = $from || self.$editable; + $from = $from || self.getEditableArea(); return $from.filter(selector).add(dom.cssFind($from, selector)); }; } @@ -1120,7 +1225,6 @@ var SnippetsMenu = Widget.extend({ if (!this.$snippets.length) { this.$el.detach(); } - $('body').toggleClass('editor_has_snippets', this.$snippets.length > 0); // Register the text nodes that needs to be auto-selected on click this._registerDefaultTexts(); @@ -1142,6 +1246,9 @@ var SnippetsMenu = Widget.extend({ this.$el.html($html); this._makeSnippetDraggable(this.$snippets); this._disableUndroppableSnippets(); + + this.$el.addClass('o_loaded'); + $('body.editor_enable').addClass('editor_has_snippets'); }, /** * Creates a snippet editor to associated to the given snippet. If the given @@ -1152,13 +1259,13 @@ var SnippetsMenu = Widget.extend({ * * @private * @param {jQuery} $snippet - * @returns {Deferred<SnippetEditor>} + * @returns {Promise<SnippetEditor>} */ _createSnippetEditor: function ($snippet) { var self = this; var snippetEditor = $snippet.data('snippet-editor'); if (snippetEditor) { - return $.when(snippetEditor); + return Promise.resolve(snippetEditor); } var def; @@ -1167,8 +1274,9 @@ var SnippetsMenu = Widget.extend({ def = this._createSnippetEditor($parent); } - return $.when(def).then(function (parentEditor) { - snippetEditor = new SnippetEditor(parentEditor || self, $snippet, self.templateOptions); + return Promise.resolve(def).then(function (parentEditor) { + 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 () { @@ -1203,17 +1311,6 @@ var SnippetsMenu = Widget.extend({ $snippet.toggleClass('o_disabled', !check); }); }, - /** - * Returns the URL where to find the snippets template. This URL might have - * been set in the global 'snippetsURL' variable, otherwise this function - * returns a default one. - * - * @private - * @returns {string} - */ - _getSnippetURL: function () { - return odoo.snippetsURL || '/web_editor/snippets'; - }, /** * Make given snippets be draggable/droppable thanks to their thumbnail. * @@ -1230,8 +1327,7 @@ var SnippetsMenu = Widget.extend({ $snippets.draggable({ greedy: true, helper: 'clone', - zIndex: '1000', - appendTo: 'body', + appendTo: this.$body, cursor: 'move', handle: '.oe_snippet_thumbnail', distance: 30, @@ -1249,12 +1345,10 @@ var SnippetsMenu = Widget.extend({ for (var k in temp) { if ($base_body.is(temp[k].base_selector) && !$base_body.is(temp[k].base_exclude)) { if (temp[k]['drop-near']) { - if (!$selectorSiblings) $selectorSiblings = temp[k]['drop-near'].all(); - else $selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all()); + $selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all()); } if (temp[k]['drop-in']) { - if (!$selectorChildren) $selectorChildren = temp[k]['drop-in'].all(); - else $selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all()); + $selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all()); } } } @@ -1269,11 +1363,12 @@ var SnippetsMenu = Widget.extend({ self._activateSnippet(false); self._activateInsertionZones($selectorSiblings, $selectorChildren); - $('.oe_drop_zone').droppable({ + 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 () { @@ -1282,22 +1377,25 @@ var SnippetsMenu = Widget.extend({ dropped = false; $toInsert.detach(); $(this).removeClass('d-none'); + $toInsert.addClass('oe_snippet_body'); } }, }); + + self.trigger_up('cover_will_change'); }, stop: function (ev, ui) { $toInsert.removeClass('oe_snippet_body'); if (!dropped && ui.position.top > 3 && ui.position.left + 50 > self.$el.outerWidth()) { - var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone').first(); + var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone', {container: document.body}).first(); if ($el.length) { $el.after($toInsert); dropped = true; } } - self.$editable.find('.oe_drop_zone').droppable('destroy').remove(); + self.getEditableArea().find('.oe_drop_zone').droppable('destroy').remove(); if (dropped) { var prev = $toInsert.first()[0].previousSibling; @@ -1318,8 +1416,6 @@ var SnippetsMenu = Widget.extend({ $parent.prepend($toInsert); } - $toInsert.closest('.o_editable').trigger('content_changed'); - var $target = $toInsert; _.defer(function () { @@ -1327,12 +1423,12 @@ var SnippetsMenu = Widget.extend({ self._disableUndroppableSnippets(); self._callForEachChildSnippet($target, function (editor, $snippet) { - _.defer(function () { - editor.buildSnippet(); - }); + editor.buildSnippet(); }).then(function () { - $target.closest('.o_editable').trigger('content_changed'); - self._activateSnippet($target); + $target.trigger('content_changed'); + if ($target.closest('body').length) { // can be destroyed (eg in test) + self._activateSnippet($target); + } }); }); } else { 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 f481061c9e28..1278d70afda6 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -4,8 +4,8 @@ odoo.define('web_editor.snippets.options', function (require) { var core = require('web.core'); var Dialog = require('web.Dialog'); var Widget = require('web.Widget'); +var weWidgets = require('wysiwyg.widgets'); var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors'); -var weWidgets = require('web_editor.widget'); var qweb = core.qweb; var _t = core._t; @@ -18,10 +18,12 @@ var _t = core._t; var SnippetOption = Widget.extend({ events: { 'mouseenter': '_onLinkEnter', - 'mouseenter .dropdown-item': '_onLinkEnter', + 'mouseenter a': '_onLinkEnter', 'click': '_onLinkClick', + 'click a': '_onLinkClick', 'mouseleave': '_onMouseleave', - 'mouseleave .dropdown-item': '_onMouseleave', + 'mouseleave a': '_onMouseleave', + 'mouseleave .dropdown-menu': '_onMouseleave', }, /** * When editing a snippet, its options are shown alongside the ones of its @@ -38,9 +40,11 @@ var SnippetOption = Widget.extend({ * * @constructor */ - init: function (parent, $target, $overlay, data) { + init: function (parent, $target, $overlay, data, options) { this._super.apply(this, arguments); + this.options = options; this.$target = $target; + this.ownerDocument = this.$target[0].ownerDocument; this.$overlay = $overlay; this.data = data; this.__methodNames = []; @@ -240,6 +244,7 @@ var SnippetOption = Widget.extend({ if (!previewMode) { this._reset(); this.trigger_up('request_history_undo_record', {$target: this.$target}); + this.$target.trigger('content_changed'); } // Search for methods (data-...) (i.e. data-toggle-class) on the @@ -367,7 +372,7 @@ var SnippetOption = Widget.extend({ */ _onLinkClick: function (ev) { var $opt = $(ev.target).closest('.dropdown-item'); - if (!$opt.length || !$opt.is(':hasData')) { + if (ev.isDefaultPrevented() || !$opt.length || !$opt.is(':hasData')) { return; } @@ -454,7 +459,7 @@ registry.sizing = SnippetOption.extend({ var regClass = new RegExp('\\s*' + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g'); var cursor = $handle.css('cursor') + '-important'; - var $body = $(document.body); + var $body = $(this.ownerDocument.body); $body.addClass(cursor); var xy = ev['page' + XY]; @@ -842,7 +847,7 @@ registry.background = SnippetOption.extend({ */ chooseImage: function (previewMode, value, $opt) { // Put fake image in the DOM, edit it and use it as background-image - var $image = $('<img/>', {class: 'd-none', src: value}).appendTo(this.$target); + var $image = $('<img/>', {class: 'd-none', src: value === 'true' ? '' : value}).appendTo(this.$target); var $editable = this.$target.closest('.o_editable'); var _editor = new weWidgets.MediaDialog(this, { @@ -850,10 +855,11 @@ registry.background = SnippetOption.extend({ firstFilters: ['background'], res_model: $editable.data('oe-model'), res_id: $editable.data('oe-id'), - }, null, $image[0]).open(); + }, $image[0]).open(); _editor.on('save', this, function () { this._setCustomBackground($image.attr('src')); + this.$target.trigger('content_changed'); }); _editor.on('closed', this, function () { $image.remove(); @@ -908,7 +914,8 @@ registry.background = SnippetOption.extend({ if (value === undefined) { value = this.$target.css('background-image'); } - return value.replace(/url\(['"]*|['"]*\)|^none$/g, ''); + var srcValueWrapper = /url\(['"]*|['"]*\)|^none$/g; + return value && value.replace(srcValueWrapper, '') || ''; }, /** * @override @@ -1173,6 +1180,11 @@ registry.many2one = SnippetOption.extend({ */ start: function () { var self = this; + this.trigger_up('getRecordInfo', _.extend(this.options, { + callback: function (recordInfo) { + _.defaults(self.options, recordInfo); + }, + })); this.Model = this.$target.data('oe-many2one-model'); this.ID = +this.$target.data('oe-many2one-id'); @@ -1205,6 +1217,7 @@ registry.many2one = SnippetOption.extend({ this.$search.find('input') .focus() .on('keyup', function (e) { + self.$overlay.removeClass('o_keypress'); self._findExisting($(this).val()); }); @@ -1245,7 +1258,7 @@ registry.many2one = SnippetOption.extend({ * * @private * @param {string} name - * @returns {Deferred} + * @returns {Promise} */ _findExisting: function (name) { var self = this; @@ -1271,6 +1284,7 @@ registry.many2one = SnippetOption.extend({ kwargs: { order: [{name: 'name', asc: false}], limit: 5, + context: this.options.context, }, }).then(function (result) { self.$search.siblings().remove(); @@ -1309,6 +1323,7 @@ registry.many2one = SnippetOption.extend({ args: [[self.ID]], kwargs: { options: options, + context: self.options.context, }, }).then(function (html) { $node.html(html); diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js index e0968ecdb580..6c62758142d1 100644 --- a/addons/web_editor/static/src/js/editor/summernote.js +++ b/addons/web_editor/static/src/js/editor/summernote.js @@ -1011,7 +1011,18 @@ renderer.tplButtonInfo.color = function (lang, options) { dropdown: renderer.getTemplate().dropdown(backColorItems) }); return recentColorButton + foreColorButton + backColorButton; -}, +}; + +renderer.tplButtonInfo.checklist = function (lang, options) { + return '<button ' + + 'type="button" ' + + 'class="btn btn-secondary btn-sm" ' + + 'title="' + _t('Checklist') + '" ' + + 'data-event="insertCheckList" ' + + 'tabindex="-1" ' + + 'data-name="ul" ' + + '><i class="fa fa-check-square"></i></button>'; +}; //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: @@ -1662,10 +1673,13 @@ function isFormatNode(node) { return node.tagName && options.styleTags.indexOf(node.tagName.toLowerCase()) !== -1; } -$.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutInfo, sorted) { +$.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutInfo, type) { var $editable = layoutInfo.editable(); $editable.data('NoteHistory').recordUndo($editable); + type = type || "UL"; + var sorted = type === "OL"; + var parent; var r = range.create(); if (!r) return; @@ -1677,6 +1691,11 @@ $.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutI var ul = document.createElement(sorted ? "ol" : "ul"); ul.className = node.className; + if (type !== 'checklist') { + ul.classList.remove('o_checklist'); + } else { + ul.classList.add('o_checklist'); + } parent.insertBefore(ul, node); while (node.firstChild) { ul.appendChild(node.firstChild); @@ -1687,6 +1706,14 @@ $.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutI } else if (node.tagName === (sorted ? "OL" : "UL")) { + if (type === 'checklist' && !node.classList.contains('o_checklist')) { + node.classList.add('o_checklist'); + return; + } else if (type === 'UL' && node.classList.contains('o_checklist')) { + node.classList.remove('o_checklist'); + return; + } + var lis = []; for (var i=0; i<node.children.length; i++) { lis.push(node.children[i]); @@ -1729,6 +1756,9 @@ $.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutI parent = p0.parentNode; ul = document.createElement(sorted ? "ol" : "ul"); + if (type === 'checklist') { + ul.classList.add('o_checklist'); + } parent.insertBefore(ul, p0); var childNodes = parent.childNodes; var brs = []; @@ -1763,7 +1793,10 @@ $.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutI return false; }; $.summernote.pluginEvents.insertOrderedList = function (event, editor, layoutInfo) { - return $.summernote.pluginEvents.insertUnorderedList(event, editor, layoutInfo, true); + $.summernote.pluginEvents.insertUnorderedList(event, editor, layoutInfo, "OL"); +}; +$.summernote.pluginEvents.insertCheckList = function (event, editor, layoutInfo) { + $.summernote.pluginEvents.insertUnorderedList(event, editor, layoutInfo, "checklist"); }; $.summernote.pluginEvents.indent = function (event, editor, layoutInfo, outdent) { var $editable = layoutInfo.editable(); @@ -1777,8 +1810,9 @@ $.summernote.pluginEvents.indent = function (event, editor, layoutInfo, outdent) var tagName = UL.tagName; var node = UL.firstChild; var ul = document.createElement(tagName); + ul.className = UL.className; var li = document.createElement("li"); - li.style.listStyle = "none"; + li.classList.add('o_indent'); li.appendChild(ul); if (flag) { @@ -2431,17 +2465,43 @@ eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) { } }; +function mouseDownChecklist (e) { + if (!dom.isLi(e.target) || !$(e.target).parent('ul.o_checklist').length || e.offsetX > 0) { + return; + } + e.preventDefault(); + var checked = $(e.target).hasClass('o_checked'); + $(e.target).toggleClass('o_checked', !checked); + var $sublevel = $(e.target).next('ul.o_checklist, li:has(> ul.o_checklist)').find('> li, ul.o_checklist > li'); + var $parents = $(e.target).parents('ul.o_checklist').map(function () { + return this.parentNode.tagName === 'LI' ? this.parentNode : this; + }); + if (checked) { + $sublevel.removeClass('o_checked'); + $parents.prev('ul.o_checklist li').removeClass('o_checked'); + } else { + $sublevel.addClass('o_checked'); + var $lis; + do { + $lis = $parents.not(':has(li:not(.o_checked))').prev('ul.o_checklist li:not(.o_checked)'); + $lis.addClass('o_checked'); + } while ($lis.length); + } +}; + var fn_attach = eventHandler.attach; eventHandler.attach = function (oLayoutInfo, options) { var $editable = oLayoutInfo.editor().hasClass('note-editable') ? oLayoutInfo.editor() : oLayoutInfo.editor().find('.note-editable'); fn_attach.call(this, oLayoutInfo, options); $editable.on("scroll", summernote_table_scroll); + $editable.on("mousedown", mouseDownChecklist); }; var fn_detach = eventHandler.detach; eventHandler.detach = function (oLayoutInfo, options) { var $editable = oLayoutInfo.editor().hasClass('note-editable') ? oLayoutInfo.editor() : oLayoutInfo.editor().find('.note-editable'); fn_detach.call(this, oLayoutInfo, options); $editable.off("scroll", summernote_table_scroll); + $editable.off("mousedown", mouseDownChecklist); $('.o_table_handler').remove(); }; 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 22c30d501d18..cbb39ea6173e 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js @@ -1076,8 +1076,11 @@ var VideoWidget = MediaWidget.extend({ } } var allVideoClasses = /(^|\s)media_iframe_video(\s|$)/g; - this.media.className = this.media.className && this.media.className.replace(allVideoClasses, ' '); - this.media.innerHTML = ''; + var isVideo = this.media.className && this.media.className.match(allVideoClasses); + if (isVideo) { + this.media.className = this.media.className.replace(allVideoClasses, ' '); + this.media.innerHTML = ''; + } }, /** * Creates a video node according to the given URL and options. If not diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media_dialog.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media_dialog.js index 528cbd93bc6a..84dd8e7f6500 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/media_dialog.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media_dialog.js @@ -38,11 +38,13 @@ var MediaDialog = Dialog.extend({ */ init: function (parent, options, media) { var $media = $(media); + media = $media[0]; options = _.extend({}, options); - options.noDocuments = options.onlyImages || options.noDocuments; - options.noIcons = options.onlyImages || options.noIcons; - options.noVideos = options.onlyImages || options.noVideos; + var onlyImages = options.onlyImages || this.multiImages || (media && ($media.parent().data('oeField') === 'image' || $media.parent().data('oeType') === 'image')); + options.noDocuments = onlyImages || options.noDocuments; + options.noIcons = onlyImages || options.noIcons; + options.noVideos = onlyImages || options.noVideos; this._super(parent, _.extend({}, { title: _t("Select a Media"), @@ -92,19 +94,6 @@ var MediaDialog = Dialog.extend({ var promises = [this._super.apply(this, arguments)]; this.$modal.find('.modal-dialog').addClass('o_select_media_dialog'); - if (this.imageWidget) { - this.imageWidget.clear(); - } - if (this.documentWidget) { - this.documentWidget.clear(); - } - if (this.iconWidget) { - this.iconWidget.clear(); - } - if (this.videoWidget) { - this.videoWidget.clear(); - } - if (this.imageWidget) { promises.push(this.imageWidget.appendTo(this.$("#editor-media-image"))); } @@ -170,8 +159,32 @@ var MediaDialog = Dialog.extend({ var _super = this._super; var args = arguments; return this.activeWidget.save().then(function (data) { + self._clearWidgets(); self.final_data = data; - return _super.apply(self, args); + _super.apply(self, args); + $(data).trigger('content_changed'); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Call clear on all the widgets except the activeWidget. + * We clear because every widgets are modifying the "media" element. + * All widget have the responsibility to clear a previous element that + * was created from them. + */ + _clearWidgets: function () { + [ this.imageWidget, + this.documentWidget, + this.iconWidget, + this.videoWidget + ].forEach( (widget) => { + if (widget !== this.activeWidget) { + widget && widget.clear(); + } }); }, diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index a376cff945e9..cb0db917e548 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -1,30 +1,36 @@ odoo.define('web_editor.wysiwyg', function (require) { 'use strict'; - var Widget = require('web.Widget'); -var config = require('web.config'); -var core = require('web.core'); -var session = require('web.session'); -var modulesRegistry = require('web_editor.wysiwyg.plugin.registry'); -var wysiwygOptions = require('web_editor.wysiwyg.options'); - -var _t = core._t; +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; +// core.bus +// media_dialog_demand var Wysiwyg = Widget.extend({ xmlDependencies: [ - '/web_editor/static/src/xml/wysiwyg.xml', ], - custom_events: { - getRecordInfo: '_onGetRecordInfo', - wysiwyg_blur: '_onWysiwygBlur', - }, defaultOptions: { - codeview: config.isDebug(), + '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: {}, }, }, - /** * @params {Object} params * @params {Object} params.recordInfo @@ -44,12 +50,10 @@ var Wysiwyg = Widget.extend({ **/ init: function (parent, params) { this._super.apply(this, arguments); - this.options = _.extend({}, this.defaultOptions, params); - this.attachments = this.options.attachments || []; - this.hints = []; - this.$el = null; - this._dirty = false; - this.id = _.uniqueId('wysiwyg_'); + this.params = params; + this.params.isEditableNode = function (node) { + return $(node).is(':o_editable'); + }; }, /** * Load assets and color picker template then call summernote API @@ -58,91 +62,51 @@ var Wysiwyg = Widget.extend({ * @override **/ willStart: function () { - var self = this; + new SummernoteManager(this); this.$target = this.$el; - this.$el = null; // temporary null to avoid hidden error, setElement when start - return this._super() - .then(function () { - return modulesRegistry.start(self).then(function () { - return self._loadInstance(); - }); - }); + return this._super.apply(this, arguments); }, /** * * @override */ start: function () { - var value = this._summernote.code(); - this._value = value; - if (this._summernote.invoke('HelperPlugin.hasJinja', value)) { - this._summernote.invoke('codeview.forceActivate'); - } - return Promise.resolve(); + var self = this; + this.$target.wrap('<odoo-wysiwyg-container>'); + 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); + 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(() => { + this.$editor.trigger('mouseup'); + }); }, /** * @override */ destroy: function () { - if (this._summernote) { - // prevents the replacement of the target by the content of summernote - // (in order to be able to cancel) - var removeLayout = $.summernote.ui.removeLayout; - $.summernote.ui.removeLayout = function ($note, layoutInfo) { - layoutInfo.editor.remove(); - $note.show(); - }; - this._summernote.destroy(); - $.summernote.ui.removeLayout = removeLayout; + $(document).off('mousedown', this._blur); + if (this.$target && this.$target.is('textarea') && this.$target.next('.note-editor').length) { + this.$target.summernote('destroy'); } - this.$target.removeAttr('data-wysiwyg-id'); - this.$target.removeData('wysiwyg'); - $(document).off('.' + this.id); this._super(); }, - //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- - - /** - * Add a step (undo) in editor. - */ - addHistoryStep: function () { - var editor = this._summernote.modules.editor; - editor.createRange(); - editor.history.recordUndo(); - }, /** * Return the editable area. * * @returns {jQuery} */ getEditable: function () { - if (this._summernote.invoke('HelperPlugin.hasJinja', this._summernote.code())) { - return this._summernote.layoutInfo.codable; - } else if (this._summernote.invoke('codeview.isActivated')) { - this._summernote.invoke('codeview.deactivate'); - } - return this._summernote.layoutInfo.editable; - }, - /** - * Perform undo or redo in the editor. - * - * @param {integer} step - */ - history: function (step) { - if (step < 0) { - while (step) { - this._summernote.modules.editor.history.rewind(); - step++; - } - } else if (step > 0) { - while (step) { - this._summernote.modules.editor.history.redo(); - step--; - } - } + console.log('getEditable'); }, /** * Return true if the content has changed. @@ -150,10 +114,7 @@ var Wysiwyg = Widget.extend({ * @returns {Boolean} */ isDirty: function () { - if (!this._dirty && this._value !== this._summernote.code()) { - console.warn("not dirty flag ? Please fix it."); - } - return this._value !== this._summernote.code(); + return this._value !== (this.$editor.html() || this.$editor.val()); }, /** * Return true if the current node is unbreakable. @@ -165,8 +126,7 @@ var Wysiwyg = Widget.extend({ * @returns {Boolean} */ isUnbreakableNode: function (node) { - return ["TD", "TR", "TBODY", "TFOOT", "THEAD", "TABLE"].indexOf(node.tagName) !== -1 || $(node).is(this.getEditable()) || - !this.isEditableNode(node.parentNode) || !this.isEditableNode(node) || $.summernote.dom.isMedia(node); + console.log('isUnbreakableNode'); }, /** * Return true if the current node is editable (for keypress and selection). @@ -175,31 +135,24 @@ var Wysiwyg = Widget.extend({ * @returns {Boolean} */ isEditableNode: function (node) { - return $(node).is(':o_editable') && !$(node).is('table, thead, tbody, tfoot, tr'); + console.log('isEditableNode'); }, /** * Set the focus on the element. */ focus: function () { - this.$el.mousedown(); + console.log('focus'); }, /** * Get the value of the editable element. * * @param {object} [options] - * @param {Boolean} [options.keepPopover] * @param {jQueryElement} [options.$layout] * @returns {String} */ getValue: function (options) { - if (!options || !options.keepPopover) { - this._summernote.invoke('editor.hidePopover'); - } - var $editable = options && options.$layout || this.getEditable().clone(); - $editable.find('.o_wysiwyg_to_remove').remove(); + var $editable = options && options.$layout || this.$editor.clone(); $editable.find('[contenteditable]').removeAttr('contenteditable'); - $editable.find('.o_fake_not_editable').removeClass('o_fake_not_editable'); - $editable.find('.o_fake_editable').removeClass('o_fake_editable'); $editable.find('[class=""]').removeAttr('class'); $editable.find('[style=""]').removeAttr('style'); $editable.find('[title=""]').removeAttr('title'); @@ -207,8 +160,7 @@ var Wysiwyg = Widget.extend({ $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() || $editable.val(); + return $editable.html(); }, /** * Save the content in the target @@ -235,468 +187,48 @@ var Wysiwyg = Widget.extend({ * @returns {String} */ setValue: function (value, options) { - if (this._summernote.invoke('HelperPlugin.hasJinja', value)) { - this._summernote.invoke('codeview.forceActivate'); + if (this.$editor.is('textarea')) { + this.$target.val(value); + } else { + this.$target.html(value); } - this._dirty = true; - this._summernote.invoke('HistoryPlugin.clear'); - this._summernote.invoke('editor.hidePopover'); - this._summernote.invoke('editor.clearTarget'); - var $editable = this.getEditable().html(value + ''); - this._summernote.invoke('UnbreakablePlugin.secureArea'); - if (!options || options.notifyChange !== false) { - $editable.change(); + 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 //-------------------------------------------------------------------------- - - /** - * @private - * @returns {Object} the summernote configuration - */ _editorOptions: function () { var self = this; - var allowAttachment = !this.options.noAttachment; - - var options = JSON.parse(JSON.stringify(wysiwygOptions)); - - options.parent = this; - options.lang = "odoo"; - - options.focus = false; - options.disableDragAndDrop = !allowAttachment; - options.styleTags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre']; - options.fontSizes = [_t('Default'), '8', '9', '10', '11', '12', '13', '14', '18', '24', '36', '48', '62']; - options.minHeight = 180; - - options.keyMap.pc['CTRL+K'] = 'LinkPlugin.show'; - options.keyMap.mac['CMD+K'] = 'LinkPlugin.show'; - delete options.keyMap.pc['CTRL+LEFTBRACKET']; - delete options.keyMap.mac['CMD+LEFTBRACKET']; - delete options.keyMap.pc['CTRL+RIGHTBRACKET']; - delete options.keyMap.mac['CMD+RIGHTBRACKET']; - - options.toolbar = [ - ['style', ['style']], - ['font', ['bold', 'italic', 'underline', 'clear']], - ['fontsize', ['fontsize']], - ['color', ['colorpicker']], - ['para', ['ul', 'ol', 'paragraph']], - ['table', ['table']], - ['insert', allowAttachment ? ['linkPlugin', 'mediaPlugin'] : ['linkPlugin']], - ['history', ['undo', 'redo']], - ['view', this.options.codeview ? ['fullscreen', 'codeview', 'help'] : ['fullscreen', 'help']] - ]; - options.popover = { - image: [ - ['padding'], - ['imagesize', ['imageSizeAuto', 'imageSize100', 'imageSize50', 'imageSize25']], - ['float', ['alignLeft', 'alignCenter', 'alignRight', 'alignNone']], - ['imageShape'], - ['cropImage'], - ['media', ['mediaPlugin', 'removePluginMedia']], - ['alt'] - ], - video: [ - ['padding'], - ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']], - ['float', ['alignLeft', 'alignCenter', 'alignRight', 'alignNone']], - ['media', ['mediaPlugin', 'removePluginMedia']] - ], - icon: [ - ['padding'], - ['faSize'], - ['float', ['alignLeft', 'alignCenter', 'alignRight', 'alignNone']], - ['faSpin'], - ['media', ['mediaPlugin', 'removePluginMedia']] - ], - document: [ - ['float', ['alignLeft', 'alignCenter', 'alignRight', 'alignNone']], - ['media', ['mediaPlugin', 'removePluginMedia']] - ], - link: [ - ['link', ['linkPlugin', 'unlink']] - ], - table: [ - ['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']], - ['delete', ['deleteRow', 'deleteCol', 'deleteTable']] - ], - }; - - options.callbacks = { - onBlur: this._onBlurEditable.bind(this), - onFocus: this._onFocusEditable.bind(this), - onChange: this._onChange.bind(this), - onImageUpload: this._onImageUpload.bind(this), - onFocusnode: this._onFocusnode.bind(this), + var options = Object.assign({}, $.summernote.options, this.defaultOptions, this.options); + if (this.options.generateOptions) { + options = this.options.generateOptions(options); + } + options.airPopover = options.toolbar; + options.onChange = function (html, $editable) { + $editable.trigger('content_changed'); + self.trigger_up('wysiwyg_change'); }; - - options.isUnbreakableNode = function (node) { - node = node && (node.tagName ? node : node.parentNode); - if (!node) { - return true; - } - return self.isUnbreakableNode(node); + options.onUpload = function (attachments) { + self.trigger_up('wysiwyg_attachment', attachments); }; - options.isEditableNode = function (node) { - node = node && (node.tagName ? node : node.parentNode); - if (!node) { - return false; - } - return self.isEditableNode(node); + options.onFocus = function () { + self.trigger_up('wysiwyg_focus'); }; - options.displayPopover = this._isDisplayingPopover.bind(this); - options.hasFocus = function () { - return self._isFocused; + options.onBlur = function () { + self.trigger_up('wysiwyg_blur'); }; - - if (this.options.generateOptions) { - this.options.generateOptions(options); - } - return options; }, - /** - * @private - * @returns {Object} modules list to load - */ - _getPlugins: function () { - return _.extend({}, wysiwygOptions.modules, modulesRegistry.plugins()); - }, - /** - * Return an object describing the linked record. - * - * @private - * @param {Object} options - * @returns {Object} {res_id, res_model, xpath} - */ - _getRecordInfo: function (options) { - var data = this.options.recordInfo || {}; - if (typeof data === 'function') { - data = data(options); - } - if (!data.context) { - throw new Error("Context is missing"); - } - return data; - }, - /** - * Return true if the editor is displaying the popover. - * - * @private - * @param {Node} node - * @returns {Boolean} - */ - _isDisplayingPopover: function (node) { - return true; - }, - /** - * Return true if the given node is in the editor. - * Note: a button in the MediaDialog returns true. - * - * @private - * @param {Node} node - * @returns {Boolean} - */ - _isEditorContent: function (node) { - if (this.el === node) { - return true; - } - if ($.contains(this.el, node)) { - return true; - } - - var children = this.getChildren(); - var allChildren = []; - var child; - while ((child = children.pop())) { - allChildren.push(child); - children = children.concat(child.getChildren()); - } - - var childrenDom = _.filter(_.unique(_.flatten(_.map(allChildren, function (obj) { - return _.map(obj, function (value) { - return value instanceof $ ? value.get() : value; - }); - }))), function (node) { - return node && node.DOCUMENT_NODE && node.tagName && node.tagName !== 'BODY' && node.tagName !== 'HTML'; - }); - return !!$(node).closest(childrenDom).length; - }, - /** - * Create an instance with the API lib. - * - * @private - * @returns {$.Promise} - */ - _loadInstance: function () { - var defaultOptions = this._editorOptions(); - var summernoteOptions = _.extend({ - id: this.id, - }, defaultOptions, _.omit(this.options, 'isEditableNode', 'isUnbreakableNode')); - - _.extend(summernoteOptions.callbacks, defaultOptions.callbacks, this.options.callbacks); - if (this.options.keyMap) { - _.defaults(summernoteOptions.keyMap.pc, defaultOptions.keyMap.pc); - _.each(summernoteOptions.keyMap.pc, function (v, k, o) { - if (!v) { - delete o[k]; - } - }); - _.defaults(summernoteOptions.keyMap.mac, defaultOptions.keyMap.mac); - _.each(summernoteOptions.keyMap.mac, function (v, k, o) { - if (!v) { - delete o[k]; - } - }); - } - - var plugins = _.extend(this._getPlugins(), this.options.plugins); - summernoteOptions.modules = _.omit(plugins, function (v) { - return !v; - }); - - if (this.$target.parent().length) { - summernoteOptions.container = this.$target.parent().css('position', 'relative')[0]; - } else { - summernoteOptions.container = this.$target[0].ownerDocument.body; - } - - this.$target.summernote(summernoteOptions); - - this._summernote = this.$target.data('summernote'); - this._summernote.layoutInfo.editable.data('wysiwyg', this); - this.$target.attr('data-wysiwyg-id', this.id).data('wysiwyg', this); - $('.note-editor, .note-popover').not('[data-wysiwyg-id]').attr('data-wysiwyg-id', this.id); - - this.setElement(this._summernote.layoutInfo.editor); - $(document).on('mousedown.' + this.id, this._onMouseDown.bind(this)); - $(document).on('mouseenter.' + this.id, '*', this._onMouseEnter.bind(this)); - $(document).on('mouseleave.' + this.id, '*', this._onMouseLeave.bind(this)); - $(document).on('mousemove.' + this.id, this._onMouseMove.bind(this)); - - this.$el.removeClass('card'); - - return Promise.resolve(); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * trigger_up 'wysiwyg_focus'. - * - * @private - * @param {Object} [options] - */ - _onFocus: function (options) { - this.trigger_up('wysiwyg_focus', options); - }, - /** - * trigger_up 'wysiwyg_blur'. - * - * @private - * @param {Object} [options] - */ - _onBlur: function (options) { - this.trigger_up('wysiwyg_blur', options); - }, - - //-------------------------------------------------------------------------- - // Handler - //-------------------------------------------------------------------------- - - /** - * @private - * @param {jQueryEvent} ev - */ - _onMouseEnter: function (ev) { - if (this._isFocused && !this._mouseInEditor && this._isEditorContent(ev.target)) { - this._mouseInEditor = true; - } - }, - /** - * @private - * @param {jQueryEvent} ev - */ - _onMouseLeave: function (ev) { - if (this._isFocused && this._mouseInEditor) { - this._mouseInEditor = null; - } - }, - /** - * @private - * @param {jQueryEvent} ev - */ - _onMouseMove: function (ev) { - if (this._mouseInEditor === null) { - this._mouseInEditor = !!this._isEditorContent(ev.target); - } - }, - /** - * @private - * @param {jQueryEvent} ev - */ - _onMouseDown: function (ev) { - var self = this; - if (this._isEditorContent(ev.target)) { - setTimeout(function () { - if (!self._editableHasFocus && !self._isEditorContent(document.activeElement)) { - self._summernote.layoutInfo.editable.focus(); - } - if (!self._isFocused) { - self._isFocused = true; - self._onFocus(); - } - }); - } else if (this._isFocused) { - this._isFocused = false; - this._onBlur(); - } - }, - /** - * @private - * @param {jQueryEvent} ev - */ - _onBlurEditable: function (ev) { - var self = this; - this._editableHasFocus = false; - if (!this._isFocused) { - return; - } - if (!this._justFocused && !this._mouseInEditor) { - if (this._isFocused) { - this._isFocused = false; - this._onBlur(); - } - } else if (!this._forceEditableFocus) { - this._forceEditableFocus = true; - setTimeout(function () { - if (!self._isEditorContent(document.activeElement)) { - self._summernote.layoutInfo.editable.focus(); - } - self._forceEditableFocus = false; // prevent stack size exceeded. - }); - } else { - this._mouseInEditor = null; - } - }, - /** - * @private - * @param {OdooEvent} ev - */ - _onWysiwygBlur: function (ev) { - if (ev.target === this) { - return; - } - ev.stopPropagation(); - this._isFocused = false; - this._forceEditableFocus = false; - this._mouseInEditor = false; - this._summernote.disable(); - this.$target.focus(); - setTimeout(this._summernote.enable.bind(this._summernote)); - this._onBlur(ev.data); - }, - /** - * @private - * @param {jQueryEvent} ev - */ - _onFocusEditable: function (ev) { - var self = this; - this._editableHasFocus = true; - this._justFocused = true; - setTimeout(function () { - self._justFocused = false; - }); - }, - /** - * trigger_up 'wysiwyg_change' - * - * @private - */ - _onChange: function () { - var html = this._summernote.code(); - if (this.hints.length) { - var hints = []; - _.each(this.hints, function (hint) { - if (html.indexOf('@' + hint.name) !== -1) { - hints.push(hint); - } - }); - this.hints = hints; - } - - this._dirty = true; - this.trigger_up('wysiwyg_change', { - html: html, - hints: this.hints, - attachments: this.attachments, - }); - }, - /** - * trigger_up 'wysiwyg_attachment' when add an image found in the view. - * - * This method is called when an image is uploaded by the media dialog and returns the - * object attachment as recorded in the "ir.attachment" model, via a wysiwyg_attachment event. - * - * 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 - */ - _onImageUpload: function (attachments) { - var self = this; - attachments = _.filter(attachments, function (attachment) { - return !_.findWhere(self.attachments, { - id: attachment.id, - }); - }); - if (!attachments.length) { - return; - } - this.attachments = this.attachments.concat(attachments); - - // todo remove image not in the view - - this.trigger_up.bind(this, 'wysiwyg_attachment', this.attachments); - }, - /** - * Called when the carret focuses on another node (focus event, mouse event, or key arrow event) - * from Unbreakable - * - * @private - * @param {Node} node - */ - _onFocusnode: function (node) {}, - /** - * Do not override. - * - * @see _getRecordInfo - * @private - * @param {OdooEvent} ev - * @param {Object} ev.data - * @param {Object} ev.data.recordInfo - * @param {Function(recordInfo)} ev.data.callback - */ - _onGetRecordInfo: function (ev) { - var data = this._getRecordInfo(ev.data); - data.attachmentIDs = _.pluck(this.attachments, 'id'); - data.user_id = session.uid || session.user_id; - ev.data.callback(data); - }, }); - //-------------------------------------------------------------------------- // Public helper //-------------------------------------------------------------------------- - /** * @param {Node} node (editable or node inside) * @returns {Object} @@ -706,7 +238,7 @@ var Wysiwyg = Widget.extend({ * @returns {Number} eo - end offset */ Wysiwyg.getRange = function (node) { - var range = $.summernote.range.create(); + var range = $.summernote.core.range.create(); return range && { sc: range.sc, so: range.so, @@ -714,21 +246,8 @@ Wysiwyg.getRange = function (node) { eo: range.eo, }; }; -/** - * @param {Node} startNode - * @param {Number} startOffset - * @param {Node} endNode - * @param {Number} endOffset - */ -Wysiwyg.setRange = function (startNode, startOffset, endNode, endOffset) { - $(startNode).focus(); - if (endNode) { - $.summernote.range.create(startNode, startOffset, endNode, endOffset).select(); - } else { - $.summernote.range.create(startNode, startOffset).select(); - } - // trigger for Unbreakable - $(startNode.tagName ? startNode : startNode.parentNode).trigger('wysiwyg.range'); +Wysiwyg.setRange = function () { + console.log('setRange'); }; /** * @param {Node} node - dom node @@ -745,7 +264,6 @@ Wysiwyg.setRangeFromNode = function (node, options) { while (first.firstChild) { first = first.firstChild; } - if (options && options.begin && !options.end) { Wysiwyg.setRange(first, 0); } else if (options && !options.begin && options.end) { @@ -754,38 +272,13 @@ Wysiwyg.setRangeFromNode = function (node, options) { Wysiwyg.setRange(first, 0, last, last.tagName ? last.childNodes.length : last.textContent.length); } }; - -//-------------------------------------------------------------------------- -// jQuery extensions -//-------------------------------------------------------------------------- - -$.extend($.expr[':'], { - o_editable: function (node, i, m) { - while (node) { - if (node.attributes) { - var className = _.isString(node.className) && node.className || ''; - if ( - className.indexOf('o_not_editable') !== -1 || - (node.attributes.contenteditable && - node.attributes.contenteditable.value !== 'true' && - className.indexOf('o_fake_not_editable') === -1) - ) { - return false; - } - if ( - className.indexOf('o_editable') !== -1 || - (node.attributes.contenteditable && - node.attributes.contenteditable.value === 'true' && - className.indexOf('o_fake_editable') === -1) - ) { - return true; - } - } - node = node.parentNode; - } - return false; - }, -}); - 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/scss/web_editor.backend.scss b/addons/web_editor/static/src/scss/web_editor.backend.scss index af2398bc98e4..ecbe42721f3e 100644 --- a/addons/web_editor/static/src/scss/web_editor.backend.scss +++ b/addons/web_editor/static/src/scss/web_editor.backend.scss @@ -56,3 +56,29 @@ } } } + +.o_field_widgetTextHtml_fullscreen { + .oe_form_field_html.o_form_fullscreen_ancestor iframe { + position: absolute !important; + left: 0 !important; + right: 0 !important; + top: 0 !important; + bottom: 0 !important; + width: 100% !important; + min-height: 100% !important; + z-index: 1001 !important; + border: 0; + } + * { + display: none; + } + .o_form_fullscreen_ancestor { + display: block !important; + position: static !important; + top: 0 !important; + left: 0 !important; + width: auto !important; + overflow: hidden !important; + transform: none !important; + } +} diff --git a/addons/web_editor/static/src/scss/web_editor.common.scss b/addons/web_editor/static/src/scss/web_editor.common.scss index cc3b2aa9c4b5..43f174e28628 100644 --- a/addons/web_editor/static/src/scss/web_editor.common.scss +++ b/addons/web_editor/static/src/scss/web_editor.common.scss @@ -21,6 +21,17 @@ html, body { display: none; } +.note-popover .popover, .note-editor { + .dropdown-menu .dropdown-item { + > i { + visibility: hidden; + } + &.checked > i { + visibility: visible; + } + } +} + /* ----- GENERIC LAYOUTING HELPERS ---- */ /* table */ #wrapwrap, .o_editable { diff --git a/addons/web_editor/static/src/scss/wysiwyg.scss b/addons/web_editor/static/src/scss/wysiwyg.scss index af674a58a0b4..58fe5f7ec425 100644 --- a/addons/web_editor/static/src/scss/wysiwyg.scss +++ b/addons/web_editor/static/src/scss/wysiwyg.scss @@ -36,6 +36,7 @@ height: $o-wysiwyg-toolbar-height; } +#web_editor-toolbars .popover, .note-popover.popover .popover-content, .note-editor .note-toolbar { font-family: $o-wysiwyg-font-family; @@ -343,6 +344,11 @@ body .modal { } } + .o_we_attachment_selected { + border-color: $o-brand-primary; + box-shadow: 0px 0px 2px 2px $o-brand-primary; + } + .font-icons-icons { >span { text-align: center; diff --git a/addons/web_editor/static/src/xml/backend.xml b/addons/web_editor/static/src/xml/backend.xml index 904f21f440cf..c79ee9392838 100644 --- a/addons/web_editor/static/src/xml/backend.xml +++ b/addons/web_editor/static/src/xml/backend.xml @@ -9,4 +9,12 @@ </div> </t> + <t t-name="web_editor.FieldTextHtml.fullscreen"> + <span style="margin: 5px; position: fixed; top: 0; right: 0; z-index: 2000;"> + <button class="o_fullscreen btn btn-primary" style="width: 24px; height: 24px; background-color: #337ab7; border: 1px solid #2e6da4; border-radius: 4px; padding: 0; position: relative;"> + <img src="/web_editor/font_to_img/61541/rgb(255,255,255)/16" style="position: absolute; top: 3px; left: 4px;" alt="Fullscreen"/> + </button> + </span> + </t> + </templates> diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml index aa8b5a2f094f..ca653c5d7ea8 100644 --- a/addons/web_editor/static/src/xml/editor.xml +++ b/addons/web_editor/static/src/xml/editor.xml @@ -1,5 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <templates id="template" xml:space="preserve"> + <!--=================--> + <!-- Base components --> + <!--=================--> + + <!-- Editor top bar which contains the summernote tools and save/discard buttons --> + <t t-name="web_editor.editorbar"> + <div id="web_editor-toolbars"/> + </t> + <!--=================--> <!-- Snippet options --> <!--=================--> diff --git a/addons/web_editor/static/tests/field_html_tests.js b/addons/web_editor/static/tests/field_html_tests.js index d307706557fb..f9e08872bd35 100644 --- a/addons/web_editor/static/tests/field_html_tests.js +++ b/addons/web_editor/static/tests/field_html_tests.js @@ -822,7 +822,7 @@ QUnit.test('save immediately before iframe is rendered in edit mode', async func "should not have a translate button in readonly mode"); await testUtils.form.clickEdit(form); - var $button = form.$('.oe_form_field_html .note-toolbar .o_field_translate'); + var $button = form.$('.oe_form_field_html .o_field_translate'); assert.strictEqual($button.length, 1, "should have a translate button"); $button.click(); @@ -854,9 +854,8 @@ QUnit.test('save immediately before iframe is rendered in edit mode', async func return this._super.apply(this, arguments); }, }); - var $field = form.$('.oe_form_field[name="body"]'); await testUtils.form.clickEdit(form); - $field = form.$('.oe_form_field[name="body"]'); + var $field = form.$('.oe_form_field[name="body"]'); var $iframe = $field.find('iframe'); await $iframe.data('loadDef'); @@ -864,7 +863,7 @@ QUnit.test('save immediately before iframe is rendered in edit mode', async func var doc = $iframe.contents()[0]; var $content = $('#iframe_target', doc); - var $button = $content.find('.note-toolbar .o_field_translate'); + var $button = $content.find('.o_field_translate'); assert.strictEqual($button.length, 1, "should have a translate button"); await testUtils.dom.click($button); diff --git a/addons/web_editor/static/tests/test_utils.js b/addons/web_editor/static/tests/test_utils.js index c6d043a07228..3b1563d31823 100644 --- a/addons/web_editor/static/tests/test_utils.js +++ b/addons/web_editor/static/tests/test_utils.js @@ -152,40 +152,6 @@ var WysiwygTest = Wysiwyg.extend({ this.$target.remove(); this._parentToDestroyForTest.destroy(); }, - /** - * @override - */ - isUnbreakableNode: function (node) { - var Node = (node.tagName ? node : node.parentNode); - return (!this.options.useOnlyTestUnbreakable && this._super(node)) || - !this.isEditableNode(node) || - Node.tagName === "UNBREAKABLE" || - (Node.className + '').indexOf('unbreakable') !== -1; - }, - /** - * @override - */ - isEditableNode: function (node) { - if (!$(node).closest(this.$el).length) { - return false; - } - while (node) { - if (node.tagName === "EDITABLE") { - return true; - } - if (node.tagName === "NOTEDITABLE") { - return false; - } - if ((node.className + '').indexOf('editable') !== -1) { - return true; - } - if (this.$el[0] === node) { - return true; - } - node = node.parentNode; - } - return false; - }, }); diff --git a/addons/web_tour/static/src/js/running_tour_action_helper.js b/addons/web_tour/static/src/js/running_tour_action_helper.js index a9bc1c220c0b..f41c837fee44 100644 --- a/addons/web_tour/static/src/js/running_tour_action_helper.js +++ b/addons/web_tour/static/src/js/running_tour_action_helper.js @@ -94,7 +94,7 @@ var RunningTourActionHelper = core.Class.extend({ } else { values.$element.focusIn(); values.$element.trigger($.Event( "keydown", {key: '_', keyCode: 95})); - values.$element.text(text); + values.$element.text(text).trigger("input"); values.$element.focusInEnd(); values.$element.trigger($.Event( "keyup", {key: '_', keyCode: 95})); } diff --git a/addons/web_unsplash/models/ir_qweb.py b/addons/web_unsplash/models/ir_qweb.py index 8d581c472994..f4f75c2b1ade 100644 --- a/addons/web_unsplash/models/ir_qweb.py +++ b/addons/web_unsplash/models/ir_qweb.py @@ -8,7 +8,7 @@ class Image(models.AbstractModel): @api.model def from_html(self, model, field, element): - url = element.find('img').get('src') + url = element.find('.//img').get('src') url_object = urls.url_parse(url) if url_object.path.startswith('/unsplash/'): diff --git a/addons/website/static/src/js/editor/editor_menu.js b/addons/website/static/src/js/editor/editor_menu.js index b370815ccfdb..72c2e6bee81f 100644 --- a/addons/website/static/src/js/editor/editor_menu.js +++ b/addons/website/static/src/js/editor/editor_menu.js @@ -39,7 +39,6 @@ var EditorMenu = Widget.extend({ $wrapwrap.removeClass('o_editable'); // clean the dom before edition self.editable($wrapwrap).addClass('o_editable'); self.wysiwyg = self._wysiwygInstance(); - return self.wysiwyg.attachTo($wrapwrap); }); }, /** @@ -48,7 +47,7 @@ var EditorMenu = Widget.extend({ start: function () { var self = this; this.$el.css({width: '100%'}); - return this._super().then(function () { + return this.wysiwyg.attachTo($('#wrapwrap')).then(function () { self.trigger_up('edit_mode'); self.$el.css({width: ''}); }); @@ -92,6 +91,7 @@ var EditorMenu = Widget.extend({ var $wrapwrap = $('#wrapwrap'); self.editable($wrapwrap).removeClass('o_editable'); if (reload !== false) { + window.onbeforeunload = null; self.wysiwyg.destroy(); return self._reload(); } else { @@ -113,7 +113,7 @@ var EditorMenu = Widget.extend({ save: function (reload) { var self = this; this.trigger_up('edition_will_stopped'); - return this.wysiwyg.save().then(function (result) { + return this.wysiwyg.save(true).then(function (result) { var $wrapwrap = $('#wrapwrap'); self.editable($wrapwrap).removeClass('o_editable'); if (result.isDirty && reload !== false) { @@ -196,7 +196,7 @@ var EditorMenu = Widget.extend({ * @private */ _onCancelClick: function () { - this.cancel(false); + this.cancel(true); }, /** * Get the cleaned value of the editable element. diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index 87886434b37f..d659677c478d 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -1001,7 +1001,7 @@ options.registry.gallery = options.Class.extend({ addImages: function (previewMode) { var self = this; var $container = this.$('.container:first'); - var dialog = new weWidgets.MediaDialog(this, {multiImages: true, onlyImages: true, mediaWidth: 1920}, null); + var dialog = new weWidgets.MediaDialog(this, {multiImages: true, onlyImages: true, mediaWidth: 1920}); var lastImage = _.last(this._getImages()); var index = lastImage ? this._getIndex(lastImage) : -1; dialog.on('save', this, function (attachments) { diff --git a/addons/website/static/src/js/editor/wysiwyg_multizone.js b/addons/website/static/src/js/editor/wysiwyg_multizone.js index 64c3e84daed5..202257857d2b 100644 --- a/addons/website/static/src/js/editor/wysiwyg_multizone.js +++ b/addons/website/static/src/js/editor/wysiwyg_multizone.js @@ -1,73 +1,6 @@ odoo.define('web_editor.wysiwyg.multizone', function (require) { 'use strict'; -var concurrency = require('web.concurrency'); -var core = require('web.core'); -var DropzonePlugin = require('web_editor.wysiwyg.plugin.dropzone'); -var HelperPlugin = require('web_editor.wysiwyg.plugin.helper'); -var TextPlugin = require('web_editor.wysiwyg.plugin.text'); -var HistoryPlugin = require('web_editor.wysiwyg.plugin.history'); -var Wysiwyg = require('web_editor.wysiwyg.snippets'); - -var _t = core._t; - -HistoryPlugin.include({ - /** - * @override - */ - applySnapshot: function () { - this.trigger_up('content_will_be_destroyed', { - $target: this.$editable, - }); - this._super.apply(this, arguments); - this.trigger_up('content_was_recreated', { - $target: this.$editable, - }); - $('.oe_overlay').remove(); - $('.note-control-selection').hide(); - this.$editable.trigger('content_changed'); - }, -}); - -HelperPlugin.include({ - /** - * Returns true if range is on or within a field that is not of type 'html'. - * - * @returns {Boolean} - */ - isOnNonHTMLField: function () { - var range = this.context.invoke('editor.createRange'); - return !!$.summernote.dom.ancestor(range.sc, function (node) { - return $(node).data('oe-type') && $(node).data('oe-type') !== 'html'; - }); - }, -}); - -TextPlugin.include({ - /** - * Paste text only if on non-HTML field. - * - * @override - */ - pasteNodes: function (nodes, textOnly) { - textOnly = textOnly || this.context.invoke('HelperPlugin.isOnNonHTMLField'); - this._super.apply(this, [nodes, textOnly]); - }, -}); - -DropzonePlugin.include({ - /** - * Prevent dropping images in non-HTML fields. - * - * @override - */ - _canDropHere: function (dataTransfer) { - if (this.context.invoke('HelperPlugin.isOnNonHTMLField') && dataTransfer.files.length) { - return false; - } - return this._super(); - }, -}); - +var Wysiwyg = require('web_editor.wysiwyg'); /** @@ -80,499 +13,64 @@ DropzonePlugin.include({ * */ var WysiwygMultizone = Wysiwyg.extend({ - events: _.extend({}, Wysiwyg.prototype.events, { - 'keyup *': function (ev) { - if ((ev.keyCode === 8 || ev.keyCode === 46)) { - var $target = $(ev.target).closest('.o_editable'); - if (!$target.is(':has(*:not(p):not(br))') && !$target.text().match(/\S/)) { - $target.empty(); - } - } - if (ev.key.length === 1) { - this._onChangeThrottled(); - } - }, - 'click .note-editable': function (ev) { - ev.preventDefault(); - }, - 'submit .note-editable form .btn': function (ev) { - ev.preventDefault(); // Disable form submition in editable mode - }, - 'hide.bs.dropdown .dropdown': function (ev) { - // Prevent dropdown closing when a contenteditable children is focused - if (ev.originalEvent && - $(ev.target).has(ev.originalEvent.target).length && - $(ev.originalEvent.target).is('[contenteditable]')) { - ev.preventDefault(); - } - }, - }), - custom_events: _.extend({}, Wysiwyg.prototype.custom_events, { - activate_snippet: '_onActivateSnippet', - drop_images: '_onDropImages', - }), - /** - * @override - * @param {Object} options.context - the context to use for the saving rpc - * @param {boolean} [options.withLang=false] - * false if the lang must be omitted in the context (saving "master" - * page element) - */ - init: function (parent, options) { - options = options || {}; - options.addDropSelector = ':o_editable'; - this.savingMutex = new concurrency.Mutex(); - this._super(parent, options); - this._onChangeThrottled = _.throttle(this._onChange.bind(this), 300); - }, + /** - * Prevent some default features for the editable area. - * * @override */ start: function () { var self = this; - return this._super().then(function () { - // Unload preserve - var flag = false; - window.onbeforeunload = function (event) { - if (self.isDirty() && !flag) { - flag = true; - _.defer(function () { - flag = false; - }); - return _t('Changes you made may not be saved.'); - } - }; - // firefox & IE fix - try { - document.execCommand('enableObjectResizing', false, false); - document.execCommand('enableInlineTableEditing', false, false); - document.execCommand('2D-position', false, false); - } catch (e) { /* */ } - document.body.addEventListener('resizestart', function (evt) { - evt.preventDefault(); - return false; - }); - document.body.addEventListener('movestart', function (evt) { - evt.preventDefault(); - return false; - }); - document.body.addEventListener('dragstart', function (evt) { - evt.preventDefault(); - return false; - }); - // BOOTSTRAP preserve - self.init_bootstrap_carousel = $.fn.carousel; - $.fn.carousel = function () { - var res = self.init_bootstrap_carousel.apply(this, arguments); - // off bootstrap keydown event to remove event.preventDefault() - // and allow to change cursor position - $(this).off('keydown.bs.carousel'); - return res; - }; - self.$('.dropdown-toggle').dropdown(); - self.$el - .tooltip({ - selector: '[data-oe-readonly]', - container: 'body', - trigger: 'hover', - delay: { - 'show': 1000, - 'hide': 100, - }, - placement: 'bottom', - title: _t("Readonly field") - }) - .on('click', function () { - $(this).tooltip('hide'); - }); - $('body').addClass('editor_enable'); - $('.note-editor, .note-popover').filter('[data-wysiwyg-id="' + self.id + '"]').addClass('wysiwyg_multizone'); - $('.note-editable .note-editor, .note-editable .note-editable').attr('contenteditable', false); - - self._summernote.isDisabled = function () { - return false; - }; - - self.$('.note-editable').addClass('o_not_editable').attr('contenteditable', false); - self._getEditableArea().attr('contenteditable', true); - self.$('[data-oe-readonly]').addClass('o_not_editable').attr('contenteditable', false); - self.$('.oe_structure').attr('contenteditable', false).addClass('o_fake_not_editable'); - self.$('[data-oe-field][data-oe-type="image"]').attr('contenteditable', false).addClass('o_fake_not_editable'); - }); - }, - /** - * @override - */ - destroy: function () { - this._super(); - this.$target.css('display', ''); - this.$target.find('[data-old-id]').add(this.$target).each(function () { - var $node = $(this); - $node.attr('id', $node.attr('data-old-id')).removeAttr('data-old-id'); - }); - $('body').removeClass('editor_enable'); - window.onbeforeunload = null; - $.fn.carousel = this.init_bootstrap_carousel; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - * @returns {Boolean} - */ - isDirty: function () { - return !!this._getEditableArea().filter('.o_dirty').length; + this.options.toolbarHandler = $('#web_editor-top-edit'); + this.options.saveElement = function ($el, context, withLang) { + var outerHTML = this._getEscapedElement($el).prop('outerHTML'); + return self._saveElement(outerHTML, self.options.recordInfo, $el[0]); + }; + return this._super(); }, /** * @override - * @returns {$.Promise} resolve with true if the content was dirty + * @returns {Promise} */ save: function () { - var isDirty = this.isDirty(); - if (isDirty) { - this.savingMutex.exec(this._saveCroppedImages.bind(this)); + if (this.isDirty()) { + return this.editor.save().then(function() { + return {isDirty: true}; + }); + } else { + return {isDirty: false}; } - var _super = this._super.bind(this); - return this.savingMutex.lock.then(function () { - return _super().then(function (res) { - this._summernote.layoutInfo.editable.html(res.html); - - var $editable = this._getEditableArea(); - var $areaDirty = $editable.filter('.o_dirty'); - if (!$areaDirty.length) { - return { isDirty: false }; - } - $areaDirty.each(function (index, editable) { - this.savingMutex.exec(this._saveEditable.bind(this, editable)); - }.bind(this)); - return this.savingMutex.lock.then(function () { - return { isDirty: true }; - }); - }.bind(this)); - }.bind(this)); }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- - /** - * Return true if the editor is displaying the popover. - * - * @override - * @returns {Boolean} - */ - _isDisplayingPopover: function (node) { - return this._super(node) && $(node).parent().closest('[data-oe-model="ir.ui.view"], [data-oe-type="html"]').length; - }, - /** - * @override - * @returns {Object} the summernote configuration - */ - _editorOptions: function () { - var options = this._super(); - options.toolbar[8] = ['view', ['help']]; - - // remove blockquote (it's a snippet) - var blockquote = options.styleTags.indexOf('blockquote'); - if (blockquote !== -1) options.styleTags.splice(blockquote, 1); - - options.popover.image[4] = ['editImage', ['cropImage', 'transform']]; - return _.extend(options, { - styleWithSpan: false, - followingToolbar: false, - }); - }, - /** - * Escape internal text nodes for XML storage. - * - * @private - * @param {jQuery} $el - */ - _escapeElements: function ($el) { - var toEscape = $el.find('*').addBack(); - toEscape = toEscape.not(toEscape.filter('object,iframe,script,style,[data-oe-model][data-oe-model!="ir.ui.view"]').find('*').addBack()); - toEscape.contents().each(function () { - if (this.nodeType === 3) { - this.nodeValue = $('<div />').text(this.nodeValue).html(); - } - }); - }, - /** - * Gets jQuery cloned element with clean for XML storage. - * - * @private - * @param {jQuery} $el - * @returns {jQuery} - */ - _getCleanedHtml: function (editable) { - var $el = $(editable).clone().removeClass('o_editable o_dirty'); - this._escapeElements($el); - return $el; - }, - /** - * Return an object describing the linked record. - * - * @override - * @param {Object} options - * @returns {Object} {res_id, res_model, xpath} - */ - _getRecordInfo: function (options) { - options = options || {}; - var $editable = $(options.target).closest(this._getEditableArea()); - if (!$editable.length) { - $editable = $(this._getFocusedEditable()); - } - var data = this._super(); - var res_id = $editable.data('oe-id'); - var res_model = $editable.data('oe-model'); - if (!$editable.data('oe-model') && $('html').data('editable')) { - var object = $('html').data('main-object'); - res_model = object.split('(')[0]; - res_id = +object.split('(')[1].split(',')[0]; - } - var xpath = $editable.data('oe-xpath'); - - if (options.type === 'media' && (res_model === 'website.page' || res_model === 'ir.ui.view')) { - res_id = 0; - res_model = 'ir.ui.view'; - xpath = null; - } - - return _.extend(data, { - res_id: res_id, - res_model: res_model, - xpath: xpath, - }); - }, - /** - * Return the focused editable area. - * - * @private - * @returns {Node} - */ - _getFocusedEditable: function () { - var $focusedNode = $(this._focusedNode); - var $editableArea = this._getEditableArea(); - return $focusedNode.closest($editableArea)[0] || - $focusedNode.find($editableArea)[0]; - }, - /** - * Return the editable areas. - * - * @private - * @returns {JQuery} - */ - _getEditableArea: function () { - if (!this._summernote) { - return $(); - } - return $(this.selectorEditableArea, this._summernote.layoutInfo.editable); - }, - - /** - * @override - * @returns {Promise} - */ - _loadInstance: function () { - return this._super().then(function () { - var $target = this.$target; - var id = $target.attr('id'); - var className = $target.attr('class'); - $target.off('.WysiwygFrontend'); - this.$target.find('[id]').add(this.$target).each(function () { - var $node = $(this); - $node.attr('data-old-id', $node.attr('id')).removeAttr('id'); - }); - this.$('.note-editable:first').attr('id', id).addClass(className); - this.selectorEditableArea = '.o_editable'; - }.bind(this)); - }, - /** - * @private - * @returns {Promise} - */ - _saveEditable: function (editable) { - var self = this; - var recordInfo = this._getRecordInfo({target: editable}); - var outerHTML = this._getCleanedHtml(editable).prop('outerHTML'); - var def = this._saveElement(outerHTML, recordInfo, editable); - def.then(function () { - self.trigger_up('saved', recordInfo); - }).guardedCatch(function () { - self.trigger_up('canceled', recordInfo); - }); - return def; - }, - /** - * @private - * @returns {$.Promise} - */ - _saveCroppedImages: function () { - var self = this; - var $area = $(this.selectorEditableArea, this.$target); - var defs = $area.find('.o_cropped_img_to_save').map(function () { - var $croppedImg = $(this); - $croppedImg.removeClass('o_cropped_img_to_save'); - var resModel = $croppedImg.data('crop:resModel'); - var resID = $croppedImg.data('crop:resID'); - var cropID = $croppedImg.data('crop:id'); - var mimetype = $croppedImg.data('crop:mimetype'); - var originalSrc = $croppedImg.data('crop:originalSrc'); - var datas = $croppedImg.attr('src').split(',')[1]; - if (!cropID) { - var name = originalSrc + '.crop'; - return self._rpc({ - model: 'ir.attachment', - method: 'create', - args: [{ - res_model: resModel, - res_id: resID, - name: name, - datas: datas, - mimetype: mimetype, - url: originalSrc, // To save the original image that was cropped - }], - }).then(function (attachmentID) { - return self._rpc({ - model: 'ir.attachment', - method: 'generate_access_token', - args: [[attachmentID]], - }).then(function (access_token) { - $croppedImg.attr('src', '/web/image/' + attachmentID + '?access_token=' + access_token[0]); - }); - }); - } else { - return self._rpc({ - model: 'ir.attachment', - method: 'write', - args: [[cropID], {datas: datas}], - }); - } - }).get(); - return Promise.all(defs); + _getEditableArea: function() { + return $(':o_editable'); }, /** * Saves one (dirty) element of the page. * * @private - * @param {string} outerHTML - * @param {Object} recordInfo - * @returns {Promise} + * @param {jQuery} $el - the element to save + * @param {Object} context - the context to use for the saving rpc + * @param {boolean} [withLang=false] + * false if the lang must be omitted in the context (saving "master" + * page element) */ - _saveElement: function (outerHTML, recordInfo) { + _saveElement: function (outerHTML, recordInfo, editable) { + var $el = $(editable); return this._rpc({ model: 'ir.ui.view', method: 'save', args: [ - recordInfo.res_id, + $el.data('oe-id'), outerHTML, - recordInfo.xpath, + $el.data('oe-xpath') || null, ], - kwargs: { - context: recordInfo.context, - }, - }); - }, - - //-------------------------------------------------------------------------- - // Handler - //-------------------------------------------------------------------------- - - /** - * @override - * @param {OdooEvent} ev - */ - _onActivateSnippet: function (ev) { - if (!$.contains(ev.data[0], this._focusedNode)) { - this._focusedNode = ev.data[0]; - } - ev.data.closest('.oe_structure > *:o_editable').addClass('o_fake_editable').attr('contenteditable', true); - }, - /** - * @override - */ - _onChange: function () { - var editable = this._getFocusedEditable(); - $(editable).addClass('o_dirty'); - this._super.apply(this, arguments); - }, - /** - * @override - * @param {OdooEvent} ev - */ - _onContentChange: function (ev) { - this._focusedNode = ev.target; - this._super.apply(this, arguments); - }, - /** - * Triggered when the user begin to drop iamges in the editor. - * - * @private - * @param {OdooEvent} ev - * @param {Object} ev.data - * @param {Node[]} ev.data.spinners - * @param {$.Promise} ev.data.promises - */ - _onDropImages: function (ev) { - if (!this.snippets) { - return; - } - var gallerySelector = this.snippets.$('[data-js="gallery"]').data('selector'); - var $gallery = this.snippets.$activeSnippet && this.snippets.$activeSnippet.closest(gallerySelector) || $(); - - if (!$gallery.length && ev.data.spinners.length >= 2) { - $gallery = this.snippets.$snippets.filter(':has(' + gallerySelector + ')').first(); - var $drag = $gallery.find('.oe_snippet_thumbnail'); - var pos = $drag.offset(); - - $gallery.find('section').attr('id', 'onDropImagesGallery'); - - $drag.trigger($.Event("mousedown", { - which: 1, - pageX: pos.left, - pageY: pos.top - })); - - pos = $(ev.data.spinners[0]).offset(); - $drag.trigger($.Event("mousemove", { - which: 1, - pageX: pos.left, - pageY: pos.top - })); - $drag.trigger($.Event("mouseup", { - which: 1, - pageX: pos.left, - pageY: pos.top - })); - - $gallery = $('#wrapwrap #onDropImagesGallery').removeAttr('id'); - } - - if (!$gallery.length) { - return; - } - - $(ev.data.spinners).remove(); - - _.each(ev.data.promises, function (promise) { - promise.then(function (image) { - $gallery.find('.container:first').append(image); - }); + context: recordInfo.context, }); }, - /** - * @override - */ - _onFocusnode: function (node) { - this._focusedNode = node; - this._super.apply(this, arguments); - }, }); return WysiwygMultizone; diff --git a/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js b/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js index 85eecb1a85ac..20d181c5c536 100644 --- a/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js +++ b/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js @@ -3,12 +3,40 @@ odoo.define('web_editor.wysiwyg.multizone.translate', function (require) { var core = require('web.core'); var webDialog = require('web.Dialog'); -var Dialog = require('wysiwyg.widgets.Dialog'); var WysiwygMultizone = require('web_editor.wysiwyg.multizone'); +var rte = require('web_editor.rte'); +var Dialog = require('wysiwyg.widgets.Dialog'); +var websiteNavbarData = require('website.navbar'); var _t = core._t; +var RTETranslatorWidget = rte.Class.extend({ + /** + * If the element holds a translation, saves it. Otherwise, fallback to the + * standard saving but with the lang kept. + * + * @override + */ + _saveElement: function ($el, context, withLang) { + var self = this; + if ($el.data('oe-translation-id')) { + return this._rpc({ + model: 'ir.translation', + method: 'save_html', + args: [ + [+$el.data('oe-translation-id')], + this._getEscapedElement($el).html() + ], + context: context, + }).catch(function (error) { + Dialog.alert(null, error.data.message); + }); + } + return this._super($el, context, withLang === undefined ? true : withLang); + }, +}); + var AttributeTranslateDialog = Dialog.extend({ /** * @constructor @@ -44,6 +72,10 @@ var AttributeTranslateDialog = Dialog.extend({ }); var WysiwygTranslate = WysiwygMultizone.extend({ + custom_events: _.extend({}, WysiwygMultizone.prototype.custom_events || {}, { + ready_to_save: '_onSave', + }), + /** * @override * @param {string} options.lang @@ -60,7 +92,12 @@ var WysiwygTranslate = WysiwygMultizone.extend({ */ start: function () { var self = this; - return this._super().then(function () { + this.editor = new (this.Editor)(this, Object.assign({Editor: RTETranslatorWidget}, this.options)); + this.$editor = this.editor.rte.editable(); + var promise = this.editor.prependTo(this.$editor[0].ownerDocument.body); + + return promise.then(function () { + self.options.toolbarHandler.append(self.editor.$el); var attrs = ['placeholder', 'title', 'alt']; _.each(attrs, function (attr) { self._getEditableArea().filter('[' + attr + '*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () { @@ -107,14 +144,6 @@ var WysiwygTranslate = WysiwygMultizone.extend({ isDirty: function () { return this._super() || this.$editables_attribute.hasClass('o_dirty'); }, - /** - * @override - * @param {Node} node - * @returns {Boolean} - */ - isUnbreakableNode: function (node) { - return !!this._super(node) || !!$(node).data('oe-readonly'); - }, //-------------------------------------------------------------------------- // Private @@ -148,34 +177,6 @@ var WysiwygTranslate = WysiwygMultizone.extend({ recordInfo.translation_id = $editable.data('oe-translation-id')|0; return recordInfo; }, - /** - * Saves one (dirty) element of the page. - * - * @override - * @param {string} outerHTML - * @param {Object} recordInfo - * @param {DOM} editable - * @returns {Promise} - */ - _saveElement: function (outerHTML, recordInfo, editable) { - if (!recordInfo.translation_id) { - return this._super(outerHTML, recordInfo); - } - return this._rpc({ - model: 'ir.translation', - method: 'save_html', - args: [ - [recordInfo.translation_id], - $(editable).html(), - ], - kwargs: { - context: recordInfo.context, - }, - }).guardedCatch(function (error) { - console.error(error.data.message); - webDialog.alert(null, error.data.message); - }); - }, /** * @override * @returns {Object} the summernote configuration @@ -192,14 +193,6 @@ var WysiwygTranslate = WysiwygMultizone.extend({ ]; return options; }, - /** - * @override - * @returns {Object} modules list to load - */ - _getPlugins: function () { - var plugins = this._super(); - return _.omit(plugins, 'linkPopover', 'ImagePopover', 'MediaPlugin', 'ImagePlugin', 'VideoPlugin', 'IconPlugin', 'DocumentPlugin', 'tablePopover'); - }, /** * Called when text is edited -> make sure text is not messed up and mark * the element as dirty. @@ -283,6 +276,14 @@ var WysiwygTranslate = WysiwygMultizone.extend({ new AttributeTranslateDialog(self, {}, ev.target).open(); }); }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onSave: function (ev) { + ev.stopPropagation(); + }, }); return WysiwygTranslate; diff --git a/addons/website/static/src/js/menu/edit.js b/addons/website/static/src/js/menu/edit.js index a601ae3fb8a7..7e113516a179 100644 --- a/addons/website/static/src/js/menu/edit.js +++ b/addons/website/static/src/js/menu/edit.js @@ -88,7 +88,7 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ * @private * @returns {Promise} */ - _startEditMode: function () { + _startEditMode: async function () { var self = this; if (this.editModeEnable) { return; @@ -96,30 +96,27 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ this.trigger_up('widgets_stop_request', { $target: this._targetForEdition(), }); - var $welcomeMessageParent = null; if (this.$welcomeMessage) { - $welcomeMessageParent = this.$welcomeMessage.parent(); this.$welcomeMessage.detach(); // detach from the readonly rendering before the clone by summernote } this.editModeEnable = true; - return new EditorMenu(this).prependTo(document.body).then(function () { - if (self.$welcomeMessage) { - $welcomeMessageParent.append(self.$welcomeMessage); // reappend if the user cancel the edition - } - - var $target = self._targetForEdition(); - self.$editorMessageElements = $target - .find('.oe_structure.oe_empty, [data-oe-type="html"]') - .not('[data-editor-message]') - .attr('data-editor-message', _t('DRAG BUILDING BLOCKS HERE')); - new Promise(function (resolve, reject) { - self.trigger_up('widgets_start_request', { - editableMode: true, - onSuccess: resolve, - onFailure: reject, - }); + await new EditorMenu(this).prependTo(document.body); + var $target = this._targetForEdition(); + this.$editorMessageElements = $target + .find('.oe_structure.oe_empty, [data-oe-type="html"]') + .not('[data-editor-message]') + .attr('data-editor-message', _t('DRAG BUILDING BLOCKS HERE')); + var res = await new Promise(function (resolve, reject) { + self.trigger_up('widgets_start_request', { + editableMode: true, + onSuccess: resolve, + onFailure: reject, }); }); + // Trigger a mousedown on the main edition area to focus it, + // which is required for Summernote to activate. + this.$editorMessageElements.mousedown(); + return res; }, /** * On save, the editor will ask to parent widgets if something needs to be @@ -186,7 +183,7 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ * @param {OdooEvent} ev */ _onEditionWillStop: function (ev) { - this.$editorMessageElements.removeAttr('data-editor-message'); + this.$editorMessageElements && this.$editorMessageElements.removeAttr('data-editor-message'); this.trigger_up('widgets_stop_request', { $target: this._targetForEdition(), }); diff --git a/addons/website/static/src/scss/website.wysiwyg.scss b/addons/website/static/src/scss/website.wysiwyg.scss index d3fb73c55918..1f5151f5da8b 100644 --- a/addons/website/static/src/scss/website.wysiwyg.scss +++ b/addons/website/static/src/scss/website.wysiwyg.scss @@ -2,38 +2,38 @@ // Modifications applied on normal website DOM while being in the editor. //------------------------------------------------------------------------------ -body.editor_enable { - overflow: hidden; - padding-top: 0 !important; - - #wrapwrap { - height: calc(100vh - #{$o-navbar-height}); - padding-top: 0px; - - .btn { - -webkit-user-select: none; - } - } -} - //------------------------------------------------------------------------------ // Editor components style. //------------------------------------------------------------------------------ +body.editor_enable { + padding-top: $o-navbar-height !important; +} + +#wrapwrap { + height: calc(100vh - #{$o-navbar-height}); +} + // TOP BAR (EDIT) #web_editor-top-edit { height: $o-navbar-height; background-color: $o-we-color-dark; + border: 0; z-index: $zindex-modal - 9; - position: fixed; - top: 0px; - right: 0px; - width: auto; - white-space: nowrap; - background-color: rgba(0, 0, 0, 0); - transition: background-color 400ms $o-we-md-ease 0s; - font-family: $o-we-font-family; + .note-popover .popover { + width: 100%; + height: $o-navbar-height; + text-align: left; + + .popover-body { + height: $o-navbar-height; + + .btn { + height: $o-navbar-height; + } + } + } form.navbar-form { height: $o-navbar-height; @@ -42,10 +42,9 @@ body.editor_enable { padding: 0; position: absolute; top: -1px; - right: 0; + right: -1px; transition: right 0.4s $o-we-md-ease 0s; border-left: 1px solid $o-we-color-divider; - background-color: inherit; .btn-group { height: 100%; @@ -97,6 +96,18 @@ body.editor_enable { } } +body:not(.editor_enable) form.navbar-form { + display: none; +} + +// SNIPPETS +#oe_snippets { + border: 0; + #snippets_menu { + height: $o-navbar-height; + } +} + // Translations .oe_translate_examples li { margin: 10px; @@ -120,9 +131,11 @@ html[lang] > body.editor_enable [data-oe-translation-state] { // SNIPPET PANEL #oe_snippets { @include o-w-preserve-btn; - @include o-position-absolute(0, auto, 0, -$o-we-sidebar-width); + position: fixed; width: $o-we-sidebar-width; + z-index: 1042; + border-top: 0; border-right: 1px solid $o-we-color-divider; transition: left 400ms $o-we-md-ease 0s; background-image: linear-gradient(45deg, $o-we-color-normal, darken($o-we-color-normal, 10%)); @@ -535,7 +548,7 @@ body .modal { } } -.wysiwyg_multizone.note-editor { +.note-editor { &>.note-toolbar-wrapper .note-toolbar { height: $o-navbar-height; @@ -553,16 +566,9 @@ body .modal { } } -.wysiwyg_multizone.note-popover.popover { - border-width: 0 1px; - - .popover-content .btn { - height: $o-navbar-height; - } -} - -.wysiwyg_multizone.note-popover, -.wysiwyg_multizone.note-editor>.note-toolbar-wrapper .note-toolbar { +#web_editor-toolbars .popover, +.note-popover, +.note-editor>.note-toolbar-wrapper .note-toolbar { // force this because of themes background-color: $o-we-color-dark !important; diff --git a/addons/website/static/tests/tours/rte.js b/addons/website/static/tests/tours/rte.js index 7b2c8511fc38..a57b282bd095 100644 --- a/addons/website/static/tests/tours/rte.js +++ b/addons/website/static/tests/tours/rte.js @@ -15,7 +15,6 @@ tour.register('rte_translator', { test: true, url: '/', wait_for: ready, - url: '/', }, [{ content: "click on Add a language", trigger: '.js_language_selector a:has(i.fa)', @@ -132,10 +131,6 @@ tour.register('rte_translator', { trigger: 'input[placeholder="test french placeholder"]', run: function () {}, // it's a check -}, { - content : "click language dropdown", - trigger : '.js_language_selector .dropdown-toggle', - }, { content: "open language selector", trigger: '.js_language_selector button:first', @@ -149,18 +144,22 @@ tour.register('rte_translator', { extra_trigger: 'body:not(:has(#wrap p font:first:containsExact(paragraphs <b>describing</b>)))', }, { content: "select text", - extra_trigger: 'button[data-action=save]', + extra_trigger: '#oe_snippets.o_loaded', trigger: '#wrap p', run: function (action_helper) { action_helper.click(); var el = this.$anchor[0]; - this.$anchor.trigger('mousedown'); + var mousedown = document.createEvent('MouseEvents'); + mousedown.initMouseEvent('mousedown', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); + el.dispatchEvent(mousedown); + var mouseup = document.createEvent('MouseEvents'); Wysiwyg.setRange(el.childNodes[2], 6, el.childNodes[2], 13); - this.$anchor.trigger('mouseup'); + mouseup.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); + el.dispatchEvent(mouseup); }, }, { content: "underline", - trigger: 'button.note-btn-underline', + trigger: '.note-air-popover button[data-event="underline"]', }, { content: "save new change", trigger: 'button[data-action=save]', @@ -169,7 +168,6 @@ tour.register('rte_translator', { }, { content : "click language dropdown", trigger : '.js_language_selector .dropdown-toggle', - extra_trigger: '#wrap p u', }, { content: "return in french", trigger : 'html[lang="en-US"] .js_language_selector .js_change_lang[data-lang="fr_BE"]', diff --git a/addons/website_blog/static/src/js/tours/website_blog.js b/addons/website_blog/static/src/js/tours/website_blog.js index ecc655ac4c71..c8f04ed46599 100644 --- a/addons/website_blog/static/src/js/tours/website_blog.js +++ b/addons/website_blog/static/src/js/tours/website_blog.js @@ -18,7 +18,7 @@ odoo.define("website_blog.tour", function (require) { content: _t("Select the blog you want to add the post to."), }, { trigger: "div[data-oe-expression=\"blog_post.name\"]", - extra_trigger: ".o_snippets_loaded", + extra_trigger: "#oe_snippets.o_loaded", content: _t("Write a title, the subtitle is optional."), position: "top", run: "text", diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js index d945f2a5c476..b4f15dad5c8e 100644 --- a/addons/website_forum/static/src/js/website_forum.js +++ b/addons/website_forum/static/src/js/website_forum.js @@ -150,7 +150,7 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({ wysiwyg.attachTo($textarea).then(function () { // float-left class messes up the post layout OPW 769721 $form.find('.note-editable').find('img.float-left').removeClass('float-left'); - $form.on('click', 'button, .a-submit', function () { + $form.on('click', 'button .a-submit', function () { $form.find('textarea').data('wysiwyg').save(); }); }); diff --git a/addons/website_mass_mailing/views/website_mass_mailing_templates.xml b/addons/website_mass_mailing/views/website_mass_mailing_templates.xml index 5377a809600d..5fa151ffca4c 100644 --- a/addons/website_mass_mailing/views/website_mass_mailing_templates.xml +++ b/addons/website_mass_mailing/views/website_mass_mailing_templates.xml @@ -10,7 +10,7 @@ </xpath> </template> -<template id="assets_editor" inherit_id="website.assets_editor"> +<template id="assets_editor" inherit_id="website.assets_wysiwyg"> <xpath expr="//link[last()]" position="after"> <link rel="stylesheet" type="text/scss" href="/website_mass_mailing/static/src/scss/website_mass_mailing.editor.scss"/> </xpath> diff --git a/addons/website_sale/static/src/js/tours/website_sale_shop.js b/addons/website_sale/static/src/js/tours/website_sale_shop.js index 49f4c164365c..e2d5315a5446 100644 --- a/addons/website_sale/static/src/js/tours/website_sale_shop.js +++ b/addons/website_sale/static/src/js/tours/website_sale_shop.js @@ -25,7 +25,7 @@ odoo.define("website_sale.tour_shop", function (require) { position: "right", }, { trigger: ".product_price .oe_currency_value:visible", - extra_trigger: ".note-editable", + extra_trigger: ".editor_enable", content: _t("Edit the price of this product by clicking on the amount."), position: "bottom", run: "text 1.99", diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index e980b4bc0f2b..0f9999e40dee 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -35,10 +35,13 @@ </xpath> </template> - <template id="assets_editor" inherit_id="website.assets_editor" name="Shop Editor"> + <template id="assets_wysiwyg" inherit_id="website.assets_wysiwyg" name="Shop Wysiwyg Editor"> <xpath expr="//link[last()]" position="after"> <link rel="stylesheet" type="text/scss" href="/website_sale/static/src/scss/website_sale.editor.scss"/> </xpath> + </template> + + <template id="assets_editor" inherit_id="website.assets_editor" name="Shop Editor"> <xpath expr="//script[last()]" position="after"> <script type="text/javascript" src="/website_sale/static/src/js/website_sale.editor.js"></script> <script type="text/javascript" src="/website_sale/static/src/js/website_sale_form_editor.js"></script> -- GitLab