diff --git a/Gruntfile.js b/Gruntfile.js
index 578539a1..1a23e6d0 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -4,11 +4,50 @@ module.exports = function (grunt) {
require('bsp-grunt')(grunt, {
bsp: {
+ bower: {
+ // Gallery uses Masonry javascript, which has a few dependencies
+ // BEGIN dependencies for Masonry
+ 'outlayer': [
+ {
+ src: 'outlayer.js',
+ dest: 'outlayer/outlayer.js'
+ },
+ {
+ src: 'item.js',
+ dest: 'outlayer/item.js'
+ }
+ ],
+ 'ev-emitter': [
+ {
+ src: 'ev-emitter.js',
+ dest: 'ev-emitter/ev-emitter.js'
+ }
+ ],
+ 'get-size': [
+ {
+ src: 'get-size.js',
+ dest: 'get-size/get-size.js'
+ }
+ ],
+ 'fizzy-ui-utils': [
+ {
+ src: 'utils.js',
+ dest: 'fizzy-ui-utils/utils.js'
+ }
+ ],
+ 'desandro-matches-selector': [
+ {
+ src: 'matches-selector.js',
+ dest: 'desandro-matches-selector/matches-selector.js'
+ }
+ ]
+ // END dependencies for Masonry
+ },
+
styles: {
dir: '',
less: [
- 'base/All.less',
- 'sample/All.less'
+ 'base/All.less'
],
options : {
autoprefixer: true
diff --git a/bower.json b/bower.json
index 633a33e6..3f783f48 100644
--- a/bower.json
+++ b/bower.json
@@ -3,9 +3,13 @@
"version": "3.0.0",
"homepage": "https://github.com/perfectsense/brightspot-base",
"dependencies": {
- "bsp-utils": "perfectsense/brightspot-js-utils#^3.1.0",
- "jquery": "^2.1.4",
- "normalize-css": "necolas/normalize.css#^3.0.3"
+ "bsp-utils": "perfectsense/brightspot-js-utils#3.1.0",
+ "jquery": "2.1.4",
+ "normalize-css": "necolas/normalize.css#3.0.3",
+ "masonry": "masonry#4.1.0",
+ "bsp-modal": "perfectsense/brightspot-js-modal#1.0.2",
+ "bsp-carousel": "perfectsense/brightspot-js-carousel#1.4.1",
+ "vex": "2.3.2"
},
"main": "src/main/webapp/main.js",
"ignore": [
@@ -36,10 +40,10 @@
}
],
"normalize-css": [
- {
- "src": "normalize.css",
- "dest": "bower/normalize-css/normalize.css"
- }
+ {
+ "src": "normalize.css",
+ "dest": "bower/normalize-css/normalize.css"
+ }
]
}
}
diff --git a/src/main/webapp/base/All.less b/src/main/webapp/base/All.less
index 1af23f15..b8837a08 100644
--- a/src/main/webapp/base/All.less
+++ b/src/main/webapp/base/All.less
@@ -13,6 +13,10 @@
@import "main/ArticleMain.less";
@import "main/CreativeWorkMain.less";
@import "main/Main.less";
+@import "main/GalleryMain.less";
+
+@import "GalleryIntro.less";
+@import "GallerySlide.less";
@import "page/Page.less";
@import "page/PageFooter.less";
diff --git a/src/main/webapp/base/GalleryIntro.hbs b/src/main/webapp/base/GalleryIntro.hbs
new file mode 100644
index 00000000..2d495670
--- /dev/null
+++ b/src/main/webapp/base/GalleryIntro.hbs
@@ -0,0 +1,27 @@
+{{#defineBlock "GalleryIntro"}}
+
+ {{#defineElement "titleShort"}}data-shorttext="{{this}}"{{/defineElement}}
+ {{#defineElement "title" noWith=true}}
+
+
+ {{#defineBlockBody}}
+ {{element "title"}}
+ {{element "description"}}
+ {{/defineBlockBody}}
+
+
+ {{/defineBlockContainer}}
+{{/defineBlock}}
diff --git a/src/main/webapp/base/GalleryIntro.less b/src/main/webapp/base/GalleryIntro.less
new file mode 100644
index 00000000..66a5ef95
--- /dev/null
+++ b/src/main/webapp/base/GalleryIntro.less
@@ -0,0 +1,11 @@
+.GalleryIntro {
+
+ &-title {
+ &:extend(.GalleryIntro-title all);
+ }
+
+ &-description {
+ &:extend(.GalleryIntro-description all);
+ }
+
+}
diff --git a/src/main/webapp/base/GallerySlide.hbs b/src/main/webapp/base/GallerySlide.hbs
new file mode 100644
index 00000000..2e50633c
--- /dev/null
+++ b/src/main/webapp/base/GallerySlide.hbs
@@ -0,0 +1,60 @@
+{{#defineBlock "GallerySlide"}}
+
+ {{#defineElement "mediaContent"}}
+
+ {{element "mediaControls"}}
+ {{element "mediaContent"}}
+
+ {{/defineElement}}
+
+ {{#defineElement "infoTitleShort"}}data-shorttext="{{this}}"{{/defineElement}}
+ {{#defineElement "infoTitle" noWith=true}}
+
+ {{element "infoTitle"}}
+ {{element "infoDescription"}}
+ {{element "infoAttribution"}}
+
+ {{/defineElement}}
+
+ {{#defineBlockContainer}}
+
+
+ {{#defineBlockBody}}
+ {{element "media"}}
+ {{element "info"}}
+ {{/defineBlockBody}}
+
+
+ {{/defineBlockContainer}}
+{{/defineBlock}}
diff --git a/src/main/webapp/base/GallerySlide.less b/src/main/webapp/base/GallerySlide.less
new file mode 100644
index 00000000..3dc65425
--- /dev/null
+++ b/src/main/webapp/base/GallerySlide.less
@@ -0,0 +1,23 @@
+.GallerySlide {
+
+ &-image {
+ &:extend(.GallerySlide-image all);
+ }
+
+ &-info {
+ &:extend(.GallerySlide-info all);
+ }
+
+ &-infoTitle {
+ &:extend(.GallerySlide-infoTitle all);
+ }
+
+ &-infoDescription {
+ &:extend(.GallerySlide-infoDescription all);
+ }
+
+ &-infoAttribution {
+ &:extend(.GallerySlide-infoAttribution all);
+ }
+
+}
diff --git a/src/main/webapp/base/main/GalleryMain.hbs b/src/main/webapp/base/main/GalleryMain.hbs
new file mode 100644
index 00000000..45769b39
--- /dev/null
+++ b/src/main/webapp/base/main/GalleryMain.hbs
@@ -0,0 +1,85 @@
+{{#defineBlock "Gallery"}}
+
+ {{#defineElement "intro"}}
+
+ {{element "modalControlsClose"}}
+ {{element "modalControlsFullscreen"}}
+ {{element "modalControlsPrev"}}
+ {{element "modalControlsCount"}}
+ {{element "modalControlsNext"}}
+ {{element "modalControlsInfo"}}
+
+ {{/defineElement}}
+
+ {{#defineElement "modalCarousel" noWith=true}}
+
+ {{element "modalControls"}}
+ {{element "modalCarousel"}}
+
+ {{/defineElement}}
+
+ {{#defineBlockContainer}}
+ This is my intro.
+ *
+ * Then after the background is added:
+ * ', {'class': this.classNames.introBackground}).prependTo(this.$intro);
+
+ // Set a single background image, or a montage of random images from the slides
+ if (this.settings.introBackgroundImage) {
+ this.initIntroBackgroundSingle();
+ } else {
+ this.initIntroBackgroundMontage();
+ }
+ }
+
+
+ /**
+ * Initialize the intro background with a single image.
+ */
+ initIntroBackgroundSingle() {
+
+ // Add an attribute so the image can be styled
+ this.$introBackground.attr(this.attr.introBackground, 'single');
+
+ // Add a single image to the background
+ $('
![]()
', {src:this.settings.introBackgroundImage, alt:''}).appendTo(this.$introBackground);
+ }
+
+
+ /**
+ * Initialize the intro background with a montage of images randomly chosen from the slides.
+ */
+ initIntroBackgroundMontage() {
+
+ // Get the image urls from the slides and randomize the order
+ let urls = this.slideImages;
+ urls = this.shuffle(urls);
+
+ // If there are not enough images for the montage, switch to single image with a random image
+ if (urls.length && urls.length < this.settings.introBackgroundMontageCount) {
+ this.settings.introBackgroundImage = urls[0];
+ this.initIntroBackgroundSingle();
+ return;
+ }
+
+ // Add an attribute so the images can be styled
+ this.$introBackground.attr(this.attr.introBackground, 'montage');
+
+ // Add all the slide images to the background so they can be styled in a montage
+ for (let i=0; i < urls.length && i < this.settings.introBackgroundMontageCount; i++) {
+ $('
![]()
', {src:urls[i], alt:''}).appendTo(this.$introBackground);
+ }
+ }
+
+
+ /**
+ * Initialize the gallery controls to switch from list to tile mode.
+ * The gallery controls consist of the following parts:
+ * Slide count ("64 Photos")
+ * Button to switch to List view
+ * Button to switch to Tile view
+ *
+ * The HTML is expected to look like this:
+ *
+ */
+ initViewControls() {
+
+ // Find the control hooks within the DOM
+ this.$controlsCount = this.$el.find(this.selectors.controlsCount);
+ this.$controlsButtonsList = this.$el.find(this.selectors.controlsButtonsList);
+ this.$controlsButtonsTiles = this.$el.find(this.selectors.controlsButtonsTiles);
+
+ // Set up click events for the buttons
+ this.$controlsButtonsList.on('click', event => {
+ this.modeSetList();
+ return false;
+ });
+ this.$controlsButtonsTiles.on('click', event => {
+ this.modeSetTiles();
+ return false;
+ });
+
+ // Set the initial mode to "List"
+ this.modeSetList();
+
+ // Update the slide count
+ this.controlsUpdateCount();
+ }
+
+
+ initViewList() {
+ let $viewList;
+
+ // Duplicate the slide container
+ // Note: must use CSS to show and hide the list!
+ $viewList = this.$slidesContainer.clone()
+ .attr(this.attr.view, 'list')
+ .insertAfter(this.$slidesContainer);
+
+ // Cache for later use
+ this.$viewList = $viewList;
+ }
+
+
+ initViewTiles() {
+ let $viewTiles;
+
+ // Duplicate the slide container
+ // Note: must use CSS to show and hide the tiles!
+ $viewTiles = this.$slidesContainer.clone()
+ .attr(this.attr.view, 'tiles')
+ .insertAfter(this.$slidesContainer);
+
+ // Cache for later use
+ this.$viewTiles = $viewTiles;
+
+ // Set up masonry layout
+ this.masonry = new Masonry($viewTiles[0], {
+
+ itemSelector: this.selectors.masonryItem,
+
+ // Turn off animations
+ transitionDuration: 0,
+
+ // The left-right gutter between masonry tiles can only be set in the javascript.
+ // Hardcoded for now but should be some kind of option.
+ gutter: 20
+ });
+ }
+
+
+ /**
+ * Initialize the event handlers to show modal view when user clicks image,
+ * plus set up the modal popup.
+ */
+ modalInit() {
+
+ // Find the modal container
+ this.$modal = this.$el.find(this.selectors.modal);
+ if (!this.$modal.length) {
+ // If we can't find the modal container don't bother continuing
+ // because we cannot support zooming
+ return;
+ }
+ this.$modal.detach();
+
+ // Add an attribute to the modal to indicate if fullscreen mode is supported
+ // so we can use CSS to display or hide the fullscreen icons
+ if (this.fullscreenIsSupported()) {
+ this.$modal.attr(this.attr.fullscreenIsSupported, '');
+ }
+
+ // Turn the modal container into a modal but don't open it.
+ // Note this will remove the modal container from the DOM.
+ this.modal = Object.create(bspModal);
+ this.modal.init(this.$modal, {theme: 'Gallery', id: 'Gallery'});
+
+ // Create a single event handler for all clicks on slideMedia containers.
+ // This container contains the modal controls, the slide image, and possibly other
+ // things like social media share buttons.
+ this.$el.on('click', this.selectors.slideMedia, (event) => {
+
+ let $target = $(event.target);
+
+ // Only do something if clicking on the slide image or the modal control
+ // Do not do anything if clicking on other things in the image container
+ // (like social media share buttons)
+ if ($target.is(this.selectors.slideMediaImg) || $target.is(this.selectors.slideMediaZoom)) {
+ // Get the start of the slide that was clicked
+ let $slide = $target.closest(this.selectors.slide);
+ let $siblings = $slide.parent().find(this.selectors.slide);
+ let slideIndex = $siblings.index($slide);
+
+ // Display the modal and start on the slide that was clicked
+ this.modalOpen(slideIndex);
+ }
+ return false;
+ });
+ }
+
+
+ /**
+ * Open the modal to display a slide carousel.
+ *
+ * @param {Number} index
+ * The index of the slide to zoom (0=first slide, n-1=last slide)
+ */
+ modalOpen(index) {
+
+ let $modalCarousel;
+ let $modalSlides;
+
+ // Because the vex modal calls jQuery.remove() when the modal is closed,
+ // all the jQuery events are destroyed, so we need to create the events
+ // each time we open the modal.
+ this.$modal.find(this.selectors.modalClose).on('click', event => {
+ this.modalClose();
+ return false;
+ });
+ this.$modal.find(this.selectors.modalPrev).on('click', event => {
+ this.modalPrev();
+ return false;
+ });
+ this.$modal.find(this.selectors.modalNext).on('click', event => {
+ this.modalNext();
+ return false;
+ });
+ this.$modal.find(this.selectors.modalInfo).on('click', event => {
+ this.modalInfoToggle();
+ return false;
+ });
+ this.$modal.find(this.selectors.modalFullscreen).on('click', event => {
+ this.modalFullscreen();
+ return false;
+ });
+
+ // Open the modal, using class 'modal-theme-gallery'
+ this.modal.open(this.$modal, {theme: 'gallery', id: 'gallery'});
+
+ // Because the vex modal calls jQuery.remove() when the modal is closed,
+ // all the jQuery events are destroyed, so we need to recreate the carousel.
+ $modalCarousel = this.$modal.find(this.selectors.modalCarousel);
+ $modalCarousel.empty();
+
+ // If this is a singleton gallery, and it has an id that will be used to tie multiple galleries together,
+ // grab all the slides on the page that share the same id.
+ if (this.settings.singleton && this.settings.id) {
+
+ $modalSlides = this.$slidesContainer.clone().empty().attr(this.attr.view, 'modal').appendTo($modalCarousel);
+
+ // Find all the galleries that match our gallery id
+ let $galleries = $(this.selectors.main).filter((index, el) => {
+ let id = $(el).attr(this.attr.singleton) || '';
+ return id === this.settings.id;
+ });
+
+ // Get all the slides within the matching galleries
+ let $slides = $galleries.find(this.selectors.slide);
+
+ // Update the index number for the slide we want to show first
+ index = $slides.index( this.$slides[0] ) || 0;
+
+ // Clone the slides and add them to the modal
+ $slides.clone().appendTo($modalSlides);
+
+ } else {
+ // This is not a singleton gallery with an id, so just get the slides from this gallery
+ $modalSlides = this.$slidesContainer.clone().attr(this.attr.view, 'modal').appendTo($modalCarousel);
+ }
+
+ // Whenever the carousel slide changes update the count
+ $modalCarousel.on('afterChange', event => {
+ this.modalUpdateCount();
+ });
+
+ // Create the carousel within the modal
+ this.carousel = Object.create(bspCarousel);
+ this.carousel.init($modalSlides, {
+ // theme: '',
+ themeConfig: {
+ initialSlide: index,
+ arrows: false
+ }
+ });
+
+ // Focus on the carousel the keyboard navigation will work
+ this.$modal.find('.slick-list').focus();
+
+ // Intialize the count so it shows the initial slide number
+ this.modalUpdateCount();
+
+ // Hide the info by default
+ this.modalInfoHide();
+
+ // Optionally go to full screen mode
+ if (this.settings.fullscreen) {
+ this.modalFullscreen();
+ }
+ }
+
+
+ /**
+ * When in modal view, close the modal and return to the gallery.
+ */
+ modalClose() {
+ if (this.modal) {
+ this.modal.close();
+ }
+ }
+
+
+ /**
+ * When in modal view, go to the next slide in the carousel.
+ */
+ modalNext() {
+ if (this.carousel) {
+ this.carousel.next();
+ }
+ }
+
+
+ /**
+ * When in modal view, go to the previous slide in the carousel.
+ */
+ modalPrev() {
+ if (this.carousel) {
+ this.carousel.prev();
+ }
+ }
+
+
+ /**
+ * Toggle show or hide the slide info in the modal view.
+ * @param {Boolean} [show=toggle]
+ * Use true to show the info, or false to hide the info.
+ * If not specied, the default is to toggle the current state.
+ */
+ modalInfoToggle(flag) {
+ let show;
+ show = (flag === undefined) ? !this.modalInfoIsShowing() : flag;
+ if (show) {
+ this.modalInfoShow();
+ } else {
+ this.modalInfoHide();
+ }
+ }
+
+
+ /**
+ * Show the slide info when in modal mode.
+ */
+ modalInfoShow() {
+ this.$modal.attr(this.attr.showInfo, '');
+ }
+
+
+ /**
+ * Hide the slide info when in modal view.
+ */
+ modalInfoHide() {
+ this.$modal.removeAttr(this.attr.showInfo);
+ }
+
+
+ /**
+ * Determine if the slide info is currently showing in modal view.
+ * @return {Boolean} Returns true if the info is currently showing.
+ */
+ modalInfoIsShowing() {
+ return this.$modal[0].hasAttribute(this.attr.showInfo);
+ }
+
+
+ /**
+ * Show the slide navigation when in modal mode.
+ */
+ modalNavShow() {
+ this.$modal.removeAttr(this.attr.hideNav);
+ }
+
+
+ /**
+ * Hide the slide navigation when in modal view.
+ */
+ modalNavHide() {
+ this.$modal.attr(this.attr.hideNav, '');
+ }
+
+
+ /**
+ * When in modal view, update the slide count (current slide and total number of slides).
+ */
+ modalUpdateCount() {
+ let $count;
+ let currentSlide;
+ let total;
+ if (this.carousel) {
+ $count = this.$modal.find(this.selectors.modalCount);
+ currentSlide = this.carousel.currentSlide() + 1;
+ total = this.carousel.slideCount();
+ $count.text(currentSlide + '/' + total);
+
+ // Add an attribute so the navigation can be hidden if there is only one slide
+ if (total > 1) {
+ this.modalNavShow();
+ } else {
+ this.modalNavHide();
+ }
+ }
+ }
+
+
+ /**
+ * Take the modal view to full screen width.
+ *
+ * Note this can be called only from a user-intiated event (like a click event handler)
+ * and may not work in all browsers or in other situations (like when running in an iframe).
+ */
+ modalFullscreen() {
+
+ let el = this.$modal[0];
+
+ if (el) {
+ this.fullscreenElement(el);
+ }
+ }
+
+
+ /**
+ * Get a list of slide images.
+ * @return {Array of Strings} Array of slide image urls.
+ */
+ get slideImages() {
+ let urls = [];
+ this.$el.find(this.selectors.slideMedia).each((i, el) => {
+ // Find the first img element within the slideMedia block
+ let src = $(el).find('img').attr('src');
+ if (src) {
+ urls.push(src)
+ }
+ });
+ return urls;
+ }
+
+
+ /**
+ * Return the number of slides in the gallery.
+ * @return {Number} Number of slides in the gallery.
+ */
+ get count() {
+ return this.$slides.length;
+ }
+
+
+ /**
+ * Set the gallery count display ("13 Photos").
+ */
+ controlsUpdateCount() {
+ // Template "13 Photos" is hardcoded.
+ // Should probably make an option to configure the template.
+ this.$controlsCount.html(`${this.count} Photos`);
+ }
+
+
+ /**
+ * Set the mode to List mode.
+ * Hides the tile display, shows the list display, and makes the List button active.
+ */
+ modeSetList() {
+ this._modeClear();
+ this.$controlsButtonsList.attr(this.attr.buttonActive, '');
+ this.$viewList.attr(this.attr.viewActive, '');
+ }
+
+
+ /**
+ * Set the mode to Tiles mode.
+ * Hides the list display, shows the tiles display, and makes the Tiles button active.
+ */
+ modeSetTiles() {
+ this._modeClear();
+ this.$controlsButtonsTiles.attr(this.attr.buttonActive, '');
+ this.$viewTiles.attr(this.attr.viewActive, '');
+ this.masonry.layout();
+ }
+
+
+ /**
+ * Clear the mode display (list or tile mode). This unsets the active button and hides all modes.
+ */
+ _modeClear() {
+
+ this.$controlsButtonsTiles.removeAttr(this.attr.buttonActive);
+ this.$viewTiles.removeAttr(this.attr.viewActive);
+
+ this.$controlsButtonsList.removeAttr(this.attr.buttonActive);
+ this.$viewList.removeAttr(this.attr.viewActive);
+ }
+
+
+ /**
+ * Determine if fullscreen is supported in this browser, and if so add an attribute to the
+ * modal so we can use CSS to style and display a fullscreen icon.
+ */
+ fullscreenInit() {
+ if (this.fullscreenIsSupported()) {
+ this.$modal.attr(this.attr.fullscreenIsSupported, '');
+ }
+ }
+
+
+ /**
+ * Determine if this browser supports full screen mode.
+ * @return {Boolean}
+ */
+ fullscreenIsSupported() {
+ let el = this.$el[0];
+ return Boolean(
+ el.requestFullscreen
+ || el.webkitRequestFullscreen
+ || el.mozRequestFullScreen
+ || el.msRequestFullscreen
+ );
+ }
+
+
+ /**
+ * Set an element to fullscreen mode.
+ *
+ * Note this can be called only from a user-intiated event (like a click event handler)
+ * and may not work in all browsers or in other situations (like when running in an iframe).
+ *
+ * @param {Element} el
+ */
+ fullscreenElement(el) {
+
+ if (el.requestFullscreen) {
+ el.requestFullscreen();
+ } else if (el.webkitRequestFullscreen) {
+ el.webkitRequestFullscreen();
+ } else if (el.mozRequestFullScreen) {
+ el.mozRequestFullScreen();
+ } else if (el.msRequestFullscreen) {
+ el.msRequestFullscreen();
+ }
+ }
+
+
+ /**
+ * Randomly shuffle an array in place.
+ * @param {Array} array
+ * @return {Array}
+ */
+ shuffle(array) {
+ let currentIndex = array.length;
+ let temporaryValue;
+ let randomIndex;
+
+ // While there remain elements to shuffle...
+ while (0 !== currentIndex) {
+
+ // Pick a remaining element...
+ randomIndex = Math.floor(Math.random() * currentIndex);
+ currentIndex -= 1;
+
+ // And swap it with the current element.
+ temporaryValue = array[currentIndex];
+ array[currentIndex] = array[randomIndex];
+ array[randomIndex] = temporaryValue;
+ }
+
+ return array;
+ }
+}
+
+
+// Set up a bspUtils.plugin so an element with data-bsp-gallery-list
+// will automatically create a Gallery list
+export default bspUtils.plugin(false, 'bsp-gallery', 'list', {
+
+ '_each': function(item) {
+
+ // Get options from the data-bsp-gallery-options attribute
+ let options = this.option(item);
+
+ // Create the gallery object
+ let gallery = new Gallery(item, options);
+
+ // Save the Gallery object on the element so it can be accessed later if necessary
+ $(item).data('bsp-gallery', gallery);
+
+ // Run it!
+ gallery.init();
+ }
+});
diff --git a/src/main/webapp/base/main/GalleryMain.less b/src/main/webapp/base/main/GalleryMain.less
new file mode 100644
index 00000000..f2352818
--- /dev/null
+++ b/src/main/webapp/base/main/GalleryMain.less
@@ -0,0 +1,86 @@
+.Gallery {
+
+ //---------------
+ // Gallery-intro
+ // Javascript also adds either Gallery-intro-background-single or Gallery-intro-background-montage
+ // to display images as a background behind the intro content.
+ //---------------
+ &-intro {
+ &:extend(.Gallery-intro all);
+
+ &-background { // Optional element added by javascript
+ &:extend(.Gallery-intro-background all);
+ }
+
+ &-background-single { // Optional element added by javascript
+ &:extend(.Gallery-intro-background-single all);
+ }
+
+ &-background-montage { // Optional element added by javascript
+ &:extend(.Gallery-intro-background-montage all);
+ }
+ }
+
+ //------------------
+ // Gallery-controls
+ // Contains a photo count, plus buttons to switch between list and tiles views.
+ //------------------
+ &-controls {
+ &:extend(.Gallery-controls all);
+
+ &-count {
+ &:extend(.Gallery-controls-count all);
+ }
+
+ &-buttons {
+ &:extend(.Gallery-controls-buttons all);
+
+ // List button
+ &-list {
+ &:extend(.Gallery-controls-buttons-list all);
+ }
+
+ // Tiles button
+ &-tiles {
+ &:extend(.Gallery-controls-buttons-tiles all);
+ }
+
+ // Make a button appear selected
+ &-active {
+ &:extend(.Gallery-controls-buttons-active all);
+ }
+ }
+ }
+
+ //----------------
+ // Gallery-slides
+ // Container for the gallery slides.
+ // However note that this is hidden because=, but the javascript duplicates the content
+ // into Gallery-view-list and Gallery-view-tiles
+ // Contains GallerySlide blocks.
+ // ---------------
+ &-slides {
+ &:extend(.Gallery-slides all);
+ // Refer to GallerySlide.less
+ }
+
+ //-------------------
+ // Gallery-view-list
+ // Added by javascript to represent slides in a list view.
+ // Contains GallerySlide blocks.
+ //-------------------
+ &-view-list { // Elements added by javascript
+ &:extend(.Gallery-view-list all);
+ // Refer to GallerySlide.less
+ }
+
+ //-------------------
+ // Gallery-view-tiles
+ // Added by javascript to represent slides in a tile view.
+ // Contains GallerySlide blocks.
+ //-------------------
+ &-view-tiles { // Elements added by javascript
+ &:extend(.Gallery-view-tiles all);
+ // Refer to GallerySlide.less
+ }
+}
diff --git a/src/main/webapp/main.js b/src/main/webapp/main.js
index 6db5a4d2..737439db 100644
--- a/src/main/webapp/main.js
+++ b/src/main/webapp/main.js
@@ -18,4 +18,6 @@ import Comment from "community/commenting/Comment.js";
import CommentEntry from "community/commenting/CommentEntry.js";
import Commenting from "community/commenting/Commenting.js";
+import Gallery from "base/main/GalleryMain.js";
+
export default {};
diff --git a/src/main/webapp/sample/All.less b/src/main/webapp/sample/All.less
deleted file mode 100644
index 192c19fc..00000000
--- a/src/main/webapp/sample/All.less
+++ /dev/null
@@ -1 +0,0 @@
-@import "../base/All.less";
diff --git a/styleguide/_config.json b/styleguide/_config.json
index 13287a89..31b1c4e3 100644
--- a/styleguide/_config.json
+++ b/styleguide/_config.json
@@ -3,10 +3,6 @@
{
"name": "Base",
"href": "/base/All.min.css"
- },
- {
- "name": "Samples",
- "href": "/sample/All.min.css"
}
]
}
diff --git a/styleguide/main/gallery-main/_gallery-slide.json b/styleguide/main/gallery-main/_gallery-slide.json
new file mode 100644
index 00000000..4ba8e8d2
--- /dev/null
+++ b/styleguide/main/gallery-main/_gallery-slide.json
@@ -0,0 +1,16 @@
+{
+ "_template": "/base/GallerySlide",
+
+ "mediaContent": {
+ "_template": "/base/util/Image",
+ "src": "{{image([200, 800], [200, 640])}}"
+ },
+
+ "infoTitle": "{{words(8,12)}}",
+ "infoTitleShort": "{{words(3,6)}}",
+
+ "infoDescription": "{{words(60,120)}}",
+ "infoDescriptionShort": "{{words(20,40)}}",
+
+ "infoAttribution": "Photo by {{words(6,10)}}"
+}
diff --git a/styleguide/main/gallery-main/gallery-main-single-intro-image.json b/styleguide/main/gallery-main/gallery-main-single-intro-image.json
new file mode 100644
index 00000000..1b188855
--- /dev/null
+++ b/styleguide/main/gallery-main/gallery-main-single-intro-image.json
@@ -0,0 +1,18 @@
+{
+ "_template": "/base/main/GalleryMain",
+
+ "intro": {
+ "_template": "/base/GalleryIntro",
+ "title": "{{words(8,12)}}",
+ "description": "{{words(60,120)}}"
+ },
+
+ "introBackgroundImage": "{{image(640, 400)}}",
+
+ "slides": [
+ {
+ "_dataUrl": "/main/gallery-main/_gallery-slide.json",
+ "_repeat": [8,20]
+ }
+ ]
+}
diff --git a/styleguide/main/gallery-main/gallery-main-single-intro-image.md b/styleguide/main/gallery-main/gallery-main-single-intro-image.md
new file mode 100644
index 00000000..ff26d8ae
--- /dev/null
+++ b/styleguide/main/gallery-main/gallery-main-single-intro-image.md
@@ -0,0 +1,4 @@
+## Gallery with single intro background image
+
+This shows the gallery if you specify the `introBackgroundImage` property.
+Instead of a matrix of images, you get a single image background.
diff --git a/styleguide/main/gallery-main/gallery-main-single-slide.json b/styleguide/main/gallery-main/gallery-main-single-slide.json
new file mode 100644
index 00000000..309133cb
--- /dev/null
+++ b/styleguide/main/gallery-main/gallery-main-single-slide.json
@@ -0,0 +1,47 @@
+{
+ "_template": "/base/util/Concatenated",
+ "items" : [
+ {
+ "_template": "/base/util/RawHtml",
+ "html": "{{paragraphs(1)}}"
+ },
+ {
+ "_template": "/base/main/GalleryMain",
+ "slides": [
+ {
+ "_dataUrl": "/main/gallery-main/_gallery-slide.json"
+ }
+ ]
+ },
+ {
+ "_template": "/base/util/RawHtml",
+ "html": "{{paragraphs(1)}}"
+ },
+ {
+ "_template": "/base/main/GalleryMain",
+ "id": "my-singleton-unique-id-2",
+ "slides": [
+ {
+ "_dataUrl": "/main/gallery-main/_gallery-slide.json"
+ }
+ ]
+ },
+ {
+ "_template": "/base/util/RawHtml",
+ "html": "{{paragraphs(1)}}"
+ },
+ {
+ "_template": "/base/main/GalleryMain",
+ "id": "my-singleton-unique-id-2",
+ "slides": [
+ {
+ "_dataUrl": "/main/gallery-main/_gallery-slide.json"
+ }
+ ]
+ },
+ {
+ "_template": "/base/util/RawHtml",
+ "html": "{{paragraphs(1)}}"
+ }
+ ]
+}
diff --git a/styleguide/main/gallery-main/gallery-main-single-slide.md b/styleguide/main/gallery-main/gallery-main-single-slide.md
new file mode 100644
index 00000000..47f4a59c
--- /dev/null
+++ b/styleguide/main/gallery-main/gallery-main-single-slide.md
@@ -0,0 +1,19 @@
+## Gallery singleton image
+
+You can display a single image embedded in other content (for example, an image in an article), and still get
+the gallery experience. Clicking the image opens the modal carousel as usual. The modal can display just a single
+image, or you can associate multiple gallery singletons to be displayed in the same modal carousel (by specifying the
+same id for each gallery singleton).
+
+In this demonstration, the first image opens by itself. The second and third images are associated, so if you open the
+modal carousel you can navigate between those two slides.
+
+### Blocks and properties
+The singleton gallery experience is triggered automatically when the gallery contains a only a single slide.
+The singleton gallery uses the same properties as the main gallery, but it does not display the intro block
+or the list/tiles view. It has an additional (optional) property:
+* `Gallery`:
+ * `id`: (String/Optional) An id can be used to associate multiple singletons, so when the user clicks the singleton image,
+ the modal carousel lets the user navigate between all the images that have the same id. If this is not specified
+ then the modal carousel will show only the image that was clicked, and will not have navigation controls.
+
diff --git a/styleguide/main/gallery-main/gallery-main.json b/styleguide/main/gallery-main/gallery-main.json
new file mode 100644
index 00000000..6f9c5519
--- /dev/null
+++ b/styleguide/main/gallery-main/gallery-main.json
@@ -0,0 +1,20 @@
+{
+ "_template": "/base/main/GalleryMain",
+
+ "intro": {
+ "_template": "/base/GalleryIntro",
+ "title": "{{words(8,12)}}",
+ "titleShort": "{{words(4,8)}}",
+ "description": "{{words(60,80)}}",
+ "descriptionShort": "{{words(20,50)}}"
+ },
+
+ "introBackgroundImage": "",
+
+ "slides": [
+ {
+ "_dataUrl": "/main/gallery-main/_gallery-slide.json",
+ "_repeat": [8,12]
+ }
+ ]
+}
diff --git a/styleguide/main/gallery-main/gallery-main.md b/styleguide/main/gallery-main/gallery-main.md
new file mode 100644
index 00000000..c7c6bee1
--- /dev/null
+++ b/styleguide/main/gallery-main/gallery-main.md
@@ -0,0 +1,30 @@
+## Gallery with random image montage
+
+### Requirements
+* [Jira Ticket and Requirements](https://perfectsense.atlassian.net/browse/BSP-1571)
+* Designs: [Zeplin](https://zpl.io/Z11ulFh), [PNG](https://www.dropbox.com/sh/n1ljua526dyo6l3/AAB43ovSiTFXz4LxKuIARrGfa?dl=0), [Sketch](https://www.dropbox.com/s/bz9w6u8mn742rjt/Base%20Photo%20Gallery.sketch?dl=0)
+
+### Blocks and properties
+* `Gallery`:
+ * `intro`: (Block) A single `GalleryIntro` block.
+ * `introBackgroundImage`: (URL/Optional) By default, the gallery displays a randomized matrix of images
+ as a background. However, you can specify a single image here to use as the background. This URL will
+ be placed in the `data-gallery-intro-background-image` HTML parameter and used by the JavaScript.
+ * `slides`: (Array) List of `GallerySlide` blocks.
+ * `modal` (HTML) Placeholder for the gallery modal, including additional properties for adding controls to the modal.
+* `GalleryIntro`: Introductory content for the gallery.
+ * `title` (String|HTML)
+ * `titleShort` (String|HTML) Optional short version of the title.
+ * `description` (HTML)
+ * `descriptionShort` (HTML) Optional short version of the description.
+* `GallerySlide`: A single slide for the gallery.
+ * `media`
+ * `mediaContent`: (Image Element) Image or other content for the slide.
+ * `mediaControls` (HTML) Controls to overlay on top of the media.
+ * `mediaControlsZoom` (HTML) Control to zoom the image (display the modal)
+ * info: (HTML) Descriptive content for the image. By default contains infoTitle, infoDescription, infoAttribution.
+ * `infoTitle` (HTML) Title for the image.
+ * `infoTitleShort` (HTML) Optional short title for the image.
+ * `infoDescription` (HTML) Description for the image.
+ * `infoDescriptionShort` (HTML) Optional short description for the image.
+ * `infoAttribution` (HTML) Attribution for the image.