From 85990768592cbdefbb178b5ffa38c1e29b9eeb87 Mon Sep 17 00:00:00 2001 From: Younn Olivier Date: Tue, 14 Jun 2022 10:12:40 +0200 Subject: [PATCH] [IMP] web_editor, website, *: use new media dialog *: mass_mailing See merge commit for more information. task-2687506 --- .../static/src/snippets/s_rating/options.js | 32 +++---- .../components/media_dialog/media_dialog.js | 53 ++++++++++- .../components/media_dialog/search_media.js | 9 +- .../static/src/js/editor/snippets.options.js | 39 ++++---- .../static/src/js/wysiwyg/widgets/widgets.js | 2 - .../static/src/js/wysiwyg/wysiwyg.js | 93 ++++++++----------- .../src/snippets/s_image_gallery/options.js | 36 ++++--- .../static/src/snippets/s_rating/options.js | 33 +++---- 8 files changed, 170 insertions(+), 127 deletions(-) diff --git a/addons/mass_mailing/static/src/snippets/s_rating/options.js b/addons/mass_mailing/static/src/snippets/s_rating/options.js index ed6e493bccc..fc798052a5c 100644 --- a/addons/mass_mailing/static/src/snippets/s_rating/options.js +++ b/addons/mass_mailing/static/src/snippets/s_rating/options.js @@ -1,7 +1,8 @@ odoo.define('mass_mailing.s_rating_options', function (require) { 'use strict'; -const weWidgets = require('wysiwyg.widgets'); +const { ComponentWrapper } = require('web.OwlCompatibility'); +const { MediaDialogWrapper } = require('@web_editor/components/media_dialog/media_dialog'); const options = require('web_editor.snippets.options'); options.registry.Rating = options.Class.extend({ @@ -37,16 +38,15 @@ options.registry.Rating = options.Class.extend({ * @see this.selectClass for parameters */ customIcon: async function (previewMode, widgetValue, params) { - return new Promise(resolve => { - const dialog = new weWidgets.MediaDialog( - this, - {noImages: true, noDocuments: true, noVideos: true, mediaWidth: 1920}, - $('') - ); - this._saving = false; - dialog.on('save', this, function (attachments) { - this._saving = true; - const customClass = 'fa ' + attachments.className; + const media = document.createElement('i'); + media.className = params.customActiveIcon === 'true' ? this.faClassActiveCustomIcons : this.faClassInactiveCustomIcons; + const dialog = new ComponentWrapper(this, MediaDialogWrapper, { + noImages: true, + noDocuments: true, + noVideos: true, + media, + save: icon => { + const customClass = icon.className; const $activeIcons = this.$target.find('.s_rating_active_icons > i'); const $inactiveIcons = this.$target.find('.s_rating_inactive_icons > i'); const $icons = params.customActiveIcon === 'true' ? $activeIcons : $inactiveIcons; @@ -57,15 +57,9 @@ options.registry.Rating = options.Class.extend({ this.$target[0].dataset.inactiveCustomIcon = this.faClassInactiveCustomIcons; this.$target[0].dataset.icon = 'custom'; this.iconType = 'custom'; - resolve(); - }); - dialog.on('closed', this, function () { - if (!this._saving) { - resolve(); - } - }); - dialog.open(); + } }); + dialog.mount(document.body); }, /** * Sets the number of active icons. diff --git a/addons/web_editor/static/src/components/media_dialog/media_dialog.js b/addons/web_editor/static/src/components/media_dialog/media_dialog.js index a8093cd2250..82df7ee24d9 100644 --- a/addons/web_editor/static/src/components/media_dialog/media_dialog.js +++ b/addons/web_editor/static/src/components/media_dialog/media_dialog.js @@ -1,13 +1,14 @@ /** @odoo-module **/ import { useService } from '@web/core/utils/hooks'; +import { useWowlService } from '@web/legacy/utils'; import { Dialog } from '@web/core/dialog/dialog'; import { ImageSelector } from './image_selector'; import { DocumentSelector } from './document_selector'; import { IconSelector } from './icon_selector'; import { VideoSelector } from './video_selector'; -const { useState } = owl; +const { Component, useState, onRendered, xml } = owl; export const TABS = { IMAGES: { @@ -45,6 +46,8 @@ export class MediaDialog extends Dialog { this.tabs = []; this.selectedMedia = useState({}); + this.initialIconClasses = []; + this.addTabs(); this.state = useState({ @@ -110,6 +113,7 @@ export class MediaDialog extends Dialog { if (mediaFont) { const selectedIcon = mediaFont.icons.find(icon => icon.names.some(name => classes.includes(name))); if (selectedIcon) { + this.initialIconClasses.push(...selectedIcon.names); this.selectMedia(selectedIcon, TABS.ICONS.id); } } @@ -139,7 +143,29 @@ export class MediaDialog extends Dialog { async save() { const selectedMedia = this.selectedMedia[this.state.activeTab]; if (selectedMedia.length) { - const elements = await TABS[this.state.activeTab].Component.createElements(selectedMedia); + const elements = await TABS[this.state.activeTab].Component.createElements(selectedMedia, { rpc: this.rpc, orm: this.orm }); + elements.forEach(element => { + if (this.props.media) { + element.classList.add(...this.props.media.classList); + const style = this.props.media.getAttribute('style'); + if (style) { + element.setAttribute('style', style); + } + if (this.props.media.dataset.shape) { + element.dataset.shape = this.props.media.dataset.shape; + } + if (this.props.media.dataset.shapeColors) { + element.dataset.shapeColors = this.props.media.dataset.shapeColors; + } + } + element.classList.add(...TABS[this.state.activeTab].Component.mediaSpecificClasses); + for (const otherTab of Object.keys(TABS).filter(key => key !== this.state.activeTab)) { + element.classList.remove(...TABS[otherTab].Component.mediaSpecificClasses); + } + element.classList.remove(...this.initialIconClasses); + element.classList.remove('o_modified_image_to_save'); + element.classList.remove('oe_edited_link'); + }); if (this.props.multiImages) { this.props.save(elements); } else { @@ -151,6 +177,29 @@ export class MediaDialog extends Dialog { } MediaDialog.bodyTemplate = 'web_editor.MediaDialogBody'; MediaDialog.footerTemplate = 'web_editor.MediaDialogFooter'; +MediaDialog.defaultProps = { + useMediaLibrary: true, +}; MediaDialog.components = { ...Object.keys(TABS).map(key => TABS[key].Component), }; + +export class MediaDialogWrapper extends Component { + setup() { + this.dialogs = useWowlService('dialog'); + + onRendered(() => { + this.dialogs.add(MediaDialog, { + ...this.props, + close: () => { + if (this.props.close) { + this.props.close(); + } + this.destroy(); + + }, + }); + }); + } +} +MediaDialogWrapper.template = xml``; diff --git a/addons/web_editor/static/src/components/media_dialog/search_media.js b/addons/web_editor/static/src/components/media_dialog/search_media.js index 0b99df7f1f7..ab05905bebb 100644 --- a/addons/web_editor/static/src/components/media_dialog/search_media.js +++ b/addons/web_editor/static/src/components/media_dialog/search_media.js @@ -1,12 +1,15 @@ /** @odoo-module **/ -import { throttle } from '@web/core/utils/timing'; +import { debounce } from '@web/core/utils/timing'; -const { Component, xml } = owl; +const { Component, onWillUnmount, xml } = owl; export class SearchMedia extends Component { setup() { - this.search = throttle((ev) => this.props.search(ev.target.value), 1000); + this.search = debounce((ev) => this.props.search(ev.target.value), 1000); + onWillUnmount(() => { + this.search.cancel(); + }); } } SearchMedia.template = xml` diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 739a8f4a372..a7e6e6123a5 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -1,6 +1,8 @@ odoo.define('web_editor.snippets.options', function (require) { 'use strict'; +const { ComponentWrapper } = require('web.OwlCompatibility'); +const { MediaDialogWrapper } = require('@web_editor/components/media_dialog/media_dialog'); var core = require('web.core'); const {ColorpickerWidget} = require('web.Colorpicker'); const Dialog = require('web.Dialog'); @@ -1650,10 +1652,10 @@ const MediapickerUserValueWidget = UserValueWidget.extend({ * @param {boolean} [videos] whether videos should be available * default: false */ - _openDialog(el, {images = false, videos = false}) { + _openDialog(el, {images = false, videos = false, save}) { el.src = this._value; const $editable = this.$target.closest('.o_editable'); - const mediaDialog = new weWidgets.MediaDialog(this, { + const mediaDialogWrapper = new ComponentWrapper(this, MediaDialogWrapper, { noImages: !images, noVideos: !videos, noIcons: true, @@ -1663,8 +1665,10 @@ const MediapickerUserValueWidget = UserValueWidget.extend({ '312451183', '415226028', '367762632', '340475898', '374265101', '370467553'], 'res_model': $editable.data('oe-model'), 'res_id': $editable.data('oe-id'), - }, el).open(); - return mediaDialog; + save, + media: el, + }); + return mediaDialogWrapper.mount(this.el); }, //-------------------------------------------------------------------------- @@ -1703,13 +1707,15 @@ const ImagepickerUserValueWidget = MediapickerUserValueWidget.extend({ _onEditMedia(ev) { // Need a dummy element for the media dialog to modify. const dummyEl = document.createElement('img'); - const dialog = this._openDialog(dummyEl, {images: true}); - dialog.on('save', this, data => { - // Accessing the value directly through dummyEl.src converts the url to absolute, - // using getAttribute allows us to keep the url as it was inserted in the DOM - // which can be useful to compare it to values stored in db. - this._value = dummyEl.getAttribute('src'); - this._onUserValueChange(); + this._openDialog(dummyEl, { + images: true, + save: (media) => { + // Accessing the value directly through dummyEl.src converts the url to absolute, + // using getAttribute allows us to keep the url as it was inserted in the DOM + // which can be useful to compare it to values stored in db. + this._value = media.getAttribute('src'); + this._onUserValueChange(); + } }); }, }); @@ -1725,11 +1731,12 @@ const VideopickerUserValueWidget = MediapickerUserValueWidget.extend({ _onEditMedia(ev) { // Need a dummy element for the media dialog to modify. const dummyEl = document.createElement('iframe'); - const dialog = this._openDialog(dummyEl, {videos: true}); - dialog.on('save', this, data => { - this._value = data.bgVideoSrc; - this._onUserValueChange(); - }); + this._openDialog(dummyEl, { + videos: true, + save: (media) => { + this._value = media.querySelector('iframe').src; + this._onUserValueChange(); + }}); }, }); diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/widgets.js b/addons/web_editor/static/src/js/wysiwyg/widgets/widgets.js index e70fe9ec9fb..4cd99e20832 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/widgets.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/widgets.js @@ -3,7 +3,6 @@ odoo.define('wysiwyg.widgets', function (require) { var Dialog = require('wysiwyg.widgets.Dialog'); var AltDialog = require('wysiwyg.widgets.AltDialog'); -var MediaDialog = require('wysiwyg.widgets.MediaDialog'); var LinkDialog = require('wysiwyg.widgets.LinkDialog'); var LinkTools = require('wysiwyg.widgets.LinkTools'); var ImageCropWidget = require('wysiwyg.widgets.ImageCropWidget'); @@ -15,7 +14,6 @@ var media = require('wysiwyg.widgets.media'); return { Dialog: Dialog, AltDialog: AltDialog, - MediaDialog: MediaDialog, LinkDialog: LinkDialog, LinkTools: LinkTools, ImageCropWidget: ImageCropWidget, diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index 7475886e40c..ca7e05286ce 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -1,6 +1,9 @@ odoo.define('web_editor.wysiwyg', function (require) { 'use strict'; +const { ComponentWrapper } = require('web.OwlCompatibility'); +const { MediaDialogWrapper } = require('@web_editor/components/media_dialog/media_dialog'); +const { saveVideos, videoSpecificClasses } = require('@web_editor/components/media_dialog/video_selector'); const dom = require('web.dom'); const core = require('web.core'); const session = require('web.session'); @@ -37,7 +40,6 @@ const setSelection = OdooEditorLib.setSelection; const endPos = OdooEditorLib.endPos; var id = 0; -const faZoomClassRegex = RegExp('fa-[0-9]x'); const basicMediaSelector = 'img, .fa, .o_image, .media_iframe_video'; // (see isImageSupportedForStyle). const mediaSelector = basicMediaSelector.split(',').map(s => `${s}:not([data-oe-xpath])`).join(','); @@ -128,9 +130,13 @@ const Wysiwyg = Widget.extend({ } const getYoutubeVideoElement = async (url) => { - const videoWidget = new weWidgets.VideoWidget(this, undefined, {}); - const src = (await videoWidget.createVideoNode(url)).$video.attr('src'); - return videoWidget.getWrappedIframe(src)[0]; + const { embed_url: src } = await this._rpc({ + route: '/web_editor/video_url/data', + params: { video_url: url }, + }); + const [savedVideo] = saveVideos([{src}]); + savedVideo.classList.add(...videoSpecificClasses); + return savedVideo; }; this.odooEditor = new OdooEditor(this.$editable[0], Object.assign({ @@ -256,16 +262,9 @@ const Wysiwyg = Widget.extend({ this.$editable.on('dblclick', mediaSelector, function () { self.showTooltip = false; const $el = $(this); - let params = {node: $el}; + let params = {node: this}; $el.selectElement(); - if ($el.is('.fa')) { - // save layouting classes from icons to not break the page if you edit an icon - params.htmlClass = [...$el[0].classList].filter((className) => { - return !className.startsWith('fa') || faZoomClassRegex.test(className); - }).join(' '); - } - if (!$el.parent().hasClass('o_stars')) { self.openMediaDialog(params); } @@ -1166,14 +1165,7 @@ const Wysiwyg = Widget.extend({ */ openMediaDialog(params = {}, MediaDialog = weWidgets.MediaDialog) { const sel = this.odooEditor.document.getSelection(); - const fontawesomeIcon = getInSelection(this.odooEditor.document, '.fa'); - if (fontawesomeIcon && sel.toString().trim() === "") { - params.node = $(fontawesomeIcon); - // save layouting classes from icons to not break the page if you edit an icon - params.htmlClass = [...fontawesomeIcon.classList].filter((className) => { - return !className.startsWith('fa') || faZoomClassRegex.test(className); - }).join(' '); - } + if (!sel.rangeCount) { return; } @@ -1183,36 +1175,29 @@ const Wysiwyg = Widget.extend({ // selection when the modal is closed. const restoreSelection = preserveCursor(this.odooEditor.document); - const $node = $(params.node); - // We need to keep track of FA icon because media.js will _clear those classes - const wasFontAwesome = $node.hasClass('fa'); const $editable = $(OdooEditorLib.closestElement(range.startContainer, '.o_editable')); const model = $editable.data('oe-model'); const field = $editable.data('oe-field'); const type = $editable.data('oe-type'); - const mediaParams = Object.assign({ - res_model: model, - res_id: $editable.data('oe-id'), + this.mediaDialogWrapper = new ComponentWrapper(this, MediaDialogWrapper, { + resModel: model, + resId: $editable.data('oe-id'), domain: $editable.data('oe-media-domain'), - useMediaLibrary: field && (model === 'ir.ui.view' && field === 'arch' || type === 'html'), - }, this.options.mediaModalParams, params); - const mediaDialog = new MediaDialog(this, mediaParams, $node); - mediaDialog.open(); - - mediaDialog.on('save', this, this._onMediaDialogSave.bind(this, { - $node: $node, - wasFontAwesome: wasFontAwesome, - htmlClass: params.htmlClass, - restoreSelection: restoreSelection, - })); - mediaDialog.on('closed', this, function () { - // if the mediaDialog content has been saved - // the previous selection in not relevant anymore - if (mediaDialog.destroyAction !== 'save') { - restoreSelection(); - } + useMediaLibrary: !!(field && (model === 'ir.ui.view' && field === 'arch' || type === 'html')), + media: params.node, + save: this._onMediaDialogSave.bind(this, { + node: params.node, + restoreSelection: restoreSelection, + }), + close: () => restoreSelection(), + ...this.options.mediaModalParams, + ...params, }); + + // The wysiwyg can be instanciated inside an iframe. The dialog + // component is mounted on the global document. + return this.mediaDialogWrapper.mount(document.body); }, /** * Sets custom CSS Variables on the snippet menu element. @@ -1243,23 +1228,28 @@ const Wysiwyg = Widget.extend({ * @param {Element} element provided by the dialog */ _onMediaDialogSave: function (params, element) { + params.restoreSelection(); if (!element) { return; } - // restore saved html classes - if (params.htmlClass) { - element.className += " " + params.htmlClass; - } - params.restoreSelection(); - if (wysiwygUtils.isImg(params.$node[0]) || params.wasFontAwesome) { - params.$node.replaceWith(element); + + if (params.node) { + params.node.replaceWith(element); this.odooEditor.unbreakableStepUnactive(); this.odooEditor.historyStep(); - } else if (element) { + } else { const fragment = new DocumentFragment(); fragment.append(element); return this.odooEditor.execCommand('insertFragment', fragment); } + + if (this.snippetsMenu) { + this.snippetsMenu.activateSnippet($(element)).then(() => { + if (element.tagName === 'IMG') { + $(element).trigger('image_changed'); + } + }); + } }, //-------------------------------------------------------------------------- @@ -2222,7 +2212,6 @@ odoo.define('web_editor.widget', function (require) { 'use strict'; return { Dialog: require('wysiwyg.widgets.Dialog'), - MediaDialog: require('wysiwyg.widgets.MediaDialog'), LinkDialog: require('wysiwyg.widgets.LinkDialog'), }; }); diff --git a/addons/website/static/src/snippets/s_image_gallery/options.js b/addons/website/static/src/snippets/s_image_gallery/options.js index 8afb5c1f95a..74f68657a2c 100644 --- a/addons/website/static/src/snippets/s_image_gallery/options.js +++ b/addons/website/static/src/snippets/s_image_gallery/options.js @@ -1,8 +1,9 @@ odoo.define('website.s_image_gallery_options', function (require) { 'use strict'; +const { MediaDialogWrapper } = require('@web_editor/components/media_dialog/media_dialog'); +const { ComponentWrapper } = require('web.OwlCompatibility'); var core = require('web.core'); -var weWidgets = require('wysiwyg.widgets'); var options = require('web_editor.snippets.options'); var _t = core._t; @@ -87,29 +88,36 @@ options.registry.gallery = options.Class.extend({ addImages: function (previewMode) { const $images = this.$('img'); var $container = this.$('> .container, > .container-fluid, > .o_container_small'); - var dialog = new weWidgets.MediaDialog(this, {multiImages: true, onlyImages: true, mediaWidth: 1920}); - var lastImage = _.last(this._getImages()); - var index = lastImage ? this._getIndex(lastImage) : -1; - return new Promise(resolve => { - dialog.on('save', this, function (attachments) { - for (var i = 0; i < attachments.length; i++) { - $('', { + const lastImage = _.last(this._getImages()); + let index = lastImage ? this._getIndex(lastImage) : -1; + const dialog = new ComponentWrapper(this, MediaDialogWrapper, { + multiImages: true, + onlyImages: true, + save: images => { + let $newImageToSelect; + for (const image of images) { + const $img = $('', { class: $images.length > 0 ? $images[0].className : 'img img-fluid d-block ', - src: attachments[i].image_src, + src: image.src, 'data-index': ++index, - alt: attachments[i].description || '', + alt: image.alt || '', 'data-name': _t('Image'), style: $images.length > 0 ? $images[0].style.cssText : '', }).appendTo($container); + if (!$newImageToSelect) { + $newImageToSelect = $img; + } } - if (attachments.length > 0) { + if (images.length > 0) { this.mode('reset', this.getMode()); this.trigger_up('cover_update'); + // Triggers the re-rendering of the thumbnail + $newImageToSelect.trigger('image_changed'); + } - }); - dialog.on('closed', this, () => resolve()); - dialog.open(); + }, }); + dialog.mount(this.el); }, /** * Allows to change the number of columns when displaying images with a diff --git a/addons/website/static/src/snippets/s_rating/options.js b/addons/website/static/src/snippets/s_rating/options.js index b24d7daf9f6..782664233c8 100644 --- a/addons/website/static/src/snippets/s_rating/options.js +++ b/addons/website/static/src/snippets/s_rating/options.js @@ -1,7 +1,9 @@ odoo.define('website.s_rating_options', function (require) { 'use strict'; -const weWidgets = require('wysiwyg.widgets'); +const { ComponentWrapper } = require('web.OwlCompatibility'); +const { MediaDialogWrapper } = require('@web_editor/components/media_dialog/media_dialog'); + const options = require('web_editor.snippets.options'); options.registry.Rating = options.Class.extend({ @@ -37,16 +39,15 @@ options.registry.Rating = options.Class.extend({ * @see this.selectClass for parameters */ customIcon: async function (previewMode, widgetValue, params) { - return new Promise(resolve => { - const dialog = new weWidgets.MediaDialog( - this, - {noImages: true, noDocuments: true, noVideos: true, mediaWidth: 1920}, - $('') - ); - this._saving = false; - dialog.on('save', this, function (attachments) { - this._saving = true; - const customClass = 'fa ' + attachments.className; + const media = document.createElement('i'); + media.className = params.customActiveIcon === 'true' ? this.faClassActiveCustomIcons : this.faClassInactiveCustomIcons; + const dialog = new ComponentWrapper(this, MediaDialogWrapper, { + noImages: true, + noDocuments: true, + noVideos: true, + media, + save: icon => { + const customClass = icon.className; const $activeIcons = this.$target.find('.s_rating_active_icons > i'); const $inactiveIcons = this.$target.find('.s_rating_inactive_icons > i'); const $icons = params.customActiveIcon === 'true' ? $activeIcons : $inactiveIcons; @@ -57,15 +58,9 @@ options.registry.Rating = options.Class.extend({ this.$target[0].dataset.inactiveCustomIcon = this.faClassInactiveCustomIcons; this.$target[0].dataset.icon = 'custom'; this.iconType = 'custom'; - resolve(); - }); - dialog.on('closed', this, function () { - if (!this._saving) { - resolve(); - } - }); - dialog.open(); + } }); + dialog.mount(this.el); }, /** * Sets the number of active icons.