diff options
| author | qsm-odoo <qsm@odoo.com> | 2017-03-28 11:38:26 +0200 |
|---|---|---|
| committer | qsm-odoo <qsm@odoo.com> | 2017-08-17 12:41:15 +0200 |
| commit | 6c9a56a65fda9066dee9efde49b7ade6aff1ca46 (patch) | |
| tree | c0a216ba4b7e3ccfdc08c891fb3ee76d6c69e09f /addons/web_editor/static | |
| parent | 56972f8b0549804513dc7acaceacccbdc9bce1aa (diff) | |
[REF] web_editor: improve "Video" dialog design and behavior
Original design by @stefanorigano.
Diffstat (limited to 'addons/web_editor/static')
4 files changed, 392 insertions, 175 deletions
diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js index 20b67828fad..5340efb821e 100644 --- a/addons/web_editor/static/src/js/widgets/widgets.js +++ b/addons/web_editor/static/src/js/widgets/widgets.js @@ -621,164 +621,80 @@ var fontIconsDialog = Widget.extend({ }, }); - -function createVideoNode(url, options) { - options = options || {}; - - // video url patterns(youtube, instagram, vimeo, dailymotion, youku) - var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/; - var ytMatch = url.match(ytRegExp); - - var igRegExp = /\/\/instagram.com\/p\/(.[a-zA-Z0-9]*)/; - var igMatch = url.match(igRegExp); - - var vRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/; - var vMatch = url.match(vRegExp); - - var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/; - var vimMatch = url.match(vimRegExp); - - var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/; - var dmMatch = url.match(dmRegExp); - - var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)\.html/; - var youkuMatch = url.match(youkuRegExp); - - var $video = $('<iframe>'); - if (ytMatch && ytMatch[1].length === 11) { - var youtubeId = ytMatch[1]; - $video = $('<iframe>', { - src: '//www.youtube.com/embed/' + youtubeId, - width: '640', - height: '360' - }); - } else if (igMatch && igMatch[0].length) { - $video = $('<iframe>', { - src: igMatch[0] + '/embed/', - width: '612', - height: '710', - scrolling: 'no', - allowtransparency: 'true' - }); - } else if (vMatch && vMatch[0].length) { - $video = $('<iframe>', { - src: vMatch[0] + '/embed/simple', - width: '600', - height: '600', - class: 'vine-embed' - }); - } else if (vimMatch && vimMatch[3].length) { - $video = $('<iframe>', { - src: '//player.vimeo.com/video/' + vimMatch[3], - width: '640', - height: '360' - }); - } else if (dmMatch && dmMatch[2].length) { - $video = $('<iframe>', { - src: '//www.dailymotion.com/embed/video/' + dmMatch[2], - width: '640', - height: '360' - }); - } else if (youkuMatch && youkuMatch[1].length) { - $video = $('<iframe>', { - height: '498', - width: '510', - src: '//player.youku.com/embed/' + youkuMatch[1] - }); - } else { - // this is not a known video link. Now what, Cat? Now what? - $video = $('<iframe>', { - width: '640', - height: '360', - src: url - }); - } - - if (options.autoplay) { - $video.attr("src", $video.attr("src") + "?autoplay=1"); - } - - $video.attr('frameborder', 0); - - return $video; -} - /** - * VideoDialog widget. Lets users change a video, support all summernote - * video, and embled iframe + * VideoDialog widget. Let users change a video, support all summernote + * video, and embed iframe. */ var VideoDialog = Widget.extend({ template: 'web_editor.dialog.video', xmlDependencies: ['/web_editor/static/src/xml/editor.xml'], - events : _.extend({}, Dialog.prototype.events, { - 'click input#urlvideo ~ button': 'get_video', - 'click input#embedvideo ~ button': 'get_embed_video', - 'change input#autoplay': 'get_video', - 'change input#urlvideo': 'change_input', - 'keyup input#urlvideo': 'change_input', - 'change input#embedvideo': 'change_input', - 'keyup input#embedvideo': 'change_input', - 'keydown.dismiss.bs.modal': function () {}, - }), + 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) { this._super.apply(this, arguments); this.parent = parent; this.media = media; + this._onVideoCodeInput = _.debounce(this._onVideoCodeInput, 1000); }, + /** + * @override + */ start: function () { this.$preview = this.$('.preview-container').detach(); - this.$iframe = this.$("iframe"); + this.$content = this.$('.o_video_dialog_iframe'); + var $media = $(this.media); - if ($media.hasClass("media_iframe_video")) { - var src = $media.data('src'); - this.$("input#urlvideo").val(src); - this.$("input#autoplay").prop("checked", (src || "").indexOf("autoplay") >= 0); - this.get_video(); + if ($media.hasClass('media_iframe_video')) { + var src = $media.data('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); }, - change_input: function (e) { - var $input = $(e.target); - var $button = $input.parent().find("button"); - if ($input.val() === "") { - $button.addClass("btn-default").removeClass("btn-primary"); - } else { - $button.removeClass("btn-default").addClass("btn-primary"); - } - }, - get_embed_video: function (event) { - event.preventDefault(); - var embedvideo = this.$("input#embedvideo").val().match(/src=["']?([^"']+)["' ]?/); - if (embedvideo) { - this.$("input#urlvideo").val(embedvideo[1]); - this.get_video(event); - } - return false; - }, - get_video: function (event) { - if (event) event.preventDefault(); - var $video = createVideoNode(this.$("input#urlvideo").val(), {autoplay: this.$("input#autoplay").is(":checked")}); - this.$iframe.replaceWith($video); - this.$iframe = $video; - return false; - }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ save: function () { - var video_id = this.$("#video_id").val(); - if (!video_id) { - this.$("button.btn-primary").click(); - video_id = this.$("#video_id").val(); + this._updateVideo(); + + if (this.$('.o_video_dialog_iframe').is('iframe')) { + var $content = $( + '<div class="media_iframe_video" data-src="' + this.$content.attr('src') + '">'+ + '<div class="css_editable_mode_display"> </div>'+ + '<div class="media_iframe_video_size" contenteditable="false"> </div>'+ + '<iframe src="' + this.$content.attr('src') + '" frameborder="0" contenteditable="false"></iframe>'+ + '</div>' + ); + $(this.media).replaceWith($content); + this.media = $content[0]; + return this.media; } - var $iframe = $( - '<div class="media_iframe_video" data-src="'+this.$iframe.attr("src")+'">'+ - '<div class="css_editable_mode_display"> </div>'+ - '<div class="media_iframe_video_size" contentEditable="false"> </div>'+ - '<iframe src="'+this.$iframe.attr("src")+'" frameborder="0" allowfullscreen="allowfullscreen" contentEditable="false"></iframe>'+ - '</div>'); - $(this.media).replaceWith($iframe); - this.media = $iframe[0]; - - return this.media; }, + /** + * @override + */ clear: function () { if (this.media.dataset.src) { try { @@ -789,6 +705,205 @@ var VideoDialog = Widget.extend({ } this.media.className = this.media.className.replace(/(^|\s)media_iframe_video(\s|$)/g, ' '); }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Creates a video node according to the given URL and options. If not + * possible, returns an error code. + * + * @private + * @param {string} url + * @param {Object} options + * @returns {Object} + * $video -> the created video jQuery node + * type -> the type of the created video + * errorCode -> if defined, either '0' for invalid URL or '1' for + * unsupported video provider + */ + _createVideoNode: function (url, options) { + options = options || {}; + + // Video url patterns(youtube, instagram, vimeo, dailymotion, youku, ...) + var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.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)){ + return {errorCode: 0}; + } + + var autoplay = options.autoplay ? '?autoplay=1' : '?autoplay=0'; + + if (ytMatch && ytMatch[1].length === 11) { + $video.attr('src', '//www.youtube.com/embed/' + ytMatch[1] + 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); + videoType = 'vim'; + } else if (dmMatch && dmMatch[2].length) { + var just_id = dmMatch[2].replace('video/',''); + $video.attr('src', '//www.dailymotion.com/embed/video/' + just_id + autoplay); + videoType = 'dm'; + } else if (ykuMatch && ykuMatch[3].length) { + var yku_id = ykuMatch[3].indexOf('.html?') >= 0 ? ykuMatch[3].substring(0, ykuMatch[3].indexOf('.html?')) : ykuMatch[3]; + $video.attr('src', '//player.youku.com/embed/' + yku_id); + videoType = 'yku'; + } else { + return {errorCode: 1}; + } + + if (options.loop && (ytMatch || vimMatch)) { + $video.attr('src', $video.attr('src') + '&loop=1'); + } + 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}; + }, + /** + * Updates the video preview according to video code and enabled options. + * + * @private + */ + _updateVideo: function () { + // Reset the feedback + this.$content.empty(); + this.$('#o_video_form_group').removeClass('has-error has-success'); + this.$('.o_video_dialog_options li').addClass('hidden'); + + // Check video code + var $textarea = this.$('textarea#o_video_text'); + var code = $textarea.val().trim(); + if (!code) { + return; + } + + // Detect if we have an embed code rather than an URL + var embedMatch = code.match(/(src|href)=["']?([^"']+)?/); + if (embedMatch && embedMatch[2].length > 0 && embedMatch[2].indexOf('instagram')){ + embedMatch[1] = embedMatch[2]; // Instagram embed code is different + } + var url = embedMatch ? embedMatch[1] : code; + + var query = this._createVideoNode(url, { + autoplay: this.$('input#o_video_autoplay').is(':checked'), + hide_controls: this.$('input#o_video_hide_controls').is(':checked'), + loop: this.$('input#o_video_loop').is(':checked'), + hide_fullscreen: this.$('input#o_video_hide_fullscreen').is(':checked'), + hide_yt_logo: this.$('input#o_video_hide_yt_logo').is(':checked'), + hide_dm_logo: this.$('input#o_video_hide_dm_logo').is(':checked'), + hide_dm_share: this.$('input#o_video_hide_dm_share').is(':checked'), + }); + + var $opt_box = this.$('.o_video_dialog_options'); + + // Show / Hide preview elements + this.$el.find('.o_video_dialog_preview_text, .media_iframe_video_size').add($opt_box).toggleClass('hidden', !query.$video); + // Toggle validation classes + this.$el.find('#o_video_form_group').toggleClass('has-error', !query.$video).toggleClass('has-success', !!query.$video); + + // Individually show / hide options base on the video provider + $opt_box.find('li.o_' + query.type + '_option').removeClass('hidden'); + + // Hide the entire options box if no options are available + $opt_box.toggleClass('hidden', $opt_box.find('li:not(.hidden)').length === 0); + + if (query.type === 'yt') { + // Youtube only: If 'hide controls' is checked, hide 'fullscreen' + // and 'youtube logo' options too + this.$('input#o_video_hide_fullscreen, input#o_video_hide_yt_logo').closest('li').toggleClass('hidden', this.$('input#o_video_hide_controls').is(':checked')); + } + + var $content = query.$video; + if (!$content) { + switch (query.errorCode) { + case 0: + $content = $('<div/>', { + class: 'alert alert-danger o_video_dialog_iframe', + text: _t("The provided url is not valid"), + }); + break; + case 1: + $content = $('<div/>', { + class: 'alert alert-warning o_video_dialog_iframe', + text: _t("The provided url does not reference any supported video"), + }); + break; + } + } + this.$content.replaceWith($content); + this.$content = $content; + }, + + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when a video option changes -> Updates the video preview. + * + * @private + */ + _onUpdateVideoOption: function () { + this._updateVideo(); + }, + /** + * Called when the video code (URL / Iframe) change is confirmed -> Updates + * the video preview immediately. + * + * @private + */ + _onVideoCodeChange: function () { + this._updateVideo(); + }, + /** + * Called when the video code (URL / Iframe) changes -> Updates the video + * preview (note: this function is automatically debounced). + * + * @private + */ + _onVideoCodeInput: function () { + this._updateVideo(); + }, }); /** diff --git a/addons/web_editor/static/src/less/web_editor.ui.components.less b/addons/web_editor/static/src/less/web_editor.ui.components.less index 5ec0dfdc697..c5f1f189b9b 100644 --- a/addons/web_editor/static/src/less/web_editor.ui.components.less +++ b/addons/web_editor/static/src/less/web_editor.ui.components.less @@ -241,14 +241,6 @@ body .modal { .well { border-color: @gray-lighter; border-top-width: 0; - - label { - margin: auto 10px; - } - - .btn-group { - margin-right: 10px; - } } .o_existing_attachment_cell { @@ -306,6 +298,83 @@ body .modal { } } } + + #editor-media-image, #editor-media-document { + .well { + label { + margin: auto 10px; + } + + .btn-group { + margin-right: 10px; + } + } + } + + // VIDEO TAB + #editor-media-video .well { + padding: 0; + .o-flex-display(); + .o-flex-flow(row, wrap); + + .o_video_dialog_form { + .o-flex(1, 1, auto); + min-width: 0; + padding: 20px; + + #o_video_form_group { + position: relative; + width: 100%; + + > textarea { + width: 100%; + min-height: 95px; + padding-bottom: 25px; + overflow-y: scroll; + } + + > .o_validate_feedback { + .o-position-absolute(auto, 10px, 10px); + + > .fa { + display: none; + } + } + &.has-error > .o_validate_feedback > .fa-exclamation-triangle { + display: block; + } + &.has-success > .o_validate_feedback > .fa-check { + display: block; + } + } + } + + #video-preview { + position: relative; + .o-flex(1, 0, 50%); + .o-flex-display(); + .o-flex-flow(column, nowrap); + .o-justify-content(center); + + .o-we-preview-box(0 19px); + border: none; + + .media_iframe_video { + width: 100%; + } + + .o_video_dialog_iframe { + .o-we-preview-content(); + max-width: 100%; + max-height: 100%; + + &.alert { + .o-animation(fadeInDownSmall, 700ms, @fill-mode: forwards); + margin: 0 auto; + } + } + } + } } } @@ -351,22 +420,18 @@ body .modal { } .o_bg_img_opt_cover_edition { - border-top: 1px solid black; - border-bottom: 1px solid white; - padding: 0px 25px 25px 25px; - .o-linear-gradient(-150deg, lighten(#2c2c36, 5%), darken(#2c2c36, 10%)); + .o-we-preview-box(); + h6 { color: white; font-weight: bold; } .o_bg_img_opt_object { - display: inline-block; - max-width: 100%; + position: relative; background: white; min-height: 10px; - position: relative; - overflow: hidden; - box-shadow: 0 0 10px #000; + .o-we-preview-content(); + > img { cursor: crosshair; border-top: 1px solid #5A5A5A; diff --git a/addons/web_editor/static/src/less/web_editor.variables.less b/addons/web_editor/static/src/less/web_editor.variables.less index 188d62b955d..c735967af86 100644 --- a/addons/web_editor/static/src/less/web_editor.variables.less +++ b/addons/web_editor/static/src/less/web_editor.variables.less @@ -85,6 +85,26 @@ } +// ============ 'Preview' component Mixins =============== +// Create a "preview" work-area to edit/customize specific elements + +.o-we-preview-box(@padding: 0px 25px 25px 25px, @color-text: white ) { + padding: @padding; + border-top: 1px solid black; + border-bottom: 1px solid white; + .o-linear-gradient(-150deg, @o-we-color-dark, @o-we-color-darker); + + color: @color-text; +} + +.o-we-preview-content() { + display: inline-block; + max-width: 100%; + overflow: hidden; + box-shadow: 0 0 15px 2px #000; +} + + // ============ Preservetion mixins =============== // Used to preserve web_editor and website UI from themes customizations diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml index 3fdbc462110..ee3f0cd70e3 100644 --- a/addons/web_editor/static/src/xml/editor.xml +++ b/addons/web_editor/static/src/xml/editor.xml @@ -187,31 +187,48 @@ <!-- Video choosing part of the Media Dialog --> <t t-name="web_editor.dialog.video"> - <form action="#" class="form-inline"> - <div class="well"> - <div class="form-group btn-group"> - <span class="text-muted pull-right" style="margin-left:10px; line-height: 2em;">(Youtube, Vimeo, Dailymotion)</span> - <label class="pull-left" for="urlvideo" style="width: 220px; line-height: 2em;">Set a video URL</label> - <input class="form-control url pull-left" id="urlvideo" name="url" placeholder="//www.youtube.com/embed/yws1tbgNV7k" style="width: 400px;" t-translation="off" type="text"/> - <button class="btn btn-default">Preview</button> + <form action="#" class="form-inline well"> + <div class="o_video_dialog_form"> + <div class="form-group" id="o_video_form_group"> + <label class="mt8" for="o_video_text">Video code <small class="text-muted">(URL or Embed)</small></label> + <textarea class="form-control url" id="o_video_text" placeholder="Copy-paste your URL or embed code here"/> + <label class="control-label o_validate_feedback" for="o_video_text"><i class="fa fa-check"/><i class="fa fa-exclamation-triangle"/></label> </div> - <div class="form-group btn-group"> - <label class="pull-left" for="urlvideo" style="width: 220px; line-height: 2em;"> - <span class="text-muted">— or —</span>Embed Video (HTML)</label> - <input class="form-control url pull-left" id="embedvideo" name="embed" placeholder="<iframe src="//www.youtube.com/embed/yws1tbgNV7k"></iframe>" style="width: 400px;" t-translation="off" type="text"/> - <button class="btn btn-default">Preview</button> + <div class="text-right"> + <small class="text-muted">Accepts <b><i>Youtube</i></b>, <b><i>Instagram</i></b>, <b><i>Vine.co</i></b>, <b><i>Vimeo</i></b>, <b><i>Dailymotion</i></b> and <b><i>Youku</i></b> videos</small> + </div> + <div class="o_video_dialog_options hidden mt48"> + <small class="text-muted">Options</small> + <ul class="list-group"> + <li class="list-group-item o_yt_option o_vim_option o_dm_option"> + <label class="o_switch mb0"><input id="o_video_autoplay" type="checkbox"/><span/>Autoplay</label> + </li> + <li class="list-group-item o_yt_option o_vim_option"> + <label class="o_switch mb0"><input id="o_video_loop" type="checkbox"/><span/>Loop</label> + </li> + <li class="list-group-item o_yt_option o_dm_option"> + <label class="o_switch mb0"><input id="o_video_hide_controls" type="checkbox"/><span/>Hide player controls</label> + </li> + <li class="list-group-item o_yt_option"> + <label class="o_switch mb0"><input id="o_video_hide_fullscreen" type="checkbox"/><span/>Hide fullscreen button</label> + </li> + <li class="list-group-item o_yt_option"> + <label class="o_switch mb0"><input id="o_video_hide_yt_logo" type="checkbox"/><span/>Hide Youtube logo</label> + </li> + <li class="list-group-item o_dm_option"> + <label class="o_switch mb0"><input id="o_video_hide_dm_logo" type="checkbox"/><span/>Hide Dailymotion logo</label> + </li> + <li class="list-group-item o_dm_option"> + <label class="o_switch mb0"><input id="o_video_hide_dm_share" type="checkbox"/><span/>Hide sharing button</label> + </li> + </ul> </div> </div> <div id="video-preview"> + <div class="o_video_dialog_preview_text small mt16 mb8 hidden">Preview</div> <div class="media_iframe_video"> <div class="media_iframe_video_size"/> - <iframe allowfullscreen="allowfullscreen" frameborder="0" src=""/> - </div> - </div> - <div class="text-center mt8"> - <div class="checkbox"> - <label title="The feature might not be supported for every video type"> - <input id="autoplay" style="margin-right:5px" type="checkbox"/>Autoplay</label> + <iframe class="o_video_dialog_iframe" allowfullscreen="allowfullscreen" frameborder="0" src=""/> </div> </div> </form> |
