[REF] website: move s_image_gallery snippet to dedicated files

This commit is contained in:
qsm-odoo
2019-12-18 02:14:51 +01:00
10 changed files with 864 additions and 820 deletions
+1
View File
@@ -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,
},
});
},
});
});
+1
View File
@@ -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'