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/snippets/s_image_gallery/000.xml'],
/**
* @override
*/
start: function () {
var self = this;
// Make sure image previews are updated if images are changed
this.$target.on('image_changed', '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');
});
});
}
});
const $container = this.$('> .container, > .container-fluid, > .o_container_small');
if ($container.find('> *:not(div)').length) {
self.mode(null, self.getMode());
}
return this._super.apply(this, arguments);
},
/**
* @override
*/
onBuilt: function () {
if (this.$target.find('.o_add_images').length) {
this.addImages(false);
}
// TODO should consider the async parts
this._adaptNavigationIDs();
},
/**
* @override
*/
onClone: function () {
this._adaptNavigationIDs();
},
/**
* @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) {
const $images = this.$('img');
var $container = this.$('> .container, > .container-fluid, > .o_container_small');
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++) {
$('
', {
class: $images.length > 0 ? $images[0].className : 'img img-fluid d-block ',
src: attachments[i].image_src,
'data-index': ++index,
alt: attachments[i].description || '',
'data-name': _t('Image'),
style: $images.length > 0 ? $images[0].style.cssText : '',
}).appendTo($container);
}
if (attachments.length > 0) {
this.mode('reset', this.getMode());
this.trigger_up('cover_update');
}
});
dialog.on('closed', this, () => 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 = $('
', {class: 'row s_nb_column_fixed'});
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 = $('', {class: colClass});
$col.append($img).appendTo($row);
if ((index + 1) % columns === 0) {
$row = $('', {class: 'row s_nb_column_fixed'});
$row.appendTo($container);
}
});
this.$target.css('height', '');
},
/**
* 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 = $('', {class: 'row s_nb_column_fixed'});
this._replaceContent($row);
// Create columns
for (var c = 0; c < columns; c++) {
var $col = $('', {class: 'o_masonry_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.shift());
}
},
/**
* 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.$target
.removeClass('o_nomode o_masonry o_grid o_slideshow')
.addClass('o_' + widgetValue);
this[widgetValue]();
this.trigger_up('cover_update');
this._refreshPublicWidgets();
},
/**
* Displays the images with the standard layout: floating images.
*/
nomode: function () {
var $row = $('', {class: 'row s_nb_column_fixed'});
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 = $('', {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 = $('', {
class: 'alert alert-info css_non_editable_mode_hidden text-center',
});
var $text = $('', {
class: 'o_add_images',
style: 'cursor: pointer;',
text: _t(" Add Images"),
});
var $icon = $('', {
class: ' fa fa-plus-circle',
});
this._replaceContent($addImg.append($icon).append($text));
},
/**
* Displays the images with a "slideshow" layout.
*/
slideshow: function () {
const imageEls = this._getImages();
const images = _.map(imageEls, img => ({
// Use getAttribute to get the attribute value otherwise .src
// returns the absolute url.
src: img.getAttribute('src'),
alt: img.getAttribute('alt'),
}));
var currentInterval = this.$target.find('.carousel:first').attr('data-interval');
var params = {
images: images,
index: 0,
title: "",
interval: currentInterval || 0,
id: 'slideshow_' + new Date().getTime(),
attrClass: imageEls.length > 0 ? imageEls[0].className : '',
attrStyle: imageEls.length > 0 ? imageEls[0].style.cssText : '',
},
$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');
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Handles image removals and image index updates.
*
* @override
*/
notify: function (name, data) {
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;
}
position = imgs.indexOf(data.$image[0]);
_.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);
});
const currentMode = this.getMode();
this.mode('reset', currentMode);
if (currentMode === 'slideshow') {
const $carousel = this.$target.find('.carousel');
$carousel.removeClass('slide');
$carousel.carousel(position);
this.$target.find('.carousel-indicators li').removeClass('active');
this.$target.find('.carousel-indicators li[data-slide-to="' + position + '"]').addClass('active');
this.trigger_up('activate_snippet', {
$snippet: this.$target.find('.carousel-item.active img'),
ifInactiveOptions: true,
});
$carousel.addClass('slide');
} else {
this.trigger_up('activate_snippet', {
$snippet: data.$image,
ifInactiveOptions: true,
});
}
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
*/
_adaptNavigationIDs: function () {
var uuid = new Date().getTime();
this.$target.find('.carousel').attr('id', 'slideshow_' + uuid);
_.each(this.$target.find('[data-slide], [data-slide-to]'), function (el) {
var $el = $(el);
if ($el.attr('data-target')) {
$el.attr('data-target', '#slideshow_' + uuid);
} else if ($el.attr('href')) {
$el.attr('href', '#slideshow_' + uuid);
}
});
},
/**
* @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 'columns': {
return `${this._getColumns()}`;
}
}
return this._super(...arguments);
},
/**
* @private
*/
async _computeWidgetVisibility(widgetName, params) {
if (widgetName === 'slideshow_mode_opt') {
return false;
}
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, > .container-fluid, > .o_container_small');
$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('option_update', {
optionName: 'gallery',
name: 'image_index_request',
data: {
$image: this.$target,
position: widgetValue,
},
});
},
});
});