diff options
| author | Robot Odoo <xmo+roboto@odoo.com> | 2019-01-17 19:17:22 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-01-17 19:17:22 +0100 |
| commit | 22efdfbecc1e8c688000be98903a5a355d1ece8a (patch) | |
| tree | e097625171b2b67ed5f5288ced67446608b9f633 /addons/website/static/src | |
| parent | f50fbc6d168f0d17fa1e932b4ecc968a92b02b65 (diff) | |
| parent | eb0ea8efbe932c140302e9f8da33a839bcd73146 (diff) | |
[IMP] web_editor,*: Refactoring the wysiwyg editor and 'html' field
_Task #1878502_
- The refactoring allows for the update of the _Summernote_ library.
- A new structure was created by transforming all the plugins in the library using the odoo inheritance system. Plugins are easier to implement with the `AbstractPlugin` to add Odoo behaviors.
- The cleanup allowed us to remove 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.
- The refactoring also had to be done in order to create a set of unit tests. Each behavior can be tested, including the behaviors performed as a consequence of keyboard interactions (for instance: Enter, Tab...).
- From now on, the methods of the library (in this case _Summernote_) can no longer be called by other modules or files. Only the `wysiwyg` widgets can access it, to simplify the updating process. The `wysiwyg` object serves as an interface. A lot of tests are done, ensuring the consistency of outside behavior.
- Depending on the options the snippets will be loaded or not, the editor will be in an iframe or not... all of this is transparent from the outside.
- Regarding iframes, all controllers related to editing have been removed: the new API no longer needs them. This speeds up loading, eases testing and removes complexity for the same features.
**PUBLIC FEATURES**
There are several public methods on the `Wysiwyg` class:
- `Wysiwyg.prepare (WidgetParent)` which returns a deferred resolved when the library (xml, lazy, assets...) is loaded.
- `Wysiwyg.getRange (DOM)` returns the range (selection in the dom)
- `Wysiwyg.setRange (startNode, startOffset, endNode, endOffset)` which creates a range (selection in the dom)
- `Wysiwyg.setRangeFromNode (DOM, options)` that creates a range from an element (option available to select all, start or end)
A jQuery selector was added: `:o_editable`, which indicates whether the current element is editable. That is, if it is contained in a tag with the attribute `contentEditable = "true"` or in a tag with the class `o_editable`.
Several methods are also present:
- `focusIn`: makes a focus and places the cursor at the beginning of the element
- `focusInEnd`: makes a focus and places the cursor at the end of the element
- `selectContent`: makes a focus and selects the content
**HTML FIELD**
The HTML field can receive different options:
- `style-inline:` {boolean} transforms a class into an inline style when saving and vice versa when reading.
- `no-attachment`: {boolean} prevents the use of attachments (in media dialog)
- `cssEdit`: {xml_id} to use a template containing the css to loaded in an iframe when editing
- `cssReadonly`: {xml_id} to use a template containing the css to load into an iframe when viewing in readonly
- `snippets`: {xml_id} snippets template (can be used with or without `cssEdit`)
- `wrapper`: {template} qweb static template (containing a tag: `id = "wrapper"`) that will include the content during editing (removed on save)
**MASS MAILING**
A widget was created for `mass mailing`. There are now two fields: `body_html` and `body_arch`.
`body_arch` contains the code with the class without conversion into inline style, useful when editing and one with the inline style that is visible in readonly mode and sent by email.
Advantage: no spreading errors, able to update css/theme, able to do more changes when converting to inline style so that a maximum of mail clients have an impeccable rendering.
**REMAINING KNOW ISSUES**
These will be fixed in a subsequent commit.
- A few committed changes made during the development of this refactoring were lost in the process and should be reapplied.
- Some minor (mostly range) issues remain with Firefox.
--
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
closes odoo/odoo#29775
Diffstat (limited to 'addons/website/static/src')
26 files changed, 2330 insertions, 127 deletions
diff --git a/addons/website/static/src/js/content/base.js b/addons/website/static/src/js/content/base.js new file mode 100644 index 00000000000..bc5aa9892d0 --- /dev/null +++ b/addons/website/static/src/js/content/base.js @@ -0,0 +1,28 @@ +odoo.define('web_editor.base', function (require) { +'use strict'; + +var ajax = require('web.ajax'); +var session = require('web.session'); + +var domReady = $.Deferred(); +$(domReady.resolve.bind(domReady)); + +return { + /** + * If a widget needs to be instantiated on page loading, it needs to wait + * for appropriate resources in order to be loaded. This function returns a + * Deferred that 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 + * instantiated (directly or not) by the page's main component (webclient, + * website root, editor bar, ...). The DOM will be ready then, the main + * 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} + */ + ready: function () { + return $.when(domReady, session.is_bound, ajax.loadXML()); + }, +}; +}); diff --git a/addons/website/static/src/js/content/body_manager.js b/addons/website/static/src/js/content/body_manager.js new file mode 100644 index 00000000000..1045e94a65a --- /dev/null +++ b/addons/website/static/src/js/content/body_manager.js @@ -0,0 +1,60 @@ +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/website/static/src/js/content/context.js b/addons/website/static/src/js/content/context.js new file mode 100644 index 00000000000..b76e7cb403d --- /dev/null +++ b/addons/website/static/src/js/content/context.js @@ -0,0 +1,24 @@ +odoo.define('web_editor.context', function (require) { +'use strict'; + +function getContext(context) { + var html = document.documentElement; + return _.extend({ + lang: (html.getAttribute('lang') || 'en_US').replace('-', '_'), + }, context || {}); +} + +function getExtraContext(context) { + var html = document.documentElement; + return _.extend(getContext(), { + editable: !!(html.dataset.editable || $('[data-oe-model]').length), // temporary hack, this should be done in python + translatable: !!html.dataset.translatable, + edit_translations: !!html.dataset.edit_translations, + }, context || {}); +} + +return { + get: getContext, + getExtra: getExtraContext, +}; +}); diff --git a/addons/website/static/src/js/content/ready.js b/addons/website/static/src/js/content/ready.js new file mode 100644 index 00000000000..214e7c2c34f --- /dev/null +++ b/addons/website/static/src/js/content/ready.js @@ -0,0 +1,7 @@ +odoo.define('web_editor.ready', function (require) { +'use strict'; + +var base = require('web_editor.base'); + +return base.ready(); +}); diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index 587cbca61d4..65729146dfb 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -494,7 +494,9 @@ var Animation = Widget.extend({ /** * The registry object contains the list of available animations. */ -var registry = {}; +var Registry = require('web.Registry'); +var registryObject = new Registry(); +var registry = registryObject.map; registry.slider = Animation.extend({ selector: '.carousel', @@ -1108,5 +1110,6 @@ registry.anchorSlide = Animation.extend({ return { Class: Animation, registry: registry, + registryObject: registryObject, }; }); diff --git a/addons/website/static/src/js/content/website_root.js b/addons/website/static/src/js/content/website_root.js index cf4812b28b9..b9f3bfe25e9 100644 --- a/addons/website/static/src/js/content/website_root.js +++ b/addons/website/static/src/js/content/website_root.js @@ -56,6 +56,7 @@ var WebsiteRoot = BodyManager.extend({ init: function () { this._super.apply(this, arguments); this.animations = []; + sAnimation.registryObject.onAdd(this._startMissingAnimations.bind(this)); }, /** * @override @@ -141,7 +142,7 @@ var WebsiteRoot = BodyManager.extend({ _startAnimations: function (editableMode, $from) { var self = this; - editableMode = editableMode || false; + this.startInEditableMode = editableMode || false; if ($from === undefined) { $from = this.$('#wrapwrap'); } @@ -153,7 +154,7 @@ var WebsiteRoot = BodyManager.extend({ var $target = $from.find(selector).addBack(selector); var defs = _.map($target, function (el) { - var animation = new Animation(self, editableMode); + var animation = new Animation(self, self.startInEditableMode); self.animations.push(animation); return animation.attachTo($(el)); }); @@ -161,6 +162,11 @@ var WebsiteRoot = BodyManager.extend({ }); return $.when.apply($, defs); }, + _startMissingAnimations: function () { + if (this.animations.length) { + this._startAnimations(this.startInEditableMode); + } + }, /** * Destroys all animation instances. Especially needed before saving while * in edition mode for example. diff --git a/addons/website/static/src/js/editor/editor_menu.js b/addons/website/static/src/js/editor/editor_menu.js new file mode 100644 index 00000000000..996b6b99e9c --- /dev/null +++ b/addons/website/static/src/js/editor/editor_menu.js @@ -0,0 +1,213 @@ +odoo.define('website.editor.menu', function (require) { +'use strict'; + +var Dialog = require('web.Dialog'); +var Widget = require('web.Widget'); +var core = require('web.core'); +var weContext = require('web_editor.context'); +var WysiwygMultizone = require('web_editor.wysiwyg.multizone'); + +var _t = core._t; + +var EditorMenu = Widget.extend({ + template: 'website.editorbar', + xmlDependencies: ['/website/static/src/xml/website.editor.xml'], + events: { + 'click button[data-action=save]': '_onSaveClick', + 'click button[data-action=cancel]': '_onCancelClick', + }, + custom_events: { + request_save: '_onSnippetRequestSave', + }, + + LOCATION_SEARCH: 'enable_editor', + + /** + * @override + */ + willStart: function () { + var self = this; + this.$el = null; // temporary null to avoid hidden error (@see start) + return this._super() + .then(function () { + var $wrapwrap = $('#wrapwrap'); + $wrapwrap.removeClass('o_editable'); // clean the dom before edition + self.editable($wrapwrap).addClass('o_editable'); + self.wysiwyg = self._wysiwygInstance(); + return self.wysiwyg.attachTo($wrapwrap); + }); + }, + /** + * @override + */ + start: function () { + var self = this; + this.$el.css({width: '100%'}); + return this._super().then(function () { + self.trigger_up('edit_mode'); + self.$el.css({width: ''}); + }); + }, + /** + * @override + */ + destroy: function () { + this.trigger_up('readonly_mode'); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Asks the user if they really wants to discard their changes (if any), + * then simply reloads the page if they want 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 (!this.wysiwyg.isDirty()) { + 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 () { + self.trigger_up('edition_will_stopped'); + var $wrapwrap = $('#wrapwrap'); + self.editable($wrapwrap).removeClass('o_editable'); + if (reload !== false) { + self.wysiwyg.destroy(); + return self._reload(); + } else { + self.wysiwyg.destroy(); + self.trigger_up('readonly_mode'); + self.trigger_up('edition_was_stopped'); + self.destroy(); + } + }); + }, + /** + * 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; + this.trigger_up('edition_will_stopped'); + return this.wysiwyg.save().then(function (dirty) { + var $wrapwrap = $('#wrapwrap'); + self.editable($wrapwrap).removeClass('o_editable'); + if (dirty && reload !== false) { + // remove top padding because the connected bar is not visible + $('body').removeClass('o_connected_user'); + return self._reload(); + } else { + self.wysiwyg.destroy(); + self.trigger_up('edition_was_stopped'); + self.destroy(); + } + }); + }, + /** + * Returns the editable areas on the page. + * + * @param {DOM} $wrapwrap + * @returns {jQuery} + */ + editable: function ($wrapwrap) { + return $wrapwrap.find('[data-oe-model]') + .not('.o_not_editable') + .filter(function () { + return !$(this).closest('.o_not_editable').length; + }) + .not('link, script') + .not('[data-oe-readonly]') + .not('img[data-oe-field="arch"], br[data-oe-field="arch"], input[data-oe-field="arch"]') + .not('.oe_snippet_editor') + .add('.o_editable'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _wysiwygInstance: function () { + return new WysiwygMultizone(this, { + snippets: 'website.snippets', + recordInfo: { + context: weContext.get(), + data_res_model: 'website', + data_res_id: weContext.get().website_id, + } + }); + }, + /** + * Reloads the page in non-editable mode, with the right scrolling. + * + * @private + * @returns {Deferred} (never resolved, the page is reloading anyway) + */ + _reload: function () { + $('body').addClass('o_wait_reload'); + this.wysiwyg.destroy(); + window.location.hash = 'scrollTop=' + window.document.body.scrollTop; + if (window.location.search.indexOf(this.LOCATION_SEARCH) >= 0) { + var regExp = new RegExp('[&?]' + this.LOCATION_SEARCH + '(=[^&]*)?', 'g'); + window.location.href = window.location.href.replace(regExp, '?'); + } else { + window.location.reload(true); + } + return $.Deferred(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the "Discard" button is clicked -> discards the changes. + * + * @private + */ + _onCancelClick: function () { + this.cancel(false); + }, + /** + * Snippet (menu_data) can request to save the document to leave the page + * + * @private + * @param {OdooEvent} ev + * @param {object} ev.data + * @param {function} ev.data.onSuccess + * @param {function} ev.data.onFailure + */ + _onSnippetRequestSave: function (ev) { + this.save(false).then(ev.data.onSuccess, ev.data.onFailure); + }, + /** + * Called when the "Save" button is clicked -> saves the changes. + * + * @private + */ + _onSaveClick: function () { + this.save(); + }, +}); + +return EditorMenu; +}); diff --git a/addons/website/static/src/js/editor/editor_menu_translate.js b/addons/website/static/src/js/editor/editor_menu_translate.js new file mode 100644 index 00000000000..a99c0c1bc72 --- /dev/null +++ b/addons/website/static/src/js/editor/editor_menu_translate.js @@ -0,0 +1,105 @@ +odoo.define('website.editor.menu.translate', function (require) { +'use strict'; + +require('web_editor.ready'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var localStorage = require('web.local_storage'); +var weContext = require('web_editor.context'); +var WysiwygTranslate = require('web_editor.wysiwyg.multizone.translate'); +var EditorMenu = require('website.editor.menu'); + +var lang = $('html').attr('lang').replace('-', '_'); +if ($('.js_change_lang[data-lang="' + lang + '"]').data('default-lang')) { + return $.Deferred().reject("It's the default language"); +} + +var _t = core._t; + +var localStorageNoDialogKey = 'website_translator_nodialog'; + + +var TranslatorInfoDialog = Dialog.extend({ + template: 'website.TranslatorInfoDialog', + xmlDependencies: Dialog.prototype.xmlDependencies.concat( + ['/website/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 TranslatorMenu = EditorMenu.extend({ + LOCATION_SEARCH: 'edit_translations', + + /** + * @override + */ + start: function () { + if (!localStorage.getItem(localStorageNoDialogKey)) { + new TranslatorInfoDialog(this).open(); + } + + return this._super(); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Returns the editable areas on the page. + * + * @param {DOM} $wrapwrap + * @returns {jQuery} + */ + editable: function ($wrapwrap) { + var selector = '[data-oe-translation-id], '+ + '[data-oe-model][data-oe-id][data-oe-field], ' + + '[placeholder*="data-oe-translation-id="], ' + + '[title*="data-oe-translation-id="], ' + + '[alt*="data-oe-translation-id="]'; + var $edit = $wrapwrap.find(selector); + $edit.filter(':has(' + selector + ')').attr('data-oe-readonly', true); + return $edit.not('[data-oe-readonly]'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _wysiwygInstance: function () { + return new WysiwygTranslate(this, {lang: lang || weContext.get().lang}); + }, +}); + +return TranslatorMenu; + +}); diff --git a/addons/website/static/src/js/editor/rte.summernote.js b/addons/website/static/src/js/editor/rte.summernote.js deleted file mode 100644 index 36cadca7a8d..00000000000 --- a/addons/website/static/src/js/editor/rte.summernote.js +++ /dev/null @@ -1,59 +0,0 @@ -odoo.define('website.rte.summernote', function (require) { -'use strict'; - -var core = require('web.core'); -require('web_editor.rte.summernote'); - -var eventHandler = $.summernote.eventHandler; -var renderer = $.summernote.renderer; -var tplIconButton = renderer.getTemplate().iconButton; -var _t = core._t; - -var fn_tplPopovers = renderer.tplPopovers; -renderer.tplPopovers = function (lang, options) { - var $popover = $(fn_tplPopovers.call(this, lang, options)); - $popover.find('.note-image-popover .btn-group:has([data-value="img-thumbnail"])').append( - tplIconButton('fa fa-object-ungroup', { - title: _t('Transform the picture (click twice to reset transformation)'), - event: 'transform', - })); - return $popover; -}; - -$.summernote.pluginEvents.transform = function (event, editor, layoutInfo, sorted) { - var $selection = layoutInfo.handle().find('.note-control-selection'); - var $image = $($selection.data('target')); - - if ($image.data('transfo-destroy')) { - $image.removeData('transfo-destroy'); - return; - } - - $image.transfo(); - - var mouseup = function (event) { - $('.note-popover button[data-event="transform"]').toggleClass('active', $image.is('[style*="transform"]')); - }; - $(document).on('mouseup', mouseup); - - var mousedown = function (event) { - if (!$(event.target).closest('.transfo-container').length) { - $image.transfo('destroy'); - $(document).off('mousedown', mousedown).off('mouseup', mouseup); - } - if ($(event.target).closest('.note-popover').length) { - $image.data('transfo-destroy', true).attr('style', ($image.attr('style') || '').replace(/[^;]*transform[\w:]*;?/g, '')); - } - $image.trigger('content_changed'); - }; - $(document).on('mousedown', mousedown); -}; - -var fn_boutton_update = eventHandler.modules.popover.button.update; -eventHandler.modules.popover.button.update = function ($container, oStyle) { - fn_boutton_update.call(this, $container, oStyle); - $container.find('button[data-event="transform"]') - .toggleClass('active', $(oStyle.image).is('[style*="transform"]')) - .toggleClass('d-none', !$(oStyle.image).is('img')); -}; -}); diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index 44ad7e92e11..b516de3c939 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -1,9 +1,9 @@ -odoo.define('website.snippets.options', function (require) { +odoo.define('website.editor.snippets.options', function (require) { 'use strict'; var core = require('web.core'); var Dialog = require('web.Dialog'); -var weWidgets = require('web_editor.widget'); +var weWidgets = require('wysiwyg.widgets'); var options = require('web_editor.snippets.options'); var _t = core._t; @@ -814,8 +814,8 @@ options.registry.ul = options.Class.extend({ this._super(); if (!this.$target.hasClass('o_ul_folded')) { this.$target.find('.o_close').removeClass('o_close'); + this.$target.find('li').css('list-style', ''); } - this.$target.find('li:not(:has(>ul))').css('list-style', ''); }, //-------------------------------------------------------------------------- @@ -844,13 +844,11 @@ options.registry.ul = options.Class.extend({ }) .prepend('<a href="#" class="o_ul_toggle_self fa" />'); var $li = this.$target.find('li:has(+li:not(>.o_ul_toggle_self)>ul, +li:not(>.o_ul_toggle_self)>ol)'); + $li.css('list-style', this.$target.hasClass('o_ul_folded') ? 'none' : ''); $li.map(function () { return $(this).children()[0] || this; }) .prepend('<a href="#" class="o_ul_toggle_next fa" />'); $li.removeClass('o_open').next().addClass('o_close'); - this.$target.find('li').removeClass('o_open').css('list-style', ''); - this.$target.find('li:has(.o_ul_toggle_self, .o_ul_toggle_next), li:has(>ul,>ol):not(:has(>li))').css('list-style', 'none'); - - this.$target.find('li:not(:has(>ul))').css('list-style', ''); + this.$target.find('li').removeClass('o_open'); this._refreshAnimations(); }, }); @@ -943,7 +941,7 @@ options.registry.gallery = options.Class.extend({ var self = this; // The snippet should not be editable - this.$target.attr('contentEditable', false); + this.$target.addClass('o_fake_not_editable').attr('contentEditable', false); // Make sure image previews are updated if images are changed this.$target.on('save', 'img', function (ev) { @@ -960,6 +958,21 @@ options.registry.gallery = options.Class.extend({ self.addImages(false); }); + this.$target.on('dropped', 'img', function (ev) { + self.mode(null, self.getMode()); + if (!ev.target.height) { + $(ev.target).one('load', function () { + setTimeout(function () { + self.trigger_up('cover_update'); + }); + }); + } + }); + + if (this.$('.container:first > *:not(div)').length) { + self.mode(null, self.getMode()); + } + return this._super.apply(this, arguments); }, /** @@ -1020,6 +1033,24 @@ options.registry.gallery = options.Class.extend({ this.mode(null, $activeMode.data('mode'), $activeMode); }, /** + * Get the image target's layout mode (slideshow, masonry, grid or nomode). + * + * @returns {String('slideshow'|'masonry'|'grid'|'nomode')} + */ + getMode: function () { + var mode = 'slideshow'; + if (this.$target.hasClass('o_masonry')) { + mode = 'masonry'; + } + if (this.$target.hasClass('o_grid')) { + mode = 'grid'; + } + if (this.$target.hasClass('o_nomode')) { + mode = 'nomode'; + } + return mode; + }, + /** * Displays the images with the "grid" layout. */ grid: function () { @@ -1094,6 +1125,7 @@ options.registry.gallery = options.Class.extend({ this.$target .removeClass('o_nomode o_masonry o_grid o_slideshow') .addClass('o_' + value); + this.trigger_up('cover_update'); }, /** * Displays the images with the standard layout: floating images. diff --git a/addons/website/static/src/js/editor/editor.js b/addons/website/static/src/js/editor/widget_link.js index 12dc17daba6..9dd329f14e6 100644 --- a/addons/website/static/src/js/editor/editor.js +++ b/addons/website/static/src/js/editor/widget_link.js @@ -1,7 +1,7 @@ -odoo.define('website.editor', function (require) { +odoo.define('website.editor.link', function (require) { 'use strict'; -var weWidgets = require('web_editor.widget'); +var weWidgets = require('wysiwyg.widgets'); var wUtils = require('website.utils'); weWidgets.LinkDialog.include({ diff --git a/addons/website/static/src/js/editor/wysiwyg_multizone.js b/addons/website/static/src/js/editor/wysiwyg_multizone.js new file mode 100644 index 00000000000..1441a7ce3e3 --- /dev/null +++ b/addons/website/static/src/js/editor/wysiwyg_multizone.js @@ -0,0 +1,582 @@ +odoo.define('web_editor.wysiwyg.multizone', function (require) { +'use strict'; +var concurrency = require('web.concurrency'); +var core = require('web.core'); +var DropzonePlugin = require('web_editor.wysiwyg.plugin.dropzone'); +var HelperPlugin = require('web_editor.wysiwyg.plugin.helper'); +var TextPlugin = require('web_editor.wysiwyg.plugin.text'); +var HistoryPlugin = require('web_editor.wysiwyg.plugin.history'); +var Wysiwyg = require('web_editor.wysiwyg'); + +var _t = core._t; + +HistoryPlugin.include({ + /** + * @override + */ + applySnapshot: function () { + this.trigger_up('content_will_be_destroyed', { + $target: this.$editable, + }); + this._super.apply(this, arguments); + this.trigger_up('content_was_recreated', { + $target: this.$editable, + }); + $('.oe_overlay').remove(); + $('.note-control-selection').hide(); + this.$editable.trigger('content_changed'); + }, +}); + +HelperPlugin.include({ + /** + * Returns true if range is on or within a field that is not of type 'html'. + * + * @returns {Boolean} + */ + isOnNonHTMLField: function () { + var range = this.context.invoke('editor.createRange'); + return !!$.summernote.dom.ancestor(range.sc, function (node) { + return $(node).data('oe-type') && $(node).data('oe-type') !== 'html'; + }); + }, +}); + +TextPlugin.include({ + /** + * Paste text only if on non-HTML field. + * + * @override + */ + pasteNodes: function (nodes, textOnly) { + textOnly = textOnly || this.context.invoke('HelperPlugin.isOnNonHTMLField'); + this._super.apply(this, [nodes, textOnly]); + }, +}); + +DropzonePlugin.include({ + /** + * Prevent dropping images in non-HTML fields. + * + * @override + */ + _canDropHere: function (dataTransfer) { + if (this.context.invoke('HelperPlugin.isOnNonHTMLField') && dataTransfer.files.length) { + return false; + } + return this._super(); + }, +}); + + + + + +/** + * HtmlEditor + * Intended to edit HTML content. This widget uses the Wysiwyg editor + * improved by odoo. + * + * class editable: o_editable + * class non editable: o_not_editable + * + */ +var WysiwygMultizone = Wysiwyg.extend({ + events: _.extend({}, Wysiwyg.prototype.events, { + 'keyup *': function (ev) { + if ((ev.keyCode === 8 || ev.keyCode === 46)) { + var $target = $(ev.target).closest('.o_editable'); + if (!$target.is(':has(*:not(p):not(br))') && !$target.text().match(/\S/)) { + $target.empty(); + } + } + if (ev.key.length === 1) { + this._onChange(); + } + }, + 'click .note-editable': function (ev) { + ev.preventDefault(); + }, + 'submit .note-editable form .btn': function (ev) { + ev.preventDefault(); // Disable form submition in editable mode + }, + 'hide.bs.dropdown .dropdown': function (ev) { + // Prevent dropdown closing when a contenteditable children is focused + if (ev.originalEvent && + $(ev.target).has(ev.originalEvent.target).length && + $(ev.originalEvent.target).is('[contenteditable]')) { + ev.preventDefault(); + } + }, + }), + custom_events: _.extend({}, Wysiwyg.prototype.custom_events, { + activate_snippet: '_onActivateSnippet', + drop_images: '_onDropImages', + }), + /** + * @override + * @param {Object} options.context - the context to use for the saving rpc + * @param {boolean} [options.withLang=false] + * false if the lang must be omitted in the context (saving "master" + * page element) + */ + init: function (parent, options) { + options = options || {}; + options.addDropSelector = ':o_editable'; + this.savingMutex = new concurrency.Mutex(); + this._super(parent, options); + }, + /** + * Prevent some default features for the editable area. + * + * @override + */ + start: function () { + var self = this; + return this._super().then(function () { + // Unload preserve + var flag = false; + window.onbeforeunload = function (event) { + if (self.isDirty() && !flag) { + flag = true; + _.defer(function () { + flag = false; + }); + return _t('Changes you made may not be saved.'); + } + }; + // firefox & IE fix + try { + document.execCommand('enableObjectResizing', false, false); + document.execCommand('enableInlineTableEditing', false, false); + document.execCommand('2D-position', false, false); + } catch (e) { /* */ } + document.body.addEventListener('resizestart', function (evt) { + evt.preventDefault(); + return false; + }); + document.body.addEventListener('movestart', function (evt) { + evt.preventDefault(); + return false; + }); + document.body.addEventListener('dragstart', function (evt) { + evt.preventDefault(); + return false; + }); + // BOOTSTRAP preserve + self.init_bootstrap_carousel = $.fn.carousel; + $.fn.carousel = function () { + var res = self.init_bootstrap_carousel.apply(this, arguments); + // off bootstrap keydown event to remove event.preventDefault() + // and allow to change cursor position + $(this).off('keydown.bs.carousel'); + return res; + }; + self.$('.dropdown-toggle').dropdown(); + self.$el + .tooltip({ + selector: '[data-oe-readonly]', + container: 'body', + trigger: 'hover', + delay: { + 'show': 1000, + 'hide': 100, + }, + placement: 'bottom', + title: _t("Readonly field") + }) + .on('click', function () { + $(this).tooltip('hide'); + }); + $('body').addClass('editor_enable'); + $('.note-editor, .note-popover').filter('[data-wysiwyg-id="' + self.id + '"]').addClass('wysiwyg_multizone'); + $('.note-editable .note-editor, .note-editable .note-editable').attr('contenteditable', false); + + self._summernote.isDisabled = function () { + return false; + }; + + self.$('.note-editable').addClass('o_not_editable').attr('contenteditable', false); + self._getEditableArea().attr('contenteditable', true); + self.$('[data-oe-readonly]').addClass('o_not_editable').attr('contenteditable', false); + self.$('.oe_structure').attr('contenteditable', false).addClass('o_fake_not_editable'); + self.$('[data-oe-field][data-oe-type="image"]').attr('contenteditable', false).addClass('o_fake_not_editable'); + self.$('[data-oe-field]:not([contenteditable])').attr('contenteditable', true).addClass('o_fake_editable'); + }); + }, + /** + * @override + */ + destroy: function () { + this._super(); + this.$target.css('display', ''); + this.$target.find('[data-old-id]').add(this.$target).each(function () { + var $node = $(this); + $node.attr('id', $node.attr('data-old-id')).removeAttr('data-old-id'); + }); + $('body').removeClass('editor_enable'); + window.onbeforeunload = null; + $.fn.carousel = this.init_bootstrap_carousel; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + * @returns {Boolean} + */ + isDirty: function () { + return !!this._getEditableArea().filter('.o_dirty').length; + }, + /** + * @override + * @returns {$.Promise} resolve with true if the content was dirty + */ + save: function () { + var isDirty = this.isDirty(); + if (isDirty) { + this.savingMutex.exec(this._saveCroppedImages.bind(this)); + } + var _super = this._super.bind(this); + return this.savingMutex.def.then(function () { + return _super().then(function (_isDirty, html) { + this._summernote.layoutInfo.editable.html(html); + + var $editable = this._getEditableArea(); + var $areaDirty = $editable.filter('.o_dirty'); + if (!$areaDirty.length) { + return false; + } + $areaDirty.each(function (index, editable) { + this.savingMutex.exec(this._saveEditable.bind(this, editable)); + }.bind(this)); + return this.savingMutex.def.then(function () { + return true; + }); + }.bind(this)); + }.bind(this)); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Return true if the editor is displaying the popover. + * + * @override + * @returns {Boolean} + */ + _isDisplayingPopover: function (node) { + return this._super(node) && $(node).parent().closest('[data-oe-model="ir.ui.view"], [data-oe-type="html"]').length; + }, + /** + * @override + * @returns {Object} the summernote configuration + */ + _editorOptions: function () { + var options = this._super(); + options.toolbar[8] = ['view', ['help']]; + + // remove blockquote (it's a snippet) + var blockquote = options.styleTags.indexOf('blockquote'); + if (blockquote !== -1) options.styleTags.splice(blockquote, 1); + + options.popover.image[4] = ['editImage', ['cropImage', 'transform']]; + return _.extend(options, { + styleWithSpan: false, + followingToolbar: false, + }); + }, + /** + * Escape internal text nodes for XML storage. + * + * @private + * @param {jQuery} $el + */ + _escapeElements: function ($el) { + var toEscape = $el.find('*').addBack(); + toEscape = toEscape.not(toEscape.filter('object,iframe,script,style,[data-oe-model][data-oe-model!="ir.ui.view"]').find('*').addBack()); + toEscape.contents().each(function () { + if (this.nodeType === 3) { + this.nodeValue = $('<div />').text(this.nodeValue).html(); + } + }); + }, + /** + * Gets jQuery cloned element with clean for XML storage. + * + * @private + * @param {jQuery} $el + * @returns {jQuery} + */ + _getCleanedHtml: function (editable) { + var $el = $(editable).clone().removeClass('o_editable o_dirty'); + this._escapeElements($el); + return $el; + }, + /** + * Return an object describing the linked record. + * + * @override + * @param {Object} options + * @returns {Object} {res_id, res_model, xpath} + */ + _getRecordInfo: function (options) { + options = options || {}; + var $editable = $(options.target).closest(this._getEditableArea()); + if (!$editable.length) { + $editable = $(this._getFocusedEditable()); + } + var data = this._super(); + var res_id = $editable.data('oe-id'); + var res_model = $editable.data('oe-model'); + if (!$editable.data('oe-model')) { + var object = $('html').data('main-object'); + res_model = object.split('(')[0]; + res_id = +object.split('(')[1].split(',')[0]; + } + var xpath = $editable.data('oe-xpath'); + + if (options.type === 'media' && (res_model === 'website.page' || res_model === 'ir.ui.view')) { + res_id = 0; + res_model = 'ir.ui.view'; + xpath = null; + } + + return _.extend(data, { + res_id: res_id, + res_model: res_model, + xpath: xpath, + }); + }, + /** + * Return the focused editable area. + * + * @private + * @returns {Node} + */ + _getFocusedEditable: function () { + var $focusedNode = $(this._focusedNode); + var $editableArea = this._getEditableArea(); + return $focusedNode.closest($editableArea)[0] || + $focusedNode.find($editableArea)[0]; + }, + /** + * Return the editable areas. + * + * @private + * @returns {JQuery} + */ + _getEditableArea: function () { + if (!this._summernote) { + return $(); + } + return $(this.selectorEditableArea, this._summernote.layoutInfo.editable); + }, + + /** + * @override + * @returns {Promise} + */ + _loadInstance: function () { + return this._super().then(function () { + var $target = this.$target; + var id = $target.attr('id'); + var className = $target.attr('class'); + $target.off('.WysiwygFrontend'); + this.$target.find('[id]').add(this.$target).each(function () { + var $node = $(this); + $node.attr('data-old-id', $node.attr('id')).removeAttr('id'); + }); + this.$('.note-editable:first').attr('id', id).addClass(className); + this.selectorEditableArea = '.o_editable'; + }.bind(this)); + }, + /** + * @private + * @returns {Promise} + */ + _saveEditable: function (editable) { + var self = this; + var recordInfo = this._getRecordInfo({target: editable}); + var outerHTML = this._getCleanedHtml(editable).prop('outerHTML'); + var def = this._saveElement(outerHTML, recordInfo, editable); + def.done(function () { + self.trigger_up('saved', recordInfo); + }).fail(function () { + self.trigger_up('canceled', recordInfo); + }); + return def; + }, + /** + * @private + * @returns {$.Promise} + */ + _saveCroppedImages: function () { + var self = this; + var $area = $(this.selectorEditableArea, this.$target); + var defs = $area.find('.o_cropped_img_to_save').map(function () { + var $croppedImg = $(this); + $croppedImg.removeClass('o_cropped_img_to_save'); + var resModel = $croppedImg.data('crop:resModel'); + var resID = $croppedImg.data('crop:resID'); + var cropID = $croppedImg.data('crop:id'); + var mimetype = $croppedImg.data('crop:mimetype'); + var originalSrc = $croppedImg.data('crop:originalSrc'); + var datas = $croppedImg.attr('src').split(',')[1]; + if (!cropID) { + var name = originalSrc + '.crop'; + return self._rpc({ + model: 'ir.attachment', + method: 'create', + args: [{ + res_model: resModel, + res_id: resID, + name: name, + datas_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}], + }); + } + }).get(); + return $.when.apply($, defs); + }, + /** + * Saves one (dirty) element of the page. + * + * @private + * @param {string} outerHTML + * @param {Object} recordInfo + * @returns {Promise} + */ + _saveElement: function (outerHTML, recordInfo) { + return this._rpc({ + model: 'ir.ui.view', + method: 'save', + args: [ + recordInfo.res_id, + outerHTML, + recordInfo.xpath, + ], + kwargs: { + context: recordInfo.context, + }, + }); + }, + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + + /** + * @override + * @param {OdooEvent} ev + */ + _onActivateSnippet: function (ev) { + if (!$.contains(ev.data[0], this._focusedNode)) { + this._focusedNode = ev.data[0]; + } + ev.data.closest('.oe_structure > *:not(.o_fake_editable)').addClass('o_fake_editable').attr('contenteditable', true); + }, + /** + * @override + */ + _onChange: function () { + var editable = this._getFocusedEditable(); + $(editable).addClass('o_dirty'); + this._super.apply(this, arguments); + }, + /** + * @override + * @param {OdooEvent} ev + */ + _onContentChange: function (ev) { + this._focusedNode = ev.target; + this._super.apply(this, arguments); + }, + /** + * Triggered when the user begin to drop iamges in the editor. + * + * @private + * @param {OdooEvent} ev + * @param {Object} ev.data + * @param {Node[]} ev.data.spinners + * @param {$.Promise} ev.data.promises + */ + _onDropImages: function (ev) { + if (!this.snippets) { + return; + } + var gallerySelector = this.snippets.$('[data-js="gallery"]').data('selector'); + var $gallery = this.snippets.$activeSnippet && this.snippets.$activeSnippet.closest(gallerySelector) || $(); + + if (!$gallery.length && ev.data.spinners.length >= 2) { + $gallery = this.snippets.$snippets.filter(':has(' + gallerySelector + ')').first(); + var $drag = $gallery.find('.oe_snippet_thumbnail'); + var pos = $drag.offset(); + + $gallery.find('section').attr('id', 'onDropImagesGallery'); + + $drag.trigger($.Event("mousedown", { + which: 1, + pageX: pos.left, + pageY: pos.top + })); + + pos = $(ev.data.spinners[0]).offset(); + $drag.trigger($.Event("mousemove", { + which: 1, + pageX: pos.left, + pageY: pos.top + })); + $drag.trigger($.Event("mouseup", { + which: 1, + pageX: pos.left, + pageY: pos.top + })); + + $gallery = $('#wrapwrap #onDropImagesGallery').removeAttr('id'); + } + + if (!$gallery.length) { + return; + } + + $(ev.data.spinners).remove(); + + _.each(ev.data.promises, function (promise) { + promise.then(function (image) { + $gallery.find('.container:first').append(image); + }); + }); + }, + /** + * @override + */ + _onFocusnode: function (node) { + this._focusedNode = node; + this._super.apply(this, arguments); + }, +}); + +return WysiwygMultizone; +}); diff --git a/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js b/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js new file mode 100644 index 00000000000..5f68ff09e0f --- /dev/null +++ b/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js @@ -0,0 +1,289 @@ +odoo.define('web_editor.wysiwyg.multizone.translate', function (require) { +'use strict'; + +var core = require('web.core'); +var webDialog = require('web.Dialog'); +var Dialog = require('wysiwyg.widgets.Dialog'); +var WysiwygMultizone = require('web_editor.wysiwyg.multizone'); + +var _t = core._t; + + +var AttributeTranslateDialog = 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.translation = $(node).data('translation'); + }, + /** + * @override + */ + start: function () { + 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'); + $node.trigger('change'); + }); + $group.append($label).append($input); + }); + return this._super.apply(this, arguments); + } +}); + +var WysiwygTranslate = WysiwygMultizone.extend({ + /** + * @override + * @param {string} options.lang + */ + init: function (parent, options) { + this.lang = options.lang; + options.recordInfo = _.defaults({ + context: {lang: this.lang} + }, options.recordInfo, options); + this._super.apply(this, arguments); + }, + /** + * @override + */ + start: function () { + var self = this; + return this._super().then(function () { + var attrs = ['placeholder', 'title', 'alt']; + _.each(attrs, function (attr) { + self._getEditableArea().filter('[' + attr + '*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () { + 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); + }); + }); + + self.translations = []; + self.$editables_attr = self._getEditableArea().filter('.o_translatable_attribute'); + self.$editables_attribute = $('.o_editable_translatable_attribute'); + + self.$editables_attribute.on('change', self._onChange.bind(self)); + + self._markTranslatableNodes(); + }); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + * @returns {Boolean} + */ + isDirty: function () { + return this._super() || this.$editables_attribute.hasClass('o_dirty'); + }, + /** + * @override + * @param {Node} node + * @returns {Boolean} + */ + isUnbreakableNode: function (node) { + return !!this._super(node) || !!$(node).data('oe-readonly'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Return the editable area. + * + * @override + * @returns {JQuery} + */ + _getEditableArea: function () { + var $editables = this._super(); + return $editables.add(this.$editables_attribute); + }, + /** + * Return an object describing the linked record. + * + * @override + * @param {Object} options + * @returns {Object} {res_id, res_model, xpath} + */ + _getRecordInfo: function (options) { + options = options || {}; + var recordInfo = this._super(options); + var $editable = $(options.target).closest(this._getEditableArea()); + if (!$editable.length) { + $editable = $(this._getFocusedEditable()); + } + recordInfo.context.lang = this.lang; + recordInfo.translation_id = $editable.data('oe-translation-id')|0; + return recordInfo; + }, + /** + * Saves one (dirty) element of the page. + * + * @override + * @param {string} outerHTML + * @param {Object} recordInfo + * @param {DOM} editable + * @returns {Promise} + */ + _saveElement: function (outerHTML, recordInfo, editable) { + if (!recordInfo.translation_id) { + return this._super(outerHTML, recordInfo); + } + return this._rpc({ + model: 'ir.translation', + method: 'save_html', + args: [ + [recordInfo.translation_id], + $(editable).html(), + ], + kwargs: { + context: recordInfo.context, + }, + }).fail(function (error) { + console.error(error.data.message); + webDialog.alert(null, error.data.message); + }); + }, + /** + * @override + * @returns {Object} the summernote configuration + */ + _editorOptions: function () { + var options = this._super(); + options.toolbar = [ + // todo: hide this feature for field (data-oe-model) + ['font', ['bold', 'italic', 'underline', 'clear']], + ['fontsize', ['fontsize']], + ['color', ['color']], + // keep every time + ['history', ['undo', 'redo']], + ]; + return options; + }, + /** + * @override + * @returns {Object} modules list to load + */ + _getPlugins: function () { + var plugins = this._super(); + return _.omit(plugins, 'linkPopover', 'ImagePopover', 'MediaPlugin', 'ImagePlugin', 'VideoPlugin', 'IconPlugin', 'DocumentPlugin', 'tablePopover'); + }, + /** + * Called when text is edited -> make sure text is not messed up and mark + * the element as dirty. + * + * @override + * @param {Jquery Event} [ev] + */ + _onChange: function (ev) { + var $node = $(ev && ev.target || this._getFocusedEditable()); + if (!$node.length) { + return; + } + $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]+/, ' ')); + }, + /** + * Returns a translation object. + * + * @private + * @param {Node} node + * @returns {Object} + */ + _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._getEditableArea().each(function () { + var $node = $(this); + var trans = self._getTranlationObject(this); + trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' '); + }); + this._getEditableArea().prependEvent('click.translator', function (ev) { + if (ev.ctrlKey || !$(ev.target).is(':o_editable')) { + return; + } + ev.preventDefault(); + ev.stopPropagation(); + }); + + // attributes + + this.$editables_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.$editables_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(); + }); + }, +}); + +return WysiwygTranslate; +}); diff --git a/addons/website/static/src/js/menu/content.js b/addons/website/static/src/js/menu/content.js index cf7041883db..92281f26886 100644 --- a/addons/website/static/src/js/menu/content.js +++ b/addons/website/static/src/js/menu/content.js @@ -6,7 +6,7 @@ var core = require('web.core'); var Dialog = require('web.Dialog'); var time = require('web.time'); var weContext = require('web_editor.context'); -var weWidgets = require('web_editor.widget'); +var weWidgets = require('wysiwyg.widgets'); var websiteNavbarData = require('website.navbar'); var websiteRootData = require('website.WebsiteRoot'); var Widget = require('web.Widget'); @@ -364,13 +364,14 @@ var MenuEntryDialog = weWidgets.LinkDialog.extend({ /** * @constructor */ - init: function (parent, options, editor, data) { + init: function (parent, options, data) { data.text = data.name || ''; data.isNewWindow = data.new_window; this.data = data; + this._super(parent, _.extend({}, { title: _t("Create Menu"), - }, options || {}), editor, data); + }, options || {}), data); }, /** * @override @@ -570,7 +571,7 @@ var EditMenuDialog = weWidgets.Dialog.extend({ */ _onAddMenuButtonClick: function () { var self = this; - var dialog = new MenuEntryDialog(this, {}, undefined, {}); + var dialog = new MenuEntryDialog(this, {}, {}); dialog.on('save', this, function (link) { var new_menu = { id: _.uniqueId('new-'), @@ -611,7 +612,7 @@ var EditMenuDialog = weWidgets.Dialog.extend({ var menu_id = $(ev.currentTarget).closest('[data-menu-id]').data('menu-id'); var menu = self.flat[menu_id]; if (menu) { - var dialog = new MenuEntryDialog(this, {}, undefined, menu); + var dialog = new MenuEntryDialog(this, {}, menu); dialog.on('save', this, function (link) { var id = link.id; var menu_obj = self.flat[id]; diff --git a/addons/website/static/src/js/menu/customize.js b/addons/website/static/src/js/menu/customize.js index b931e938cc6..6b54cf10416 100644 --- a/addons/website/static/src/js/menu/customize.js +++ b/addons/website/static/src/js/menu/customize.js @@ -87,7 +87,7 @@ var CustomizeMenu = Widget.extend({ $menu.append('<li class="dropdown-header">' + currentGroup + '</li>'); } var $a = $('<a/>', {href: '#', class: 'dropdown-item', 'data-view-id': item.id, role: 'menuitem'}) - .append(qweb.render('web_editor.components.switch', {id: 'switch-' + item.id, label: item.name})); + .append(qweb.render('website.components.switch', {id: 'switch-' + item.id, label: item.name})); $a.find('input').prop('checked', !!item.active); $menu.append($a); }); diff --git a/addons/website/static/src/js/menu/edit.js b/addons/website/static/src/js/menu/edit.js index d2ffa32268a..98322d9e092 100644 --- a/addons/website/static/src/js/menu/edit.js +++ b/addons/website/static/src/js/menu/edit.js @@ -3,7 +3,7 @@ odoo.define('website.editMenu', function (require) { var core = require('web.core'); var weContext = require('web_editor.context'); -var editor = require('web_editor.editor'); +var EditorMenu = require('website.editor.menu'); var websiteNavbarData = require('website.navbar'); var _t = core._t; @@ -22,6 +22,8 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ content_was_recreated: '_onContentWasRecreated', snippet_cloned: '_onSnippetCloned', snippet_dropped: '_onSnippetDropped', + edition_will_stopped: '_onEditionWillStop', + edition_was_stopped: '_onEditionWasStopped', }), /** @@ -39,22 +41,17 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ start: function () { var def = this._super.apply(this, arguments); - // If we auto start the editor, do not show a welcome message - if (this._editorAutoStart) { - this._startEditMode(); - return def; - } - // Check that the page is empty - var $wrap = $('#wrapwrap.homepage #wrap'); // TODO find this element another way - if (!$wrap.length || $wrap.html().trim() !== '') { - return def; - } + var $wrap = this._targetForEdition().find('#wrap'); + this.$wrap = $wrap; - // If readonly empty page, show the welcome message - this.$welcomeMessage = $(core.qweb.render('website.homepage_editor_welcome_message')); - this.$welcomeMessage.css('min-height', $wrap.parent('main').height() - ($wrap.outerHeight(true) - $wrap.height())); - $wrap.empty().append(this.$welcomeMessage); + if ($wrap.length && $wrap.html().trim() === '') { + // If readonly empty page, show the welcome message + this.$welcomeMessage = $(core.qweb.render('website.homepage_editor_welcome_message')); + this.$welcomeMessage.addClass('o_homepage_editor_welcome_message'); + this.$welcomeMessage.css('min-height', $wrap.parent('main').height() - ($wrap.outerHeight(true) - $wrap.height())); + $wrap.empty().append(this.$welcomeMessage); + } setTimeout(function () { if ($('.o_tooltip.o_animated').length) { @@ -62,6 +59,10 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ } }, 1000); // ugly hack to wait that tooltip is loaded + // If we auto start the editor, do not show a welcome message + if (this._editorAutoStart) { + return $.when(def, this._startEditMode()); + } return def; }, @@ -78,11 +79,17 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ */ _startEditMode: function () { var self = this; - return (new (editor.Class)(this)).prependTo(document.body).then(function () { + this.trigger_up('animation_stop_demand', { + $target: this._targetForEdition(), + }); + if (this.$welcomeMessage) { + this.$welcomeMessage.detach(); // detach from the readonly rendering before the clone by summernote + } + return new EditorMenu(this).prependTo(document.body).then(function () { if (self.$welcomeMessage) { - self.$welcomeMessage.remove(); + self.$wrap.append(self.$welcomeMessage); // reappend if the user cancel the edition } - var $wrapwrap = $('#wrapwrap'); // TODO find this element another way + var $wrapwrap = self._targetForEdition(); var $htmlEditable = $wrapwrap.find('.oe_structure.oe_empty, [data-oe-type="html"]').not('[data-editor-message]'); $htmlEditable.attr('data-editor-message', _t('DRAG BUILDING BLOCKS HERE')); var def = $.Deferred(); @@ -109,6 +116,21 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ _onSave: function () {}, //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Returns the target for edition. + * + * @private + * @returns {JQuery} + */ + _targetForEdition: function () { + // in edit mode, we have .note-editable className + return $('#wrapwrap:not(.note-editable), #wrapwrap.note-editable'); + }, + + //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- @@ -125,7 +147,7 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ }); }, /** - * Called when content will be recreated in the page. Notifies the + * Called when content was recreated in the page. Notifies the * WebsiteRoot that is should start the animations. * * @private @@ -138,6 +160,33 @@ var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ }); }, /** + * Called when edition will stop. Notifies the + * WebsiteRoot that is should stop the animations. + * + * @private + * @param {OdooEvent} ev + */ + _onEditionWillStop: function (ev) { + var $target = this._targetForEdition(); + $target.find('[data-editor-message]').removeAttr('data-editor-message'); + this.trigger_up('animation_stop_demand', { + $target: $target, + }); + }, + /** + * Called when edition was stopped. Notifies the + * WebsiteRoot that is should start the animations. + * + * @private + * @param {OdooEvent} ev + */ + _onEditionWasStopped: function (ev) { + var $target = this._targetForEdition(); + this.trigger_up('animation_start_demand', { + $target: $target, + }); + }, + /** * Called when a snippet is cloned in the page. Notifies the WebsiteRoot * that is should start the animations for this snippet. * diff --git a/addons/website/static/src/js/menu/navbar.js b/addons/website/static/src/js/menu/navbar.js index 8b7c11f58a7..aa1646b2eaa 100644 --- a/addons/website/static/src/js/menu/navbar.js +++ b/addons/website/static/src/js/menu/navbar.js @@ -17,6 +17,7 @@ var WebsiteNavbar = rootWidget.RootWidget.extend({ custom_events: _.extend({}, rootWidget.RootWidget.prototype.custom_events || {}, { action_demand: '_onActionDemand', edit_mode: '_onEditMode', + readonly_mode: '_onReadonlyMode', ready_to_save: '_onSave', }), @@ -141,11 +142,8 @@ var WebsiteNavbar = rootWidget.RootWidget.extend({ * @private */ _onEditMode: function () { - var self = this; this.$el.addClass('editing_mode'); - _.delay(function () { - self.do_hide(); - }, 800); + this.do_hide(); }, /** * Called when a submenu is hovered -> automatically opens it if another @@ -171,6 +169,15 @@ var WebsiteNavbar = rootWidget.RootWidget.extend({ this.$el.parent().toggleClass('o_mobile_menu_opened'); }, /** + * Called in response to edit mode activation -> hides the navbar. + * + * @private + */ + _onReadonlyMode: function () { + this.$el.removeClass('editing_mode'); + this.do_show(); + }, + /** * Called in response to edit mode saving -> checks if action-capable * children have something to save. * diff --git a/addons/website/static/src/js/menu/new_content.js b/addons/website/static/src/js/menu/new_content.js index 490e4f75516..0e1a10ebc80 100644 --- a/addons/website/static/src/js/menu/new_content.js +++ b/addons/website/static/src/js/menu/new_content.js @@ -83,7 +83,7 @@ var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ var $add = $('<div/>', {'class': 'form-group mb0'}) .append($('<span/>', {'class': 'offset-md-3 col-md-9 text-left'}) - .append(qweb.render('web_editor.components.switch', {id: 'switch_addTo_menu', label: _t("Add to menu")}))); + .append(qweb.render('website.components.switch', {id: 'switch_addTo_menu', label: _t("Add to menu")}))); $add.find('input').prop('checked', true); $group.after($add); } diff --git a/addons/website/static/src/js/menu/seo.js b/addons/website/static/src/js/menu/seo.js index a0a2d0f6b60..706f219761b 100644 --- a/addons/website/static/src/js/menu/seo.js +++ b/addons/website/static/src/js/menu/seo.js @@ -8,7 +8,7 @@ var mixins = require('web.mixins'); var rpc = require('web.rpc'); var Widget = require('web.Widget'); var weContext = require('web_editor.context'); -var weWidgets = require('web_editor.widget'); +var weWidgets = require('wysiwyg.widgets'); var websiteNavbarData = require('website.navbar'); var _t = core._t; diff --git a/addons/website/static/src/js/menu/translate.js b/addons/website/static/src/js/menu/translate.js index 34924a83188..9c6c6312a13 100644 --- a/addons/website/static/src/js/menu/translate.js +++ b/addons/website/static/src/js/menu/translate.js @@ -3,7 +3,7 @@ odoo.define('website.translateMenu', function (require) { var utils = require('web.utils'); var weContext = require('web_editor.context'); -var translate = require('web_editor.translate'); +var TranslatorMenu = require('website.editor.menu.translate'); var websiteNavbarData = require('website.navbar'); var ctx = weContext.getExtra(); @@ -67,7 +67,7 @@ var TranslatePageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ window.location.search += '&edit_translations'; return $.Deferred(); } - var translator = new (translate.Class)(this, $('#wrapwrap')); + var translator = new TranslatorMenu(this); return translator.prependTo(document.body); }, }); diff --git a/addons/website/static/src/js/tours/rte.js b/addons/website/static/src/js/tours/rte.js index 46fbbff3fce..bdc676f4b33 100644 --- a/addons/website/static/src/js/tours/rte.js +++ b/addons/website/static/src/js/tours/rte.js @@ -1,12 +1,18 @@ odoo.define('website.tour.rte', function (require) { 'use strict'; +var ajax = require('web.ajax'); +var session = require('web.session'); var tour = require('web_tour.tour'); -var base = require('web_editor.base'); +var Wysiwyg = require('web_editor.wysiwyg'); + +var domReady = $.Deferred(); +$(domReady.resolve.bind(domReady)); +var ready = $.when(domReady, session.is_bound, ajax.loadXML()); tour.register('rte_translator', { test: true, - wait_for: base.ready(), + wait_for: ready, }, [{ content: "click on Add a language", trigger: '.js_language_selector a:has(i.fa)', @@ -59,10 +65,10 @@ tour.register('rte_translator', { content: "save", trigger: 'button[data-action=save]', extra_trigger: '#wrap p:first b', - }, { content : "click language dropdown", trigger : '.js_language_selector .dropdown-toggle', + extra_trigger: 'body:not(.o_wait_reload):not(:has(.note-editor))', }, { content: "click on french version", trigger: '.js_language_selector a[data-lang="fr_BE"]', @@ -81,14 +87,17 @@ tour.register('rte_translator', { trigger: '#wrap p font:first', run: function (action_helper) { action_helper.text('translated french text'); - this.$anchor.trigger('keydown'); + Wysiwyg.setRange(this.$anchor.contents()[0], 22); + this.$anchor.trigger($.Event( "keyup", {key: '_', keyCode: 95})); }, }, { content: "translate text with special char", trigger: '#wrap input + p span:first', run: function (action_helper) { action_helper.click(); - this.$anchor.prepend('<{translated}>').trigger('keydown'); + this.$anchor.prepend('<{translated}>'); + Wysiwyg.setRange(this.$anchor.contents()[0], 0); + this.$anchor.trigger($.Event( "keyup", {key: '_', keyCode: 95})); }, }, { content: "click on input", @@ -125,9 +134,12 @@ tour.register('rte_translator', { trigger : '.js_language_selector .dropdown-toggle', }, { + content: "open language selector", + trigger: '.js_language_selector button:first', + extra_trigger: 'html[lang*="fr"]:not(:has(#wrap p span))', +}, { content: "return to english version", trigger: '.js_language_selector a[data-lang="en_US"]', - extra_trigger: 'html[lang*="fr"]:not(:has(#wrap p span))', }, { content: "edit english version", trigger: 'a[data-action=edit]', @@ -139,27 +151,25 @@ tour.register('rte_translator', { run: function (action_helper) { action_helper.click(); var el = this.$anchor[0]; - var evt = document.createEvent("MouseEvents"); - $.summernote.core.range.create(el.childNodes[2], 6, el.childNodes[2], 13).select(); - evt.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); - el.dispatchEvent(evt); + this.$anchor.trigger('mousedown'); + Wysiwyg.setRange(el.childNodes[2], 6, el.childNodes[2], 13); + this.$anchor.trigger('mouseup'); }, }, { content: "underline", - trigger: '.note-air-popover button[data-event="underline"]', + trigger: 'button.note-btn-underline', }, { content: "save new change", trigger: 'button[data-action=save]', - extra_trigger: '#wrap p u', + extra_trigger: '#wrap.o_dirty p u', }, { content : "click language dropdown", trigger : '.js_language_selector .dropdown-toggle', - + extra_trigger: '#wrap p u', }, { content: "return in french", - trigger: 'html[lang="en-US"] .js_language_selector a[data-lang="fr_BE"]', - extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))', + trigger : 'html[lang="en-US"] .js_language_selector .js_change_lang[data-lang="fr_BE"]', }, { content: "check bis: content is translated", trigger: '#wrap p font:first:contains(translated french text)', diff --git a/addons/website/static/src/js/widgets/theme.js b/addons/website/static/src/js/widgets/theme.js index 46a3ddcccf3..2de299b55c0 100644 --- a/addons/website/static/src/js/widgets/theme.js +++ b/addons/website/static/src/js/widgets/theme.js @@ -3,9 +3,10 @@ odoo.define('website.theme', function (require) { var config = require('web.config'); var core = require('web.core'); -var ColorpickerDialog = require('web.colorpicker'); +var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog'); var Dialog = require('web.Dialog'); -var widgets = require('web_editor.widget'); +var weContext = require('web_editor.context'); +var widgets = require('wysiwyg.widgets'); var websiteNavbarData = require('website.navbar'); var _t = core._t; @@ -43,7 +44,7 @@ var ThemeCustomizeDialog = Dialog.extend({ templateDef = this._rpc({ model: 'ir.ui.view', method: 'read_template', - args: ['website.theme_customize'], + args: ['website.theme_customize', weContext.get()], }).then(function (data) { return core.qweb.add_template(data); }); @@ -285,6 +286,7 @@ var ThemeCustomizeDialog = Dialog.extend({ data[1], '#wrapwrap { background-image: url("' + src + '"); }', '//style', + weContext.get(), ], }); }).then(function () { @@ -472,7 +474,7 @@ var ThemeCustomizeDialog = Dialog.extend({ }); var colors = {}; - colors[colorName] = ev.data.cssColor; + colors[colorName] = ev.data.hex; if (colorName === 'alpha') { colors['beta'] = 'null'; colors['gamma'] = 'null'; diff --git a/addons/website/static/src/scss/website.editor.ui.scss b/addons/website/static/src/scss/website.editor.ui.scss new file mode 100644 index 00000000000..00de5fc2e27 --- /dev/null +++ b/addons/website/static/src/scss/website.editor.ui.scss @@ -0,0 +1,812 @@ +// TOP BAR (EDIT) +#web_editor-top-edit { + height: $o-navbar-height; + background-color: $o-we-color-dark; + z-index: $zindex-modal - 9; + position: fixed; + top: 0px; + right: 0px; + width: auto; + white-space: nowrap; + + background-color: rgba(0, 0, 0, 0); + transition: background-color 400ms $o-we-md-ease 0s; + font-family: $o-we-font-family; + + form.navbar-form { + height: $o-navbar-height; + z-index: 1060; + margin: 0; + padding: 0; + @include o-position-absolute($top: -1px, $right: -$o-we-sidebar-width); + transition: right 0.4s $o-we-md-ease 0s; + border-left: 1px solid $o-we-color-divider; + background-color: inherit; + + .btn-group { + height: 100%; + } + + .btn { + height: 100%; + margin: 0; + padding: 10px; + line-height: 1.2; + font-size: 13px; + font-family: $o-we-font-family; + + transition: all 0.3s ease 0s; + border: none; + border-radius: 0; + + .fa { + margin-right: $grid-gutter-width/4; + + &.fa-times { + color: $o-we-color-danger; + } + } + + &.btn-primary { + @include button-variant($o-brand-odoo, $o-brand-odoo); + } + + &.btn-secondary { + @include button-variant($o-we-color-dark, $o-we-color-dark); + + &:hover { + background: rgba(black, 0.5); + } + } + + &:focus, + &:active, + &:focus:active { + outline: none; + } + } + + .dropdown-menu { + left: auto; + right: 0; + } + } +} + +// Translations +.oe_translate_examples li { + margin: 10px; + padding: 4px; +} + +html[lang]>body.editor_enable [data-oe-translation-state] { + background: rgba($o-we-content-to-translate-color, 0.5) !important; + + &[data-oe-translation-state="translated"] { + background: rgba($o-we-translated-content-color, 0.5) !important; + } + + &.o_dirty { + background: rgba($o-we-translated-content-color, 0.25) !important; + } +} + +// NOTE EDITOR + +// SNIPPET PANEL +#oe_snippets { + @include o-w-preserve-btn; + @include o-position-absolute(0, auto, 0, -$o-we-sidebar-width); + width: $o-we-sidebar-width; + + border-right: 1px solid $o-we-color-divider; + transition: left 400ms $o-we-md-ease 0s; + background-image: linear-gradient(45deg, $o-we-color-normal, darken($o-we-color-normal, 10%)); + + #snippets_menu { + color: #999999; + background: $o-we-color-dark; + font-family: $o-we-font-family; + height: $o-navbar-height; + line-height: $o-navbar-height; + } + + #o_scroll { + .o_panel_header { + color: #999999; + } + } +} + +.oe_snippet { + background-color: $o-we-color-normal; + + .oe_snippet_thumbnail { + .oe_snippet_thumbnail_img { + border: 1px solid $o-we-color-normal; + } + + img.oe_snippet_thumbnail_img {} + + .oe_snippet_thumbnail_title { + border: 1px solid $o-we-color-dark; + color: $o-we-color-text-light; + } + + &:hover .oe_snippet_thumbnail_title { + color: $o-we-color-text-lighter; + } + } +} + +// SNIPPET OPTIONS +.colorpicker { + .o_colorpicker_sections { + .note-color-palette>div { + color: $o-we-color-text-light; + } + } + + .o_colorpicker_section { + &:after { + content: ""; + display: table; + clear: both; + } + + >button { + + &:hover, + &.selected { + box-shadow: 0px 0px 2px 2px $o-we-color-light; + } + + &[data-event="foreColor"] { + background-color: $o-we-color-normal; + + &:before { + background-color: rgba(white, 0.3); + } + } + } + } + + .note-palette-title { + color: $o-we-color-text-light; + } + + .palette-reset { + .note-color-reset { + color: desaturate(rgba($o-we-color-danger, 0.6), 40%); + + &:hover { + color: $o-we-color-danger; + } + } + } +} + +// DROPZONES +#wrapwrap .oe_drop_zone { + &.oe_insert { + height: $o-we-dropzone-size; + margin: (-$o-we-dropzone-size/2) 0; + + &:after { + border-bottom: $o-we-dropzone-border; + } + + &.oe_vertical { + width: $o-we-dropzone-size; + margin: 0 (-$o-we-dropzone-size/2); + + &:after { + border-right: $o-we-dropzone-border; + } + } + } +} + +// MANIPULATORS +#oe_manipulators { + + // SNIPPET MANIPULATORS + .oe_overlay { + + // OVERLAY OPTIONS + >.oe_overlay_options { + font-family: $o-we-font-family; + + >.btn-group { + .btn { + width: $o-we-overlay-option-size; + height: $o-we-overlay-option-size; + border: 1px solid $o-we-color-dark; + line-height: #{$o-we-overlay-option-size - 2px}; + + color: $o-we-color-text-light; + background-color: $o-we-color-light; + + &:hover { + background-color: lighten($o-we-color-dark, 10%); + } + + &.oe_snippet_remove { + background-color: $o-we-color-danger; + border-color: darken($o-we-color-danger, 20%); + + &:hover { + background-color: darken($o-we-color-danger, 20%); + } + } + + >.fa { + color: $o-we-color-text-light; + } + } + + // CUSTOMIZE MENU BUTTON + >.oe_options { + .btn { + background-color: $o-we-color-dark; + color: $o-we-color-text-light; + + &:hover, + &:active, + &:focus { + background-color: lighten($o-we-color-dark, 5%); + border-color: lighten($o-we-color-dark, 10%); + } + + &:before, + &:after { + background-color: $o-we-color-text-light; + } + } + + // Open menu + &.show .btn { + // background-color: $o-we-color-light; + } + + // CUSTOMIZE MENU + .dropdown-menu { + border: 1px solid $o-we-color-dark; + background-color: $o-we-color-light; + + .dropdown-item { + color: $o-we-color-text-light; + + &:hover { + background-color: fade-out($o-we-color-dark, 0.5); + } + + &.active { + background-color: fade-out($o-we-color-dark, 0.5); + } + } + + .dropdown-header { + color: wheat; + + &.o_parent_editor_header { + color: wheat; + } + } + + .dropdown-submenu { + &::before { + border-left-color: $o-we-color-text-light; + } + } + } + } + } + } + + &.o_top_cover>.oe_overlay_options { + top: $o-we-handle-border-width; + right: $o-we-handle-border-width; + } + + } +} + +// Animations +@keyframes fadeInDownSmall { + 0% { + opacity: 0; + transform: translate(0, -5px); + } + + 100% { + opacity: 1; + transform: translate(0, 0); + } +} + +@keyframes fadeInOut { + 0% { + opacity: 0; + } + + 25% { + opacity: 1; + } + + 75% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +@keyframes inputHighlighter { + from { + background: $o-brand-primary; + } + + to { + width: 0; + background: transparent; + } +} + +// INPUTS +.o_switch { + display: flex; + align-items: center; + font-weight: normal; + cursor: pointer; + + >input { + display: none; + + +span { + background-color: $o-we-switch-inactive-color; + box-shadow: inset 0 0 0px 1px darken($o-we-switch-inactive-color, 10%); + border-radius: 100rem; + height: $o-we-switch-size; + width: $o-we-switch-size * 1.8; + margin-right: 0.5em; + display: inline-block; + transition: all 0.3s $o-we-md-ease; + + &:after { + content: ""; + background: $o-we-color-paper; + display: block; + width: $o-we-switch-size - 0.2; + height: $o-we-switch-size - 0.2; + margin-top: 0.1ex; + margin-left: 0.1ex; + border-radius: 100rem; + transition: all 0.3s $o-we-md-ease; + box-shadow: 0 1px 1px darken($o-we-switch-inactive-color, 35%), inset 0 0 0 1px lighten($o-we-switch-inactive-color, 10%); + } + } + + &:checked+span { + box-shadow: none; + background-image: linear-gradient(0deg, $o-we-color-success, darken($o-we-color-success, 10%)); + + &:after { + margin-left: ($o-we-switch-size*1.8 - $o-we-switch-size) + 0.1; + } + } + } + + &.o_switch_danger { + >input { + &:not(:checked)+span { + box-shadow: none; + background-image: linear-gradient(0deg, lighten(theme-color('danger'), 5%), darken(theme-color('danger'), 5%)); + } + } + } +} + +// ACE EDITOR +.o_ace_view_editor { + background: $o-we-ace-color; + color: white; + display: flex; + flex-flow: column nowrap; + opacity: 0.97; + + .o_ace_view_editor_title { + flex: 0 0 auto; + display: flex; + align-items: center; + padding: $grid-gutter-width/4; + + >.o_ace_type_switcher>button::after { + @include o-caret-down; + margin-left: 4px; + } + + >* { + flex: 0 0 auto; + margin: 0 $grid-gutter-width/4; + + &.o_include_option { + display: flex; + align-items: center; + font-size: 11px; + + >.custom-control { + margin-right: $grid-gutter-width/4; + } + } + + &.o_res_list { + flex: 1 1 auto; + min-width: 60px; + } + } + } + + #ace-view-id { + flex: 0 0 auto; + padding: $grid-gutter-width/4 $grid-gutter-width/2; + background-color: lighten($o-we-ace-color, 10%); + } + + #ace-view-editor { + @mixin ace-line-error-mixin { + content: ""; + z-index: 1000; + display: block; + background-color: theme-color('danger'); + opacity: 0.5; + pointer-events: none; + } + + height: 70%; // in case flex is not supported + flex: 1 1 auto; + + .ace_gutter { + cursor: ew-resize; + + .ace_gutter-cell.o_error { + position: relative; + + &::after { + @include o-position-absolute(-100%, 0, -100%, 0); + @include ace-line-error-mixin; + } + } + } + + .ace_resize_bar { + @include o-position-absolute($right: 0); + width: 25px; + height: 100%; + cursor: ew-resize; + } + + .ace_scroller.o_error::after { + @include o-position-absolute(0, auto, 0, 0); + width: 3px; + @include ace-line-error-mixin; + } + } +} + +.o_ace_select2_dropdown { + width: auto !important; + padding-top: 4px; + font-family: monospace !important; + + >.select2-results { + max-height: none; + max-height: 70vh; + + .select2-result-label { + padding-top: 1px; + padding-bottom: 2px; + + >.o_ace_select2_result { + padding: 0; + font-size: 12px; + white-space: nowrap; + } + } + } +} + +// MODALS +body .modal { + + // BACKGROUND IMAGE OPTIONS + .o_bg_img_opt_modal .o_bg_img_opt { + margin: 15px 0; + + .help-control { + @include o-position-absolute(15px, 15px); + } + + .help { + margin: 15px 0; + + .simulator { + position: relative; + float: left; + margin-right: 15px; + width: 120px; + height: 100px; + + .bg { + @include o-position-absolute(0, 0, 0, 0); + border: 1px dotted #888787; + background-image: url(/web/image); + background-position: center center; + background-size: cover; + } + + .el { + @include o-position-absolute(10px, 0, 10px, 0); + border: 1px solid #222; + } + + &.contain_bg { + .bg { + @include o-position-absolute(0, 12px, 20px, 12px); + } + + .el { + @include o-position-absolute(0, 0, 20px, 0); + } + } + } + + +* { + clear: left; + } + } + + .o_bg_img_opt_cover_edition { + @include o-we-preview-box; + padding: 0 25px 25px 25px; + + h6 { + color: white; + font-weight: bold; + } + + .o_bg_img_opt_object { + position: relative; + background: white; + min-height: 10px; + @include o-we-preview-content; + + >img { + cursor: crosshair; + border-top: 1px solid #5A5A5A; + border-bottom: 1px solid #111; + } + + &:hover .o_focus_point:before { + opacity: 0.5; + } + } + + .o_bg_img_opt_ui_info { + animation: fadeInOut 2s ease forwards; + @include o-position-absolute($bottom: 1px, $right: 0); + display: block; + padding: 3px 13px; + background-color: rgba(255, 255, 255, 0.8); + color: #333; + text-align: center; + font-weight: bold; + pointer-events: none; + + span { + font-weight: normal; + } + + .o_x { + margin-right: 10px + } + } + + .grid { + @include o-position-absolute($top: 33.33%, $left: 0); + display: block; + width: 100%; + height: 1px; + background: fade-out(lighten($o-brand-primary, 30%), 0.5); + pointer-events: none; + + &.grid-2 { + top: 66.66% + } + + &.grid-3 { + top: 0; + left: 33.33%; + width: 1px; + height: 100% + } + + &.grid-4 { + top: 0; + left: 66.66%; + width: 1px; + height: 100% + } + } + + .o_focus_point { + @include o-position-absolute($top: 0, $left: 0); + width: 30px; + height: 30px; + border: 2px solid white; + border-radius: 100%; + margin-top: -15px; + margin-left: -15px; + box-shadow: 0 0 1px #333; + pointer-events: none; + + &.o_with_transition { + transition: all 0.2s ease 0s; + } + + &:before { + pointer-events: none; + content: ""; + display: block; + width: 100px; + height: 100px; + margin-top: -37px; + margin-left: -37px; + border: 1px solid #EAEAEA; + background: rgba(247, 76, 76, 0); + border-radius: 100em; + opacity: 1; + box-shadow: 0 0 0 100em rgba(0, 0, 0, 0.33); + transition: opacity 0.2s ease 0s; + } + } + } + } +} + +// editor_enable + +body.editor_enable { + overflow: hidden; + padding-top: 0 !important; + + #web_editor-top-edit { + background-color: $o-we-color-dark; + + form.navbar-form { + right: 0; + } + } + + #wrapwrap { + height: calc(100vh - #{$o-navbar-height}); + padding-top: 0px; + + .btn { + -webkit-user-select: none; + } + } +} + +.wysiwyg_multizone.note-editor { + &>.note-toolbar-wrapper .note-toolbar { + height: $o-navbar-height; + + .btn { + height: $o-navbar-height; + } + } + + &>.note-statusbar { + display: none; + } + + &>.note-editing-area { + overflow: visible; + } +} + +.wysiwyg_multizone.note-popover.popover { + border-width: 0 1px; + + .popover-content .btn { + height: $o-navbar-height; + } +} + +.wysiwyg_multizone.note-popover, +.wysiwyg_multizone.note-editor>.note-toolbar-wrapper .note-toolbar { + background-color: $o-we-color-dark; + + .btn { + background-color: $o-we-color-dark; + color: $o-we-color-text-normal; + border: none; + padding: 0.0625rem 0.3125rem !important; // force this because of themes + + >.fa { + color: $o-we-color-text-normal; + } + + &:hover { + color: $o-we-color-text-light; + + >.fa { + color: $o-we-color-text-light; + } + } + + &.active { + background-color: $o-we-color-dark; + color: $o-we-color-text-light; + + >.fa { + color: $o-we-color-text-light; + } + } + } + + >.btn-group { + margin-top: 0; + + &.show, + .btn-group.show { + .dropdown-toggle { + border-top: 2px solid white; + background-color: $o-we-color-darker; + color: white; + box-shadow: none; + + >.fa, + >span { + color: white; + } + } + } + } + + .dropdown-menu { + margin: 0; + border-radius: 0; + background-color: $o-we-color-darker; + color: $o-we-color-text-normal; + white-space: normal; + + .dropdown-item { + width: 100%; + color: $o-we-color-text-light; + + >.fa { + color: $o-we-color-text-light; + } + + &:hover { + background-color: $o-we-tab-active-bg; + color: white; + + >.fa { + color: white; + } + } + } + } + + .note-para .dropdown-menu { + min-width: 147px; + } + + .note-style .dropdown-item * { + color: $o-we-color-text-light; + } +} diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index 21ee1c74f73..3b7091b0e3c 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -194,8 +194,7 @@ font[class*='bg-'] { // Probably outdated // Lists -.o_ul_toggle_self, -.o_ul_toggle_next { +.o_ul_toggle { display: none; } @@ -979,7 +978,8 @@ table.table_desc tr td { // // Header -#wrapwrap .o_header_affix { +.o_header_affix { + display: block; @include o-position-absolute(0, 0, auto, 0); position: fixed; z-index: $zindex-modal - 20; diff --git a/addons/website/static/src/xml/translator.xml b/addons/website/static/src/xml/translator.xml new file mode 100644 index 00000000000..d3067f5e699 --- /dev/null +++ b/addons/website/static/src/xml/translator.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> +<div t-name="website.TranslatorInfoDialog"> + <p>You are about to enter the translation mode.</p> + <p>Here are the visuals used to help you translate efficiently:</p> + <ul class="oe_translate_examples"> + <li data-oe-translation-state="to_translate">Content to translate</li> + <li data-oe-translation-state="translated">Translated content</li> + </ul> + <p> + In this mode, you can only translate texts. To change the structure of the page, you must edit the master page. + Each modification on the master page is automatically applied to all translated versions. + </p> +</div> +</templates> diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml index a9ed7b66601..552c0b26ada 100644 --- a/addons/website/static/src/xml/website.editor.xml +++ b/addons/website/static/src/xml/website.editor.xml @@ -10,6 +10,23 @@ <p class="text-muted">Your current changes will be saved automatically.</p> </div> + <!-- Editor top bar which contains the summernote tools and save/discard buttons --> + <t t-name="website.editorbar"> + <div id="web_editor-top-edit"> + <form class="navbar-form text-muted"> + <button type="button" class="btn btn-secondary" data-action="cancel"><i class="fa fa-times"/> Discard</button> + <button type="button" class="btn btn-primary" data-action="save"><i class="fa fa-floppy-o"/> Save</button> + </form> + </div> + </t> + <!-- Custom checkbox (material-design-like toggle) --> + <t t-name="website.components.switch"> + <label class="o_switch" t-att-for="id"> + <input type="checkbox" t-att-id="id"/> + <span/> + <t t-if="label"><t t-esc="label"/></t> + </label> + </t> <div t-name="website.theme_customize_modal_layout" class="d-flex align-items-start"> <ul class="nav flex-column flex-shrink-0 w-25"/> |
