[IMP] web_editor, website, *: use new media dialog

*: mass_mailing

See merge commit for more information.

task-2687506
This commit is contained in:
Younn Olivier
2022-06-24 10:28:06 +02:00
committed by qsm-odoo
parent 7fd0698cf7
commit 8599076859
8 changed files with 170 additions and 127 deletions
@@ -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},
$('<i/>')
);
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.
@@ -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``;
@@ -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`
@@ -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();
}});
},
});
@@ -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,
@@ -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'),
};
});
@@ -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++) {
$('<img/>', {
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 = $('<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
@@ -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},
$('<i/>')
);
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.