summaryrefslogtreecommitdiff
path: root/addons/website/static/src/snippets
diff options
context:
space:
mode:
authorqsm-odoo <qsm@odoo.com>2019-12-18 02:05:34 +0100
committerqsm-odoo <qsm@odoo.com>2019-12-18 02:14:51 +0100
commitbb25b70a749bcfd6f84cb62936612e311fe6fe85 (patch)
tree1e0369470e1ed33f0ba89d2351f9494146866a22 /addons/website/static/src/snippets
parent885a015b8af83ab2674034af42f201f97ec45f5b (diff)
parentb0c220536c0faf2e7f9279eedb776bf3b7f18363 (diff)
[REF] website: move s_image_gallery snippet to dedicated files
Diffstat (limited to 'addons/website/static/src/snippets')
-rw-r--r--addons/website/static/src/snippets/s_image_gallery/000.js172
-rw-r--r--addons/website/static/src/snippets/s_image_gallery/000.scss156
-rw-r--r--addons/website/static/src/snippets/s_image_gallery/options.js456
3 files changed, 784 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 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++) {
+ $('<img/>', {
+ class: 'img img-fluid',
+ src: attachments[i].image_src,
+ 'data-index': ++index,
+ }).appendTo($container);
+ }
+ this.mode('reset', this.getMode());
+ this.trigger_up('cover_update');
+ resolve();
+ });
+ dialog.open();
+ });
+ },
+ /**
+ * Allows to change the number of columns when displaying images with a
+ * grid-like layout.
+ *
+ * @see this.selectClass for parameters
+ */
+ columns: function (previewMode, widgetValue, params) {
+ const nbColumns = parseInt(widgetValue || '1');
+ this.$target.attr('data-columns', nbColumns);
+
+ this.mode(previewMode, this.getMode(), {}); // TODO improve
+ },
+ /**
+ * Get the image target's layout mode (slideshow, masonry, grid or nomode).
+ *
+ * @returns {String('slideshow'|'masonry'|'grid'|'nomode')}
+ */
+ getMode: function () {
+ var mode = 'slideshow';
+ if (this.$target.hasClass('o_masonry')) {
+ mode = 'masonry';
+ }
+ if (this.$target.hasClass('o_grid')) {
+ mode = 'grid';
+ }
+ if (this.$target.hasClass('o_nomode')) {
+ mode = 'nomode';
+ }
+ return mode;
+ },
+ /**
+ * Displays the images with the "grid" layout.
+ */
+ grid: function () {
+ var imgs = this._getImages();
+ var $row = $('<div/>', {class: 'row'});
+ var columns = this._getColumns();
+ var colClass = 'col-lg-' + (12 / columns);
+ var $container = this._replaceContent($row);
+
+ _.each(imgs, function (img, index) {
+ var $img = $(img);
+ var $col = $('<div/>', {class: colClass});
+ $col.append($img).appendTo($row);
+ if ((index + 1) % columns === 0) {
+ $row = $('<div/>', {class: 'row'});
+ $row.appendTo($container);
+ }
+ });
+ this.$target.css('height', '');
+ },
+ /**
+ * Allows to changes the interval of automatic slideshow (not active in
+ * edit mode).
+ */
+ interval: function (previewMode, widgetValue) {
+ this.$target.find('.carousel:first').attr('data-interval', widgetValue || '0');
+ },
+ /**
+ * Displays the images with the "masonry" layout.
+ */
+ masonry: function () {
+ var self = this;
+ var imgs = this._getImages();
+ var columns = this._getColumns();
+ var colClass = 'col-lg-' + (12 / columns);
+ var cols = [];
+
+ var $row = $('<div/>', {class: 'row'});
+ this._replaceContent($row);
+
+ // Create columns
+ for (var c = 0; c < columns; c++) {
+ var $col = $('<div/>', {class: 'col o_snippet_not_selectable ' + colClass});
+ $row.append($col);
+ cols.push($col[0]);
+ }
+
+ // Dispatch images in columns by always putting the next one in the
+ // smallest-height column
+ while (imgs.length) {
+ var min = Infinity;
+ var $lowest;
+ _.each(cols, function (col) {
+ var $col = $(col);
+ var height = $col.is(':empty') ? 0 : $col.find('img').last().offset().top + $col.find('img').last().height() - self.$target.offset().top;
+ if (height < min) {
+ min = height;
+ $lowest = $col;
+ }
+ });
+ $lowest.append(imgs.pop());
+ }
+ },
+ /**
+ * Allows to change the images layout. @see grid, masonry, nomode, slideshow
+ *
+ * @see this.selectClass for parameters
+ */
+ mode: function (previewMode, widgetValue, params) {
+ widgetValue = widgetValue || 'slideshow'; // FIXME should not be needed
+ this.$target.css('height', '');
+ this[widgetValue]();
+ this.$target
+ .removeClass('o_nomode o_masonry o_grid o_slideshow')
+ .addClass('o_' + widgetValue);
+ this.trigger_up('cover_update');
+ },
+ /**
+ * Displays the images with the standard layout: floating images.
+ */
+ nomode: function () {
+ var $row = $('<div/>', {class: 'row'});
+ var imgs = this._getImages();
+
+ this._replaceContent($row);
+
+ _.each(imgs, function (img) {
+ var wrapClass = 'col-lg-3';
+ if (img.width >= img.height * 2 || img.width > 600) {
+ wrapClass = 'col-lg-6';
+ }
+ var $wrap = $('<div/>', {class: wrapClass}).append(img);
+ $row.append($wrap);
+ });
+ },
+ /**
+ * Allows to remove all images. Restores the snippet to the way it was when
+ * it was added in the page.
+ *
+ * @see this.selectClass for parameters
+ */
+ removeAllImages: function (previewMode) {
+ var $addImg = $('<div>', {
+ class: 'alert alert-info css_editable_mode_display text-center',
+ });
+ var $text = $('<span>', {
+ class: 'o_add_images',
+ style: 'cursor: pointer;',
+ text: _t(" Add Images"),
+ });
+ var $icon = $('<i>', {
+ class: ' fa fa-plus-circle',
+ });
+ this._replaceContent($addImg.append($icon).append($text));
+ },
+ /**
+ * Displays the images with a "slideshow" layout.
+ */
+ slideshow: function () {
+ var imgStyle = this.$el.find('.active[data-styling]').data('styling') || '';
+ var urls = _.map(this._getImages(), function (img) {
+ return $(img).attr('src');
+ });
+ var currentInterval = this.$target.find('.carousel:first').attr('data-interval');
+ var params = {
+ srcs: urls,
+ index: 0,
+ title: "",
+ interval: currentInterval || this.$target.data('interval') || 0,
+ id: 'slideshow_' + new Date().getTime(),
+ userStyle: imgStyle,
+ },
+ $slideshow = $(qweb.render('website.gallery.slideshow', params));
+ this._replaceContent($slideshow);
+ _.each(this.$('img'), function (img, index) {
+ $(img).attr({contenteditable: true, 'data-index': index});
+ });
+ this.$target.css('height', Math.round(window.innerHeight * 0.7));
+
+ // Apply layout animation
+ this.$target.off('slide.bs.carousel').off('slid.bs.carousel');
+ this.$('li.fa').off('click');
+ },
+ /**
+ * Allows to change the style of the individual images.
+ *
+ * @see this.selectClass for parameters
+ */
+ styling: function (previewMode, value) {
+ var classes = _.map(this.$el.find('[data-styling]'), function (el) {
+ return $(el).data('styling');
+ }).join(' ');
+ this.$('img').removeClass(classes).addClass(value);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Handles image removals and image index updates.
+ *
+ * @override
+ */
+ notify: async function (name, data) {
+ await this._super(...arguments);
+ if (name === 'image_removed') {
+ data.$image.remove(); // Force the removal of the image before reset
+ this.mode('reset', this.getMode());
+ } else if (name === 'image_index_request') {
+ var imgs = this._getImages();
+ var position = _.indexOf(imgs, data.$image[0]);
+ imgs.splice(position, 1);
+ switch (data.position) {
+ case 'first':
+ imgs.unshift(data.$image[0]);
+ break;
+ case 'prev':
+ imgs.splice(position - 1, 0, data.$image[0]);
+ break;
+ case 'next':
+ imgs.splice(position + 1, 0, data.$image[0]);
+ break;
+ case 'last':
+ imgs.push(data.$image[0]);
+ break;
+ }
+ _.each(imgs, function (img, index) {
+ // Note: there might be more efficient ways to do that but it is
+ // more simple this way and allows compatibility with 10.0 where
+ // indexes were not the same as positions.
+ $(img).attr('data-index', index);
+ });
+ this.mode('reset', this.getMode());
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _computeWidgetState: function (methodName, params) {
+ switch (methodName) {
+ case 'mode': {
+ let activeModeName = 'slideshow';
+ for (const modeName of params.possibleValues) {
+ if (this.$target.hasClass(`o_${modeName}`)) {
+ activeModeName = modeName;
+ break;
+ }
+ }
+ this.activeMode = activeModeName;
+ return activeModeName;
+ }
+ case 'interval': {
+ const carousel = this.$target[0].querySelector('.carousel');
+ return (carousel && carousel.dataset.interval || '0');
+ }
+ case 'columns': {
+ return `${this._getColumns()}`;
+ }
+ case 'styling': {
+ const img = this.$target[0].querySelector('img');
+ if (!img) {
+ return '';
+ }
+ for (const className of params.possibleValues) {
+ if (className && img.classList.contains(className)) {
+ return className;
+ }
+ }
+ return '';
+ }
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * Returns the images, sorted by index.
+ *
+ * @private
+ * @returns {DOMElement[]}
+ */
+ _getImages: function () {
+ var imgs = this.$('img').get();
+ var self = this;
+ imgs.sort(function (a, b) {
+ return self._getIndex(a) - self._getIndex(b);
+ });
+ return imgs;
+ },
+ /**
+ * Returns the index associated to a given image.
+ *
+ * @private
+ * @param {DOMElement} img
+ * @returns {integer}
+ */
+ _getIndex: function (img) {
+ return img.dataset.index || 0;
+ },
+ /**
+ * Returns the currently selected column option.
+ *
+ * @private
+ * @returns {integer}
+ */
+ _getColumns: function () {
+ return parseInt(this.$target.attr('data-columns')) || 3;
+ },
+ /**
+ * Empties the container, adds the given content and returns the container.
+ *
+ * @private
+ * @param {jQuery} $content
+ * @returns {jQuery} the main container of the snippet
+ */
+ _replaceContent: function ($content) {
+ var $container = this.$('.container:first');
+ $container.empty().append($content);
+ return $container;
+ },
+});
+
+options.registry.gallery_img = options.Class.extend({
+ /**
+ * Rebuilds the whole gallery when one image is removed.
+ *
+ * @override
+ */
+ onRemove: function () {
+ this.trigger_up('option_update', {
+ optionName: 'gallery',
+ name: 'image_removed',
+ data: {
+ $image: this.$target,
+ },
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
+
+ /**
+ * Allows to change the position of an image (its order in the image set).
+ *
+ * @see this.selectClass for parameters
+ */
+ position: function (previewMode, widgetValue, params) {
+ this.trigger_up('deactivate_snippet');
+ this.trigger_up('option_update', {
+ optionName: 'gallery',
+ name: 'image_index_request',
+ data: {
+ $image: this.$target,
+ position: widgetValue,
+ },
+ });
+ },
+});
+});