diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/website/static/src/snippets/s_image_gallery | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/website/static/src/snippets/s_image_gallery')
5 files changed, 1165 insertions, 0 deletions
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 00000000..9bd0f8e3 --- /dev/null +++ b/addons/website/static/src/snippets/s_image_gallery/000.js @@ -0,0 +1,180 @@ +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: '.s_image_gallery:not(.o_slideshow)', + xmlDependencies: ['/website/static/src/snippets/s_image_gallery/000.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 $images = $cur.closest('.s_image_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) + }; + + var $img = ($cur.is('img') === true) ? $cur : $cur.closest('img'); + + const milliseconds = $cur.closest('.s_image_gallery').data('interval') || false; + var $modal = $(qweb.render('website.gallery.slideshow.lightbox', { + images: $images.get(), + index: $images.index($img), + 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/snippets/s_image_gallery/000.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]'); + let indicatorWidth = this.$indicator.width(); + if (indicatorWidth === 0) { + // An ancestor may be hidden so we try to find it and make it + // visible just to take the correct width. + const $indicatorParent = this.$indicator.parents().not(':visible').last(); + if (!$indicatorParent[0].style.display) { + $indicatorParent[0].style.display = 'block'; + indicatorWidth = this.$indicator.width(); + $indicatorParent[0].style.display = ''; + } + } + let nbPerPage = Math.floor(indicatorWidth / $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 (page <= 0) { + self.$prev.detach(); + } else { + self.$prev.removeClass('d-none'); + self.$prev.prependTo(self.$indicator); + } + if (page >= nbPages - 1) { + self.$next.detach(); + } else { + self.$next.removeClass('d-none'); + self.$next.appendTo(self.$indicator); + } + } + + function update() { + const active = $lis.filter('.active'); + index = active.length ? $lis.index(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); + // We dont use hide() before the slide animation in the editor because there is a traceback + // TO DO: fix this traceback + if (!self.editableMode) { + 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 00000000..a99bc86f --- /dev/null +++ b/addons/website/static/src/snippets/s_image_gallery/000.scss @@ -0,0 +1,155 @@ + +.o_gallery:not([data-vcss]) { + &.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; + } + > div:first-child { + height: 100%; + } + .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; + line-height: 40px; + margin: 2.5px 2.5px 2.5px 2.5px; + padding: 0 !important; + 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); + } + } + } + } + } + .carousel-inner .item img { + max-width: none; + } +} + +// Note: the s_gallery_lightbox is always using the right dom and classes of the +// most recent version of the snippet as it is generated by JS after page load. diff --git a/addons/website/static/src/snippets/s_image_gallery/000.xml b/addons/website/static/src/snippets/s_image_gallery/000.xml new file mode 100644 index 00000000..a50a1af5 --- /dev/null +++ b/addons/website/static/src/snippets/s_image_gallery/000.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <!-- + ======================================================================== + Gallery Slideshow + + This template is used to display a slideshow of images inside a + bootstrap carousel. + + ======================================================================== + --> + <t t-name="website.gallery.slideshow"> + <div t-attf-id="#{id}" class="carousel slide" data-ride="carousel" t-attf-data-interval="#{interval}" style="margin: 0 12px;"> + <div class="carousel-inner" style="padding: 0;"> + <t t-foreach="images" t-as="image"> + <div t-attf-class="carousel-item #{image_index == index and 'active' or None}"> + <img t-attf-class="#{attrClass || 'img img-fluid d-block'}" t-att-src="image.src" t-att-style="attrStyle" t-att-alt="image.alt" data-name="Image"/> + </div> + </t> + </div> + + <ul class="carousel-indicators"> + <li class="o_indicators_left text-center d-none" aria-label="Previous" title="Previous"> + <i class="fa fa-chevron-left"/> + </li> + <t t-foreach="images" t-as="image"> + <li t-attf-data-target="##{id}" t-att-data-slide-to="image_index" t-att-class="image_index == index and 'active' or None" t-attf-style="background-image: url(#{image.src})"></li> + </t> + <li class="o_indicators_right text-center d-none" aria-label="Next" title="Next"> + <i class="fa fa-chevron-right"/> + </li> + </ul> + + <a class="carousel-control-prev o_we_no_overlay" t-attf-href="##{id}" data-slide="prev" aria-label="Previous" title="Previous"> + <span class="fa fa-chevron-left fa-2x text-white"></span> + <span class="sr-only">Previous</span> + </a> + <a class="carousel-control-next o_we_no_overlay" t-attf-href="##{id}" data-slide="next" aria-label="Next" title="Next"> + <span class="fa fa-chevron-right fa-2x text-white"></span> + <span class="sr-only">Next</span> + </a> + </div> + </t> + + <!-- + ======================================================================== + Gallery Slideshow LightBox + + This template is used to display a lightbox with a slideshow. + + This template wraps website.gallery.slideshow in a bootstrap modal + dialog. + ======================================================================== + --> + <t t-name="website.gallery.slideshow.lightbox"> + <div role="dialog" class="modal o_technical_modal fade s_gallery_lightbox p-0" aria-labbelledby="Image Gallery Dialog"> + <div class="modal-dialog m-0" role="Picture Gallery" + t-attf-style=""> + <div class="modal-content bg-transparent"> + <main class="modal-body o_slideshow bg-transparent"> + <button type="button" class="close text-white" data-dismiss="modal" style="position: absolute; right: 10px; top: 10px;"><span role="img" aria-label="Close">×</span><span class="sr-only">Close</span></button> + <t t-call="website.gallery.slideshow"></t> + </main> + </div> + </div> + </div> + </t> +</templates> diff --git a/addons/website/static/src/snippets/s_image_gallery/001.scss b/addons/website/static/src/snippets/s_image_gallery/001.scss new file mode 100644 index 00000000..b20d0be3 --- /dev/null +++ b/addons/website/static/src/snippets/s_image_gallery/001.scss @@ -0,0 +1,281 @@ + +.s_image_gallery[data-vcss="001"] { + &.o_grid, &.o_masonry { + .img { + width: 100%; + } + } + &.o_grid { + &.o_spc-none div.row { + margin-bottom: 0px; + } + &.o_spc-small div.row > div { + margin-bottom: $spacer; + } + &.o_spc-medium div.row > div { + margin-bottom: $spacer * 2; + } + &.o_spc-big div.row > div { + margin-bottom: $spacer * 3; + } + } + &.o_masonry { + &.o_spc-none div.o_masonry_col { + padding: 0; + > img { + margin: 0 !important; + } + } + &.o_spc-small div.o_masonry_col { + padding: 0 ($spacer * .5); + > img { + margin-bottom: $spacer !important; + } + } + &.o_spc-medium div.o_masonry_col { + padding: 0 $spacer; + > img { + margin-bottom: $spacer * 2 !important; + } + } + &.o_spc-big div.o_masonry_col { + padding: 0 ($spacer * 1.5); + > img { + margin-bottom: $spacer * 3 !important; + } + } + } + &.o_nomode { + &.o_spc-none .row div { + padding-top: 0; + padding-bottom: 0; + } + &.o_spc-small .row div { + padding-top: $spacer * .5; + padding-bottom: $spacer * .5; + } + &.o_spc-medium .row div { + padding-top: $spacer; + padding-bottom: $spacer; + } + &.o_spc-big .row div { + padding-top: $spacer * 1.5; + padding-bottom: $spacer * 1.5; + } + } + &:not(.o_slideshow) { + img { + cursor: pointer; + } + } + &.o_slideshow { + .carousel { + .carousel-item.active, + .carousel-item-next, + .carousel-item-prev, + .carousel-control-next, + .carousel-control-prev { + padding-bottom: 64px; + } + ul.carousel-indicators li { + border: 1px solid #aaa; + } + } + ul.carousel-indicators { + position: absolute; + left: 0%; + bottom: 0; + width: 100%; + height: auto; + margin-left: 0; + padding: 0; + border-width: 0; + > * { + list-style-image: none; + display: inline-block; + width: 40px; + height: 40px; + line-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; + background-position: center; + border-radius: 0; + vertical-align: bottom; + flex: 0 0 40px; + &:not(.active) { + opacity: 0.8; + filter: grayscale(1); + } + } + } + > .container, > .container-fluid, > .o_container_small { + height: 100%; + } + &.s_image_gallery_cover .carousel-item { + > a { + width: 100%; + height: 100%; + } + > a > img, + > img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + &:not(.s_image_gallery_show_indicators) .carousel { + ul.carousel-indicators { + display: none; + } + .carousel-item.active, + .carousel-item-next, + .carousel-item-prev, + .carousel-control-next, + .carousel-control-prev { + padding-bottom: 0px; + } + } + &.s_image_gallery_indicators_arrows_boxed, &.s_image_gallery_indicators_arrows_rounded { + .carousel { + .carousel-control-prev .fa, + .carousel-control-next .fa { + text-shadow: none; + } + } + } + &.s_image_gallery_indicators_arrows_boxed { + .carousel { + .carousel-control-prev .fa:before { + content: "\f104"; + padding-right: 2px; + } + .carousel-control-next .fa:before { + content: "\f105"; + padding-left: 2px; + } + .carousel-control-prev .fa:before, + .carousel-control-next .fa:before { + display: block; + width: 3rem; + height: 3rem; + line-height: 3rem; + color: black; + background: white; + font-size: 1.25rem; + border: 1px solid $gray-500; + } + } + } + &.s_image_gallery_indicators_arrows_rounded { + .carousel { + .carousel-control-prev .fa:before { content: "\f060"; } + .carousel-control-next .fa:before { content: "\f061"; } + .carousel-control-prev .fa:before, + .carousel-control-next .fa:before { + color: black; + background: white; + font-size: 1.25rem; + border-radius: 50%; + padding: 1.25rem; + border: 1px solid $gray-500; + } + } + } + &.s_image_gallery_indicators_rounded { + .carousel { + ul.carousel-indicators li { + border-radius: 50%; + } + } + } + &.s_image_gallery_indicators_dots { + .carousel { + ul.carousel-indicators { + height: 40px; + margin: auto; + + li { + max-width: 8px; + max-height: 8px; + margin: 0 6px; + border-radius: 10px; + background-color: $black; + background-image: none !important; + + &:not(.active) { + opacity: .4; + } + } + } + } + } + + @extend %image-gallery-slideshow-styles; + } + .carousel-inner .item img { + max-width: none; + } +} + +.s_gallery_lightbox { + .close { + font-size: 2rem; + } + .modal-dialog { + height: 100%; + background-color: rgba(0,0,0,0.7); + } + @include media-breakpoint-up(sm) { + .modal-dialog { + max-width: 100%; + padding: 0; + } + } + ul.carousel-indicators { + display: none; + } + + .modal-body.o_slideshow { + @extend %image-gallery-slideshow-styles; + } +} + +%image-gallery-slideshow-styles { + &:not(.s_image_gallery_cover) .carousel-item { + > a { + display: flex; + height: 100%; + width: 100%; + } + > a > img, + > img { + max-height: 100%; + max-width: 100%; + margin: auto; + } + } + .carousel { + height: 100%; + + .carousel-inner { + height: 100%; + } + .carousel-item.active, + .carousel-item-next, + .carousel-item-prev, + .carousel-control-next, + .carousel-control-prev { + display: flex; + align-items: center; + height: 100%; + } + .carousel-control-next .fa, + .carousel-control-prev .fa { + text-shadow: 0px 0px 3px $gray-800; + } + } +} 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 00000000..8afb5c1f --- /dev/null +++ b/addons/website/static/src/snippets/s_image_gallery/options.js @@ -0,0 +1,481 @@ +odoo.define('website.s_image_gallery_options', function (require) { +'use strict'; + +var core = require('web.core'); +var weWidgets = require('wysiwyg.widgets'); +var options = require('web_editor.snippets.options'); + +var _t = core._t; +var qweb = core.qweb; + +options.registry.gallery = options.Class.extend({ + xmlDependencies: ['/website/static/src/snippets/s_image_gallery/000.xml'], + + /** + * @override + */ + start: function () { + var self = this; + + // Make sure image previews are updated if images are changed + this.$target.on('image_changed', 'img', function (ev) { + var $img = $(ev.currentTarget); + var index = self.$target.find('.carousel-item.active').index(); + self.$('.carousel:first li[data-target]:eq(' + index + ')') + .css('background-image', 'url(' + $img.attr('src') + ')'); + }); + + // When the snippet is empty, an edition button is the default content + // TODO find a nicer way to do that to have editor style + this.$target.on('click', '.o_add_images', function (e) { + e.stopImmediatePropagation(); + self.addImages(false); + }); + + this.$target.on('dropped', 'img', function (ev) { + self.mode(null, self.getMode()); + if (!ev.target.height) { + $(ev.target).one('load', function () { + setTimeout(function () { + self.trigger_up('cover_update'); + }); + }); + } + }); + + const $container = this.$('> .container, > .container-fluid, > .o_container_small'); + if ($container.find('> *:not(div)').length) { + self.mode(null, self.getMode()); + } + + return this._super.apply(this, arguments); + }, + /** + * @override + */ + onBuilt: function () { + if (this.$target.find('.o_add_images').length) { + this.addImages(false); + } + // TODO should consider the async parts + this._adaptNavigationIDs(); + }, + /** + * @override + */ + onClone: function () { + this._adaptNavigationIDs(); + }, + /** + * @override + */ + cleanForSave: function () { + if (this.$target.hasClass('slideshow')) { + this.$target.removeAttr('style'); + } + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Allows to select images to add as part of the snippet. + * + * @see this.selectClass for parameters + */ + addImages: function (previewMode) { + const $images = this.$('img'); + var $container = this.$('> .container, > .container-fluid, > .o_container_small'); + var dialog = new weWidgets.MediaDialog(this, {multiImages: true, onlyImages: true, mediaWidth: 1920}); + var lastImage = _.last(this._getImages()); + var index = lastImage ? this._getIndex(lastImage) : -1; + return new Promise(resolve => { + dialog.on('save', this, function (attachments) { + for (var i = 0; i < attachments.length; i++) { + $('<img/>', { + class: $images.length > 0 ? $images[0].className : 'img img-fluid d-block ', + src: attachments[i].image_src, + 'data-index': ++index, + alt: attachments[i].description || '', + 'data-name': _t('Image'), + style: $images.length > 0 ? $images[0].style.cssText : '', + }).appendTo($container); + } + if (attachments.length > 0) { + this.mode('reset', this.getMode()); + this.trigger_up('cover_update'); + } + }); + dialog.on('closed', this, () => resolve()); + dialog.open(); + }); + }, + /** + * Allows to change the number of columns when displaying images with a + * grid-like layout. + * + * @see this.selectClass for parameters + */ + columns: function (previewMode, widgetValue, params) { + const nbColumns = parseInt(widgetValue || '1'); + this.$target.attr('data-columns', nbColumns); + + this.mode(previewMode, this.getMode(), {}); // TODO improve + }, + /** + * Get the image target's layout mode (slideshow, masonry, grid or nomode). + * + * @returns {String('slideshow'|'masonry'|'grid'|'nomode')} + */ + getMode: function () { + var mode = 'slideshow'; + if (this.$target.hasClass('o_masonry')) { + mode = 'masonry'; + } + if (this.$target.hasClass('o_grid')) { + mode = 'grid'; + } + if (this.$target.hasClass('o_nomode')) { + mode = 'nomode'; + } + return mode; + }, + /** + * Displays the images with the "grid" layout. + */ + grid: function () { + var imgs = this._getImages(); + var $row = $('<div/>', {class: 'row s_nb_column_fixed'}); + var columns = this._getColumns(); + var colClass = 'col-lg-' + (12 / columns); + var $container = this._replaceContent($row); + + _.each(imgs, function (img, index) { + var $img = $(img); + var $col = $('<div/>', {class: colClass}); + $col.append($img).appendTo($row); + if ((index + 1) % columns === 0) { + $row = $('<div/>', {class: 'row s_nb_column_fixed'}); + $row.appendTo($container); + } + }); + this.$target.css('height', ''); + }, + /** + * Displays the images with the "masonry" layout. + */ + masonry: function () { + var self = this; + var imgs = this._getImages(); + var columns = this._getColumns(); + var colClass = 'col-lg-' + (12 / columns); + var cols = []; + + var $row = $('<div/>', {class: 'row s_nb_column_fixed'}); + this._replaceContent($row); + + // Create columns + for (var c = 0; c < columns; c++) { + var $col = $('<div/>', {class: 'o_masonry_col o_snippet_not_selectable ' + colClass}); + $row.append($col); + cols.push($col[0]); + } + + // Dispatch images in columns by always putting the next one in the + // smallest-height column + while (imgs.length) { + var min = Infinity; + var $lowest; + _.each(cols, function (col) { + var $col = $(col); + var height = $col.is(':empty') ? 0 : $col.find('img').last().offset().top + $col.find('img').last().height() - self.$target.offset().top; + if (height < min) { + min = height; + $lowest = $col; + } + }); + $lowest.append(imgs.shift()); + } + }, + /** + * Allows to change the images layout. @see grid, masonry, nomode, slideshow + * + * @see this.selectClass for parameters + */ + mode: function (previewMode, widgetValue, params) { + widgetValue = widgetValue || 'slideshow'; // FIXME should not be needed + this.$target.css('height', ''); + this.$target + .removeClass('o_nomode o_masonry o_grid o_slideshow') + .addClass('o_' + widgetValue); + this[widgetValue](); + this.trigger_up('cover_update'); + this._refreshPublicWidgets(); + }, + /** + * Displays the images with the standard layout: floating images. + */ + nomode: function () { + var $row = $('<div/>', {class: 'row s_nb_column_fixed'}); + var imgs = this._getImages(); + + this._replaceContent($row); + + _.each(imgs, function (img) { + var wrapClass = 'col-lg-3'; + if (img.width >= img.height * 2 || img.width > 600) { + wrapClass = 'col-lg-6'; + } + var $wrap = $('<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_non_editable_mode_hidden 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 () { + const imageEls = this._getImages(); + const images = _.map(imageEls, img => ({ + // Use getAttribute to get the attribute value otherwise .src + // returns the absolute url. + src: img.getAttribute('src'), + alt: img.getAttribute('alt'), + })); + var currentInterval = this.$target.find('.carousel:first').attr('data-interval'); + var params = { + images: images, + index: 0, + title: "", + interval: currentInterval || 0, + id: 'slideshow_' + new Date().getTime(), + attrClass: imageEls.length > 0 ? imageEls[0].className : '', + attrStyle: imageEls.length > 0 ? imageEls[0].style.cssText : '', + }, + $slideshow = $(qweb.render('website.gallery.slideshow', params)); + this._replaceContent($slideshow); + _.each(this.$('img'), function (img, index) { + $(img).attr({contenteditable: true, 'data-index': index}); + }); + this.$target.css('height', Math.round(window.innerHeight * 0.7)); + + // Apply layout animation + this.$target.off('slide.bs.carousel').off('slid.bs.carousel'); + this.$('li.fa').off('click'); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Handles image removals and image index updates. + * + * @override + */ + notify: function (name, data) { + this._super(...arguments); + if (name === 'image_removed') { + data.$image.remove(); // Force the removal of the image before reset + this.mode('reset', this.getMode()); + } else if (name === 'image_index_request') { + var imgs = this._getImages(); + var position = _.indexOf(imgs, data.$image[0]); + imgs.splice(position, 1); + switch (data.position) { + case 'first': + imgs.unshift(data.$image[0]); + break; + case 'prev': + imgs.splice(position - 1, 0, data.$image[0]); + break; + case 'next': + imgs.splice(position + 1, 0, data.$image[0]); + break; + case 'last': + imgs.push(data.$image[0]); + break; + } + position = imgs.indexOf(data.$image[0]); + _.each(imgs, function (img, index) { + // Note: there might be more efficient ways to do that but it is + // more simple this way and allows compatibility with 10.0 where + // indexes were not the same as positions. + $(img).attr('data-index', index); + }); + const currentMode = this.getMode(); + this.mode('reset', currentMode); + if (currentMode === 'slideshow') { + const $carousel = this.$target.find('.carousel'); + $carousel.removeClass('slide'); + $carousel.carousel(position); + this.$target.find('.carousel-indicators li').removeClass('active'); + this.$target.find('.carousel-indicators li[data-slide-to="' + position + '"]').addClass('active'); + this.trigger_up('activate_snippet', { + $snippet: this.$target.find('.carousel-item.active img'), + ifInactiveOptions: true, + }); + $carousel.addClass('slide'); + } else { + this.trigger_up('activate_snippet', { + $snippet: data.$image, + ifInactiveOptions: true, + }); + } + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _adaptNavigationIDs: function () { + var uuid = new Date().getTime(); + this.$target.find('.carousel').attr('id', 'slideshow_' + uuid); + _.each(this.$target.find('[data-slide], [data-slide-to]'), function (el) { + var $el = $(el); + if ($el.attr('data-target')) { + $el.attr('data-target', '#slideshow_' + uuid); + } else if ($el.attr('href')) { + $el.attr('href', '#slideshow_' + uuid); + } + }); + }, + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'mode': { + let activeModeName = 'slideshow'; + for (const modeName of params.possibleValues) { + if (this.$target.hasClass(`o_${modeName}`)) { + activeModeName = modeName; + break; + } + } + this.activeMode = activeModeName; + return activeModeName; + } + case 'columns': { + return `${this._getColumns()}`; + } + } + return this._super(...arguments); + }, + /** + * @private + */ + async _computeWidgetVisibility(widgetName, params) { + if (widgetName === 'slideshow_mode_opt') { + return false; + } + return this._super(...arguments); + }, + /** + * Returns the images, sorted by index. + * + * @private + * @returns {DOMElement[]} + */ + _getImages: function () { + var imgs = this.$('img').get(); + var self = this; + imgs.sort(function (a, b) { + return self._getIndex(a) - self._getIndex(b); + }); + return imgs; + }, + /** + * Returns the index associated to a given image. + * + * @private + * @param {DOMElement} img + * @returns {integer} + */ + _getIndex: function (img) { + return img.dataset.index || 0; + }, + /** + * Returns the currently selected column option. + * + * @private + * @returns {integer} + */ + _getColumns: function () { + return parseInt(this.$target.attr('data-columns')) || 3; + }, + /** + * Empties the container, adds the given content and returns the container. + * + * @private + * @param {jQuery} $content + * @returns {jQuery} the main container of the snippet + */ + _replaceContent: function ($content) { + var $container = this.$('> .container, > .container-fluid, > .o_container_small'); + $container.empty().append($content); + return $container; + }, +}); + +options.registry.gallery_img = options.Class.extend({ + /** + * Rebuilds the whole gallery when one image is removed. + * + * @override + */ + onRemove: function () { + this.trigger_up('option_update', { + optionName: 'gallery', + name: 'image_removed', + data: { + $image: this.$target, + }, + }); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Allows to change the position of an image (its order in the image set). + * + * @see this.selectClass for parameters + */ + position: function (previewMode, widgetValue, params) { + this.trigger_up('option_update', { + optionName: 'gallery', + name: 'image_index_request', + data: { + $image: this.$target, + position: widgetValue, + }, + }); + }, +}); +}); |
