summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src
diff options
context:
space:
mode:
authorGorash <chm@odoo.com>2019-08-20 07:41:18 +0000
committerDavid Monjoie <dmo@odoo.com>2019-08-21 19:11:31 +0000
commit912996fe902c41cafa12dcfe09713ce9857f95ab (patch)
treeffb9ee4bafdf92bbbf389c38a54c327981be3f32 /addons/web_editor/static/src
parent35b61822a8b12ebbdb455495479df6f2e13db03e (diff)
[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>
Diffstat (limited to 'addons/web_editor/static/src')
-rw-r--r--addons/web_editor/static/src/js/backend/field_html.js28
-rw-r--r--addons/web_editor/static/src/js/base.js11
-rw-r--r--addons/web_editor/static/src/js/common/ace.js270
-rw-r--r--addons/web_editor/static/src/js/editor/editor.js64
-rw-r--r--addons/web_editor/static/src/js/editor/rte.js9
-rw-r--r--addons/web_editor/static/src/js/editor/rte.summernote.js57
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.editor.js334
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js35
-rw-r--r--addons/web_editor/static/src/js/editor/summernote.js68
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/widgets/media.js7
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/widgets/media_dialog.js47
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/wysiwyg.js681
-rw-r--r--addons/web_editor/static/src/scss/web_editor.backend.scss26
-rw-r--r--addons/web_editor/static/src/scss/web_editor.common.scss11
-rw-r--r--addons/web_editor/static/src/scss/wysiwyg.scss6
-rw-r--r--addons/web_editor/static/src/xml/backend.xml8
-rw-r--r--addons/web_editor/static/src/xml/editor.xml9
17 files changed, 754 insertions, 917 deletions
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 5ba630edc89..f6d86c9020c 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 d69cace3100..4eb0da20fab 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 37c21f2aa36..dc2d78a53c7 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 63fa8c1c667..dc0f53c8e57 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 ba6e1fed7ec..bfd7994b161 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 464c58ed4ee..b5b74f64466 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 776d9992bb7..2f2e55a0084 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) {
@@ -506,6 +529,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.
*
* @private
@@ -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
@@ -867,16 +982,6 @@ var SnippetsMenu = Widget.extend({
});
},
/**
- * 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 () {
@@ -1204,17 +1312,6 @@ var SnippetsMenu = Widget.extend({
});
},
/**
- * 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.
*
* @private
@@ -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 f481061c9e2..1278d70afda 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 e0968ecdb58..6c62758142d 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 22c30d501d1..cbb39ea6173 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 528cbd93bc6..84dd8e7f650 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"),
@@ -93,19 +95,6 @@ var MediaDialog = Dialog.extend({
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")));
}
if (this.documentWidget) {
@@ -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 a376cff945e..cb0db917e54 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 af2398bc98e..ecbe42721f3 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 cc3b2aa9c4b..43f174e2862 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 af674a58a0b..58fe5f7ec42 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 904f21f440c..c79ee939283 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 aa8b5a2f094..ca653c5d7ea 100644
--- a/addons/web_editor/static/src/xml/editor.xml
+++ b/addons/web_editor/static/src/xml/editor.xml
@@ -1,6 +1,15 @@
<?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 -->
<!--=================-->