[REF] website: move s_image_gallery snippet to dedicated files
This commit is contained in:
@@ -44,6 +44,7 @@
|
||||
'views/snippets/s_btn.xml',
|
||||
'views/snippets/s_hr.xml',
|
||||
'views/snippets/s_facebook_page.xml',
|
||||
'views/snippets/s_image_gallery.xml',
|
||||
'views/website_views.xml',
|
||||
'views/website_visitor_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
|
||||
@@ -805,162 +805,6 @@ registry.ul = publicWidget.Widget.extend({
|
||||
},
|
||||
});
|
||||
|
||||
registry.gallery = publicWidget.Widget.extend({
|
||||
selector: '.o_gallery:not(.o_slideshow)',
|
||||
xmlDependencies: ['/website/static/src/xml/website.gallery.xml'],
|
||||
events: {
|
||||
'click img': '_onClickImg',
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when an image is clicked. Opens a dialog to browse all the images
|
||||
* with a bigger size.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onClickImg: function (ev) {
|
||||
var self = this;
|
||||
var $cur = $(ev.currentTarget);
|
||||
|
||||
var urls = [];
|
||||
var idx = undefined;
|
||||
var milliseconds = undefined;
|
||||
var params = undefined;
|
||||
var $images = $cur.closest('.o_gallery').find('img');
|
||||
var size = 0.8;
|
||||
var dimensions = {
|
||||
min_width: Math.round(window.innerWidth * size * 0.9),
|
||||
min_height: Math.round(window.innerHeight * size),
|
||||
max_width: Math.round(window.innerWidth * size * 0.9),
|
||||
max_height: Math.round(window.innerHeight * size),
|
||||
width: Math.round(window.innerWidth * size * 0.9),
|
||||
height: Math.round(window.innerHeight * size)
|
||||
};
|
||||
|
||||
$images.each(function () {
|
||||
urls.push($(this).attr('src'));
|
||||
});
|
||||
var $img = ($cur.is('img') === true) ? $cur : $cur.closest('img');
|
||||
idx = urls.indexOf($img.attr('src'));
|
||||
|
||||
milliseconds = $cur.closest('.o_gallery').data('interval') || false;
|
||||
var $modal = $(qweb.render('website.gallery.slideshow.lightbox', {
|
||||
srcs: urls,
|
||||
index: idx,
|
||||
dim: dimensions,
|
||||
interval: milliseconds || 0,
|
||||
id: _.uniqueId('slideshow_'),
|
||||
}));
|
||||
$modal.modal({
|
||||
keyboard: true,
|
||||
backdrop: true,
|
||||
});
|
||||
$modal.on('hidden.bs.modal', function () {
|
||||
$(this).hide();
|
||||
$(this).siblings().filter('.modal-backdrop').remove(); // bootstrap leaves a modal-backdrop
|
||||
$(this).remove();
|
||||
});
|
||||
$modal.find('.modal-content, .modal-body.o_slideshow').css('height', '100%');
|
||||
$modal.appendTo(document.body);
|
||||
|
||||
$modal.one('shown.bs.modal', function () {
|
||||
self.trigger_up('widgets_start_request', {
|
||||
editableMode: false,
|
||||
$target: $modal.find('.modal-body.o_slideshow'),
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
registry.gallerySlider = publicWidget.Widget.extend({
|
||||
selector: '.o_slideshow',
|
||||
xmlDependencies: ['/website/static/src/xml/website.gallery.xml'],
|
||||
disabledInEditableMode: false,
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
this.$carousel = this.$target.is('.carousel') ? this.$target : this.$target.find('.carousel');
|
||||
this.$indicator = this.$carousel.find('.carousel-indicators');
|
||||
this.$prev = this.$indicator.find('li.o_indicators_left').css('visibility', ''); // force visibility as some databases have it hidden
|
||||
this.$next = this.$indicator.find('li.o_indicators_right').css('visibility', '');
|
||||
var $lis = this.$indicator.find('li[data-slide-to]');
|
||||
var nbPerPage = Math.floor(this.$indicator.width() / $lis.first().outerWidth(true)) - 3; // - navigator - 1 to leave some space
|
||||
var realNbPerPage = nbPerPage || 1;
|
||||
var nbPages = Math.ceil($lis.length / realNbPerPage);
|
||||
|
||||
var index;
|
||||
var page;
|
||||
update();
|
||||
|
||||
function hide() {
|
||||
$lis.each(function (i) {
|
||||
$(this).toggleClass('d-none', i < page * nbPerPage || i >= (page + 1) * nbPerPage);
|
||||
});
|
||||
if (self.editableMode) { // do not remove DOM in edit mode
|
||||
return;
|
||||
}
|
||||
if (page <= 0) {
|
||||
self.$prev.detach();
|
||||
} else {
|
||||
self.$prev.prependTo(self.$indicator);
|
||||
}
|
||||
if (page >= nbPages - 1) {
|
||||
self.$next.detach();
|
||||
} else {
|
||||
self.$next.appendTo(self.$indicator);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
index = $lis.index($lis.filter('.active')) || 0;
|
||||
page = Math.floor(index / realNbPerPage);
|
||||
hide();
|
||||
}
|
||||
|
||||
this.$carousel.on('slide.bs.carousel.gallery_slider', function () {
|
||||
setTimeout(function () {
|
||||
var $item = self.$carousel.find('.carousel-inner .carousel-item-prev, .carousel-inner .carousel-item-next');
|
||||
var index = $item.index();
|
||||
$lis.removeClass('active')
|
||||
.filter('[data-slide-to="' + index + '"]')
|
||||
.addClass('active');
|
||||
}, 0);
|
||||
});
|
||||
this.$indicator.on('click.gallery_slider', '> li:not([data-slide-to])', function () {
|
||||
page += ($(this).hasClass('o_indicators_left') ? -1 : 1);
|
||||
page = Math.max(0, Math.min(nbPages - 1, page)); // should not be necessary
|
||||
self.$carousel.carousel(page * realNbPerPage);
|
||||
hide();
|
||||
});
|
||||
this.$carousel.on('slid.bs.carousel.gallery_slider', update);
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if (!this.$indicator) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$prev.prependTo(this.$indicator);
|
||||
this.$next.appendTo(this.$indicator);
|
||||
this.$carousel.off('.gallery_slider');
|
||||
this.$indicator.off('.gallery_slider');
|
||||
},
|
||||
});
|
||||
|
||||
registry.socialShare = publicWidget.Widget.extend({
|
||||
selector: '.oe_social_share',
|
||||
xmlDependencies: ['/website/static/src/xml/website.share.xml'],
|
||||
@@ -844,414 +844,6 @@ options.registry.collapse = options.Class.extend({
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.gallery = options.Class.extend({
|
||||
xmlDependencies: ['/website/static/src/xml/website.gallery.xml'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
// The snippet should not be editable
|
||||
this.$target.addClass('o_fake_not_editable').attr('contentEditable', false);
|
||||
|
||||
// Make sure image previews are updated if images are changed
|
||||
this.$target.on('save', 'img', function (ev) {
|
||||
var $img = $(ev.currentTarget);
|
||||
var index = self.$target.find('.carousel-item.active').index();
|
||||
self.$('.carousel:first li[data-target]:eq(' + index + ')')
|
||||
.css('background-image', 'url(' + $img.attr('src') + ')');
|
||||
});
|
||||
|
||||
// When the snippet is empty, an edition button is the default content
|
||||
// TODO find a nicer way to do that to have editor style
|
||||
this.$target.on('click', '.o_add_images', function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
self.addImages(false);
|
||||
});
|
||||
|
||||
this.$target.on('dropped', 'img', function (ev) {
|
||||
self.mode(null, self.getMode());
|
||||
if (!ev.target.height) {
|
||||
$(ev.target).one('load', function () {
|
||||
setTimeout(function () {
|
||||
self.trigger_up('cover_update');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.$('.container:first > *:not(div)').length) {
|
||||
self.mode(null, self.getMode());
|
||||
}
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
onBuilt: function () {
|
||||
var uuid = new Date().getTime();
|
||||
this.$target.find('.carousel').attr('id', 'slideshow_' + uuid);
|
||||
this.$target.find('[data-target]').attr('data-target', '#slideshow_' + uuid);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
cleanForSave: function () {
|
||||
if (this.$target.hasClass('slideshow')) {
|
||||
this.$target.removeAttr('style');
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Allows to select images to add as part of the snippet.
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
addImages: function (previewMode) {
|
||||
var $container = this.$('.container:first');
|
||||
var dialog = new weWidgets.MediaDialog(this, {multiImages: true, onlyImages: true, mediaWidth: 1920});
|
||||
var lastImage = _.last(this._getImages());
|
||||
var index = lastImage ? this._getIndex(lastImage) : -1;
|
||||
return new Promise(resolve => {
|
||||
dialog.on('save', this, function (attachments) {
|
||||
for (var i = 0; i < attachments.length; i++) {
|
||||
$('<img/>', {
|
||||
class: 'img img-fluid',
|
||||
src: attachments[i].image_src,
|
||||
'data-index': ++index,
|
||||
}).appendTo($container);
|
||||
}
|
||||
this.mode('reset', this.getMode());
|
||||
this.trigger_up('cover_update');
|
||||
resolve();
|
||||
});
|
||||
dialog.open();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Allows to change the number of columns when displaying images with a
|
||||
* grid-like layout.
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
columns: function (previewMode, widgetValue, params) {
|
||||
const nbColumns = parseInt(widgetValue || '1');
|
||||
this.$target.attr('data-columns', nbColumns);
|
||||
|
||||
this.mode(previewMode, this.getMode(), {}); // TODO improve
|
||||
},
|
||||
/**
|
||||
* Get the image target's layout mode (slideshow, masonry, grid or nomode).
|
||||
*
|
||||
* @returns {String('slideshow'|'masonry'|'grid'|'nomode')}
|
||||
*/
|
||||
getMode: function () {
|
||||
var mode = 'slideshow';
|
||||
if (this.$target.hasClass('o_masonry')) {
|
||||
mode = 'masonry';
|
||||
}
|
||||
if (this.$target.hasClass('o_grid')) {
|
||||
mode = 'grid';
|
||||
}
|
||||
if (this.$target.hasClass('o_nomode')) {
|
||||
mode = 'nomode';
|
||||
}
|
||||
return mode;
|
||||
},
|
||||
/**
|
||||
* Displays the images with the "grid" layout.
|
||||
*/
|
||||
grid: function () {
|
||||
var imgs = this._getImages();
|
||||
var $row = $('<div/>', {class: 'row'});
|
||||
var columns = this._getColumns();
|
||||
var colClass = 'col-lg-' + (12 / columns);
|
||||
var $container = this._replaceContent($row);
|
||||
|
||||
_.each(imgs, function (img, index) {
|
||||
var $img = $(img);
|
||||
var $col = $('<div/>', {class: colClass});
|
||||
$col.append($img).appendTo($row);
|
||||
if ((index + 1) % columns === 0) {
|
||||
$row = $('<div/>', {class: 'row'});
|
||||
$row.appendTo($container);
|
||||
}
|
||||
});
|
||||
this.$target.css('height', '');
|
||||
},
|
||||
/**
|
||||
* Allows to changes the interval of automatic slideshow (not active in
|
||||
* edit mode).
|
||||
*/
|
||||
interval: function (previewMode, widgetValue) {
|
||||
this.$target.find('.carousel:first').attr('data-interval', widgetValue || '0');
|
||||
},
|
||||
/**
|
||||
* Displays the images with the "masonry" layout.
|
||||
*/
|
||||
masonry: function () {
|
||||
var self = this;
|
||||
var imgs = this._getImages();
|
||||
var columns = this._getColumns();
|
||||
var colClass = 'col-lg-' + (12 / columns);
|
||||
var cols = [];
|
||||
|
||||
var $row = $('<div/>', {class: 'row'});
|
||||
this._replaceContent($row);
|
||||
|
||||
// Create columns
|
||||
for (var c = 0; c < columns; c++) {
|
||||
var $col = $('<div/>', {class: 'col o_snippet_not_selectable ' + colClass});
|
||||
$row.append($col);
|
||||
cols.push($col[0]);
|
||||
}
|
||||
|
||||
// Dispatch images in columns by always putting the next one in the
|
||||
// smallest-height column
|
||||
while (imgs.length) {
|
||||
var min = Infinity;
|
||||
var $lowest;
|
||||
_.each(cols, function (col) {
|
||||
var $col = $(col);
|
||||
var height = $col.is(':empty') ? 0 : $col.find('img').last().offset().top + $col.find('img').last().height() - self.$target.offset().top;
|
||||
if (height < min) {
|
||||
min = height;
|
||||
$lowest = $col;
|
||||
}
|
||||
});
|
||||
$lowest.append(imgs.pop());
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Allows to change the images layout. @see grid, masonry, nomode, slideshow
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
mode: function (previewMode, widgetValue, params) {
|
||||
widgetValue = widgetValue || 'slideshow'; // FIXME should not be needed
|
||||
this.$target.css('height', '');
|
||||
this[widgetValue]();
|
||||
this.$target
|
||||
.removeClass('o_nomode o_masonry o_grid o_slideshow')
|
||||
.addClass('o_' + widgetValue);
|
||||
this.trigger_up('cover_update');
|
||||
},
|
||||
/**
|
||||
* Displays the images with the standard layout: floating images.
|
||||
*/
|
||||
nomode: function () {
|
||||
var $row = $('<div/>', {class: 'row'});
|
||||
var imgs = this._getImages();
|
||||
|
||||
this._replaceContent($row);
|
||||
|
||||
_.each(imgs, function (img) {
|
||||
var wrapClass = 'col-lg-3';
|
||||
if (img.width >= img.height * 2 || img.width > 600) {
|
||||
wrapClass = 'col-lg-6';
|
||||
}
|
||||
var $wrap = $('<div/>', {class: wrapClass}).append(img);
|
||||
$row.append($wrap);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Allows to remove all images. Restores the snippet to the way it was when
|
||||
* it was added in the page.
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
removeAllImages: function (previewMode) {
|
||||
var $addImg = $('<div>', {
|
||||
class: 'alert alert-info css_editable_mode_display text-center',
|
||||
});
|
||||
var $text = $('<span>', {
|
||||
class: 'o_add_images',
|
||||
style: 'cursor: pointer;',
|
||||
text: _t(" Add Images"),
|
||||
});
|
||||
var $icon = $('<i>', {
|
||||
class: ' fa fa-plus-circle',
|
||||
});
|
||||
this._replaceContent($addImg.append($icon).append($text));
|
||||
},
|
||||
/**
|
||||
* Displays the images with a "slideshow" layout.
|
||||
*/
|
||||
slideshow: function () {
|
||||
var imgStyle = this.$el.find('.active[data-styling]').data('styling') || '';
|
||||
var urls = _.map(this._getImages(), function (img) {
|
||||
return $(img).attr('src');
|
||||
});
|
||||
var currentInterval = this.$target.find('.carousel:first').attr('data-interval');
|
||||
var params = {
|
||||
srcs: urls,
|
||||
index: 0,
|
||||
title: "",
|
||||
interval: currentInterval || this.$target.data('interval') || 0,
|
||||
id: 'slideshow_' + new Date().getTime(),
|
||||
userStyle: imgStyle,
|
||||
},
|
||||
$slideshow = $(qweb.render('website.gallery.slideshow', params));
|
||||
this._replaceContent($slideshow);
|
||||
_.each(this.$('img'), function (img, index) {
|
||||
$(img).attr({contenteditable: true, 'data-index': index});
|
||||
});
|
||||
this.$target.css('height', Math.round(window.innerHeight * 0.7));
|
||||
|
||||
// Apply layout animation
|
||||
this.$target.off('slide.bs.carousel').off('slid.bs.carousel');
|
||||
this.$('li.fa').off('click');
|
||||
},
|
||||
/**
|
||||
* Allows to change the style of the individual images.
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
styling: function (previewMode, value) {
|
||||
var classes = _.map(this.$el.find('[data-styling]'), function (el) {
|
||||
return $(el).data('styling');
|
||||
}).join(' ');
|
||||
this.$('img').removeClass(classes).addClass(value);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handles image removals and image index updates.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
notify: async function (name, data) {
|
||||
await this._super(...arguments);
|
||||
if (name === 'image_removed') {
|
||||
data.$image.remove(); // Force the removal of the image before reset
|
||||
this.mode('reset', this.getMode());
|
||||
} else if (name === 'image_index_request') {
|
||||
var imgs = this._getImages();
|
||||
var position = _.indexOf(imgs, data.$image[0]);
|
||||
imgs.splice(position, 1);
|
||||
switch (data.position) {
|
||||
case 'first':
|
||||
imgs.unshift(data.$image[0]);
|
||||
break;
|
||||
case 'prev':
|
||||
imgs.splice(position - 1, 0, data.$image[0]);
|
||||
break;
|
||||
case 'next':
|
||||
imgs.splice(position + 1, 0, data.$image[0]);
|
||||
break;
|
||||
case 'last':
|
||||
imgs.push(data.$image[0]);
|
||||
break;
|
||||
}
|
||||
_.each(imgs, function (img, index) {
|
||||
// Note: there might be more efficient ways to do that but it is
|
||||
// more simple this way and allows compatibility with 10.0 where
|
||||
// indexes were not the same as positions.
|
||||
$(img).attr('data-index', index);
|
||||
});
|
||||
this.mode('reset', this.getMode());
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_computeWidgetState: function (methodName, params) {
|
||||
switch (methodName) {
|
||||
case 'mode': {
|
||||
let activeModeName = 'slideshow';
|
||||
for (const modeName of params.possibleValues) {
|
||||
if (this.$target.hasClass(`o_${modeName}`)) {
|
||||
activeModeName = modeName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.activeMode = activeModeName;
|
||||
return activeModeName;
|
||||
}
|
||||
case 'interval': {
|
||||
const carousel = this.$target[0].querySelector('.carousel');
|
||||
return (carousel && carousel.dataset.interval || '0');
|
||||
}
|
||||
case 'columns': {
|
||||
return `${this._getColumns()}`;
|
||||
}
|
||||
case 'styling': {
|
||||
const img = this.$target[0].querySelector('img');
|
||||
if (!img) {
|
||||
return '';
|
||||
}
|
||||
for (const className of params.possibleValues) {
|
||||
if (className && img.classList.contains(className)) {
|
||||
return className;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
/**
|
||||
* Returns the images, sorted by index.
|
||||
*
|
||||
* @private
|
||||
* @returns {DOMElement[]}
|
||||
*/
|
||||
_getImages: function () {
|
||||
var imgs = this.$('img').get();
|
||||
var self = this;
|
||||
imgs.sort(function (a, b) {
|
||||
return self._getIndex(a) - self._getIndex(b);
|
||||
});
|
||||
return imgs;
|
||||
},
|
||||
/**
|
||||
* Returns the index associated to a given image.
|
||||
*
|
||||
* @private
|
||||
* @param {DOMElement} img
|
||||
* @returns {integer}
|
||||
*/
|
||||
_getIndex: function (img) {
|
||||
return img.dataset.index || 0;
|
||||
},
|
||||
/**
|
||||
* Returns the currently selected column option.
|
||||
*
|
||||
* @private
|
||||
* @returns {integer}
|
||||
*/
|
||||
_getColumns: function () {
|
||||
return parseInt(this.$target.attr('data-columns')) || 3;
|
||||
},
|
||||
/**
|
||||
* Empties the container, adds the given content and returns the container.
|
||||
*
|
||||
* @private
|
||||
* @param {jQuery} $content
|
||||
* @returns {jQuery} the main container of the snippet
|
||||
*/
|
||||
_replaceContent: function ($content) {
|
||||
var $container = this.$('.container:first');
|
||||
$container.empty().append($content);
|
||||
return $container;
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.countdown = options.Class.extend({
|
||||
events: _.extend({}, options.Class.prototype.events || {}, {
|
||||
'click .toggle-edit-message': '_onToggleEndMessageClick',
|
||||
@@ -1368,44 +960,6 @@ options.registry.countdown = options.Class.extend({
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.gallery_img = options.Class.extend({
|
||||
/**
|
||||
* Rebuilds the whole gallery when one image is removed.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
onRemove: function () {
|
||||
this.trigger_up('option_update', {
|
||||
optionName: 'gallery',
|
||||
name: 'image_removed',
|
||||
data: {
|
||||
$image: this.$target,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Allows to change the position of an image (its order in the image set).
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
position: function (previewMode, widgetValue, params) {
|
||||
this.trigger_up('deactivate_snippet');
|
||||
this.trigger_up('option_update', {
|
||||
optionName: 'gallery',
|
||||
name: 'image_index_request',
|
||||
data: {
|
||||
$image: this.$target,
|
||||
position: widgetValue,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.topMenuTransparency = options.Class.extend({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -713,165 +713,6 @@ table.table_desc tr td {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Gallery
|
||||
.o_gallery {
|
||||
&.o_grid, &.o_masonry {
|
||||
.img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&.o_grid {
|
||||
&.o_spc-none div.row {
|
||||
margin: 0;
|
||||
> div {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
&.o_spc-small div.row {
|
||||
margin: 5px 0;
|
||||
> div {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
&.o_spc-medium div.row {
|
||||
margin: 10px 0;
|
||||
> div {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
&.o_spc-big div.row {
|
||||
margin: 15px 0;
|
||||
> div {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
&.size-auto .row {
|
||||
height: auto;
|
||||
}
|
||||
&.size-small .row {
|
||||
height: 100px;
|
||||
}
|
||||
&.size-medium .row {
|
||||
height: 250px;
|
||||
}
|
||||
&.size-big .row {
|
||||
height: 400px;
|
||||
}
|
||||
&.size-small, &.size-medium, &.size-big {
|
||||
img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.o_masonry {
|
||||
&.o_spc-none div.col {
|
||||
padding: 0;
|
||||
> img {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
&.o_spc-small div.col {
|
||||
padding: 0 5px;
|
||||
> img {
|
||||
margin: 5px 0 !important;
|
||||
}
|
||||
}
|
||||
&.o_spc-medium div.col {
|
||||
padding: 0 10px;
|
||||
> img {
|
||||
margin: 10px 0 !important;
|
||||
}
|
||||
}
|
||||
&.o_spc-big div.col {
|
||||
padding: 0 15px;
|
||||
> img {
|
||||
margin: 15px 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.o_nomode {
|
||||
&.o_spc-none .img {
|
||||
padding: 0;
|
||||
}
|
||||
&.o_spc-small .img {
|
||||
padding: 5px;
|
||||
}
|
||||
&.o_spc-medium .img {
|
||||
padding: 10px;
|
||||
}
|
||||
&.o_spc-big .img {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
&.o_slideshow {
|
||||
.carousel ul.carousel-indicators li {
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
> .container {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.carousel-inner .item img {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
.o_gallery,
|
||||
.modal-body {
|
||||
&.o_slideshow {
|
||||
.carousel {
|
||||
height: 100%;
|
||||
|
||||
.carousel-inner {
|
||||
height: 100%;
|
||||
}
|
||||
.carousel-item.active,
|
||||
.carousel-item-next,
|
||||
.carousel-item-prev {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
ul.carousel-indicators {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
left: 0%;
|
||||
> * {
|
||||
list-style-image: none;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 2.5px 2.5px 2.5px 2.5px;
|
||||
padding: 0;
|
||||
border: 1px solid #aaa;
|
||||
text-indent: initial;
|
||||
background-size: cover;
|
||||
background-color: #fff;
|
||||
border-radius: 0;
|
||||
vertical-align: bottom;
|
||||
flex: 0 0 40px;
|
||||
&:not(.active) {
|
||||
opacity: 0.8;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Parallax
|
||||
.parallax {
|
||||
position: relative;
|
||||
@@ -0,0 +1,172 @@
|
||||
odoo.define('website.s_image_gallery', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var publicWidget = require('web.public.widget');
|
||||
|
||||
var qweb = core.qweb;
|
||||
|
||||
const GalleryWidget = publicWidget.Widget.extend({
|
||||
selector: '.o_gallery:not(.o_slideshow)',
|
||||
xmlDependencies: ['/website/static/src/xml/website.gallery.xml'],
|
||||
events: {
|
||||
'click img': '_onClickImg',
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when an image is clicked. Opens a dialog to browse all the images
|
||||
* with a bigger size.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onClickImg: function (ev) {
|
||||
var self = this;
|
||||
var $cur = $(ev.currentTarget);
|
||||
|
||||
var urls = [];
|
||||
var idx = undefined;
|
||||
var milliseconds = undefined;
|
||||
var params = undefined;
|
||||
var $images = $cur.closest('.o_gallery').find('img');
|
||||
var size = 0.8;
|
||||
var dimensions = {
|
||||
min_width: Math.round(window.innerWidth * size * 0.9),
|
||||
min_height: Math.round(window.innerHeight * size),
|
||||
max_width: Math.round(window.innerWidth * size * 0.9),
|
||||
max_height: Math.round(window.innerHeight * size),
|
||||
width: Math.round(window.innerWidth * size * 0.9),
|
||||
height: Math.round(window.innerHeight * size)
|
||||
};
|
||||
|
||||
$images.each(function () {
|
||||
urls.push($(this).attr('src'));
|
||||
});
|
||||
var $img = ($cur.is('img') === true) ? $cur : $cur.closest('img');
|
||||
idx = urls.indexOf($img.attr('src'));
|
||||
|
||||
milliseconds = $cur.closest('.o_gallery').data('interval') || false;
|
||||
var $modal = $(qweb.render('website.gallery.slideshow.lightbox', {
|
||||
srcs: urls,
|
||||
index: idx,
|
||||
dim: dimensions,
|
||||
interval: milliseconds || 0,
|
||||
id: _.uniqueId('slideshow_'),
|
||||
}));
|
||||
$modal.modal({
|
||||
keyboard: true,
|
||||
backdrop: true,
|
||||
});
|
||||
$modal.on('hidden.bs.modal', function () {
|
||||
$(this).hide();
|
||||
$(this).siblings().filter('.modal-backdrop').remove(); // bootstrap leaves a modal-backdrop
|
||||
$(this).remove();
|
||||
});
|
||||
$modal.find('.modal-content, .modal-body.o_slideshow').css('height', '100%');
|
||||
$modal.appendTo(document.body);
|
||||
|
||||
$modal.one('shown.bs.modal', function () {
|
||||
self.trigger_up('widgets_start_request', {
|
||||
editableMode: false,
|
||||
$target: $modal.find('.modal-body.o_slideshow'),
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const GallerySliderWidget = publicWidget.Widget.extend({
|
||||
selector: '.o_slideshow',
|
||||
xmlDependencies: ['/website/static/src/xml/website.gallery.xml'],
|
||||
disabledInEditableMode: false,
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
this.$carousel = this.$target.is('.carousel') ? this.$target : this.$target.find('.carousel');
|
||||
this.$indicator = this.$carousel.find('.carousel-indicators');
|
||||
this.$prev = this.$indicator.find('li.o_indicators_left').css('visibility', ''); // force visibility as some databases have it hidden
|
||||
this.$next = this.$indicator.find('li.o_indicators_right').css('visibility', '');
|
||||
var $lis = this.$indicator.find('li[data-slide-to]');
|
||||
var nbPerPage = Math.floor(this.$indicator.width() / $lis.first().outerWidth(true)) - 3; // - navigator - 1 to leave some space
|
||||
var realNbPerPage = nbPerPage || 1;
|
||||
var nbPages = Math.ceil($lis.length / realNbPerPage);
|
||||
|
||||
var index;
|
||||
var page;
|
||||
update();
|
||||
|
||||
function hide() {
|
||||
$lis.each(function (i) {
|
||||
$(this).toggleClass('d-none', i < page * nbPerPage || i >= (page + 1) * nbPerPage);
|
||||
});
|
||||
if (self.editableMode) { // do not remove DOM in edit mode
|
||||
return;
|
||||
}
|
||||
if (page <= 0) {
|
||||
self.$prev.detach();
|
||||
} else {
|
||||
self.$prev.prependTo(self.$indicator);
|
||||
}
|
||||
if (page >= nbPages - 1) {
|
||||
self.$next.detach();
|
||||
} else {
|
||||
self.$next.appendTo(self.$indicator);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
index = $lis.index($lis.filter('.active')) || 0;
|
||||
page = Math.floor(index / realNbPerPage);
|
||||
hide();
|
||||
}
|
||||
|
||||
this.$carousel.on('slide.bs.carousel.gallery_slider', function () {
|
||||
setTimeout(function () {
|
||||
var $item = self.$carousel.find('.carousel-inner .carousel-item-prev, .carousel-inner .carousel-item-next');
|
||||
var index = $item.index();
|
||||
$lis.removeClass('active')
|
||||
.filter('[data-slide-to="' + index + '"]')
|
||||
.addClass('active');
|
||||
}, 0);
|
||||
});
|
||||
this.$indicator.on('click.gallery_slider', '> li:not([data-slide-to])', function () {
|
||||
page += ($(this).hasClass('o_indicators_left') ? -1 : 1);
|
||||
page = Math.max(0, Math.min(nbPages - 1, page)); // should not be necessary
|
||||
self.$carousel.carousel(page * realNbPerPage);
|
||||
hide();
|
||||
});
|
||||
this.$carousel.on('slid.bs.carousel.gallery_slider', update);
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if (!this.$indicator) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$prev.prependTo(this.$indicator);
|
||||
this.$next.appendTo(this.$indicator);
|
||||
this.$carousel.off('.gallery_slider');
|
||||
this.$indicator.off('.gallery_slider');
|
||||
},
|
||||
});
|
||||
|
||||
publicWidget.registry.gallery = GalleryWidget;
|
||||
publicWidget.registry.gallerySlider = GallerySliderWidget;
|
||||
|
||||
return {
|
||||
GalleryWidget: GalleryWidget,
|
||||
GallerySliderWidget: GallerySliderWidget,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
|
||||
.o_gallery {
|
||||
&.o_grid, &.o_masonry {
|
||||
.img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&.o_grid {
|
||||
&.o_spc-none div.row {
|
||||
margin: 0;
|
||||
> div {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
&.o_spc-small div.row {
|
||||
margin: 5px 0;
|
||||
> div {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
&.o_spc-medium div.row {
|
||||
margin: 10px 0;
|
||||
> div {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
&.o_spc-big div.row {
|
||||
margin: 15px 0;
|
||||
> div {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
&.size-auto .row {
|
||||
height: auto;
|
||||
}
|
||||
&.size-small .row {
|
||||
height: 100px;
|
||||
}
|
||||
&.size-medium .row {
|
||||
height: 250px;
|
||||
}
|
||||
&.size-big .row {
|
||||
height: 400px;
|
||||
}
|
||||
&.size-small, &.size-medium, &.size-big {
|
||||
img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.o_masonry {
|
||||
&.o_spc-none div.col {
|
||||
padding: 0;
|
||||
> img {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
&.o_spc-small div.col {
|
||||
padding: 0 5px;
|
||||
> img {
|
||||
margin: 5px 0 !important;
|
||||
}
|
||||
}
|
||||
&.o_spc-medium div.col {
|
||||
padding: 0 10px;
|
||||
> img {
|
||||
margin: 10px 0 !important;
|
||||
}
|
||||
}
|
||||
&.o_spc-big div.col {
|
||||
padding: 0 15px;
|
||||
> img {
|
||||
margin: 15px 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.o_nomode {
|
||||
&.o_spc-none .img {
|
||||
padding: 0;
|
||||
}
|
||||
&.o_spc-small .img {
|
||||
padding: 5px;
|
||||
}
|
||||
&.o_spc-medium .img {
|
||||
padding: 10px;
|
||||
}
|
||||
&.o_spc-big .img {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
&.o_slideshow {
|
||||
.carousel ul.carousel-indicators li {
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
> .container {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.carousel-inner .item img {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
.o_gallery,
|
||||
.modal-body {
|
||||
&.o_slideshow {
|
||||
.carousel {
|
||||
height: 100%;
|
||||
|
||||
.carousel-inner {
|
||||
height: 100%;
|
||||
}
|
||||
.carousel-item.active,
|
||||
.carousel-item-next,
|
||||
.carousel-item-prev {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
ul.carousel-indicators {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
left: 0%;
|
||||
> * {
|
||||
list-style-image: none;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 2.5px 2.5px 2.5px 2.5px;
|
||||
padding: 0;
|
||||
border: 1px solid #aaa;
|
||||
text-indent: initial;
|
||||
background-size: cover;
|
||||
background-color: #fff;
|
||||
border-radius: 0;
|
||||
vertical-align: bottom;
|
||||
flex: 0 0 40px;
|
||||
&:not(.active) {
|
||||
opacity: 0.8;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
odoo.define('website.s_image_gallery_options', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var weWidgets = require('wysiwyg.widgets');
|
||||
var options = require('web_editor.snippets.options');
|
||||
|
||||
var _t = core._t;
|
||||
var qweb = core.qweb;
|
||||
|
||||
options.registry.gallery = options.Class.extend({
|
||||
xmlDependencies: ['/website/static/src/xml/website.gallery.xml'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
// The snippet should not be editable
|
||||
this.$target.addClass('o_fake_not_editable').attr('contentEditable', false);
|
||||
|
||||
// Make sure image previews are updated if images are changed
|
||||
this.$target.on('save', 'img', function (ev) {
|
||||
var $img = $(ev.currentTarget);
|
||||
var index = self.$target.find('.carousel-item.active').index();
|
||||
self.$('.carousel:first li[data-target]:eq(' + index + ')')
|
||||
.css('background-image', 'url(' + $img.attr('src') + ')');
|
||||
});
|
||||
|
||||
// When the snippet is empty, an edition button is the default content
|
||||
// TODO find a nicer way to do that to have editor style
|
||||
this.$target.on('click', '.o_add_images', function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
self.addImages(false);
|
||||
});
|
||||
|
||||
this.$target.on('dropped', 'img', function (ev) {
|
||||
self.mode(null, self.getMode());
|
||||
if (!ev.target.height) {
|
||||
$(ev.target).one('load', function () {
|
||||
setTimeout(function () {
|
||||
self.trigger_up('cover_update');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.$('.container:first > *:not(div)').length) {
|
||||
self.mode(null, self.getMode());
|
||||
}
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
onBuilt: function () {
|
||||
var uuid = new Date().getTime();
|
||||
this.$target.find('.carousel').attr('id', 'slideshow_' + uuid);
|
||||
this.$target.find('[data-target]').attr('data-target', '#slideshow_' + uuid);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
cleanForSave: function () {
|
||||
if (this.$target.hasClass('slideshow')) {
|
||||
this.$target.removeAttr('style');
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Allows to select images to add as part of the snippet.
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
addImages: function (previewMode) {
|
||||
var $container = this.$('.container:first');
|
||||
var dialog = new weWidgets.MediaDialog(this, {multiImages: true, onlyImages: true, mediaWidth: 1920});
|
||||
var lastImage = _.last(this._getImages());
|
||||
var index = lastImage ? this._getIndex(lastImage) : -1;
|
||||
return new Promise(resolve => {
|
||||
dialog.on('save', this, function (attachments) {
|
||||
for (var i = 0; i < attachments.length; i++) {
|
||||
$('<img/>', {
|
||||
class: 'img img-fluid',
|
||||
src: attachments[i].image_src,
|
||||
'data-index': ++index,
|
||||
}).appendTo($container);
|
||||
}
|
||||
this.mode('reset', this.getMode());
|
||||
this.trigger_up('cover_update');
|
||||
resolve();
|
||||
});
|
||||
dialog.open();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Allows to change the number of columns when displaying images with a
|
||||
* grid-like layout.
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
columns: function (previewMode, widgetValue, params) {
|
||||
const nbColumns = parseInt(widgetValue || '1');
|
||||
this.$target.attr('data-columns', nbColumns);
|
||||
|
||||
this.mode(previewMode, this.getMode(), {}); // TODO improve
|
||||
},
|
||||
/**
|
||||
* Get the image target's layout mode (slideshow, masonry, grid or nomode).
|
||||
*
|
||||
* @returns {String('slideshow'|'masonry'|'grid'|'nomode')}
|
||||
*/
|
||||
getMode: function () {
|
||||
var mode = 'slideshow';
|
||||
if (this.$target.hasClass('o_masonry')) {
|
||||
mode = 'masonry';
|
||||
}
|
||||
if (this.$target.hasClass('o_grid')) {
|
||||
mode = 'grid';
|
||||
}
|
||||
if (this.$target.hasClass('o_nomode')) {
|
||||
mode = 'nomode';
|
||||
}
|
||||
return mode;
|
||||
},
|
||||
/**
|
||||
* Displays the images with the "grid" layout.
|
||||
*/
|
||||
grid: function () {
|
||||
var imgs = this._getImages();
|
||||
var $row = $('<div/>', {class: 'row'});
|
||||
var columns = this._getColumns();
|
||||
var colClass = 'col-lg-' + (12 / columns);
|
||||
var $container = this._replaceContent($row);
|
||||
|
||||
_.each(imgs, function (img, index) {
|
||||
var $img = $(img);
|
||||
var $col = $('<div/>', {class: colClass});
|
||||
$col.append($img).appendTo($row);
|
||||
if ((index + 1) % columns === 0) {
|
||||
$row = $('<div/>', {class: 'row'});
|
||||
$row.appendTo($container);
|
||||
}
|
||||
});
|
||||
this.$target.css('height', '');
|
||||
},
|
||||
/**
|
||||
* Allows to changes the interval of automatic slideshow (not active in
|
||||
* edit mode).
|
||||
*/
|
||||
interval: function (previewMode, widgetValue) {
|
||||
this.$target.find('.carousel:first').attr('data-interval', widgetValue || '0');
|
||||
},
|
||||
/**
|
||||
* Displays the images with the "masonry" layout.
|
||||
*/
|
||||
masonry: function () {
|
||||
var self = this;
|
||||
var imgs = this._getImages();
|
||||
var columns = this._getColumns();
|
||||
var colClass = 'col-lg-' + (12 / columns);
|
||||
var cols = [];
|
||||
|
||||
var $row = $('<div/>', {class: 'row'});
|
||||
this._replaceContent($row);
|
||||
|
||||
// Create columns
|
||||
for (var c = 0; c < columns; c++) {
|
||||
var $col = $('<div/>', {class: 'col o_snippet_not_selectable ' + colClass});
|
||||
$row.append($col);
|
||||
cols.push($col[0]);
|
||||
}
|
||||
|
||||
// Dispatch images in columns by always putting the next one in the
|
||||
// smallest-height column
|
||||
while (imgs.length) {
|
||||
var min = Infinity;
|
||||
var $lowest;
|
||||
_.each(cols, function (col) {
|
||||
var $col = $(col);
|
||||
var height = $col.is(':empty') ? 0 : $col.find('img').last().offset().top + $col.find('img').last().height() - self.$target.offset().top;
|
||||
if (height < min) {
|
||||
min = height;
|
||||
$lowest = $col;
|
||||
}
|
||||
});
|
||||
$lowest.append(imgs.pop());
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Allows to change the images layout. @see grid, masonry, nomode, slideshow
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
mode: function (previewMode, widgetValue, params) {
|
||||
widgetValue = widgetValue || 'slideshow'; // FIXME should not be needed
|
||||
this.$target.css('height', '');
|
||||
this[widgetValue]();
|
||||
this.$target
|
||||
.removeClass('o_nomode o_masonry o_grid o_slideshow')
|
||||
.addClass('o_' + widgetValue);
|
||||
this.trigger_up('cover_update');
|
||||
},
|
||||
/**
|
||||
* Displays the images with the standard layout: floating images.
|
||||
*/
|
||||
nomode: function () {
|
||||
var $row = $('<div/>', {class: 'row'});
|
||||
var imgs = this._getImages();
|
||||
|
||||
this._replaceContent($row);
|
||||
|
||||
_.each(imgs, function (img) {
|
||||
var wrapClass = 'col-lg-3';
|
||||
if (img.width >= img.height * 2 || img.width > 600) {
|
||||
wrapClass = 'col-lg-6';
|
||||
}
|
||||
var $wrap = $('<div/>', {class: wrapClass}).append(img);
|
||||
$row.append($wrap);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Allows to remove all images. Restores the snippet to the way it was when
|
||||
* it was added in the page.
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
removeAllImages: function (previewMode) {
|
||||
var $addImg = $('<div>', {
|
||||
class: 'alert alert-info css_editable_mode_display text-center',
|
||||
});
|
||||
var $text = $('<span>', {
|
||||
class: 'o_add_images',
|
||||
style: 'cursor: pointer;',
|
||||
text: _t(" Add Images"),
|
||||
});
|
||||
var $icon = $('<i>', {
|
||||
class: ' fa fa-plus-circle',
|
||||
});
|
||||
this._replaceContent($addImg.append($icon).append($text));
|
||||
},
|
||||
/**
|
||||
* Displays the images with a "slideshow" layout.
|
||||
*/
|
||||
slideshow: function () {
|
||||
var imgStyle = this.$el.find('.active[data-styling]').data('styling') || '';
|
||||
var urls = _.map(this._getImages(), function (img) {
|
||||
return $(img).attr('src');
|
||||
});
|
||||
var currentInterval = this.$target.find('.carousel:first').attr('data-interval');
|
||||
var params = {
|
||||
srcs: urls,
|
||||
index: 0,
|
||||
title: "",
|
||||
interval: currentInterval || this.$target.data('interval') || 0,
|
||||
id: 'slideshow_' + new Date().getTime(),
|
||||
userStyle: imgStyle,
|
||||
},
|
||||
$slideshow = $(qweb.render('website.gallery.slideshow', params));
|
||||
this._replaceContent($slideshow);
|
||||
_.each(this.$('img'), function (img, index) {
|
||||
$(img).attr({contenteditable: true, 'data-index': index});
|
||||
});
|
||||
this.$target.css('height', Math.round(window.innerHeight * 0.7));
|
||||
|
||||
// Apply layout animation
|
||||
this.$target.off('slide.bs.carousel').off('slid.bs.carousel');
|
||||
this.$('li.fa').off('click');
|
||||
},
|
||||
/**
|
||||
* Allows to change the style of the individual images.
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
styling: function (previewMode, value) {
|
||||
var classes = _.map(this.$el.find('[data-styling]'), function (el) {
|
||||
return $(el).data('styling');
|
||||
}).join(' ');
|
||||
this.$('img').removeClass(classes).addClass(value);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handles image removals and image index updates.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
notify: async function (name, data) {
|
||||
await this._super(...arguments);
|
||||
if (name === 'image_removed') {
|
||||
data.$image.remove(); // Force the removal of the image before reset
|
||||
this.mode('reset', this.getMode());
|
||||
} else if (name === 'image_index_request') {
|
||||
var imgs = this._getImages();
|
||||
var position = _.indexOf(imgs, data.$image[0]);
|
||||
imgs.splice(position, 1);
|
||||
switch (data.position) {
|
||||
case 'first':
|
||||
imgs.unshift(data.$image[0]);
|
||||
break;
|
||||
case 'prev':
|
||||
imgs.splice(position - 1, 0, data.$image[0]);
|
||||
break;
|
||||
case 'next':
|
||||
imgs.splice(position + 1, 0, data.$image[0]);
|
||||
break;
|
||||
case 'last':
|
||||
imgs.push(data.$image[0]);
|
||||
break;
|
||||
}
|
||||
_.each(imgs, function (img, index) {
|
||||
// Note: there might be more efficient ways to do that but it is
|
||||
// more simple this way and allows compatibility with 10.0 where
|
||||
// indexes were not the same as positions.
|
||||
$(img).attr('data-index', index);
|
||||
});
|
||||
this.mode('reset', this.getMode());
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_computeWidgetState: function (methodName, params) {
|
||||
switch (methodName) {
|
||||
case 'mode': {
|
||||
let activeModeName = 'slideshow';
|
||||
for (const modeName of params.possibleValues) {
|
||||
if (this.$target.hasClass(`o_${modeName}`)) {
|
||||
activeModeName = modeName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.activeMode = activeModeName;
|
||||
return activeModeName;
|
||||
}
|
||||
case 'interval': {
|
||||
const carousel = this.$target[0].querySelector('.carousel');
|
||||
return (carousel && carousel.dataset.interval || '0');
|
||||
}
|
||||
case 'columns': {
|
||||
return `${this._getColumns()}`;
|
||||
}
|
||||
case 'styling': {
|
||||
const img = this.$target[0].querySelector('img');
|
||||
if (!img) {
|
||||
return '';
|
||||
}
|
||||
for (const className of params.possibleValues) {
|
||||
if (className && img.classList.contains(className)) {
|
||||
return className;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
/**
|
||||
* Returns the images, sorted by index.
|
||||
*
|
||||
* @private
|
||||
* @returns {DOMElement[]}
|
||||
*/
|
||||
_getImages: function () {
|
||||
var imgs = this.$('img').get();
|
||||
var self = this;
|
||||
imgs.sort(function (a, b) {
|
||||
return self._getIndex(a) - self._getIndex(b);
|
||||
});
|
||||
return imgs;
|
||||
},
|
||||
/**
|
||||
* Returns the index associated to a given image.
|
||||
*
|
||||
* @private
|
||||
* @param {DOMElement} img
|
||||
* @returns {integer}
|
||||
*/
|
||||
_getIndex: function (img) {
|
||||
return img.dataset.index || 0;
|
||||
},
|
||||
/**
|
||||
* Returns the currently selected column option.
|
||||
*
|
||||
* @private
|
||||
* @returns {integer}
|
||||
*/
|
||||
_getColumns: function () {
|
||||
return parseInt(this.$target.attr('data-columns')) || 3;
|
||||
},
|
||||
/**
|
||||
* Empties the container, adds the given content and returns the container.
|
||||
*
|
||||
* @private
|
||||
* @param {jQuery} $content
|
||||
* @returns {jQuery} the main container of the snippet
|
||||
*/
|
||||
_replaceContent: function ($content) {
|
||||
var $container = this.$('.container:first');
|
||||
$container.empty().append($content);
|
||||
return $container;
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.gallery_img = options.Class.extend({
|
||||
/**
|
||||
* Rebuilds the whole gallery when one image is removed.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
onRemove: function () {
|
||||
this.trigger_up('option_update', {
|
||||
optionName: 'gallery',
|
||||
name: 'image_removed',
|
||||
data: {
|
||||
$image: this.$target,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Allows to change the position of an image (its order in the image set).
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
position: function (previewMode, widgetValue, params) {
|
||||
this.trigger_up('deactivate_snippet');
|
||||
this.trigger_up('option_update', {
|
||||
optionName: 'gallery',
|
||||
name: 'image_index_request',
|
||||
data: {
|
||||
$image: this.$target,
|
||||
position: widgetValue,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -109,6 +109,7 @@
|
||||
<script type="text/javascript" src="/website/static/src/js/editor/rte.summernote.js"/>
|
||||
<script type="text/javascript" src="/website/static/src/js/editor/snippets.options.js"/>
|
||||
<script type="text/javascript" src="/website/static/src/snippets/s_facebook_page/options.js"/>
|
||||
<script type="text/javascript" src="/website/static/src/snippets/s_image_gallery/options.js"/>
|
||||
|
||||
<script type="text/javascript" src="/website/static/src/js/editor/wysiwyg_multizone.js"/>
|
||||
<script type="text/javascript" src="/website/static/src/js/editor/wysiwyg_multizone_translate.js"/>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="s_image_gallery" name="Image Gallery">
|
||||
<section class="o_gallery o_spc-medium o_slideshow s_image_gallery" data-columns="3" style="height: 500px; overflow: hidden;">
|
||||
<div class="container">
|
||||
<div class="alert alert-info css_editable_mode_display text-center" role="status"><span class="o_add_images" style="cursor: pointer;"><i class="fa fa-plus-circle"/> Add Images</span></div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template id="s_image_gallery_options" inherit_id="website.snippet_options">
|
||||
<xpath expr="." position="inside">
|
||||
<div data-js="gallery" data-selector=".o_gallery">
|
||||
<we-button data-add-images="true" data-no-preview="true">Add images</we-button>
|
||||
<we-button data-remove-all-images="true" data-no-preview="true">Remove all images</we-button>
|
||||
<we-divider/>
|
||||
<we-select string="Mode">
|
||||
<we-button data-mode="nomode">Float</we-button>
|
||||
<we-button data-mode="masonry" data-name="masonry_mode_opt">Masonry</we-button>
|
||||
<we-button data-mode="grid" data-name="grid_mode_opt">Grid</we-button>
|
||||
<we-button data-mode="slideshow" data-name="slideshow_mode_opt">Slideshow</we-button>
|
||||
</we-select>
|
||||
<we-select string="Slideshow speed" data-dependencies="slideshow_mode_opt">
|
||||
<we-button data-interval="1000">1s</we-button>
|
||||
<we-button data-interval="2000">2s</we-button>
|
||||
<we-button data-interval="3000">3s</we-button>
|
||||
<we-button data-interval="5000">5s</we-button>
|
||||
<we-button data-interval="10000">10s</we-button>
|
||||
<we-button data-interval="0">Disable autoplay</we-button>
|
||||
</we-select>
|
||||
<we-select string="Columns" data-dependencies="masonry_mode_opt, grid_mode_opt">
|
||||
<we-button data-columns="1">1</we-button>
|
||||
<we-button data-columns="2">2</we-button>
|
||||
<we-button data-columns="3">3</we-button>
|
||||
<we-button data-columns="4">4</we-button>
|
||||
<we-button data-columns="6">6</we-button>
|
||||
<we-button data-columns="12">12</we-button>
|
||||
</we-select>
|
||||
<we-select string="Images spacing" data-dependencies="!slideshow_mode_opt">
|
||||
<we-button data-select-class="o_spc-none">None</we-button>
|
||||
<we-button data-select-class="o_spc-small">Small</we-button>
|
||||
<we-button data-select-class="o_spc-medium">Medium</we-button>
|
||||
<we-button data-select-class="o_spc-big">Big</we-button>
|
||||
</we-select>
|
||||
<we-select string="Styling">
|
||||
<we-button data-styling="">Square</we-button>
|
||||
<we-button data-styling="rounded">Rounded corners</we-button>
|
||||
<we-button data-styling="img-thumbnail">Thumbnails</we-button>
|
||||
<we-button data-styling="rounded-circle">Circle</we-button>
|
||||
<we-button data-styling="shadow">Shadows</we-button>
|
||||
</we-select>
|
||||
</div>
|
||||
<div data-js="gallery_img" data-selector=".o_gallery img">
|
||||
<div data-no-preview="true">
|
||||
<we-title>Re-order</we-title>
|
||||
<we-button data-position="first">Move to first</we-button>
|
||||
<we-button data-position="prev">Move to previous</we-button>
|
||||
<we-button data-position="next">Move to next</we-button>
|
||||
<we-button data-position="last">Move to last</we-button>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_snippet_s_image_gallery_css_000" inherit_id="website.assets_frontend">
|
||||
<xpath expr="//link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/website/static/src/snippets/s_image_gallery/000.scss"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_snippet_s_image_gallery_js_000" inherit_id="website.assets_frontend">
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/website/static/src/snippets/s_image_gallery/000.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -1,14 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="s_image_gallery" name="Image Gallery">
|
||||
<section class="o_gallery o_spc-medium o_slideshow s_image_gallery" data-columns="3" style="height: 500px; overflow: hidden;">
|
||||
<div class="container">
|
||||
<div class="alert alert-info css_editable_mode_display text-center" role="status"><span class="o_add_images" style="cursor: pointer;"><i class="fa fa-plus-circle"/> Add Images</span></div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template id="s_countdown" name="Countdown">
|
||||
<section class="s_countdown"
|
||||
data-display="dhms" data-end-action="nothing" data-size="175"
|
||||
@@ -738,57 +730,6 @@
|
||||
<we-button data-remove-tab="1" data-name="remove_tab_opt" data-no-preview="true">Remove Tab</we-button>
|
||||
</div>
|
||||
|
||||
<div data-js="gallery" data-selector=".o_gallery">
|
||||
<we-button data-add-images="true" data-no-preview="true">Add images</we-button>
|
||||
<we-button data-remove-all-images="true" data-no-preview="true">Remove all images</we-button>
|
||||
<we-divider/>
|
||||
<we-select string="Mode">
|
||||
<we-button data-mode="nomode">Float</we-button>
|
||||
<we-button data-mode="masonry" data-name="masonry_mode_opt">Masonry</we-button>
|
||||
<we-button data-mode="grid" data-name="grid_mode_opt">Grid</we-button>
|
||||
<we-button data-mode="slideshow" data-name="slideshow_mode_opt">Slideshow</we-button>
|
||||
</we-select>
|
||||
<we-select string="Slideshow speed" data-dependencies="slideshow_mode_opt">
|
||||
<we-button data-interval="1000">1s</we-button>
|
||||
<we-button data-interval="2000">2s</we-button>
|
||||
<we-button data-interval="3000">3s</we-button>
|
||||
<we-button data-interval="5000">5s</we-button>
|
||||
<we-button data-interval="10000">10s</we-button>
|
||||
<we-button data-interval="0">Disable autoplay</we-button>
|
||||
</we-select>
|
||||
<we-select string="Columns" data-dependencies="masonry_mode_opt, grid_mode_opt">
|
||||
<we-button data-columns="1">1</we-button>
|
||||
<we-button data-columns="2">2</we-button>
|
||||
<we-button data-columns="3">3</we-button>
|
||||
<we-button data-columns="4">4</we-button>
|
||||
<we-button data-columns="6">6</we-button>
|
||||
<we-button data-columns="12">12</we-button>
|
||||
</we-select>
|
||||
<we-select string="Images spacing" data-dependencies="!slideshow_mode_opt">
|
||||
<we-button data-select-class="o_spc-none">None</we-button>
|
||||
<we-button data-select-class="o_spc-small">Small</we-button>
|
||||
<we-button data-select-class="o_spc-medium">Medium</we-button>
|
||||
<we-button data-select-class="o_spc-big">Big</we-button>
|
||||
</we-select>
|
||||
<we-select string="Styling">
|
||||
<we-button data-styling="">Square</we-button>
|
||||
<we-button data-styling="rounded">Rounded corners</we-button>
|
||||
<we-button data-styling="img-thumbnail">Thumbnails</we-button>
|
||||
<we-button data-styling="rounded-circle">Circle</we-button>
|
||||
<we-button data-styling="shadow">Shadows</we-button>
|
||||
</we-select>
|
||||
</div>
|
||||
|
||||
<div data-js="gallery_img" data-selector=".o_gallery img">
|
||||
<div data-no-preview="true">
|
||||
<we-title>Re-order</we-title>
|
||||
<we-button data-position="first">Move to first</we-button>
|
||||
<we-button data-position="prev">Move to previous</we-button>
|
||||
<we-button data-position="next">Move to next</we-button>
|
||||
<we-button data-position="last">Move to last</we-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Accordion -->
|
||||
<div data-js="collapse"
|
||||
data-selector='.accordion > .card'
|
||||
Reference in New Issue
Block a user