[REF] web_editor, web_unsplash: review load more feature

* Simplify the code
* Make the feature mork without unsplash
* Handle corner cases

task-1896705
This commit is contained in:
qsm-odoo
2018-12-04 11:17:23 +00:00
parent 1f847a1e2e
commit 78a30d44cf
7 changed files with 316 additions and 408 deletions
@@ -110,7 +110,7 @@ var AltDialog = Dialog.extend({
var MediaWidget = Widget.extend({
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
events: {
'input input#icon-search': '_onSearchInput',
'input input.o_we_search': '_onSearchInput',
},
/**
@@ -120,7 +120,7 @@ var MediaWidget = Widget.extend({
this._super.apply(this, arguments);
this.media = media;
this.$media = $(media);
this._onSearchInput = _.debounce(this._onSearchInput, 250);
this._onSearchInput = _.debounce(this._onSearchInput, 500);
},
//--------------------------------------------------------------------------
@@ -136,17 +136,6 @@ var MediaWidget = Widget.extend({
}
this._clear();
},
/**
* @todo comment
*/
getControlPanelConfig: function () {
return {
searchEnabled: true,
pagerEnabled: true,
pagerLeftEnabled: false,
pagerRightEnabled: false,
};
},
/**
* @abstract
* @param {string} needle
@@ -185,6 +174,7 @@ var MediaWidget = Widget.extend({
*/
_onSearchInput: function (ev) {
this.search($(ev.currentTarget).val() || '');
this.hasSearched = true;
},
});
@@ -198,11 +188,11 @@ var ImageWidget = MediaWidget.extend({
'click .o_upload_media_button_no_optimization': '_onUploadButtonNoOptimizationClick',
'change input[type=file]': '_onImageSelection',
'click .o_upload_media_url_button': '_onUploadURLButtonClick',
'input input.url': '_onChangeInputURL',
'input input.o_we_url_input': '_onURLInputChange',
'click .existing-attachments [data-src]': '_onImageClick',
'dblclick .existing-attachments [data-src]': '_onImageDblClick',
'click .o_existing_attachment_remove': '_onRemoveClick',
'click .o_load_more': '_onLoadMore',
'click .o_load_more': '_onLoadMoreClick',
}),
IMAGES_PER_ROW: 6,
@@ -214,7 +204,8 @@ var ImageWidget = MediaWidget.extend({
init: function (parent, media, options) {
this._super.apply(this, arguments);
this.IMAGES_DISPLAYED_TOTAL = this.IMAGES_PER_ROW * this.IMAGES_ROWS;
this.imagesRows = this.IMAGES_ROWS;
this.IMAGES_DISPLAYED_TOTAL = this.IMAGES_PER_ROW * this.imagesRows;
this.options = options;
this.accept = options.accept || (options.document ? '*/*' : 'image/*');
@@ -251,7 +242,7 @@ var ImageWidget = MediaWidget.extend({
var def = this._super.apply(this, arguments);
var self = this;
this._renderImages();
this._renderImages(true);
var o = {
url: null,
@@ -354,7 +345,7 @@ var ImageWidget = MediaWidget.extend({
search: function (needle, noRender) {
var self = this;
if (!noRender) {
this.$('input.url').val('').trigger('input').trigger('change');
this.$('input.o_we_url_input').val('').trigger('input').trigger('change');
}
// TODO: Expand this for adding SVG
var domain = this.domain.concat(['|', ['mimetype', '=', false], ['mimetype', this.options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]]);
@@ -403,6 +394,7 @@ var ImageWidget = MediaWidget.extend({
});
if (!noRender) {
self._renderImages();
self._adaptLoadMore();
}
});
},
@@ -411,6 +403,14 @@ var ImageWidget = MediaWidget.extend({
// Private
//--------------------------------------------------------------------------
/**
* @private
*/
_adaptLoadMore: function () {
var noMoreImgToLoad = this.IMAGES_DISPLAYED_TOTAL >= this.records.length;
this.$('.o_load_more').toggleClass('d-none', noMoreImgToLoad);
this.$('.o_load_done_msg').toggleClass('d-none', !noMoreImgToLoad);
},
/**
* @override
*/
@@ -435,30 +435,37 @@ var ImageWidget = MediaWidget.extend({
/**
* @private
*/
_renderImages: function () {
_loadMoreImages: function (forceSearch) {
this.imagesRows += 2;
this.IMAGES_DISPLAYED_TOTAL = this.imagesRows * this.IMAGES_PER_ROW;
if (!forceSearch) {
this._renderImages();
this._adaptLoadMore();
} else {
this.search(this.$('.o_we_search').val() || '');
}
},
/**
* @private
*/
_renderImages: function (withEffect) {
var self = this;
var rows = _(this.records).chain()
.slice(0, this.IMAGES_DISPLAYED_TOTAL)
.groupBy(function (a, index) { return Math.floor(index / self.IMAGES_PER_ROW); })
.values()
.value();
var noMoreImgToLoad = this.IMAGES_DISPLAYED_TOTAL >= this.records.length;
this.$('.o_load_more').toggleClass('d-none', noMoreImgToLoad);
this.$('.o_load_done_msg').toggleClass('d-none', !noMoreImgToLoad);
this.$('.form-text').empty();
//Rendering menu & content for 'Image' or 'Document' tab
var templateMenu = 'web_editor.dialog.files.submenu';
var templateContent = 'web_editor.dialog.files.existing.content';
this.$('.o_submenu').replaceWith(QWeb.render(templateMenu, {
accept: this.accept,
isDocument: this.options.document,
}));
this.$('.existing-attachments').replaceWith(QWeb.render(templateContent, {
rows: rows,
isDocument: this.options.document,
}));
// Render menu & content
this.$('.existing-attachments').replaceWith(
QWeb.render('web_editor.dialog.files.existing.content', {
rows: rows,
isDocument: this.options.document,
withEffect: withEffect,
})
);
var $divs = this.$('.o_image');
var imageDefs = _.map($divs, function (el) {
@@ -474,11 +481,13 @@ var ImageWidget = MediaWidget.extend({
return def;
}
});
$.when.apply($, imageDefs).then(function () {
_.delay(function () {
$divs.removeClass('o_image_loading');
}, 400);
});
if (withEffect) {
$.when.apply($, imageDefs).then(function () {
_.delay(function () {
$divs.removeClass('o_image_loading');
}, 400);
});
}
this._highlightSelectedImages();
},
/**
@@ -547,17 +556,10 @@ var ImageWidget = MediaWidget.extend({
}
});
this.$el.submit();
// Emptying file inputs
// Empty file input
this.$('.o_file_input').val('');
},
/**
* @private
*/
_loadMoreImages: function () {
this.IMAGES_ROWS += 2;
this.IMAGES_DISPLAYED_TOTAL = this.IMAGES_ROWS * this.IMAGES_PER_ROW;
this._renderImages();
},
//--------------------------------------------------------------------------
// Handlers
@@ -596,8 +598,7 @@ var ImageWidget = MediaWidget.extend({
*/
_onRemoveClick: function (ev) {
var self = this;
var message = _t("Are you sure you want to delete this file ?");
DialogBase.confirm(this, message, {
DialogBase.confirm(this, _t("Are you sure you want to delete this file ?"), {
confirm_callback: function () {
var $helpBlock = this.$('.form-text').empty();
var $a = $(ev.currentTarget);
@@ -616,7 +617,7 @@ var ImageWidget = MediaWidget.extend({
return;
}
$helpBlock.replaceWith(QWeb.render('web_editor.dialog.image.existing.error', {
views: prevented[id]
views: prevented[id],
}));
});
}
@@ -625,33 +626,29 @@ var ImageWidget = MediaWidget.extend({
/**
* @private
*/
_onChangeInputURL: function (ev) {
_onURLInputChange: function (ev) {
var $input = $(ev.currentTarget);
var $button = $input.next('.input-group-append').children();
var $button = this.$('.o_upload_media_url_button');
var $success = this.$('.o_we_url_success');
var $warning = this.$('.o_we_url_warning');
var $error = this.$('.o_we_url_error');
var inputValue = $input.val();
var emptyValue = (inputValue === '');
var $warning = this.$('.warning-url');
var $success = this.$('.success-url');
var $error = this.$('.error-url');
var $btn = this.$('.o_upload_media_url_button');
var regex = /^(?:https?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/;
var isURL = regex.test(inputValue);
var showWarningOrError = (inputValue.length !== 0 ? isURL : !isURL);
var isURL = /^.+\..+$/.test(inputValue); // TODO improve
var isImage = _.any(['.gif', '.jpe', '.jpg', '.png'], function (format) {
return inputValue.endsWith(format);
});
$button.toggleClass('btn-secondary', emptyValue).toggleClass('btn-primary', !emptyValue)
.prop('disabled', emptyValue);
.prop('disabled', !isURL);
if (!this.options.document) {
_.each(['.gif', '.jpe', '.jpg', '.png'], function (format) {
var endsWith = inputValue.endsWith(format);
$button.text(endsWith ? _t("Add image") : _t("Add as document"));
});
} else {
$btn.prop('disabled', !isURL);
$button.text((isURL && !isImage) ? _t("Add as document") : _t("Add image"));
}
$success.toggleClass('d-none', !isURL);
$warning.toggleClass('d-none', showWarningOrError);
$error.toggleClass('d-none', showWarningOrError);
$warning.toggleClass('d-none', !isURL || this.options.document || isImage);
$error.toggleClass('d-none', emptyValue || isURL);
},
/**
* @private
@@ -675,9 +672,17 @@ var ImageWidget = MediaWidget.extend({
/**
* @private
*/
_onLoadMore: function () {
_onLoadMoreClick: function () {
this._loadMoreImages();
},
/**
* @override
*/
_onSearchInput: function () {
this.imagesRows = this.IMAGES_ROWS;
this.IMAGES_DISPLAYED_TOTAL = this.IMAGES_PER_ROW * this.imagesRows;
this._super.apply(this, arguments);
},
});
/**
@@ -725,14 +730,6 @@ var IconWidget = MediaWidget.extend({
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
getControlPanelConfig: function () {
return _.extend(this._super.apply(this, arguments), {
pagerEnabled: false,
});
},
/**
* @override
*/
@@ -849,11 +846,11 @@ var IconWidget = MediaWidget.extend({
*/
var VideoWidget = MediaWidget.extend({
template: 'web_editor.dialog.video',
events: {
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
@@ -891,15 +888,6 @@ var VideoWidget = MediaWidget.extend({
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
getControlPanelConfig: function () {
return _.extend(this._super.apply(this, arguments), {
searchEnabled: false,
pagerEnabled: false,
});
},
/**
* @override
*/
@@ -1075,7 +1063,6 @@ var VideoWidget = MediaWidget.extend({
// Hide the entire options box if no options are available
$optBox.toggleClass('d-none', $optBox.find('div:not(.d-none)').length === 0);
$('.option_title').toggleClass('d-none');
if (query.type === 'yt') {
// Youtube only: If 'hide controls' is checked, hide 'fullscreen'
@@ -1291,29 +1291,8 @@ body .modal {
max-width: 80%;
.modal-body {
.nav-tabs .active > a[href="#editor-media-icon"] {
background-color: white;
}
li.search {
.form-group {
position: relative;
margin: 0 10px 0 0;
> input {
padding-right: 30px;
}
> span {
@include o-position-absolute(7px, 7px);
font-size: 1.3em;
}
}
}
.card {
border-color: gray('200');
border-top-width: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
.tab-pane {
min-height: 300px;
}
.o_existing_attachment_cell {
@@ -1357,8 +1336,6 @@ body .modal {
}
}
.font-icons-icons {
padding: 20px 0;
> span {
text-align: center;
font-size: 22px;
@@ -1377,34 +1354,14 @@ body .modal {
}
#editor-media-image, #editor-media-document {
.card {
label {
margin: auto 10px;
}
.btn-group {
margin-right: 10px;
}
}
.o_size_fa_icon {
font-size: 1.4rem;
}
.o_size_dropdown_item {
font-size: 0.9rem;
.o_we_url_input {
width: 300px;
}
}
// VIDEO TAB
#editor-media-video .o_video_dialog_container {
padding: 0;
display: flex;
flex-flow: row wrap;
#editor-media-video {
.o_video_dialog_form {
flex: 1 1 auto;
min-width: 0;
padding: 20px;
#o_video_form_group {
position: relative;
width: 100%;
@@ -1416,17 +1373,17 @@ body .modal {
overflow-y: scroll;
}
> .o_validate_feedback {
.o_validate_feedback {
@include o-position-absolute(auto, 10px, 10px);
> .fa {
display: none;
}
}
&.o_has_error > .o_validate_feedback > .fa-exclamation-triangle {
&.o_has_error .o_validate_feedback > .fa-exclamation-triangle {
display: block;
}
&.o_has_success > .o_validate_feedback > .fa-check {
&.o_has_success .o_validate_feedback > .fa-check {
display: block;
}
}
@@ -1434,12 +1391,8 @@ body .modal {
#video-preview {
position: relative;
flex: 1 0 50%;
display: flex;
flex-flow: column nowrap;
justify-content: center;
@include o-we-preview-box(0 19px);
@include o-we-preview-box();
border: none;
.media_iframe_video {
@@ -1504,6 +1457,7 @@ body .modal {
.o_bg_img_opt_cover_edition {
@include o-we-preview-box;
padding: 0 25px 25px 25px;
h6 {
color: white;
@@ -45,8 +45,7 @@ $o-we-switch-inactive-color: $o-we-color-paper !default;
// ============ 'Preview' component Mixins ===============
// Create a "preview" work-area to edit/customize specific elements
@mixin o-we-preview-box($padding: 0px 25px 25px 25px, $color-text: white) {
padding: $padding;
@mixin o-we-preview-box($color-text: white) {
border-top: 1px solid black;
border-bottom: 1px solid white;
background-image: linear-gradient(-150deg, $o-we-color-dark, $o-we-color-darker);
+92 -123
View File
@@ -66,12 +66,21 @@
<div role="tabpanel" class="tab-pane fade" id="editor-media-video"/>
</div>
</div>
<t t-name="web_editor.dialog.media.search">
<div class="input-group ml-auto">
<input type="text" class="form-control o_we_search" t-att-placeholder="searchPlaceholder.trim()"/>
<div class="input-group-append">
<div class="input-group-text o_we_search_icon">
<i class="fa fa-search" title="Search" role="img" aria-label="Search"/>
</div>
</div>
</div>
</t>
<!-- Image choosing part of the Media Dialog -->
<t t-name="web_editor.dialog.image">
<t t-set="iframeName" t-value="_.uniqueId('fileframe-')"/>
<form class="form-inline"
method="POST"
<form method="POST"
action="/web_editor/attachment/add"
enctype="multipart/form-data"
t-att-target="iframeName">
@@ -79,108 +88,71 @@
<input type="hidden" name="filters" t-att-value="widget.firstFilters.join('_')"/>
<input t-if="widget.options.res_id" type="hidden" name="res_id" t-att-value="widget.options.res_id"/>
<input t-if="widget.options.res_model" type="hidden" name="res_model" t-att-value="widget.options.res_model"/>
<div class="mt-4 w-100">
<div class="mb-4">
<div class="o_submenu"></div>
</div>
<div class="form-text"/>
<div class="existing-attachments">
<t t-call="web_editor.dialog.image.existing"/>
</div>
<div class="mt-4 text-center o_load_elements mx-auto">
<button class="btn btn-primary o_load_more" type="button">Load more...</button>
<div class="mt-4 o_load_done_msg d-none">
<span><i>All attachments have been loaded</i></span>
</div>
<t t-call="web_editor.dialog.files.submenu">
<t t-set="isDocument" t-value="widget.options.document"/>
<t t-set="accept" t-value="widget.accept"/>
</t>
<div class="form-text"/>
<div class="existing-attachments"/>
<div class="mt-4 text-center mx-auto">
<button class="btn btn-primary o_load_more" type="button">Load more...</button>
<div class="mt-4 o_load_done_msg d-none">
<span><i>All attachments have been loaded</i></span>
</div>
</div>
<iframe class="o_file_upload_iframe d-none" t-att-name="iframeName"/>
</form>
</t>
<t t-name="web_editor.dialog.files.submenu">
<div class="o_submenu">
<div class="row">
<div class="col-sm">
<div class="form-inline row no-gutters">
<input type="file" class="d-none o_file_input" name="upload" t-att-accept="accept" multiple="multiple"/>
<input type="hidden" name="disable_optimization" value=""/>
<div class="btn-group o_btn_group_image">
<button class="btn btn-primary o_upload_media_button" type="button">
<t t-if="isDocument">Upload a document</t>
<t t-else="">Upload an image</t>
</button>
<t t-if="!isDocument">
<button class="btn btn-primary dropdown-toggle dropdown-toggle-split o_media_optimization" data-toggle="dropdown" type="button">
<span class="sr-only">Alternate Upload</span>
</button>
<div class="dropdown-menu" role="menu">
<a role="menuitem" class="dropdown-item o_upload_media_button_no_optimization">Upload image without optimization</a>
</div>
</t>
</div>
<div class="input-group col-8 ml-2">
<input type="text" class="form-control url col-7" name="url" t-attf-placeholder="https://www.odoo.com/#{isDocument ? 'mydocument' : 'logo.png'}"/>
<div class="input-group-append ml-2">
<button type="button" class="btn btn-sm btn-secondary o_upload_media_url_button" disabled="disabled">
<t t-if="isDocument">Add document</t>
<t t-else="">Add image</t>
</button>
</div>
<div class="ml-2 o_size_fa_icon">
<span class="success-url text-success d-none fa fa-check" title="The URL seems valid"/>
<t t-if="isDocument">
<span class="error-url text-danger d-none fa fa-times" title="This URL does not seem to work"/>
</t>
<t t-else="">
<span class="warning-url text-warning d-none fa fa-warning" title="The URL provided does not contain any image. It will be added in the document section."/>
</t>
</div>
</div>
<div class="form-inline align-items-center py-4">
<input type="file" class="d-none o_file_input" name="upload" t-att-accept="accept" multiple="multiple"/>
<input type="hidden" name="disable_optimization" value=""/>
<div class="btn-group">
<button type="button" class="btn btn-primary o_upload_media_button">
<t t-if="isDocument">Upload a document</t>
<t t-else="">Upload an image</t>
</button>
<t t-if="!isDocument">
<button class="btn btn-primary dropdown-toggle dropdown-toggle-split o_media_optimization" data-toggle="dropdown" type="button">
<span class="sr-only">Alternate Upload</span>
</button>
<div class="dropdown-menu" role="menu">
<button type="button" role="menuitem" class="dropdown-item o_upload_media_button_no_optimization">Upload image without optimization</button>
</div>
</t>
</div>
<div class="input-group align-items-center ml-2">
<input type="text" class="form-control o_we_url_input" name="url" t-attf-placeholder="https://www.odoo.com/#{isDocument ? 'mydocument' : 'logo.png'}"/>
<div class="input-group-append align-items-center">
<button type="button" class="btn btn-secondary o_upload_media_url_button" disabled="disabled">
<t t-if="isDocument">Add document</t>
<t t-else="">Add image</t>
</button>
<div class="ml-2">
<span class="o_we_url_success text-success d-none fa fa-lg fa-check" title="The URL seems valid"/>
<span class="o_we_url_warning text-warning d-none fa fa-lg fa-warning" title="The URL does not contain any image. The file will be added in the document section."/>
<span class="o_we_url_error text-danger d-none fa fa-lg fa-times" title="The URL does not seem to work"/>
</div>
</div>
<div class="col-sm text-right">
<ul class="list-unstyled row">
<li class="nav-item ml-auto col-6 search">
<div class="form-group o_search_div">
<t t-set="searchPlaceholder">
<t t-if="isDocument">Search a document</t>
<t t-else="">Search an image</t>
</t>
<input class="form-control w-100" id="icon-search" type="search" t-att-placeholder="searchPlaceholder.trim()"/>
<span class="fa fa-search" title="Search" role="img" aria-label="Search"/>
</div>
</li>
</ul>
</div>
</div>
</div>
</t>
<t t-name="web_editor.dialog.image.existing">
<div role="dialog" class="modal" tabindex="-1">
<div class="modal-dialog select-image">
<div class="modal-content">
<header class="modal-header">
<h3 class="modal-title">Select a Picture</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
</header>
<main class="modal-body o_has_error">
<div class="existing-attachments"/>
<div class="form-text"/>
</main>
<footer class="modal-footer">
<button data-dismiss="modal" type="button" aria-label="Discard">Discard</button>
</footer>
</div>
</div>
<t t-call="web_editor.dialog.media.search">
<t t-set="searchPlaceholder">
<t t-if="isDocument">Search a document</t>
<t t-else="">Search an image</t>
</t>
</t>
</div>
</t>
<t t-name="web_editor.dialog.files.existing.content">
<div class="existing-attachments">
<div class="row mt16 ml-auto" t-as="row" t-foreach="rows">
<div class="col-2 o_existing_attachment_cell" t-as="attachment" t-foreach="row">
<div t-foreach="rows" t-as="row" class="row mt-2">
<div t-foreach="row" t-as="attachment" class="col-2 o_existing_attachment_cell">
<i t-if="attachment.res_model === 'ir.ui.view'" class="fa fa-trash o_existing_attachment_remove" title="This file is a public view attachment" role="img" aria-label="This file is a public view attachment" t-att-data-id="attachment.id"/>
<i t-else="" class="fa fa-trash o_existing_attachment_remove" title="This file is attached to the current record" role="img" aria-label="This file is attached to the current record" t-att-data-id="attachment.id"/>
<div class="o_attachment_border" t-att-style="attachment.res_model === 'ir.ui.view' ? null : 'border: 1px solid #5cb85c;'"><div t-att-data-src="attachment.src" t-att-data-url="attachment.url" role="img" t-att-aria-label="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" t-att-data-mimetype="attachment.mimetype" class="o_image o_image_loading"/></div>
<div class="o_attachment_border" t-att-style="attachment.res_model === 'ir.ui.view' ? null : 'border: 1px solid #5cb85c;'"><div t-att-data-src="attachment.src" t-att-data-url="attachment.url" role="img" t-att-aria-label="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" t-att-data-mimetype="attachment.mimetype" t-attf-class="o_image #{withEffect ? 'o_image_loading' : ''}"/></div>
<t t-if="isDocument">
<small class="o_file_name"><t t-esc="attachment.name"/></small>
</t>
@@ -204,14 +176,11 @@
<!-- Icon choosing part of the Media Dialog -->
<t t-name="web_editor.dialog.font-icons">
<form action="#" class="mt-2">
<div class="d-flex flex-row-reverse">
<li class="nav-item search list-unstyled col-3 p-2">
<div class="form-group">
<input class="form-control" id="icon-search" type="search" placeholder="Search a pictogram"/>
<span class="fa fa-search" title="Search" role="img" aria-label="Search"/>
</div>
</li>
<form action="#">
<div class="form-inline align-items-center py-4">
<t t-call="web_editor.dialog.media.search">
<t t-set="searchPlaceholder">Search a pictogram</t>
</t>
</div>
<div class="font-icons-icons">
<t t-call="web_editor.dialog.font-icons.icons">
@@ -232,57 +201,57 @@
</t>
<!-- Video choosing part of the Media Dialog -->
<div t-name="web_editor.dialog.video" class="o_video_dialog_container mt-4">
<form action="#" class="form-inline w-100">
<div class="o_video_dialog_form">
<t t-name="web_editor.dialog.video">
<form action="#" class="row">
<div class="col mt-4 o_video_dialog_form">
<div class="form-group mb-2" id="o_video_form_group">
<label>Video code </label>
<small class="ml-1 form-text text-muted"> (URL or Embed)</small>
<label class="ml-2 col-form-label o_validate_feedback position-static" for="o_video_text">
<i class="fa fa-check text-success" role="img" aria-label="Checked" title="Checked"/>
<i class="fa fa-exclamation-triangle text-danger" role="img" aria-label="Attention" title="Attention"/>
<label class="col-form-label" for="o_video_text">
Video code <small>(URL or Embed)</small>
<div class="o_validate_feedback">
<i class="fa fa-check text-success" role="img" aria-label="Checked" title="Checked"/>
<i class="fa fa-exclamation-triangle text-danger" role="img" aria-label="Attention" title="Attention"/>
</div>
</label>
<textarea class="form-control url" id="o_video_text" placeholder="Copy-paste your URL or embed code here"/>
<textarea class="form-control" id="o_video_text" placeholder="Copy-paste your URL or embed code here"/>
</div>
<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 d-none mt-4">
<div class="option_title mb-2 d-flex">
<label>Options</label>
</div>
<div class="o_yt_option o_vim_option o_dm_option d-flex">
<div class="o_yt_option o_vim_option o_dm_option">
<label class="o_switch mb0"><input id="o_video_autoplay" type="checkbox"/><span/>Autoplay</label>
</div>
<div class="o_yt_option o_vim_option d-flex">
<div class="o_yt_option o_vim_option">
<label class="o_switch mb0"><input id="o_video_loop" type="checkbox"/><span/>Loop</label>
</div>
<div class="o_yt_option o_dm_option d-flex">
<div class="o_yt_option o_dm_option">
<label class="o_switch mb0"><input id="o_video_hide_controls" type="checkbox"/><span/>Hide player controls</label>
</div>
<div class="o_yt_option d-flex">
<div class="o_yt_option">
<label class="o_switch mb0"><input id="o_video_hide_fullscreen" type="checkbox"/><span/>Hide fullscreen button</label>
</div>
<div class="o_yt_option d-flex">
<div class="o_yt_option">
<label class="o_switch mb0"><input id="o_video_hide_yt_logo" type="checkbox"/><span/>Hide Youtube logo</label>
</div>
<div class="o_dm_option d-flex">
<div class="o_dm_option">
<label class="o_switch mb0"><input id="o_video_hide_dm_logo" type="checkbox"/><span/>Hide Dailymotion logo</label>
</div>
<div class="o_dm_option d-flex">
<div class="o_dm_option">
<label class="o_switch mb0"><input id="o_video_hide_dm_share" type="checkbox"/><span/>Hide sharing button</label>
</div>
</div>
</div>
<div id="video-preview">
<div class="o_video_dialog_preview_text small mt16 mb8 d-none">Preview</div>
<div class="media_iframe_video">
<div class="media_iframe_video_size"/>
<iframe class="o_video_dialog_iframe" allowfullscreen="allowfullscreen" frameborder="0" src=""/>
<div class="col-md-6">
<div id="video-preview" class="p-3">
<div class="o_video_dialog_preview_text small mb-2 d-none">Preview</div>
<div class="media_iframe_video">
<div class="media_iframe_video_size"/>
<iframe class="o_video_dialog_iframe" allowfullscreen="allowfullscreen" frameborder="0" src=""/>
</div>
</div>
</div>
</form>
</div>
</t>
<!-- Link Dialog (allows to choose a style and content for a link on the page) -->
<div t-name="web_editor.dialog.link" class="o_link_dialog">
@@ -354,7 +323,7 @@
<div class="form-group text-center">
<label>Preview</label>
<div style="overflow: auto; max-width: 100%; max-height: 200px;">
<a href="#" type="button" id="link-preview" aria-label="Preview" title="Preview"/>
<a href="#" id="link-preview" aria-label="Preview" title="Preview"/>
</div>
</div>
</div>
@@ -4,7 +4,6 @@ odoo.define('web_unsplash.image_widgets', function (require) {
var core = require('web.core');
var UnsplashAPI = require('unsplash.api');
var weWidgets = require('web_editor.widget');
var QWeb = core.qweb;
var unsplashAPI = null;
@@ -16,32 +15,35 @@ weWidgets.ImageWidget.include({
'dblclick .unsplash_img_container [data-imgid]': '_onUnsplashImgDblClick',
'click .unsplash_img_container [data-imgid]': '_onUnsplashImgClick',
'click button.save_unsplash': '_onSaveUnsplash',
'click button.access_key': '_onSetAccessKey',
'click .o_load_more': '_onLoadMore',
'click .o_search_from_unsplash': '_onSearchFromUnsplash',
'click .o_search_an_image': '_onSearchImage',
'keyup input#icon-search, .o_dropdown_search': '_onPressKeySearch',
'click .o_search_an_image, .o_search_from_unsplash': '_onSourceSwitchClick',
'keyup .o_we_search, .o_search_an_image, .o_search_from_unsplash': '_onPressKeySearch',
}),
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
this._unsplash = {
isActive: false,
selectedImages: {},
isMaxed: false,
query: false,
error: false,
};
// TODO This is a `hack` to prevent the UnsplashAPI to be destroyed every time
// the media dialog is closed.
// Indeed, UnsplashAPI has a cache system to recude unsplash call, it is
// then better to keep its state to benefic from it from one media dialog
// call to another.
// Unsplash API will either be (it's still being discussed):
// * a service (ideally coming with an improvement to not auto load the service)
// * initialized in the website_root (trigger_up)
var def = this._super.apply(this, arguments);
// TODO improve this
//
// This is a `hack` to prevent the UnsplashAPI to be destroyed every
// time the media dialog is closed. Indeed, UnsplashAPI has a cache
// system to recude unsplash call, it is then better to keep its state
// to take advantage from it from one media dialog call to another.
//
// Unsplash API will either be (it's still being discussed):
// * a service (ideally coming with an improvement to not auto load
// the service)
// * initialized in the website_root (trigger_up)
if (unsplashAPI === null) {
this.unsplashAPI = new UnsplashAPI(this);
unsplashAPI = this.unsplashAPI;
@@ -49,8 +51,19 @@ weWidgets.ImageWidget.include({
this.unsplashAPI = unsplashAPI;
this.unsplashAPI.setParent(this);
}
return def;
},
/**
* @override
*/
start: function () {
if (!this.options.document) {
this.$('.o_we_search_icon').replaceWith(core.qweb.render('web_unsplash.dialog.media.search_icon'));
}
return this._super.apply(this, arguments);
},
/**
* @override
*/
destroy: function () {
// TODO See `hack` explained in `init`. This prevent the media dialog destroy
// to destroy unsplashAPI when destroying the children
@@ -62,19 +75,6 @@ weWidgets.ImageWidget.include({
// Public
// --------------------------------------------------------------------------
/**
* @override
*/
getControlPanelConfig: function () {
var config = this._super.apply(this, arguments);
if (this._unsplash.query) {
_.extend(config, {
pagerLeftEnabled: this.page > 1,
pagerRightEnabled: !this._unsplash.isMaxed,
});
}
return config;
},
/**
* @override
*/
@@ -102,74 +102,102 @@ weWidgets.ImageWidget.include({
return _super.apply(self, args);
});
},
/**
* @override
*/
search: function (needle, noRender) {
var self = this;
if (!this._unsplash.isActive) {
return this._super.apply(this, arguments);
}
this._unsplash.query = needle;
return this.unsplashAPI.getImages(needle, this.IMAGES_DISPLAYED_TOTAL).then(function (res) {
self._unsplash.isMaxed = res.isMaxed;
self._unsplash.records = res.images;
self._unsplash.error = false;
}, function (err) {
self._unsplash.error = err;
}).always(function () {
if (!noRender) {
self._renderImages();
self._adaptLoadMore();
}
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Highlights selected image, when an image is clicked and when pager change
*
* @private
*/
_adaptLoadMore: function () {
if (!this._unsplash.isActive) {
return this._super.apply(this, arguments);
}
this.$('.o_load_more').toggleClass('d-none', !!this._unsplash.error || this._unsplash.isMaxed);
this.$('.o_load_done_msg').toggleClass('d-none', !!this._unsplash.error || !this._unsplash.isMaxed);
},
/**
* @override
*/
_highlightSelectedImages: function () {
var self = this;
if (!this._unsplash.query) {
if (!this._unsplash.isActive) {
return this._super.apply(this, arguments);
}
this.$('.o_unsplash_img_cell.o_selected').removeClass("o_selected");
this.$('.o_unsplash_img_cell.o_selected').removeClass('o_selected');
var $select = this.$('.o_unsplash_img_cell [data-imgid]').filter(function () {
return $(this).data('imgid') in self._unsplash.selectedImages;
});
$select.closest('.o_unsplash_img_cell').addClass("o_selected");
$select.closest('.o_unsplash_img_cell').addClass('o_selected');
return $select;
},
/**
* @private
*/
_loadMoreImages: function (forceSearch) {
this._super(this._unsplash.isActive || forceSearch);
},
/**
* @override
*/
_renderImages: function () {
var self = this;
if (!this._unsplash.query) {
if (!this._unsplash.isActive) {
return this._super.apply(this, arguments);
}
this.unsplashAPI.getImages(this._unsplash.query, this.IMAGES_PER_PAGE, this.page).then(function (res) {
self._unsplash.isMaxed = res.isMaxed;
var rows = _(res.images).chain()
.groupBy(function (a, index) { return Math.floor(index / self.IMAGES_PER_ROW); })
.values()
.value();
self.$('.unsplash_img_container').html(QWeb.render('web_unsplash.dialog.image.content', {rows: rows}));
self._highlightSelectedImages();
}).fail(function (err) {
self.$('.unsplash_img_container').html(QWeb.render('web_unsplash.dialog.error.content', { status: err }));
}).always(function () {
self._toggleAttachmentContaines(false);
});
if (this._unsplash.error) {
this.$('.unsplash_img_container').html(
core.qweb.render('web_unsplash.dialog.error.content', {
status: this._unsplash.error,
})
);
return;
}
var rows = _(this._unsplash.records).chain()
.groupBy(function (a, index) { return Math.floor(index / self.IMAGES_PER_ROW); })
.values()
.value();
this.$('.unsplash_img_container').html(core.qweb.render('web_unsplash.dialog.image.content', {rows: rows}));
this._highlightSelectedImages();
},
/**
* @private
* @param {boolean} show
*/
_toggleAttachmentContaines: function (hideUnsplash) {
this.$('.existing-attachments').toggleClass('d-none', !hideUnsplash);
this.$('.unsplash_img_container').toggleClass('d-none', hideUnsplash);
this.$('.o_load_more').toggleClass('d-none', hideUnsplash);
this.$('.o_load_done_msg').toggleClass('d-none', !hideUnsplash);
this.trigger_up('update_control_panel');
},
/**
* @private
*/
_toggleLoadMoreBtnUnsplash: function (display) {
this.$('.o_load_done_msg').toggleClass('d-none', display);
},
/**
* @private
*/
_toggleSearchDropdown: function (show) {
var inputValue = this.$('#icon-search').val();
this.$('.o_search_value_input').text(inputValue);
this.$('.o_dropdown_search').toggleClass('show', show);
_toggleUnsplashContainer: function (show) {
this._unsplash.isActive = show;
this.$('.existing-attachments').toggleClass('d-none', show);
this.$('.unsplash_img_container').toggleClass('d-none', !show);
this.$('.o_we_search_icon > span').text(show ? "Unsplash" : "");
},
//--------------------------------------------------------------------------
@@ -204,37 +232,20 @@ weWidgets.ImageWidget.include({
}
}
},
/**
* @private
*/
_onChangeUnsplashSearch: _.debounce(function () {
// oldPage saves the original image widget pager.
// Emptying the unsplash search will set the pager to its previous state
this._unsplash.query = this.$('#icon-search').val().trim();
if (this._unsplash.query) {
this.oldPage = this.page;
this.page = 1;
this._renderImages();
} else {
this.page = this.oldPage || 1;
this._toggleAttachmentContaines(true);
}
this._toggleLoadMoreBtnUnsplash(true);
}, 1000),
/**
* @private
*/
_onUnsplashImgClick: function (ev) {
var imgid = $(ev.currentTarget).data('imgid');
var url = $(ev.currentTarget).data('url');
var downloadUrl = $(ev.currentTarget).data('download-url');
var downloadURL = $(ev.currentTarget).data('download-url');
if (!this.multiImages) {
this._unsplash.selectedImages = {};
}
if (imgid in this._unsplash.selectedImages) {
delete this._unsplash.selectedImages[imgid];
} else {
this._unsplash.selectedImages[imgid] = {url: url, downloadUrl: downloadUrl};
this._unsplash.selectedImages[imgid] = {url: url, download_url: downloadURL};
}
this._highlightSelectedImages();
},
@@ -246,46 +257,25 @@ weWidgets.ImageWidget.include({
},
/**
* @private
* @param {Event} ev
*/
_onLoadMore: function () {
this._unsplash.query = this.$('#icon-search').val().trim();
this._loadMoreImages();
},
/**
* @private
*/
_onSearchFromUnsplash: function () {
this.$('.o_dropdown_search').removeClass('show');
this._toggleAttachmentContaines(false);
this._onChangeUnsplashSearch();
},
/**
* @private
*/
_onSearchImage: function () {
var searchText = this.$('#icon-search').val().trim();
this._unsplash.query = false;
this.search(searchText);
this.$('.o_dropdown_search').removeClass('show');
this._toggleAttachmentContaines(true);
this._renderImages();
_onSourceSwitchClick: function (ev) {
this._toggleUnsplashContainer(!$(ev.currentTarget).is('.o_search_an_image'));
this.search(this.$('.o_we_search').val() || '');
},
/**
* @private
*/
_onPressKeySearch: function (ev) {
var $searchAnImage = this.$('button.dropdown-item.o_search_an_image');
switch (ev.which) {
case $.ui.keyCode.ENTER:
$searchAnImage.focus();
break;
case $.ui.keyCode.UP:
$searchAnImage.focus();
this.$('.o_search_an_image').focus();
break;
case $.ui.keyCode.DOWN:
$searchAnImage.next().focus();
this.$('.o_search_from_unsplash').focus();
break;
default: return;
default:
return;
}
ev.preventDefault();
},
@@ -294,11 +284,19 @@ weWidgets.ImageWidget.include({
*/
_onSearchInput: function (ev) {
if (!this.options.document) {
this._toggleSearchDropdown(true);
} else {
this.search($(ev.currentTarget).val() || '');
var $input = $(ev.currentTarget);
var inputValue = $input.val();
this.$('.o_search_value_input').text(inputValue);
var $icon = this.$('.o_we_search_icon');
if ($icon.parent().is('.show')) {
$icon.dropdown('update');
} else if (!this.hasSearched) {
$icon.dropdown('toggle');
$input.focus();
}
}
this._super.apply(this, arguments);
},
});
});
@@ -27,12 +27,11 @@ var UnsplashCore = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
*
* @param {String} query search terms
* @param {Integer} pageSize number of image to display per page
* @param {Integer} pageNumber page number to retrieve
*/
getImages: function (query, pageSize, pageNumber) {
getImages: function (query, pageSize) {
var self = this;
var to = pageSize * pageNumber;
var from = to - pageSize;
var from = 0;
var to = pageSize;
var cachedData = this._cache[query];
if (cachedData && (cachedData.images.length >= to || (cachedData.totalImages !== 0 && cachedData.totalImages < to))) {
@@ -7,19 +7,23 @@
</t>
</t>
<t t-extend="web_editor.dialog.files.submenu">
<t t-jquery=".o_search_div" t-operation="append">
<div class="dropdown-menu o_dropdown_search w-100" style="background-color:#efefef;">
<button class="dropdown-item o_search_an_image o_size_dropdown_item" type="button">
<t t-name="web_unsplash.dialog.media.search_icon" t-extend="">
<div class="dropdown">
<button type="button" class="btn btn-primary dropdown-toggle o_we_search_icon" data-toggle="dropdown">
<i class="fa fa-search"/>
<span/>
</button>
<div class="dropdown-menu dropdown-menu-right">
<button class="dropdown-item o_search_an_image" type="button">
Search among my images for:
<div class="font-weight-bold o_search_value_input"></div>
</button>
<button class="dropdown-item o_search_from_unsplash o_size_dropdown_item" type="button">
<button class="dropdown-item o_search_from_unsplash" type="button">
Search from Unsplash for:
<div class="font-weight-bold o_search_value_input"></div>
</button>
</div>
</t>
</div>
</t>
<t t-name="web_unsplash.dialog.image.content">
@@ -39,16 +43,14 @@
</t>
<t t-name="web_unsplash.dialog.error.credentials">
<h4 class="text-muted">
<t t-esc="title"/>
</h4>
<h4><t t-esc="title"/></h4>
<div class="details">
<t t-esc="subtitle"/>
<div class="form-group btn-group mt-4 access_key_box">
<div class="form-group mt-4 access_key_box">
<input type="text" class="form-control w-100" id="accessKeyInput" placeholder="Paste your access key here"/>
</div>
<a href="https://www.odoo.com/documentation/user/general/unsplash/unsplash_access_key.html" target="_blank"><i class="fa fa-arrow-right"/> Generate an access key</a>
<div class="form-group btn-group mt-4 access_key_box">
<div class="form-group mt-4 access_key_box">
<input type="text" class="form-control w-100" id="appIdInput" placeholder="Paste your application ID here"/>
</div>
<a href="https://www.odoo.com/documentation/user/general/unsplash/unsplash_application_id.html" target="_blank"><i class="fa fa-arrow-right"/> How to find my Unsplash Application ID?</a>
@@ -57,8 +59,8 @@
</t>
<t t-name="web_unsplash.dialog.error.content">
<div class="row mt16 text-muted unsplash_error">
<div class="m-auto text-center">
<div class="d-flex mt-2 unsplash_error">
<div class="mx-auto text-center">
<t t-if="status == 'key_not_found'">
<t t-call="web_unsplash.dialog.error.credentials">
<t t-set="title">