summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src/js
diff options
context:
space:
mode:
authorChristophe Matthieu <chm@odoo.com>2018-12-28 17:49:21 +0000
committerChristophe Matthieu <chm@odoo.com>2019-01-17 08:40:21 +0000
commitde444335b7da30a2ab3a4c5b92c9731a023f3f07 (patch)
tree40ceda25aa7157ae0923cf567a448d685798e911 /addons/web_editor/static/src/js
parent17c6d00531c55d0c392ddb6916bf1957bc490c1c (diff)
[REF] web_editor,*: Move and rename files to prepare refactoring
The refactoring of the wysiwyg editor and 'html' field allows us to move some of the code that was in web_editor but only used in website or in mass_mailing. Some parts are still in web_editor but will be moved at a later time.
Diffstat (limited to 'addons/web_editor/static/src/js')
-rw-r--r--addons/web_editor/static/src/js/backend/convert_inline.js (renamed from addons/web_editor/static/src/js/editor/transcoder.js)0
-rw-r--r--addons/web_editor/static/src/js/backend/field_html.js (renamed from addons/web_editor/static/src/js/backend/fields.js)0
-rw-r--r--addons/web_editor/static/src/js/common/ace.js (renamed from addons/web_editor/static/src/js/widgets/ace.js)0
-rw-r--r--addons/web_editor/static/src/js/content/body_manager.js60
-rw-r--r--addons/web_editor/static/src/js/editor/editor.js286
-rw-r--r--addons/web_editor/static/src/js/editor/translator.js375
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/fonts.js (renamed from addons/web_editor/static/src/js/base.js)0
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/widgets/colorpicker_dialog.js497
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/widgets/media.js (renamed from addons/web_editor/static/src/js/widgets/widgets.js)0
-rw-r--r--addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js (renamed from addons/web_editor/static/src/js/editor/snippets.editor.js)0
-rw-r--r--addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js (renamed from addons/web_editor/static/src/js/editor/snippets.options.js)0
11 files changed, 497 insertions, 721 deletions
diff --git a/addons/web_editor/static/src/js/editor/transcoder.js b/addons/web_editor/static/src/js/backend/convert_inline.js
index 85903f6b4eb..85903f6b4eb 100644
--- a/addons/web_editor/static/src/js/editor/transcoder.js
+++ b/addons/web_editor/static/src/js/backend/convert_inline.js
diff --git a/addons/web_editor/static/src/js/backend/fields.js b/addons/web_editor/static/src/js/backend/field_html.js
index 7742f49bcde..7742f49bcde 100644
--- a/addons/web_editor/static/src/js/backend/fields.js
+++ b/addons/web_editor/static/src/js/backend/field_html.js
diff --git a/addons/web_editor/static/src/js/widgets/ace.js b/addons/web_editor/static/src/js/common/ace.js
index f3c47f30050..f3c47f30050 100644
--- a/addons/web_editor/static/src/js/widgets/ace.js
+++ b/addons/web_editor/static/src/js/common/ace.js
diff --git a/addons/web_editor/static/src/js/content/body_manager.js b/addons/web_editor/static/src/js/content/body_manager.js
deleted file mode 100644
index 1045e94a65a..00000000000
--- a/addons/web_editor/static/src/js/content/body_manager.js
+++ /dev/null
@@ -1,60 +0,0 @@
-odoo.define('web_editor.BodyManager', function (require) {
-'use strict';
-
-var weContext = require('web_editor.context');
-var rootWidget = require('web_editor.root_widget');
-var ServiceProviderMixin = require('web.ServiceProviderMixin');
-var session = require('web.session');
-
-/**
- * Element which is designed to be unique and that will be the top-most element
- * in the widget hierarchy. So, all other widgets will be indirectly linked to
- * this Class instance. Its main role will be to retrieve RPC demands from its
- * children and handle them.
- */
-var BodyManager = rootWidget.RootWidget.extend(ServiceProviderMixin, {
- /**
- * @constructor
- */
- init: function () {
- this._super.apply(this, arguments);
- ServiceProviderMixin.init.call(this);
- },
- /**
- * @override
- */
- willStart: function () {
- return $.when(
- this._super.apply(this, arguments),
- session.is_bound
- );
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Automatically add the web_editor context.
- *
- * @override
- */
- _call_service: function (event) {
- if (event.data.service === 'ajax' && event.data.method === 'rpc') {
- var route = event.data.args[0];
- if (_.str.startsWith(route, '/web/dataset/call_kw/')) {
- var params = event.data.args[1];
- var options = event.data.args[2];
- params.kwargs.context = _.extend({}, weContext.get(), params.kwargs.context || {});
- if (options) {
- params.kwargs.context = _.omit(params.kwargs.context, options.noContextKeys);
- event.data.args[2] = _.omit(options, 'noContextKeys');
- }
- params.kwargs.context = JSON.parse(JSON.stringify(params.kwargs.context));
- }
- }
- return ServiceProviderMixin._call_service.apply(this, arguments);
- },
-});
-return BodyManager;
-});
diff --git a/addons/web_editor/static/src/js/editor/editor.js b/addons/web_editor/static/src/js/editor/editor.js
deleted file mode 100644
index 63fa8c1c667..00000000000
--- a/addons/web_editor/static/src/js/editor/editor.js
+++ /dev/null
@@ -1,286 +0,0 @@
-odoo.define('web_editor.editor', function (require) {
-'use strict';
-
-var Dialog = require('web.Dialog');
-var Widget = require('web.Widget');
-var core = require('web.core');
-var rte = require('web_editor.rte');
-var snippetsEditor = require('web_editor.snippet.editor');
-
-var _t = core._t;
-
-var EditorMenuBar = Widget.extend({
- template: 'web_editor.editorbar',
- xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
- events: {
- 'click button[data-action=save]': '_onSaveClick',
- 'click button[data-action=cancel]': '_onCancelClick',
- },
- custom_events: {
- request_history_undo_record: '_onHistoryUndoRecordRequest',
- request_save: '_onSaveRequest',
- },
-
- /**
- * Initializes RTE and snippets menu.
- *
- * @constructor
- */
- init: function (parent) {
- var self = this;
- var res = this._super.apply(this, arguments);
- this.rte = new rte.Class(this);
- this.rte.on('rte:start', this, function () {
- self.trigger('rte:start');
- });
-
- // Snippets edition
- var $editable = this.rte.editable();
- window.__EditorMenuBar_$editable = $editable; // TODO remove this hack asap
- this.snippetsMenu = new snippetsEditor.Class(this, $editable);
-
- return res;
- },
- /**
- * @override
- */
- start: function () {
- var self = this;
- var defs = [this._super.apply(this, arguments)];
-
- core.bus.on('editor_save_request', this, this.save);
- core.bus.on('editor_discard_request', this, this.cancel);
-
- $('.dropdown-toggle').dropdown();
-
- $(document).on('keyup', function (event) {
- if ((event.keyCode === 8 || event.keyCode === 46)) {
- var $target = $(event.target).closest('.o_editable');
- if (!$target.is(':has(*:not(p):not(br))') && !$target.text().match(/\S/)) {
- $target.empty();
- }
- }
- });
- $(document).on('click', '.note-editable', function (ev) {
- ev.preventDefault();
- });
- $(document).on('submit', '.note-editable form .btn', function (ev) {
- ev.preventDefault(); // Disable form submition in editable mode
- });
- $(document).on('hide.bs.dropdown', '.dropdown', function (ev) {
- // Prevent dropdown closing when a contenteditable children is focused
- if (ev.originalEvent
- && $(ev.target).has(ev.originalEvent.target).length
- && $(ev.originalEvent.target).is('[contenteditable]')) {
- ev.preventDefault();
- }
- });
-
- this.rte.start();
-
- var flag = false;
- window.onbeforeunload = function (event) {
- if (rte.history.getEditableHasUndo().length && !flag) {
- flag = true;
- _.defer(function () { flag=false; });
- return _t('This document is not saved!');
- }
- };
-
- // Snippets menu
- defs.push(this.snippetsMenu.insertAfter(this.$el));
- this.rte.editable().find('*').off('mousedown mouseup click');
-
- return $.when.apply($, defs).then(function () {
- self.trigger_up('edit_mode');
- });
- },
- /**
- * @override
- */
- destroy: function () {
- this._super.apply(this, arguments);
- core.bus.off('editor_save_request', this, this._onSaveRequest);
- core.bus.off('editor_discard_request', this, this._onDiscardRequest);
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Asks the user if he really wants to discard its changes (if there are
- * some of them), then simply reload the page if he wants to.
- *
- * @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}
- */
- 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 () {
- if (reload !== false) {
- window.onbeforeunload = null;
- return self._reload();
- }
- });
- },
- /**
- * Asks the snippets to clean themself, then saves the page, then reloads it
- * if asked to.
- *
- * @param {boolean} [reload=true]
- * true if the page has to be reloaded after the save
- * @returns {Deferred}
- */
- 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 self._saveCroppedImages();
- }).then(function () {
- return self.rte.save();
- }).then(function () {
- if (reload !== false) {
- return self._reload();
- }
- });
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Reloads the page in non-editable mode, with the right scrolling.
- *
- * @private
- * @returns {Deferred} (never resolved, the page is reloading anyway)
- */
- _reload: function () {
- window.location.hash = 'scrollTop=' + window.document.body.scrollTop;
- if (window.location.search.indexOf('enable_editor') >= 0) {
- window.location.href = window.location.href.replace(/&?enable_editor(=[^&]*)?/g, '');
- } else {
- window.location.reload(true);
- }
- return $.Deferred();
- },
- /**
- * @private
- */
- _saveCroppedImages: function () {
- var self = this;
- var defs = _.map(this.rte.editable().find('.o_cropped_img_to_save'), function (croppedImg) {
- var $croppedImg = $(croppedImg);
- $croppedImg.removeClass('o_cropped_img_to_save');
-
- var resModel = $croppedImg.data('crop:resModel');
- var resID = $croppedImg.data('crop:resID');
- var cropID = $croppedImg.data('crop:id');
- var mimetype = $croppedImg.data('crop:mimetype');
- var originalSrc = $croppedImg.data('crop:originalSrc');
-
- var datas = $croppedImg.attr('src').split(',')[1];
-
- if (!cropID) {
- var name = originalSrc + '.crop';
- return self._rpc({
- model: 'ir.attachment',
- method: 'create',
- args: [{
- res_model: resModel,
- res_id: resID,
- name: name,
- datas_fname: name,
- datas: datas,
- mimetype: mimetype,
- url: originalSrc, // To save the original image that was cropped
- }],
- }).then(function (attachmentID) {
- return self._rpc({
- model: 'ir.attachment',
- method: 'generate_access_token',
- args: [[attachmentID]],
- }).then(function (access_token) {
- $croppedImg.attr('src', '/web/image/' + attachmentID + '?access_token=' + access_token[0]);
- });
- });
- } else {
- return self._rpc({
- model: 'ir.attachment',
- method: 'write',
- args: [[cropID], {datas: datas}],
- });
- }
- });
- return $.when.apply($, defs);
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when the "Discard" button is clicked -> discards the changes.
- *
- * @private
- */
- _onCancelClick: function () {
- this.cancel();
- },
- /**
- * Called when an element askes to record an history undo -> records it.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onHistoryUndoRecordRequest: function (ev) {
- this.rte.historyRecordUndo(ev.data.$target, ev.data.event);
- },
- /**
- * Called when the "Save" button is clicked -> saves the changes.
- *
- * @private
- */
- _onSaveClick: function () {
- this.save();
- },
- /**
- * Called when a discard request is received -> discard the page content
- * changes.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onDiscardRequest: function (ev) {
- this.cancel(ev.data.reload).then(ev.data.onSuccess, ev.data.onFailure);
- },
- /**
- * Called when a save request is received -> saves the page content.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onSaveRequest: function (ev) {
- ev.stopPropagation();
- this.save(ev.data.reload).then(ev.data.onSuccess, ev.data.onFailure);
- },
-});
-
-return {
- Class: EditorMenuBar,
-};
-});
diff --git a/addons/web_editor/static/src/js/editor/translator.js b/addons/web_editor/static/src/js/editor/translator.js
deleted file mode 100644
index c6cadb40562..00000000000
--- a/addons/web_editor/static/src/js/editor/translator.js
+++ /dev/null
@@ -1,375 +0,0 @@
-odoo.define('web_editor.translate', function (require) {
-'use strict';
-
-var core = require('web.core');
-var Dialog = require('web.Dialog');
-var localStorage = require('web.local_storage');
-var Widget = require('web.Widget');
-var weContext = require('web_editor.context');
-var rte = require('web_editor.rte');
-var weWidgets = require('web_editor.widget');
-var Dialog = require('web.Dialog');
-
-var _t = core._t;
-
-var localStorageNoDialogKey = 'website_translator_nodialog';
-
-var RTETranslatorWidget = rte.Class.extend({
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * If the element holds a translation, saves it. Otherwise, fallback to the
- * standard saving but with the lang kept.
- *
- * @override
- */
- _saveElement: function ($el, context, withLang) {
- var self = this;
- if ($el.data('oe-translation-id')) {
- return this._rpc({
- model: 'ir.translation',
- method: 'save_html',
- args: [
- [+$el.data('oe-translation-id')],
- this._getEscapedElement($el).html()
- ],
- context: context,
- }).fail(function (error) {
- Dialog.alert(null, error.data.message);
- });
- }
- return this._super($el, context, withLang === undefined ? true : withLang);
- },
-});
-
-var AttributeTranslateDialog = weWidgets.Dialog.extend({
- /**
- * @constructor
- */
- init: function (parent, options, node) {
- this._super(parent, _.extend({
- title: _t("Translate Attribute"),
- buttons: [
- {text: _t("Close"), classes: 'btn-primary', click: this.save}
- ],
- }, options || {}));
- this.$target = $(node);
- this.translation = $(node).data('translation');
- },
- /**
- * @override
- */
- start: function () {
- var self = this;
- var $group = $('<div/>', {class: 'form-group'}).appendTo(this.$el);
- _.each(this.translation, function (node, attr) {
- var $node = $(node);
- var $label = $('<label class="col-form-label"></label>').text(attr);
- var $input = $('<input class="form-control"/>').val($node.html());
- $input.on('change keyup', function () {
- var value = $input.val();
- $node.html(value).trigger('change', node);
- $node.data('$node').attr($node.data('attribute'), value).trigger('translate');
- self.trigger_up('rte_change', {target: node});
- });
- $group.append($label).append($input);
- });
- return this._super.apply(this, arguments);
- }
-});
-
-var TranslatorInfoDialog = Dialog.extend({
- template: 'web_editor.TranslatorInfoDialog',
- xmlDependencies: Dialog.prototype.xmlDependencies.concat(
- ['/web_editor/static/src/xml/translator.xml']
- ),
-
- /**
- * @constructor
- */
- init: function (parent, options) {
- this._super(parent, _.extend({
- title: _t("Translation Info"),
- buttons: [
- {text: _t("Ok, never show me this again"), classes: 'btn-primary', close: true, click: this._onStrongOk.bind(this)},
- {text: _t("Ok"), close: true}
- ],
- }, options || {}));
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when the "strong" ok is clicked -> adapt localstorage to make sure
- * the dialog is never displayed again.
- *
- * @private
- */
- _onStrongOk: function () {
- localStorage.setItem(localStorageNoDialogKey, true);
- },
-});
-
-var TranslatorMenuBar = Widget.extend({
- template: 'web_editor.editorbar',
- xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
- events: {
- 'click [data-action="save"]': '_onSaveClick',
- 'click [data-action="cancel"]': '_onCancelClick',
- },
- custom_events: {
- 'rte_change': '_onRTEChange',
- },
-
- /**
- * @constructor
- */
- init: function (parent, $target, lang) {
- this._super.apply(this, arguments);
-
- var $edit = $target.find('[data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field]');
- $edit.filter(':has([data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field])').attr('data-oe-readonly', true);
- this.$target = $edit.not('[data-oe-readonly]');
- var attrs = ['placeholder', 'title', 'alt'];
- _.each(attrs, function (attr) {
- $target.find('['+attr+'*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () {
- var $node = $(this);
- var translation = $node.data('translation') || {};
- var trans = $node.attr(attr);
- var match = trans.match(/<span [^>]*data-oe-translation-id="([0-9]+)"[^>]*>(.*)<\/span>/);
- var $trans = $(trans).addClass('d-none o_editable o_editable_translatable_attribute').appendTo('body');
- $trans.data('$node', $node).data('attribute', attr);
- translation[attr] = $trans[0];
- $node.attr(attr, match[2]);
-
- var select2 = $node.data('select2');
- if (select2) {
- select2.blur();
- $node.on('translate', function () {
- select2.blur();
- });
- $node = select2.container.find('input');
- }
- $node.addClass('o_translatable_attribute').data('translation', translation);
- });
- });
- this.$target_attr = $target.find('.o_translatable_attribute');
- this.$target_attribute = $('.o_editable_translatable_attribute');
-
- this.lang = lang || weContext.get().lang;
-
- this.rte = new RTETranslatorWidget(this, this._getRTEConfig);
- },
- /**
- * @override
- */
- start: function () {
- this.$('#web_editor-toolbars').remove();
-
- var flag = false;
- window.onbeforeunload = function (event) {
- if ($('.o_editable.o_dirty').length && !flag) {
- flag = true;
- setTimeout(function () {
- flag = false;
- }, 0);
- return _t('This document is not saved!');
- }
- };
- this.$target.addClass('o_editable');
- this.rte.start();
- this.translations = [];
- this._markTranslatableNodes();
- this.$el.show(); // TODO seems useless
- this.trigger('edit');
-
- if (!localStorage.getItem(localStorageNoDialogKey)) {
- new TranslatorInfoDialog(this).open();
- }
-
- return this._super.apply(this, arguments);
- },
- /**
- * @override
- */
- destroy: function () {
- this._cancel();
- this._super.apply(this, arguments);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Leaves translation mode by hiding the translator bar.
- *
- * @todo should be destroyed?
- * @private
- */
- _cancel: function () {
- var self = this;
- this.rte.cancel();
- this.$target.each(function () {
- $(this).html(self._getTranlationObject(this).value);
- });
- this._unmarkTranslatableNode();
- this.trigger('cancel');
- this.$el.hide();
- window.onbeforeunload = null;
- },
- /**
- * Returns the RTE summernote configuration for translation mode.
- *
- * @private
- * @param {jQuery} $editable
- */
- _getRTEConfig: function ($editable) {
- return {
- airMode : true,
- focus: false,
- airPopover: $editable.data('oe-model') ? [
- ['history', ['undo', 'redo']],
- ] : [
- ['font', ['bold', 'italic', 'underline', 'clear']],
- ['fontsize', ['fontsize']],
- ['color', ['color']],
- ['history', ['undo', 'redo']],
- ],
- styleWithSpan: false,
- inlinemedia : ['p'],
- lang: 'odoo',
- onChange: function (html, $editable) {
- $editable.trigger('content_changed');
- },
- };
- },
- /**
- * @private
- */
- _getTranlationObject: function (node) {
- var $node = $(node);
- var id = +$node.data('oe-translation-id');
- if (!id) {
- id = $node.data('oe-model')+','+$node.data('oe-id')+','+$node.data('oe-field');
- }
- var trans = _.find(this.translations, function (trans) {
- return trans.id === id;
- });
- if (!trans) {
- this.translations.push(trans = {'id': id});
- }
- return trans;
- },
- /**
- * @private
- */
- _markTranslatableNodes: function () {
- var self = this;
- this.$target.add(this.$target_attribute).each(function () {
- var $node = $(this);
- var trans = self._getTranlationObject(this);
- trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
- });
- this.$target.parent().prependEvent('click.translator', function (ev) {
- if (ev.ctrlKey || !$(ev.target).is(':o_editable')) {
- return;
- }
- ev.preventDefault();
- ev.stopPropagation();
- });
-
- // attributes
-
- this.$target_attr.each(function () {
- var $node = $(this);
- var translation = $node.data('translation');
- _.each(translation, function (node, attr) {
- var trans = self._getTranlationObject(node);
- trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
- $node.attr('data-oe-translation-state', (trans.state || 'to_translate'));
- });
- });
-
- this.$target_attr.prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) {
- if (ev.ctrlKey) {
- return;
- }
- ev.preventDefault();
- ev.stopPropagation();
- if (ev.type !== 'mousedown') {
- return;
- }
-
- new AttributeTranslateDialog(self, {}, ev.target).open();
- });
- },
- /**
- * Saves the translation and reloads the page to leave edit mode.
- *
- * @private
- * @returns {Deferred} (never resolved as the page is reloading anyway)
- */
- _save: function () {
- return this.rte.save(weContext.get({lang: this.lang})).then(function () {
- window.location.href = window.location.href.replace(/&?edit_translations(=[^&]*)?/g, '');
- return $.Deferred();
- });
- },
- /**
- * @private
- */
- _unmarkTranslatableNode: function () {
- this.$target.removeClass('o_editable').removeAttr('contentEditable');
- this.$target.parent().off('.translator');
- this.$target_attr.off('.translator');
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when the "cancel" button is clicked -> undo changes and leaves
- * edition.
- *
- * @private
- */
- _onCancelClick: function () {
- this._cancel();
- },
- /**
- * Called when text is edited -> make sure text is not messed up and mark
- * the element as dirty.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onRTEChange: function (ev) {
- var $node = $(ev.data.target);
- $node.find('p').each(function () { // remove <p/> element which might have been inserted because of copy-paste
- var $p = $(this);
- $p.after($p.html()).remove();
- });
- var trans = this._getTranlationObject($node[0]);
- $node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' '));
- },
- /**
- * Called when the "save" button is clicked -> saves the translations.
- *
- * @private
- */
- _onSaveClick: function () {
- this._save();
- },
-});
-
-return {
- Class: TranslatorMenuBar,
-};
-});
diff --git a/addons/web_editor/static/src/js/base.js b/addons/web_editor/static/src/js/wysiwyg/fonts.js
index d69cace3100..d69cace3100 100644
--- a/addons/web_editor/static/src/js/base.js
+++ b/addons/web_editor/static/src/js/wysiwyg/fonts.js
diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/colorpicker_dialog.js b/addons/web_editor/static/src/js/wysiwyg/widgets/colorpicker_dialog.js
new file mode 100644
index 00000000000..e8ed888e1af
--- /dev/null
+++ b/addons/web_editor/static/src/js/wysiwyg/widgets/colorpicker_dialog.js
@@ -0,0 +1,497 @@
+odoo.define('web.colorpicker', function (require) {
+'use strict';
+
+var core = require('web.core');
+var utils = require('web.utils');
+var Dialog = require('web.Dialog');
+
+var _t = core._t;
+
+var Colorpicker = Dialog.extend({
+ xmlDependencies: (Dialog.prototype.xmlDependencies || [])
+ .concat(['/web/static/src/xml/colorpicker.xml']),
+
+ template: 'web.colorpicker',
+ events: _.extend({}, Dialog.prototype.events || {}, {
+ 'mousedown .o_color_pick_area': '_onMouseDownPicker',
+ 'mousedown .o_color_slider': '_onMouseDownSlider',
+ 'mousedown .o_opacity_slider': '_onMouseDownOpacitySlider',
+ 'change .o_color_picker_inputs' : '_onChangeInputs',
+ }),
+
+ /**
+ * @constructor
+ * @param {Widget} parent
+ * @param {Object} [options]
+ * @param {string} [options.defaultColor='#FF0000']
+ */
+ init: function (parent, options) {
+ options = options || {};
+
+ this._super(parent, _.extend({
+ size: 'medium',
+ title: _t('Pick a color'),
+ buttons: [
+ {text: _t('Choose'), classes: 'btn-primary', close: true, click: this._onFinalPick.bind(this)},
+ {text: _t('Discard'), close: true},
+ ],
+ }, options));
+
+ this.pickerFlag = false;
+ this.sliderFlag = false;
+ this.opacitySliderFlag = false;
+ this.colorComponents = {};
+
+ var self = this;
+ var $body = $(document.body);
+ $body.on('mousemove.colorpicker', _.throttle(function (ev) {
+ self._onMouseMovePicker(ev);
+ self._onMouseMoveSlider(ev);
+ self._onMouseMoveOpacitySlider(ev);
+ }, 10));
+ $body.on('mouseup.colorpicker', _.throttle(function (ev) {
+ self.pickerFlag = false;
+ self.sliderFlag = false;
+ self.opacitySliderFlag = false;
+ }, 10));
+
+ this.options = _.clone(options);
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ this.$colorpickerArea = this.$('.o_color_pick_area');
+ this.$colorpickerPointer = this.$('.o_picker_pointer');
+ this.$colorSlider = this.$('.o_color_slider');
+ this.$colorSliderPointer = this.$('.o_slider_pointer');
+ this.$opacitySlider = this.$('.o_opacity_slider');
+ this.$opacitySliderPointer = this.$('.o_opacity_pointer');
+
+ var defaultColor = this.options.defaultColor || '#FF0000';
+ var rgba = defaultColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
+ if (rgba) {
+ if (rgba[4] === undefined) {
+ rgba[4] = 1;
+ }
+ this._updateRgba(parseInt(rgba[1]), parseInt(rgba[2]), parseInt(rgba[3]), Math.round(parseFloat(rgba[4] * 100)));
+ } else {
+ this._updateHex(defaultColor);
+ this._updateOpacity(100);
+ }
+ this.opened().then(this._updateUI.bind(this));
+
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * @override
+ */
+ destroy: function () {
+ this._super.apply(this, arguments);
+ $(document.body).off('.colorpicker');
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Updates input values, color preview, picker and slider pointer positions.
+ *
+ * @private
+ */
+ _updateUI: function () {
+ var self = this;
+
+ // Update inputs
+ _.each(this.colorComponents, function (value, color) {
+ self.$(_.str.sprintf('.o_%s_input', color)).val(value);
+ });
+
+ // Update preview
+ this.$('.o_color_preview').css('background-color', this.colorComponents.cssColor);
+
+ // Update picker area and picker pointer position
+ this.$colorpickerArea.css('background-color', _.str.sprintf('hsl(%s, 100%%, 50%%)', this.colorComponents.hue));
+ var top = (100 - this.colorComponents.lightness) * this.$colorpickerArea.height() / 100;
+ var left = this.colorComponents.saturation * this.$colorpickerArea.width() / 100;
+ this.$colorpickerPointer.css({
+ top: (top - 5) + 'px',
+ left: (left - 5) + 'px',
+ });
+
+ // Update color slider position
+ var height = this.$colorSlider.height();
+ var y = this.colorComponents.hue * height / 360;
+ this.$colorSliderPointer.css('top', Math.round(y - 2));
+
+ // Update opacity slider position
+ var heightOpacity = this.$opacitySlider.height();
+ var z = heightOpacity * (1 - this.colorComponents.opacity / 100.0);
+ this.$opacitySliderPointer.css('top', Math.round(z - 2));
+
+ // Add gradient color on opacity slider
+ this.$opacitySlider.css('background', 'linear-gradient(' + this.colorComponents.hex + ' 0%, transparent 100%)');
+ },
+ /**
+ * Updates colors according to given hex value. Opacity is left unchanged.
+ *
+ * @private
+ * @param {string} hex - hexadecimal code
+ */
+ _updateHex: function (hex) {
+ var rgb = Colorpicker.prototype.convertHexToRgb(hex);
+ if (!rgb) {
+ return;
+ }
+ _.extend(this.colorComponents,
+ {hex: hex},
+ rgb,
+ Colorpicker.prototype.convertRgbToHsl(rgb.red, rgb.green, rgb.blue)
+ );
+ this._updateCssColor();
+ },
+ /**
+ * Updates colors according to given RGB values.
+ *
+ * @private
+ * @param {integer} r
+ * @param {integer} g
+ * @param {integer} b
+ * @param {integer} [a]
+ */
+ _updateRgba: function (r, g, b, a) {
+ var hex = Colorpicker.prototype.convertRgbToHex(r, g, b);
+ if (!hex) {
+ return;
+ }
+ _.extend(this.colorComponents,
+ {red: r, green: g, blue: b},
+ a === undefined ? {} : {opacity: a},
+ hex,
+ Colorpicker.prototype.convertRgbToHsl(r, g, b)
+ );
+ this._updateCssColor();
+ },
+ /**
+ * Updates colors according to given HSL values.
+ *
+ * @private
+ * @param {integer} h
+ * @param {integer} s
+ * @param {integer} l
+ */
+ _updateHsl: function (h, s, l) {
+ var rgb = Colorpicker.prototype.convertHslToRgb(h, s, l);
+ if (!rgb) {
+ return;
+ }
+ _.extend(this.colorComponents,
+ {hue: h, saturation: s, lightness: l},
+ rgb,
+ Colorpicker.prototype.convertRgbToHex(rgb.red, rgb.green, rgb.blue)
+ );
+ this._updateCssColor();
+ },
+ /**
+ * Updates color opacity.
+ *
+ * @private
+ * @param {integer} a
+ */
+ _updateOpacity: function (a) {
+ if (a < 0 || a > 100) {
+ return;
+ }
+ _.extend(this.colorComponents,
+ {opacity: a}
+ );
+ this._updateCssColor();
+ },
+ /**
+ * Updates css color representation.
+ *
+ * @private
+ */
+ _updateCssColor: function () {
+ var cssColor = this.colorComponents.hex;
+ if (this.colorComponents.opacity !== 100) {
+ cssColor = _.str.sprintf('rgba(%s, %s, %s, %s)',
+ this.colorComponents.red,
+ this.colorComponents.green,
+ this.colorComponents.blue,
+ this.colorComponents.opacity / 100
+ );
+ }
+ _.extend(this.colorComponents,
+ {cssColor: cssColor}
+ );
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Updates color when the user starts clicking on the picker.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onMouseDownPicker: function (ev) {
+ this.pickerFlag = true;
+ this._onMouseMovePicker(ev);
+ },
+ /**
+ * Updates saturation and lightness values on mouse drag over picker.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onMouseMovePicker: function (ev) {
+ if (!this.pickerFlag) {
+ return;
+ }
+
+ var offset = this.$colorpickerArea.offset();
+ var top = ev.pageY - offset.top;
+ var left = ev.pageX - offset.left;
+ var saturation = Math.round(100 * left / this.$colorpickerArea.width());
+ var lightness = Math.round(100 * (this.$colorpickerArea.height() - top) / this.$colorpickerArea.height());
+ saturation = utils.confine(saturation, 0, 100);
+ lightness = utils.confine(lightness, 0, 100);
+
+ this._updateHsl(this.colorComponents.hue, saturation, lightness);
+ this._updateUI();
+ },
+ /**
+ * Updates color when user starts clicking on slider.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onMouseDownSlider: function (ev) {
+ this.sliderFlag = true;
+ this._onMouseMoveSlider(ev);
+ },
+ /**
+ * Updates hue value on mouse drag over slider.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onMouseMoveSlider: function (ev) {
+ if (!this.sliderFlag) {
+ return;
+ }
+
+ var y = ev.pageY - this.$colorSlider.offset().top;
+ var hue = Math.round(360 * y / this.$colorSlider.height());
+ hue = utils.confine(hue, 0, 360);
+
+ this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness);
+ this._updateUI();
+ },
+ /**
+ * Updates opacity when user starts clicking on opacity slider.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onMouseDownOpacitySlider: function (ev) {
+ this.opacitySliderFlag = true;
+ this._onMouseMoveOpacitySlider(ev);
+ },
+ /**
+ * Updates opacity value on mouse drag over opacity slider.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onMouseMoveOpacitySlider: function (ev) {
+ if (!this.opacitySliderFlag) {
+ return;
+ }
+
+ var y = ev.pageY - this.$opacitySlider.offset().top;
+ var opacity = Math.round(100 * (1 - y / this.$opacitySlider.height()));
+ opacity = utils.confine(opacity, 0, 100);
+
+ this._updateOpacity(opacity);
+ this._updateUI();
+ },
+ /**
+ * Called when input value is changed -> Updates UI: Set picker and slider
+ * position and set colors.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onChangeInputs: function (ev) {
+ switch ($(ev.target).data('colorMethod')) {
+ case 'hex':
+ this._updateHex(this.$('.o_hex_input').val());
+ break;
+ case 'rgb':
+ this._updateRgba(
+ parseInt(this.$('.o_red_input').val()),
+ parseInt(this.$('.o_green_input').val()),
+ parseInt(this.$('.o_blue_input').val())
+ );
+ break;
+ case 'hsl':
+ this._updateHsl(
+ parseInt(this.$('.o_hue_input').val()),
+ parseInt(this.$('.o_saturation_input').val()),
+ parseInt(this.$('.o_lightness_input').val())
+ );
+ break;
+ case 'opacity':
+ this._updateOpacity(parseInt(this.$('.o_opacity_input').val()));
+ break;
+ }
+ this._updateUI();
+ },
+ /**
+ * @private
+ */
+ _onFinalPick: function () {
+ this.trigger_up('colorpicker:saved', this.colorComponents);
+ },
+
+ //--------------------------------------------------------------------------
+ // Static
+ //--------------------------------------------------------------------------
+
+ /**
+ * Converts Hexadecimal code to RGB.
+ *
+ * @static
+ * @param {string} hex - hexadecimal code
+ * @returns {Object|false} contains red, green and blue
+ */
+ convertHexToRgb: function (hex) {
+ if (!/^#[0-9A-F]{6}$/i.test(hex)) {
+ return false;
+ }
+
+ return {
+ red: parseInt(hex.substr(1, 2), 16),
+ green: parseInt(hex.substr(3, 2), 16),
+ blue: parseInt(hex.substr(5, 2), 16),
+ };
+ },
+ /**
+ * Converts RGB color to HSL.
+ *
+ * @static
+ * @param {integer} r
+ * @param {integer} g
+ * @param {integer} b
+ * @returns {Object|false} contains hue, saturation and lightness
+ */
+ convertRgbToHsl: function (r, g, b) {
+ if (typeof(r) !== 'number' || isNaN(r) || r < 0 || r > 255 ||
+ typeof(g) !== 'number' || isNaN(g) || g < 0 || g > 255 ||
+ typeof(b) !== 'number' || isNaN(b) || b < 0 || b > 255) {
+ return false;
+ }
+
+ var red = r / 255;
+ var green = g / 255;
+ var blue = b / 255;
+ var maxColor = Math.max(red, green, blue);
+ var minColor = Math.min(red, green, blue);
+ var delta = maxColor - minColor;
+ var hue = 0;
+ var saturation = 0;
+ var lightness = (maxColor + minColor) / 2;
+ if (delta) {
+ if (maxColor === red) {
+ hue = (green - blue) / delta;
+ }
+ if (maxColor === green) {
+ hue = 2 + (blue - red) / delta;
+ }
+ if (maxColor === blue) {
+ hue = 4 + (red - green) / delta;
+ }
+ if (maxColor) {
+ saturation = delta / (1 - Math.abs(2 * lightness - 1));
+ }
+ }
+ hue = 60 * hue | 0;
+ return {
+ hue: hue < 0 ? hue += 360 : hue,
+ saturation: (saturation * 100) | 0,
+ lightness: (lightness * 100) | 0,
+ };
+ },
+ /**
+ * Converts HSL color to RGB.
+ *
+ * @static
+ * @param {integer} h
+ * @param {integer} s
+ * @param {integer} l
+ * @returns {Object|false} contains red, green and blue
+ */
+ convertHslToRgb: function (h, s, l) {
+ if (typeof(h) !== 'number' || isNaN(h) || h < 0 || h > 360 ||
+ typeof(s) !== 'number' || isNaN(s) || s < 0 || s > 100 ||
+ typeof(l) !== 'number' || isNaN(l) || l < 0 || l > 100) {
+ return false;
+ }
+ var huePrime = h / 60;
+ var saturation = s / 100;
+ var lightness = l / 100;
+ var chroma = saturation * (1 - Math.abs(2 * lightness - 1));
+ var secondComponent = chroma * (1 - Math.abs(huePrime % 2 - 1));
+ var lightnessAdjustment = lightness - chroma/2;
+ var precision = 255;
+ chroma = (chroma + lightnessAdjustment) * precision | 0;
+ secondComponent = (secondComponent + lightnessAdjustment) * precision | 0;
+ lightnessAdjustment = lightnessAdjustment * precision | 0;
+ if (huePrime >= 0 && huePrime < 1) {
+ return {red: chroma, green: secondComponent, blue: lightnessAdjustment};
+ }
+ if (huePrime >= 1 && huePrime < 2) {
+ return {red: secondComponent, green: chroma, blue: lightnessAdjustment};
+ }
+ if (huePrime >= 2 && huePrime < 3) {
+ return {red: lightnessAdjustment, green: chroma, blue: secondComponent};
+ }
+ if (huePrime >= 3 && huePrime < 4) {
+ return {red: lightnessAdjustment, green: secondComponent, blue: chroma};
+ }
+ if (huePrime >= 4 && huePrime < 5) {
+ return {red: secondComponent, green: lightnessAdjustment, blue: chroma};
+ }
+ if (huePrime >= 5 && huePrime <= 6) {
+ return {red: chroma, green: lightnessAdjustment, blue: secondComponent};
+ }
+ },
+ /**
+ * Converts RGB color to Hexadecimal code.
+ *
+ * @static
+ * @param {integer} r
+ * @param {integer} g
+ * @param {integer} b
+ * @returns {Object|false} contains hexadecimal code
+ */
+ convertRgbToHex: function (r, g, b) {
+ if (typeof(r) !== 'number' || isNaN(r) || r < 0 || r > 255 ||
+ typeof(g) !== 'number' || isNaN(g) || g < 0 || g > 255 ||
+ typeof(b) !== 'number' || isNaN(b) || b < 0 || b > 255) {
+ return false;
+ }
+ var red = r < 16 ? '0' + r.toString(16) : r.toString(16);
+ var green = g < 16 ? '0' + g.toString(16) : g.toString(16);
+ var blue = b < 16 ? '0' + b.toString(16) : b.toString(16);
+ return {hex: _.str.sprintf('#%s%s%s', red, green, blue)};
+ },
+});
+
+return Colorpicker;
+});
diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
index 9dad13a0b2b..9dad13a0b2b 100644
--- a/addons/web_editor/static/src/js/widgets/widgets.js
+++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
index fe8876468b5..fe8876468b5 100644
--- a/addons/web_editor/static/src/js/editor/snippets.editor.js
+++ b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js
index 97d3777b95f..97d3777b95f 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js