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 @@