odoo.define('wysiwyg.widgets.media', function (require) {
'use strict';
var concurrency = require('web.concurrency');
var core = require('web.core');
var Dialog = require('web.Dialog');
var dom = require('web.dom');
var fonts = require('wysiwyg.fonts');
var utils = require('web.utils');
var Widget = require('web.Widget');
var session = require('web.session');
const {removeOnImageChangeAttrs} = require('web_editor.image_processing');
const {getCSSVariableValue, DEFAULT_PALETTE} = require('web_editor.utils');
var QWeb = core.qweb;
var _t = core._t;
var MediaWidget = Widget.extend({
xmlDependencies: ['/web_editor/static/src/xml/wysiwyg.xml'],
/**
* @constructor
* @param {Element} media: the target Element for which we select a media
* @param {Object} options: useful parameters such as res_id, res_model,
* context, user_id, ...
*/
init: function (parent, media, options) {
this._super.apply(this, arguments);
this.media = media;
this.$media = $(media);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @todo comment
*/
clear: function () {
if (!this.media) {
return;
}
this._clear();
},
/**
* Saves the currently configured media on the target media.
*
* @abstract
* @returns {Promise}
*/
save: function () {},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @abstract
*/
_clear: function () {},
});
var SearchableMediaWidget = MediaWidget.extend({
events: _.extend({}, MediaWidget.prototype.events || {}, {
'input .o_we_search': '_onSearchInput',
}),
/**
* @constructor
*/
init: function () {
this._super.apply(this, arguments);
this._onSearchInput = _.debounce(this._onSearchInput, 500);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Finds and displays existing attachments related to the target media.
*
* @abstract
* @param {string} needle: only return attachments matching this parameter
* @returns {Promise}
*/
search: function (needle) {},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Renders thumbnails for the attachments.
*
* @abstract
* @returns {Promise}
*/
_renderThumbnails: function () {},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onSearchInput: function (ev) {
this.attachments = [];
this.search($(ev.currentTarget).val() || '').then(() => this._renderThumbnails());
this.hasSearched = true;
},
});
/**
* Let users choose a file, including uploading a new file in odoo.
*/
var FileWidget = SearchableMediaWidget.extend({
events: _.extend({}, SearchableMediaWidget.prototype.events || {}, {
'click .o_upload_media_button': '_onUploadButtonClick',
'change .o_file_input': '_onFileInputChange',
'click .o_upload_media_url_button': '_onUploadURLButtonClick',
'input .o_we_url_input': '_onURLInputChange',
'click .o_existing_attachment_cell': '_onAttachmentClick',
'click .o_existing_attachment_remove': '_onRemoveClick',
'click .o_load_more': '_onLoadMoreClick',
}),
existingAttachmentsTemplate: undefined,
IMAGE_MIMETYPES: ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png', 'image/svg+xml'],
NUMBER_OF_ATTACHMENTS_TO_DISPLAY: 30,
MAX_DB_ATTACHMENTS: 5,
/**
* @constructor
*/
init: function (parent, media, options) {
this._super.apply(this, arguments);
this._mutex = new concurrency.Mutex();
this.numberOfAttachmentsToDisplay = this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY;
this.options = _.extend({
mediaWidth: media && media.parentElement && $(media.parentElement).width(),
useMediaLibrary: true,
}, options || {});
this.attachments = [];
this.selectedAttachments = [];
this.libraryMedia = [];
this.selectedMedia = [];
this._onUploadURLButtonClick = dom.makeAsyncHandler(this._onUploadURLButtonClick);
},
/**
* @override
*/
start: function () {
var def = this._super.apply(this, arguments);
var self = this;
this.$urlInput = this.$('.o_we_url_input');
this.$form = this.$('form');
this.$fileInput = this.$('.o_file_input');
this.$uploadButton = this.$('.o_upload_media_button');
this.$addUrlButton = this.$('.o_upload_media_url_button');
this.$urlSuccess = this.$('.o_we_url_success');
this.$urlWarning = this.$('.o_we_url_warning');
this.$urlError = this.$('.o_we_url_error');
this.$errorText = this.$('.o_we_error_text');
// If there is already an attachment on the target, select by default
// that attachment if it is among the loaded images.
var o = {
url: null,
alt: null,
};
if (this.$media.is('img')) {
o.url = this.$media.attr('src');
} else if (this.$media.is('a.o_image')) {
o.url = this.$media.attr('href').replace(/[?].*/, '');
o.id = +o.url.match(/\/web\/content\/(\d+)/, '')[1];
}
return this.search('').then(async () => {
await this._renderThumbnails();
if (o.url) {
self._selectAttachement(_.find(self.attachments, function (attachment) {
return o.url === attachment.image_src;
}) || o);
}
return def;
});
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Saves the currently selected image on the target media. If new files are
* currently being added, delays the save until all files have been added.
*
* @override
*/
save: function () {
return this._mutex.exec(this._save.bind(this));
},
/**
* @override
*/
search: function (needle) {
this.needle = needle;
return this.fetchAttachments(this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY, 0);
},
/**
* @param {Number} number - the number of attachments to fetch
* @param {Number} offset - from which result to start fetching
*/
fetchAttachments: function (number, offset) {
return this._rpc({
model: 'ir.attachment',
method: 'search_read',
args: [],
kwargs: {
domain: this._getAttachmentsDomain(this.needle),
fields: ['name', 'mimetype', 'description', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src', 'image_width', 'image_height', 'original_id'],
order: [{name: 'id', asc: false}],
context: this.options.context,
// Try to fetch first record of next page just to know whether there is a next page.
limit: number + 1,
offset: offset,
},
}).then(attachments => {
this.attachments = this.attachments.slice();
Array.prototype.splice.apply(this.attachments, [offset, attachments.length].concat(attachments));
});
},
/**
* Computes whether there is content to display in the template.
*/
hasContent() {
return this.attachments.length;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_clear: function () {
this.media.className = this.media.className && this.media.className.replace(/(^|\s+)(o_image)(?=\s|$)/g, ' ');
},
/**
* Returns the domain for attachments used in media dialog.
* We look for attachments related to the current document. If there is a value for the model
* field, it is used to search attachments, and the attachments from the current document are
* filtered to display only user-created documents.
* In the case of a wizard such as mail, we have the documents uploaded and those of the model
*
* @private
* @params {string} needle
* @returns {Array} "ir.attachment" odoo domain.
*/
_getAttachmentsDomain: function (needle) {
var domain = this.options.attachmentIDs && this.options.attachmentIDs.length ? ['|', ['id', 'in', this.options.attachmentIDs]] : [];
var attachedDocumentDomain = [
'&',
['res_model', '=', this.options.res_model],
['res_id', '=', this.options.res_id|0]
];
// if the document is not yet created, do not see the documents of other users
if (!this.options.res_id) {
attachedDocumentDomain.unshift('&');
attachedDocumentDomain.push(['create_uid', '=', this.options.user_id]);
}
if (this.options.data_res_model) {
var relatedDomain = ['&',
['res_model', '=', this.options.data_res_model],
['res_id', '=', this.options.data_res_id|0]];
if (!this.options.data_res_id) {
relatedDomain.unshift('&');
relatedDomain.push(['create_uid', '=', session.uid]);
}
domain = domain.concat(['|'], attachedDocumentDomain, relatedDomain);
} else {
domain = domain.concat(attachedDocumentDomain);
}
domain = ['|', ['public', '=', true]].concat(domain);
domain = domain.concat(this.options.mimetypeDomain);
if (needle && needle.length) {
domain.push(['name', 'ilike', needle]);
}
if (!this.options.useMediaLibrary) {
domain.push('|', ['url', '=', false], '!', ['url', '=ilike', '/web_editor/shape/%']);
}
domain.push('!', ['name', '=like', '%.crop']);
domain.push('|', ['type', '=', 'binary'], '!', ['url', '=like', '/%/static/%']);
return domain;
},
/**
* @private
*/
_highlightSelected: function () {
var self = this;
this.$('.o_existing_attachment_cell.o_we_attachment_selected').removeClass("o_we_attachment_selected");
_.each(this.selectedAttachments, function (attachment) {
self.$('.o_existing_attachment_cell[data-id=' + attachment.id + ']')
.addClass("o_we_attachment_selected").css('display', '');
});
},
/**
* @private
* @param {object} attachment
*/
_handleNewAttachment: function (attachment) {
this.attachments = this.attachments.filter(att => att.id !== attachment.id);
this.attachments.unshift(attachment);
this._renderThumbnails();
this._selectAttachement(attachment);
},
/**
* @private
* @returns {Promise}
*/
_loadMoreImages: function (forceSearch) {
return this.fetchAttachments(10, this.numberOfAttachmentsToDisplay).then(() => {
this.numberOfAttachmentsToDisplay += 10;
if (!forceSearch) {
this._renderThumbnails();
return Promise.resolve();
} else {
return this.search(this.$('.o_we_search').val() || '');
}
});
},
/**
* Renders the existing attachments and returns the result as a string.
*
* @param {Object[]} attachments
* @returns {string}
*/
_renderExisting: function (attachments) {
return QWeb.render(this.existingAttachmentsTemplate, {
attachments: attachments,
widget: this,
});
},
/**
* @private
*/
_renderThumbnails: function () {
var attachments = this.attachments.slice(0, this.numberOfAttachmentsToDisplay);
// Render menu & content
this.$('.o_we_existing_attachments').replaceWith(
this._renderExisting(attachments)
);
this._highlightSelected();
// adapt load more
this.$('.o_we_load_more').toggleClass('d-none', !this.hasContent());
var noLoadMoreButton = this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY >= this.attachments.length;
var noMoreImgToLoad = this.numberOfAttachmentsToDisplay >= this.attachments.length;
this.$('.o_load_done_msg').toggleClass('d-none', noLoadMoreButton || !noMoreImgToLoad);
this.$('.o_load_more').toggleClass('d-none', noMoreImgToLoad);
},
/**
* @private
* @returns {Promise}
*/
_save: async function () {
// Create all media-library attachments.
const toSave = Object.fromEntries(this.selectedMedia.map(media => [
media.id, {
query: media.query || '',
is_dynamic_svg: !!media.isDynamicSVG,
}
]));
let mediaAttachments = [];
if (Object.keys(toSave).length !== 0) {
mediaAttachments = await this._rpc({
route: '/web_editor/save_library_media',
params: {
media: toSave,
},
});
}
const selected = this.selectedAttachments.concat(mediaAttachments).map(attachment => {
// Color-customize dynamic SVGs with the primary theme color
if (attachment.image_src && attachment.image_src.startsWith('/web_editor/shape/')) {
const colorCustomizedURL = new URL(attachment.image_src, window.location.origin);
colorCustomizedURL.searchParams.set('c1', getCSSVariableValue('o-color-1'));
attachment.image_src = colorCustomizedURL.pathname + colorCustomizedURL.search;
}
return attachment;
});
if (this.options.multiImages) {
return selected;
}
const img = selected[0];
if (!img || !img.id || this.$media.attr('src') === img.image_src) {
return this.media;
}
if (!img.public && !img.access_token) {
await this._rpc({
model: 'ir.attachment',
method: 'generate_access_token',
args: [[img.id]]
}).then(function (access_token) {
img.access_token = access_token[0];
});
}
if (img.image_src) {
var src = img.image_src;
if (!img.public && img.access_token) {
src += _.str.sprintf('?access_token=%s', img.access_token);
}
if (!this.$media.is('img')) {
// Note: by default the images receive the bootstrap opt-in
// img-fluid class. We cannot make them all responsive
// by design because of libraries and client databases img.
this.$media = $('', {class: 'img-fluid o_we_custom_image'});
this.media = this.$media[0];
}
this.$media.attr('src', src);
} else {
if (!this.$media.is('a')) {
$('.note-control-selection').hide();
this.$media = $('');
this.media = this.$media[0];
}
var href = '/web/content/' + img.id + '?';
if (!img.public && img.access_token) {
href += _.str.sprintf('access_token=%s&', img.access_token);
}
href += 'unique=' + img.checksum + '&download=true';
this.$media.attr('href', href);
this.$media.addClass('o_image').attr('title', img.name);
}
this.$media.attr('alt', img.alt || img.description || '');
var style = this.style;
if (style) {
this.$media.css(style);
}
// Remove image modification attributes
removeOnImageChangeAttrs.forEach(attr => {
delete this.media.dataset[attr];
});
// Add mimetype for documents
if (!img.image_src) {
this.media.dataset.mimetype = img.mimetype;
}
this.media.classList.remove('o_modified_image_to_save');
this.$media.trigger('image_changed');
return this.media;
},
/**
* @param {object} attachment
* @param {boolean} [save=true] to save the given attachment in the DOM and
* and to close the media dialog
* @private
*/
_selectAttachement: function (attachment, save, {type = 'attachment'} = {}) {
const possibleProps = {
'attachment': 'selectedAttachments',
'media': 'selectedMedia'
};
const prop = possibleProps[type];
if (this.options.multiImages) {
// if the clicked attachment is already selected then unselect it
// unless it was a save request (then keep the current selection)
const index = this[prop].indexOf(attachment);
if (index !== -1) {
if (!save) {
this[prop].splice(index, 1);
}
} else {
// if the clicked attachment is not selected, add it to selected
this[prop].push(attachment);
}
} else {
Object.values(possibleProps).forEach(prop => {
this[prop] = [];
});
// select the clicked attachment
this[prop] = [attachment];
}
this._highlightSelected();
if (save) {
this.trigger_up('save_request');
}
},
/**
* Updates the add by URL UI.
*
* @private
* @param {boolean} emptyValue
* @param {boolean} isURL
* @param {boolean} isImage
*/
_updateAddUrlUi: function (emptyValue, isURL, isImage) {
this.$addUrlButton.toggleClass('btn-secondary', emptyValue)
.toggleClass('btn-primary', !emptyValue)
.prop('disabled', !isURL);
this.$urlSuccess.toggleClass('d-none', !isURL);
this.$urlError.toggleClass('d-none', emptyValue || isURL);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onAttachmentClick: function (ev) {
const attachment = ev.currentTarget;
const {id: attachmentID, mediaId} = attachment.dataset;
if (attachmentID) {
const attachment = this.attachments.find(attachment => attachment.id === parseInt(attachmentID));
this._selectAttachement(attachment, !this.options.multiImages);
} else if (mediaId) {
const media = this.libraryMedia.find(media => media.id === parseInt(mediaId));
this._selectAttachement(media, !this.options.multiImages, {type: 'media'});
}
},
/**
* Handles change of the file input: create attachments with the new files
* and open the Preview dialog for each of them. Locks the save button until
* all new files have been processed.
*
* @private
* @returns {Promise}
*/
_onFileInputChange: function () {
return this._mutex.exec(this._addData.bind(this));
},
/**
* Uploads the files that are currently selected on the file input, which
* creates new attachments. Then inserts them on the media dialog and
* selects them. If multiImages is not set, also triggers up the
* save_request event to insert the attachment in the DOM.
*
* @private
* @returns {Promise}
*/
async _addData() {
let files = this.$fileInput[0].files;
if (!files.length) {
// Case if the input is emptied, return resolved promise
return;
}
var self = this;
var uploadMutex = new concurrency.Mutex();
// Upload the smallest file first to block the user the least possible.
files = _.sortBy(files, 'size');
_.each(files, function (file) {
// Upload one file at a time: no need to parallel as upload is
// limited by bandwidth.
uploadMutex.exec(function () {
return utils.getDataURLFromFile(file).then(function (result) {
return self._rpc({
route: '/web_editor/attachment/add_data',
params: {
'name': file.name,
'data': result.split(',')[1],
'res_id': self.options.res_id,
'res_model': self.options.res_model,
'width': 0,
'quality': 0,
},
}).then(function (attachment) {
self._handleNewAttachment(attachment);
});
});
});
});
return uploadMutex.getUnlockedDef().then(function () {
if (!self.options.multiImages && !self.noSave) {
self.trigger_up('save_request');
}
self.noSave = false;
});
},
/**
* @private
*/
_onRemoveClick: function (ev) {
var self = this;
ev.stopPropagation();
Dialog.confirm(this, _t("Are you sure you want to delete this file ?"), {
confirm_callback: function () {
var $a = $(ev.currentTarget).closest('.o_existing_attachment_cell');
var id = parseInt($a.data('id'), 10);
var attachment = _.findWhere(self.attachments, {id: id});
return self._rpc({
route: '/web_editor/attachment/remove',
params: {
ids: [id],
},
}).then(function (prevented) {
if (_.isEmpty(prevented)) {
self.attachments = _.without(self.attachments, attachment);
self.attachments.filter(at => at.original_id[0] === attachment.id).forEach(at => delete at.original_id);
if (!self.attachments.length) {
self._renderThumbnails(); //render the message and image if empty
} else {
$a.closest('.o_existing_attachment_cell').remove();
}
return;
}
self.$errorText.replaceWith(QWeb.render('wysiwyg.widgets.image.existing.error', {
views: prevented[id],
widget: self,
}));
});
}
});
},
/**
* @private
*/
_onURLInputChange: function () {
var inputValue = this.$urlInput.val();
var emptyValue = (inputValue === '');
var isURL = /^.+\..+$/.test(inputValue); // TODO improve
var isImage = _.any(['.gif', '.jpeg', '.jpe', '.jpg', '.png'], function (format) {
return inputValue.endsWith(format);
});
this._updateAddUrlUi(emptyValue, isURL, isImage);
},
/**
* @private
*/
_onUploadButtonClick: function () {
this.$fileInput.click();
},
/**
* @private
*/
_onUploadURLButtonClick: function () {
if (this.$urlInput.is('.o_we_horizontal_collapse')) {
this.$urlInput.removeClass('o_we_horizontal_collapse');
this.$addUrlButton.attr('disabled', 'disabled');
return;
}
return this._mutex.exec(this._addUrl.bind(this));
},
/**
* @private
* @returns {Promise}
*/
_addUrl: function () {
var self = this;
return this._rpc({
route: '/web_editor/attachment/add_url',
params: {
'url': this.$urlInput.val(),
'res_id': this.options.res_id,
'res_model': this.options.res_model,
},
}).then(function (attachment) {
self.$urlInput.val('');
self._onURLInputChange();
self._handleNewAttachment(attachment);
if (!self.options.multiImages) {
self.trigger_up('save_request');
}
});
},
/**
* @private
*/
_onLoadMoreClick: function () {
this._loadMoreImages();
},
/**
* @override
*/
_onSearchInput: function () {
this.attachments = [];
this.numberOfAttachmentsToDisplay = this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY;
this._super.apply(this, arguments);
},
});
/**
* Let users choose an image, including uploading a new image in odoo.
*/
var ImageWidget = FileWidget.extend({
template: 'wysiwyg.widgets.image',
existingAttachmentsTemplate: 'wysiwyg.widgets.image.existing.attachments',
events: Object.assign({}, FileWidget.prototype.events, {
'change input.o_we_show_optimized': '_onShowOptimizedChange',
'change .o_we_search_select': '_onSearchSelect',
}),
MIN_ROW_HEIGHT: 128,
/**
* @constructor
*/
init: function (parent, media, options) {
this.searchService = 'all';
options = _.extend({
accept: 'image/*',
mimetypeDomain: [['mimetype', 'in', this.IMAGE_MIMETYPES]],
}, options || {});
// Binding so we can add/remove it as an addEventListener
this._onAttachmentImageLoad = this._onAttachmentImageLoad.bind(this);
this._super(parent, media, options);
},
/**
* @override
*/
start: async function () {
await this._super(...arguments);
this.el.addEventListener('load', this._onAttachmentImageLoad, true);
},
/**
* @override
*/
destroy: function () {
this.el.removeEventListener('load', this._onAttachmentImageLoad, true);
return this._super(...arguments);
},
/**
* @override
*/
async fetchAttachments(number, offset) {
if (this.needle && this.searchService !== 'database') {
number = this.MAX_DB_ATTACHMENTS;
offset = 0;
}
const result = await this._super(number, offset);
// Color-substitution for dynamic SVG attachment
const primaryColor = getCSSVariableValue('o-color-1');
this.attachments.forEach(attachment => {
if (attachment.image_src.startsWith('/')) {
const newURL = new URL(attachment.image_src, window.location.origin);
// Set the main color of dynamic SVGs to o-color-1
if (attachment.image_src.startsWith('/web_editor/shape/')) {
newURL.searchParams.set('c1', primaryColor);
} else {
// Set height so that db images load faster
newURL.searchParams.set('height', 2 * this.MIN_ROW_HEIGHT);
}
attachment.thumbnail_src = newURL.pathname + newURL.search;
}
});
if (this.needle && this.options.useMediaLibrary) {
try {
const response = await this._rpc({
route: '/web_editor/media_library_search',
params: {
'query': this.needle,
'offset': this.libraryMedia.length,
},
});
const newMedia = response.media;
this.nbMediaResults = response.results;
this.libraryMedia.push(...newMedia);
} catch (e) {
// Either API endpoint doesn't exist or is misconfigured.
console.error(`Couldn't reach API endpoint.`);
}
}
return result;
},
/**
* @override
*/
hasContent() {
if (this.searchService === 'all') {
return this._super(...arguments) || this.libraryMedia.length;
} else if (this.searchService === 'media-library') {
return !!this.libraryMedia.length;
}
return this._super(...arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_updateAddUrlUi: function (emptyValue, isURL, isImage) {
this._super.apply(this, arguments);
this.$addUrlButton.text((isURL && !isImage) ? _t("Add as document") : _t("Add image"));
const warning = isURL && !isImage;
this.$urlWarning.toggleClass('d-none', !warning);
if (warning) {
this.$urlSuccess.addClass('d-none');
}
},
/**
* @override
*/
_renderThumbnails: function () {
const alreadyLoaded = this.$('.o_existing_attachment_cell[data-loaded="true"]');
this._super(...arguments);
// Hide images until they're loaded
this.$('.o_existing_attachment_cell').addClass('d-none');
// Replace images that had been previously loaded if any to prevent scroll resetting to top
alreadyLoaded.each((index, el) => {
const toReplace = this.$(`.o_existing_attachment_cell[data-id="${el.dataset.id}"], .o_existing_attachment_cell[data-media-id="${el.dataset.mediaId}"]`);
if (toReplace.length) {
toReplace.replaceWith(el);
}
});
this._toggleOptimized(this.$('input.o_we_show_optimized')[0].checked);
// Placeholders have a 3:2 aspect ratio like most photos.
const placeholderWidth = 3 / 2 * this.MIN_ROW_HEIGHT;
this.$('.o_we_attachment_placeholder').css({
flexGrow: placeholderWidth,
flexBasis: placeholderWidth,
});
if (this.needle && ['media-library', 'all'].includes(this.searchService)) {
const noMoreImgToLoad = this.libraryMedia.length === this.nbMediaResults;
const noLoadMoreButton = noMoreImgToLoad && this.libraryMedia.length <= 15;
this.$('.o_load_done_msg').toggleClass('d-none', noLoadMoreButton || !noMoreImgToLoad);
this.$('.o_load_more').toggleClass('d-none', noMoreImgToLoad);
}
},
/**
* @override
*/
_renderExisting: function (attachments) {
if (this.needle && this.searchService !== 'database') {
attachments = attachments.slice(0, this.MAX_DB_ATTACHMENTS);
}
return QWeb.render(this.existingAttachmentsTemplate, {
attachments: attachments,
libraryMedia: this.libraryMedia,
widget: this,
});
},
/**
* @private
*
* @param {boolean} value whether to toggle optimized attachments on or off
*/
_toggleOptimized: function (value) {
this.$('.o_we_attachment_optimized').each((i, cell) => cell.style.setProperty('display', value ? null : 'none', 'important'));
},
/**
* @override
*/
_highlightSelected: function () {
this._super(...arguments);
this.selectedMedia.forEach(media => {
this.$(`.o_existing_attachment_cell[data-media-id=${media.id}]`)
.addClass("o_we_attachment_selected");
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @override
*/
_onAttachmentImageLoad: async function (ev) {
const img = ev.target;
const cell = img.closest('.o_existing_attachment_cell');
if (!cell) {
return;
}
if (cell.dataset.mediaId && !img.src.startsWith('blob')) {
const mediaUrl = img.src;
try {
const response = await fetch(mediaUrl);
if (response.headers.get('content-type') === 'image/svg+xml') {
const svg = await response.text();
const colorRegex = new RegExp(DEFAULT_PALETTE['1'], 'gi');
if (colorRegex.test(svg)) {
const fileName = mediaUrl.split('/').pop();
const file = new File([svg.replace(colorRegex, getCSSVariableValue('o-color-1'))], fileName, {
type: "image/svg+xml",
});
img.src = URL.createObjectURL(file);
const media = this.libraryMedia.find(media => media.id === parseInt(cell.dataset.mediaId));
if (media) {
media.isDynamicSVG = true;
}
// We changed the src: wait for the next load event to do the styling
return;
}
}
} catch (e) {
console.error('CORS is misconfigured on the API server, image will be treated as non-dynamic.');
}
}
let aspectRatio = img.naturalWidth / img.naturalHeight;
// Special case for SVGs with no instrinsic sizes on firefox
// See https://github.com/whatwg/html/issues/3510#issuecomment-369982529
if (img.naturalHeight === 0) {
img.width = 1000;
// Position fixed so that the image doesn't affect layout while rendering
img.style.position = 'fixed';
// Make invisible so the image doesn't briefly appear on the screen
img.style.opacity = '0';
// Image needs to be in the DOM for dimensions to be correct after render
const originalParent = img.parentElement;
document.body.appendChild(img);
aspectRatio = img.width / img.height;
originalParent.appendChild(img);
img.removeAttribute('width');
img.style.removeProperty('position');
img.style.removeProperty('opacity');
}
const width = aspectRatio * this.MIN_ROW_HEIGHT;
cell.style.flexGrow = width;
cell.style.flexBasis = `${width}px`;
cell.classList.remove('d-none');
cell.classList.add('d-flex');
cell.dataset.loaded = 'true';
},
/**
* @override
*/
_onShowOptimizedChange: function (ev) {
this._toggleOptimized(ev.target.checked);
},
/**
* @override
*/
_onSearchSelect: function (ev) {
const {value} = ev.target;
this.searchService = value;
this.$('.o_we_search').trigger('input');
},
/**
* @private
*/
_onSearchInput: function (ev) {
this.libraryMedia = [];
this._super(...arguments);
},
/**
* @override
*/
_clear: function (type) {
// Not calling _super: we don't want to call the document widget's _clear method on images
var allImgClasses = /(^|\s+)(img|img-\S*|o_we_custom_image|rounded-circle|rounded|thumbnail|shadow)(?=\s|$)/g;
this.media.className = this.media.className && this.media.className.replace(allImgClasses, ' ');
},
});
/**
* Let users choose a document, including uploading a new document in odoo.
*/
var DocumentWidget = FileWidget.extend({
template: 'wysiwyg.widgets.document',
existingAttachmentsTemplate: 'wysiwyg.widgets.document.existing.attachments',
/**
* @constructor
*/
init: function (parent, media, options) {
options = _.extend({
accept: '*/*',
mimetypeDomain: [['mimetype', 'not in', this.IMAGE_MIMETYPES]],
}, options || {});
this._super(parent, media, options);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_updateAddUrlUi: function (emptyValue, isURL, isImage) {
this._super.apply(this, arguments);
this.$addUrlButton.text((isURL && isImage) ? _t("Add as image") : _t("Add document"));
const warning = isURL && isImage;
this.$urlWarning.toggleClass('d-none', !warning);
if (warning) {
this.$urlSuccess.addClass('d-none');
}
},
/**
* @override
*/
_getAttachmentsDomain: function (needle) {
var domain = this._super.apply(this, arguments);
// the assets should not be part of the documents
return domain.concat('!', utils.assetsDomain());
},
});
/**
* Let users choose a font awesome icon, support all font awesome loaded in the
* css files.
*/
var IconWidget = SearchableMediaWidget.extend({
template: 'wysiwyg.widgets.font-icons',
events: _.extend({}, SearchableMediaWidget.prototype.events || {}, {
'click .font-icons-icon': '_onIconClick',
}),
/**
* @constructor
*/
init: function (parent, media) {
this._super.apply(this, arguments);
fonts.computeFonts();
this.iconsParser = fonts.fontIcons;
this.alias = _.flatten(_.map(this.iconsParser, function (data) {
return data.alias;
}));
},
/**
* @override
*/
start: function () {
this.$icons = this.$('.font-icons-icon');
var classes = (this.media && this.media.className || '').split(/\s+/);
for (var i = 0; i < classes.length; i++) {
var cls = classes[i];
if (_.contains(this.alias, cls)) {
this.selectedIcon = cls;
this.initialIcon = cls;
this._highlightSelectedIcon();
}
}
// Kept for compat in stable, no longer in use: remove in master
this.nonIconClasses = _.without(classes, 'media_iframe_video', this.selectedIcon);
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
save: function () {
var style = this.$media.attr('style') || '';
var iconFont = this._getFont(this.selectedIcon) || {base: 'fa', font: ''};
if (!this.$media.is('span, i')) {
var $span = $('');
$span.data(this.$media.data());
this.$media = $span;
this.media = this.$media[0];
style = style.replace(/\s*width:[^;]+/, '');
}
this.$media.removeClass(this.initialIcon).addClass([iconFont.base, iconFont.font]);
this.$media.attr('style', style || null);
return Promise.resolve(this.media);
},
/**
* @override
*/
search: function (needle) {
var iconsParser = this.iconsParser;
if (needle && needle.length) {
iconsParser = [];
_.filter(this.iconsParser, function (data) {
var cssData = _.filter(data.cssData, function (cssData) {
return _.find(cssData.names, function (alias) {
return alias.indexOf(needle) >= 0;
});
});
if (cssData.length) {
iconsParser.push({
base: data.base,
cssData: cssData,
});
}
});
}
this.$('div.font-icons-icons').html(
QWeb.render('wysiwyg.widgets.font-icons.icons', {iconsParser: iconsParser, widget: this})
);
return Promise.resolve();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_clear: function () {
var allFaClasses = /(^|\s)(fa|(text-|bg-|fa-)\S*|rounded-circle|rounded|thumbnail|shadow)(?=\s|$)/g;
this.media.className = this.media.className && this.media.className.replace(allFaClasses, ' ');
},
/**
* @private
*/
_getFont: function (classNames) {
if (!(classNames instanceof Array)) {
classNames = (classNames || "").split(/\s+/);
}
var fontIcon, cssData;
for (var k = 0; k < this.iconsParser.length; k++) {
fontIcon = this.iconsParser[k];
for (var s = 0; s < fontIcon.cssData.length; s++) {
cssData = fontIcon.cssData[s];
if (_.intersection(classNames, cssData.names).length) {
return {
base: fontIcon.base,
parser: fontIcon.parser,
font: cssData.names[0],
};
}
}
}
return null;
},
/**
* @private
*/
_highlightSelectedIcon: function () {
var self = this;
this.$icons.removeClass('o_we_attachment_selected');
this.$icons.filter(function (i, el) {
return _.contains($(el).data('alias').split(','), self.selectedIcon);
}).addClass('o_we_attachment_selected');
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onIconClick: function (ev) {
ev.preventDefault();
ev.stopPropagation();
this.selectedIcon = $(ev.currentTarget).data('id');
this._highlightSelectedIcon();
this.trigger_up('save_request');
},
});
/**
* Let users choose a video, support all summernote video, and embed iframe.
*/
var VideoWidget = MediaWidget.extend({
template: 'wysiwyg.widgets.video',
events: _.extend({}, MediaWidget.prototype.events || {}, {
'change .o_video_dialog_options input': '_onUpdateVideoOption',
'input textarea#o_video_text': '_onVideoCodeInput',
'change textarea#o_video_text': '_onVideoCodeChange',
}),
/**
* @constructor
*/
init: function (parent, media, options) {
this._super.apply(this, arguments);
this.isForBgVideo = !!options.isForBgVideo;
this._onVideoCodeInput = _.debounce(this._onVideoCodeInput, 1000);
},
/**
* @override
*/
start: function () {
this.$content = this.$('.o_video_dialog_iframe');
if (this.media) {
var $media = $(this.media);
var src = $media.data('oe-expression') || $media.data('src') || ($media.is('iframe') ? $media.attr('src') : '') || '';
this.$('textarea#o_video_text').val(src);
this.$('input#o_video_autoplay').prop('checked', src.indexOf('autoplay=1') >= 0);
this.$('input#o_video_hide_controls').prop('checked', src.indexOf('controls=0') >= 0);
this.$('input#o_video_loop').prop('checked', src.indexOf('loop=1') >= 0);
this.$('input#o_video_hide_fullscreen').prop('checked', src.indexOf('fs=0') >= 0);
this.$('input#o_video_hide_yt_logo').prop('checked', src.indexOf('modestbranding=1') >= 0);
this.$('input#o_video_hide_dm_logo').prop('checked', src.indexOf('ui-logo=0') >= 0);
this.$('input#o_video_hide_dm_share').prop('checked', src.indexOf('sharing-enable=0') >= 0);
this._updateVideo();
}
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
save: function () {
this._updateVideo();
if (this.isForBgVideo) {
return Promise.resolve({bgVideoSrc: this.$content.attr('src')});
}
if (this.$('.o_video_dialog_iframe').is('iframe')) {
this.$media = $(
'