[IMP] web_editor: add an image-picker UserValueWidget

This commit introduces a new web_editor UserValueWidget on the
we-imagepicker tag, this widget allows the user to pick an image or a
video using the MediaDialog, and calls the containing snippet option's
method (specified via data-attribute) with the url of the chosen picture
or video as widgetValue. Videos are disabled by default and can be
accessed through the use of the data-allow-videos="selector" data
attribute, any $target that matches the selector will have videos
available in the media dialog.

Part of odoo/odoo#41561
task-2155808
This commit is contained in:
Samuel Degueldre
2019-12-31 07:58:10 +00:00
parent 38b5c759f3
commit f8632ec6f3
3 changed files with 173 additions and 73 deletions
@@ -1069,6 +1069,104 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({
},
});
const ImagepickerUserValueWidget = UserValueWidget.extend({
tagName: 'we-imagepicker',
events: {
'click .o_we_edit_image': '_onEditImage',
'click .o_we_remove_image': '_onRemoveImage',
},
/**
* @override
*/
start: async function () {
await this._super(...arguments);
const allowedSelector = this.el.dataset.allowVideos;
this.firstFilters = (this.el.dataset.firstFilters || '').split(',').filter(s => s !== '');
this.allowVideos = allowedSelector ? this.$target.is(allowedSelector) : false;
this.editImageButton = document.createElement('we-button');
this.editImageButton.classList.add('o_we_edit_image', 'fa', 'fa-fw', 'fa-edit');
this.removeImageButton = document.createElement('we-button');
this.removeImageButton.classList.add('o_we_remove_image', 'fa', 'fa-fw', 'fa-times');
this.removeImageButton.title = _t("Remove");
this.containerEl.appendChild(this.editImageButton);
this.containerEl.appendChild(this.removeImageButton);
},
/**
* @override
*/
getMethodsParams: function (methodName) {
return _.extend({isVideo: this.isVideo}, this._super(...arguments));
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_updateUI: async function () {
await this._super(...arguments);
this.removeImageButton.classList.toggle('d-none', !this.isActive());
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the edit background button is clicked.
*
* @private
*/
_onEditImage: function (ev) {
// Need a dummy element for the media dialog to modify.
const dummyEl = document.createElement(this.isVideo ? 'iframe' : 'img');
dummyEl.src = this._value;
if (this.isVideo) {
// Allows the mediaDialog to select the video tab immediately.
dummyEl.classList.add('media_iframe_video');
}
const $editable = this.$target.closest('.o_editable');
const mediaDialog = new weWidgets.MediaDialog(this, {
noIcons: true,
noDocuments: true,
noVideos: !this.allowVideos,
isForBgVideo: true,
res_model: $editable.data('oe-model'),
res_id: $editable.data('oe-id'),
firstFilters: this.firstFilters,
}, dummyEl).open();
mediaDialog.on('save', this, data => {
if (data.bgVideoSrc) {
this._value = data.bgVideoSrc;
this.isVideo = true;
} else {
// 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.isVideo = false;
}
this._notifyValueChange(false);
});
},
/**
* Called when the remove background button is clicked.
*
* @private
*/
_onRemoveImage: function (ev) {
this._value = '';
this.isVideo = false;
this._notifyValueChange(false);
},
});
const DatetimePickerUserValueWidget = InputUserValueWidget.extend({
events: { // Explicitely not consider all InputUserValueWidget events
'blur input': '_onInputBlur',
@@ -1200,6 +1298,7 @@ const userValueWidgetsRegistry = {
'we-multi': MultiUserValueWidget,
'we-colorpicker': ColorpickerUserValueWidget,
'we-datetimepicker': DatetimePickerUserValueWidget,
'we-imagepicker': ImagepickerUserValueWidget,
};
/**
@@ -1102,82 +1102,19 @@ var VideoWidget = MediaWidget.extend({
*/
_createVideoNode: function (url, options) {
options = options || {};
// Video url patterns(youtube, instagram, vimeo, dailymotion, youku, ...)
var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube(-nocookie)?\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((?:\w|-){11})(?:\S+)?$/;
var ytMatch = url.match(ytRegExp);
var insRegExp = /(.*)instagram.com\/p\/(.[a-zA-Z0-9]*)/;
var insMatch = url.match(insRegExp);
var vinRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/;
var vinMatch = url.match(vinRegExp);
var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/;
var vimMatch = url.match(vimRegExp);
var dmRegExp = /.+dailymotion.com\/(video|hub|embed)\/([^_]+)[^#]*(#video=([^_&]+))?/;
var dmMatch = url.match(dmRegExp);
var ykuRegExp = /(.*).youku\.com\/(v_show\/id_|embed\/)(.+)/;
var ykuMatch = url.match(ykuRegExp);
var $video = $('<iframe>').width(1280).height(720).attr('frameborder', 0).addClass('o_video_dialog_iframe');
var videoType = 'yt';
if (!/^(http:\/\/|https:\/\/|\/\/)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i.test(url)){
const videoData = this._getVideoURLData(url, options);
if (videoData.error) {
return {errorCode: 0};
}
var autoplay = options.autoplay ? '?autoplay=1&mute=1' : '?autoplay=0';
if (ytMatch && ytMatch[2].length === 11) {
$video.attr('src', '//www.youtube' + (ytMatch[1] || '') + '.com/embed/' + ytMatch[2] + autoplay);
} else if (insMatch && insMatch[2].length) {
$video.attr('src', '//www.instagram.com/p/' + insMatch[2] + '/embed/');
videoType = 'ins';
} else if (vinMatch && vinMatch[0].length) {
$video.attr('src', vinMatch[0] + '/embed/simple');
videoType = 'vin';
} else if (vimMatch && vimMatch[3].length) {
$video.attr('src', '//player.vimeo.com/video/' + vimMatch[3] + autoplay.replace('mute', 'muted'));
videoType = 'vim';
} else if (dmMatch && dmMatch[2].length) {
var justId = dmMatch[2].replace('video/', '');
$video.attr('src', '//www.dailymotion.com/embed/video/' + justId + autoplay);
videoType = 'dm';
} else if (ykuMatch && ykuMatch[3].length) {
var ykuId = ykuMatch[3].indexOf('.html?') >= 0 ? ykuMatch[3].substring(0, ykuMatch[3].indexOf('.html?')) : ykuMatch[3];
$video.attr('src', '//player.youku.com/embed/' + ykuId);
videoType = 'yku';
} else {
if (!videoData.type) {
return {errorCode: 1};
}
const $video = $('<iframe>').width(1280).height(720)
.attr('frameborder', 0)
.attr('src', videoData.embedURL)
.addClass('o_video_dialog_iframe');
if (ytMatch) {
$video.attr('src', $video.attr('src') + '&rel=0');
}
if (options.loop && (ytMatch || vimMatch)) {
var videoSrc = _.str.sprintf('%s&loop=1', $video.attr('src'));
$video.attr('src', ytMatch ? _.str.sprintf('%s&playlist=%s', videoSrc, ytMatch[2]) : videoSrc);
}
if (options.hide_controls && (ytMatch || dmMatch)) {
$video.attr('src', $video.attr('src') + '&controls=0');
}
if (options.hide_fullscreen && ytMatch) {
$video.attr('src', $video.attr('src') + '&fs=0');
}
if (options.hide_yt_logo && ytMatch) {
$video.attr('src', $video.attr('src') + '&modestbranding=1');
}
if (options.hide_dm_logo && dmMatch) {
$video.attr('src', $video.attr('src') + '&ui-logo=0');
}
if (options.hide_dm_share && dmMatch) {
$video.attr('src', $video.attr('src') + '&sharing-enable=0');
}
return {$video: $video, type: videoType};
return {$video: $video, type: videoData.type};
},
/**
* Updates the video preview according to video code and enabled options.
@@ -1288,6 +1225,63 @@ var VideoWidget = MediaWidget.extend({
_onVideoCodeInput: function () {
this._updateVideo();
},
/**
* Parses a URL and returns the provider type and an emebedable URL.
*
* @private
*/
_getVideoURLData: function (url, options) {
if (!url.match(/^(http:\/\/|https:\/\/|\/\/)[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i)) {
return {
error: true,
message: 'The provided url is invalid',
};
}
const regexes = {
youtube: /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube(-nocookie)?\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((?:\w|-){11})(?:\S+)?$/,
instagram: /(.*)instagram.com\/p\/(.[a-zA-Z0-9]*)/,
vine: /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/,
vimeo: /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/,
dailymotion: /.+dailymotion.com\/(video|hub|embed)\/([^_]+)[^#]*(#video=([^_&]+))?/,
youku: /(.*).youku\.com\/(v_show\/id_|embed\/)(.+)/,
};
const matches = _.mapObject(regexes, regex => url.match(regex));
const autoplay = options.autoplay ? '?autoplay=1&mute=1' : '?autoplay=0';
const controls = options.hide_controls ? '&controls=0' : '';
const loop = options.loop ? '&loop=1' : '';
let embedURL;
let type;
if (matches.youtube && matches.youtube[2].length === 11) {
const fullscreen = options.hide_fullscreen ? '&fs=0' : '';
const ytLoop = loop ? loop + `&playlist=${matches.youtube[2]}` : '';
const logo = options.hide_yt_logo ? '&modestbranding=1' : '';
embedURL = `//www.youtube${matches.youtube[1] || ''}.com/embed/${matches.youtube[2]}${autoplay}&rel=0${ytLoop}${controls}${fullscreen}${logo}`;
type = 'youtube';
} else if (matches.instagram && matches.instagram[2].length) {
embedURL = `//www.instagram.com/p/${matches.instagram[2]}/embed/`;
type = 'instagram';
} else if (matches.vine && matches.vine[0].length) {
embedURL = `${matches.vine[0]}/embed/simple`;
type = 'vine';
} else if (matches.vimeo && matches.vimeo[3].length) {
const vimeoAutoplay = autoplay.replace('mute', 'muted');
embedURL = `//player.vimeo.com/video/${matches.vimeo[3]}${vimeoAutoplay}${loop}`;
type = 'vimeo';
} else if (matches.dailymotion && matches.dailymotion[2].length) {
const videoId = matches.dailymotion[2].replace('video/', '');
const logo = options.hide_dm_logo ? '&ui-logo=0' : '';
const share = options.hide_dm_share ? '&sharing-enable=0' : '';
embedURL = `//www.dailymotion.com/embed/video/${videoId}${autoplay}${controls}${logo}${share}`;
type = 'dailymotion';
} else if (matches.youku && matches.youku[3].length) {
const videoId = matches.youku[3].indexOf('.html?') >= 0 ? matches.youku[3].substring(0, matches.youku[3].indexOf('.html?')) : matches.youku[3];
embedURL = `//player.youku.com/embed/${videoId}`;
type = 'youku';
}
return {type: type, embedURL: embedURL};
},
});
return {
@@ -301,12 +301,19 @@ body.editor_enable.editor_has_snippets {
> div {
display: flex;
align-items: baseline;
justify-content: flex-end;
}
}
// Buttons
we-button.o_we_user_value_widget.active {
@include we-icon-button('\f00c', $o-we-color-success);
we-button.o_we_user_value_widget {
&.active {
@include we-icon-button('\f00c', $o-we-color-success);
}
> div {
justify-content: flex-start;
}
}
// Checkboxes