diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py
index 51d2dee023d..a9e01a1c51b 100644
--- a/addons/website/__manifest__.py
+++ b/addons/website/__manifest__.py
@@ -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',
diff --git a/temp_js b/addons/website/static/src/js/content/snippets.animation.js
similarity index 90%
rename from temp_js
rename to addons/website/static/src/js/content/snippets.animation.js
index a7c4f164ac7..8dcd9809c31 100644
--- a/temp_js
+++ b/addons/website/static/src/js/content/snippets.animation.js
@@ -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'],
diff --git a/temp_options b/addons/website/static/src/js/editor/snippets.options.js
similarity index 81%
rename from temp_options
rename to addons/website/static/src/js/editor/snippets.options.js
index 3951b115f7c..47ba28bd791 100644
--- a/temp_options
+++ b/addons/website/static/src/js/editor/snippets.options.js
@@ -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++) {
- $('
', {
- 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 = $('
', {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 = $('', {class: colClass});
- $col.append($img).appendTo($row);
- if ((index + 1) % columns === 0) {
- $row = $('', {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 = $('', {class: 'row'});
- this._replaceContent($row);
-
- // Create columns
- for (var c = 0; c < columns; c++) {
- var $col = $('', {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 = $('', {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 = $('', {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_editable_mode_display 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 () {
- 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({
//--------------------------------------------------------------------------
diff --git a/temp_scss b/addons/website/static/src/scss/website.scss
similarity index 86%
rename from temp_scss
rename to addons/website/static/src/scss/website.scss
index 9d2276a438f..4a9729fb1f8 100644
--- a/temp_scss
+++ b/addons/website/static/src/scss/website.scss
@@ -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;
diff --git a/addons/website/static/src/snippets/s_image_gallery/000.js b/addons/website/static/src/snippets/s_image_gallery/000.js
new file mode 100644
index 00000000000..72a9f2a0fe1
--- /dev/null
+++ b/addons/website/static/src/snippets/s_image_gallery/000.js
@@ -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,
+};
+});
diff --git a/addons/website/static/src/snippets/s_image_gallery/000.scss b/addons/website/static/src/snippets/s_image_gallery/000.scss
new file mode 100644
index 00000000000..dce08072f5d
--- /dev/null
+++ b/addons/website/static/src/snippets/s_image_gallery/000.scss
@@ -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);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/addons/website/static/src/snippets/s_image_gallery/options.js b/addons/website/static/src/snippets/s_image_gallery/options.js
new file mode 100644
index 00000000000..819ac1aedc0
--- /dev/null
+++ b/addons/website/static/src/snippets/s_image_gallery/options.js
@@ -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++) {
+ $('
', {
+ 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 = $('', {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 = $('', {class: colClass});
+ $col.append($img).appendTo($row);
+ if ((index + 1) % columns === 0) {
+ $row = $('', {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 = $('', {class: 'row'});
+ this._replaceContent($row);
+
+ // Create columns
+ for (var c = 0; c < columns; c++) {
+ var $col = $('', {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 = $('', {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 = $('', {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_editable_mode_display 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 () {
+ 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,
+ },
+ });
+ },
+});
+});
diff --git a/addons/website/views/assets.xml b/addons/website/views/assets.xml
index 93c2532f29d..625fe454b0d 100644
--- a/addons/website/views/assets.xml
+++ b/addons/website/views/assets.xml
@@ -109,6 +109,7 @@
+
diff --git a/addons/website/views/snippets/s_image_gallery.xml b/addons/website/views/snippets/s_image_gallery.xml
new file mode 100644
index 00000000000..8d1a3190942
--- /dev/null
+++ b/addons/website/views/snippets/s_image_gallery.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+ Add images
+ Remove all images
+
+
+ Float
+ Masonry
+ Grid
+ Slideshow
+
+
+ 1s
+ 2s
+ 3s
+ 5s
+ 10s
+ Disable autoplay
+
+
+ 1
+ 2
+ 3
+ 4
+ 6
+ 12
+
+
+ None
+ Small
+ Medium
+ Big
+
+
+ Square
+ Rounded corners
+ Thumbnails
+ Circle
+ Shadows
+
+
+
+
+ Re-order
+ Move to first
+ Move to previous
+ Move to next
+ Move to last
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/temp_xml b/addons/website/views/snippets/snippets.xml
similarity index 94%
rename from temp_xml
rename to addons/website/views/snippets/snippets.xml
index 61454331697..7e058da30f6 100644
--- a/temp_xml
+++ b/addons/website/views/snippets/snippets.xml
@@ -1,14 +1,6 @@
-
-
-
-
-
- Add images
- Remove all images
-
-
- Float
- Masonry
- Grid
- Slideshow
-
-
- 1s
- 2s
- 3s
- 5s
- 10s
- Disable autoplay
-
-
- 1
- 2
- 3
- 4
- 6
- 12
-
-
- None
- Small
- Medium
- Big
-
-
- Square
- Rounded corners
- Thumbnails
- Circle
- Shadows
-
-
-
-
-
- Re-order
- Move to first
- Move to previous
- Move to next
- Move to last
-
-
-