summaryrefslogtreecommitdiff
path: root/addons/website/static/src/snippets/s_image_gallery
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/website/static/src/snippets/s_image_gallery
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/website/static/src/snippets/s_image_gallery')
-rw-r--r--addons/website/static/src/snippets/s_image_gallery/000.js180
-rw-r--r--addons/website/static/src/snippets/s_image_gallery/000.scss155
-rw-r--r--addons/website/static/src/snippets/s_image_gallery/000.xml68
-rw-r--r--addons/website/static/src/snippets/s_image_gallery/001.scss281
-rw-r--r--addons/website/static/src/snippets/s_image_gallery/options.js481
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,
+ },
+ });
+ },
+});
+});