PhotoSwipe implementation

While looking for a better way to show pictures on a web site I found PhotoSwipe 4.1.3 by Dmitry Semenov.

It is a really powerful library but the Getting Started page left me confused. It didn't really make it clear how to set up a simple, static page with images and the more dynamic method of automatically building the gallery links seemed to rely on a very rigid and confusing HTML structure.

So I started off creating some simple examples using the static method. These were instructional for me in learning how to use the library, and might be of interest to others but ultimately did not end up being used. They require too much manual coding of the gallery data without providing any advantages. In fact the display is worse as it doesn't show the zoom in/out effect when opening and closing the gallery.

These examples, together with the later attempts and my final script are in the following file:

Zip

This is intended to be expanded into the downloaded PhotoSwipe-4.1.3 directory at the same level as the "dist", "src" and "website" subdirectories. The images included were downloaded from the Getting Started page.

My next step was to try and amend the dynamic script from the Getting Started page. I first tried to amend the script to use normal anchors rather than the Figure tag. While it wasn't that hard to do I decided I would understand more if I tried writing my own version.

I wanted it to be more flexible in finding the images and also in grouping them together, even if there were several Divs for one gallery. Rather than search through the web page to create the gallery when an image is clicked I decided to create the links when the page is loaded and use that data when the gallery is opened. I ended up with a script called GalleryLoad.js that I load with the other PhotoSwipe scripts in the [head] section of the page.

/*
         Gallery Load script for PhotoSwipe 4.1.3
               Version 1.1    July 2019
                  by Graham O'Neill
*/

var nextGid = 1;

// =======================================================================
// Open a new gallery and (usually, but optionally) get Items from page
// =======================================================================

gallery = function (galleryIdOrClass, getItems=true) {
   this.galleryName = galleryIdOrClass;
   this.gid = nextGid;
   this.nextPid = 1;
   this.pswpElement = document.querySelectorAll('.pswp')[0];
   this.options = [];
   this.items = [];
   nextGid++;

   var   self = this,
         sectionHeader = document.querySelectorAll(this.galleryName);

   // Init DIV (or whatever) for gallery GID
   for (var i=0, ln=sectionHeader.length; i<ln; i++) {
      sectionHeader[i].setAttribute('data-gid', this.gid);
      sectionHeader[i].onclick = onThumbnailsClick.bind(this);
   }

   // Init OPTIONS array
   this.options = {

      galleryUID: this.gid,
      galleryPIDs: true,

      // See Options -> getThumbBoundsFn section of documentation for more info
      getThumbBoundsFn: function(index) {
         var   thumbnail = self.items[index].el,
               rect = thumbnail.getBoundingClientRect(),
               pageYScroll = window.pageYOffset || document.documentElement.scrollTop;
         return {x:rect.left, y:rect.top + pageYScroll, w:rect.width};
      }
   };

   // Init ITEMS array
   if (getItems) this.initItems();
}

// -----------------------------------------------------------------------
// Load Items from page after (optionally) clearing data from a previous load
// -----------------------------------------------------------------------

gallery.prototype.initItems = function (resetItems=false) {
   var   sectionHeader = document.querySelectorAll(this.galleryName),
         ln = sectionHeader.length;

   if (resetItems) {
      this.nextPid = 1;
      this.items = [];
      for (var i=0; i<ln; i++) {
         this._clearFromDOM(sectionHeader[i]);
      }
   }

   for (var i=0; i<ln; i++) {
      this._getFromDOM(sectionHeader[i]);
   }
}

gallery.prototype._getFromDOM = function (node) {
   var   dataSize,
         picSrc, picMsrc, picSizes, picCapt, picPid, picNode,
         picItem,
         stack, stkNode;

   if (node.nodeType === 1) {

      dataSize = node.getAttribute('data-size');
      if (dataSize !== null && dataSize !== '') {

         // Set element index starting at 0
         node.setAttribute('data-indx',this.nextPid-1);

         // Add to ITEMS array
         picSizes = dataSize.split('x');

         picCapt = node.getAttribute('data-capt');

         picPid = node.getAttribute('data-pid');
         if (picPid === null || picPid === '') {
            picPid = this.nextPid.toString(10);
         }

         picNode = node;
         if (node.tagName === 'IMG') {
            picSrc = node.getAttribute('src');
            picMsrc = picSrc;
         } else {
            if (node.tagName === 'A') {
               picSrc = node.getAttribute('href');
            } else {
               picSrc = node.getAttribute('data-src');
            }
            stack = [node];
            while (stack.length > 0) {    // Scan all descendants for IMG tag
               stkNode = stack.pop();
               if (stkNode.tagName === 'IMG') {
                  picNode = stkNode;
                  picMsrc = stkNode.getAttribute('src');
                  break;
               }
               for (var i = stkNode.childNodes.length-1; i >= 0; i--) {
                  if (stkNode.childNodes[i].nodeType === 1) stack.push(stkNode.childNodes[i]);
               }
            }
         }

         picItem = {
            ix: this.nextPid-1,     // save original array position in case of sort or shuffle
            src: picSrc,
            w: parseInt(picSizes[0], 10),
            h: parseInt(picSizes[1], 10),
            title: picCapt,
            pid: picPid,
            el: picNode             // save link to element for getThumbBoundsFn
         };
         if (picMsrc != '') picItem.msrc = picMsrc;

         this.items.push(picItem);
         this.nextPid++;
      }
   }

   node = node.firstChild;
   while (node) {
      this._getFromDOM(node);
      node = node.nextSibling;
   }
}

gallery.prototype._clearFromDOM = function (node) {
   if (node.nodeType === 1) node.removeAttribute('data-indx');
   node = node.firstChild;
   while (node) {
      this._clearFromDOM(node);
      node = node.nextSibling;
   }
}

// -----------------------------------------------------------------------
// Open a gallery (perhaps from a button) without clicking on thumbnails
// -----------------------------------------------------------------------

gallery.prototype.show = function (indx=0) {
   var   ln = this.items.length,
         pswpGallery;

   if (indx >= ln) return;
   // search for correct index in case of sort or shuffle
   for (var i=0; i<ln; i++) {
      if (this.items[i].ix == indx) {
         this.options.index = i;
         break;
      }
   }

   // open PhotoSwipe
   pswpGallery = new PhotoSwipe(this.pswpElement, PhotoSwipeUI_Default, this.items, this.options);
   pswpGallery.init();
}

// -----------------------------------------------------------------------
// Options to sort or shuffle the images in the gallery
// -----------------------------------------------------------------------

gallery.prototype.sortItemsByCapt = function () {
   var i,j,n;
   var ln=this.items.length-1;
   for (i=0; i<ln; i++) {
      n=i;
      for (j=i+1; j<=ln; j++) {
         if (this.items[j].title+_zeroPad(this.items[j].ix) < this.items[n].title+_zeroPad(this.items[n].ix)) n=j;
      }
      if (n != i) this._swapItems(i,n);
   }
}

gallery.prototype.sortItemsByPid = function () {
   var i,j,n;
   var ln=this.items.length-1;
   for (i=0; i<ln; i++) {
      n=i;
      for (j=i+1; j<=ln; j++) {
         if (this.items[j].pid+_zeroPad(this.items[j].ix) < this.items[n].pid+_zeroPad(this.items[n].ix)) n=j;
      }
      if (n != i) this._swapItems(i,n);
   }
}

gallery.prototype.shuffleItems = function () {
   var i,n;
   var ln=this.items.length-1;
   for (i=0; i<ln; i++) {
      n = _getRandomInt(ln-i+1)+i;
      if (n != i) this._swapItems(i,n);
   }
}

gallery.prototype._swapItems = function (a,b) {
   var temp;
   temp = this.items[a];
   this.items[a] = this.items[b];
   this.items[b] = temp;
}

function _zeroPad (val) {
   var str = '000' + val.toString();
   return str.slice(-3);
}

function _getRandomInt (max) {
  return Math.floor(Math.random() * Math.floor(max));    // Random integer 0..(max-1)
}

// =======================================================================
// Code that runs when a thumbnail is clicked
// =======================================================================

onThumbnailsClick = function (e) {
   // Because of the .bind() THIS is gallery object:        alert(this.galleryName);
   // NODE is set to IMG that was clicked (not the HREF):   alert(node.outerHTML);

   var   node = e.target || e.srcElement,
         dataIndx,
         indx,
         dataGid,
         pswpGallery;

   // search for data-indx in parents until top of gallery or body
   while (true) {
      dataIndx = node.getAttribute('data-indx');
      if (dataIndx !== null && dataIndx !== '') {
         indx = parseInt(dataIndx, 10);
         // search items for correct index in case of sort or shuffle
         for (var i=0, ln=this.items.length; i<ln; i++) {
            if (this.items[i].ix == indx) {
               this.options.index = i;
               break;
            }
         }
         break;
      }
      node = node.parentNode;
      if (node === document.body) break;
      dataGid = node.getAttribute('data-gid');
      if (dataGid !== null && dataGid !== '') break;
   }

   // Not an indexed image so quit
   if (dataIndx === null || dataIndx === '') return;

   // Found an index so prevent default action
   e = e || window.event;
   e.preventDefault ? e.preventDefault() : e.returnValue = false;

   // open PhotoSwipe since valid index was found
   pswpGallery = new PhotoSwipe(this.pswpElement, PhotoSwipeUI_Default, this.items, this.options);
   pswpGallery.init();

   return false;
}

// =======================================================================
// After creating galleries and getting items check if URL opens gallery
// =======================================================================

checkIfUrlCall = function (galleries) {
   var hash = window.location.hash.substring(1);
   if (hash === '') return;

   var   params = hash.toLowerCase().split('&'),
         parts,
         Gid = 0,
         Pid = '',
         checkGals,
         useGal = -1,
         useIdx = -1,
         pswpGallery;

   for (var i=0, ln=params.length; i<ln; i++) {
      parts = params[i].split('=');
      if (parts.length != 2) continue;
      if (parts[0] === 'gid') Gid = parseInt(parts[1], 10);
      if (parts[0] === 'pid') Pid = parts[1];
   }
   if (Gid <= 0 || Gid >= nextGid || Pid === '') return;

   if (galleries instanceof Array) {
      checkGals = galleries;
   } else {
      checkGals = [galleries];
   }

   for (var i=0, ln=checkGals.length; i<ln; i++) {
      if (checkGals[i].gid === Gid) {
         useGal = i;
         break;
      }
   }
   if (useGal === -1) return;

   for (var i=0, ln=checkGals[useGal].items.length; i<ln; i++) {
      if (checkGals[useGal].items[i].pid.toLowerCase() === Pid) {
         useIdx = i;
         break;
      }
   }
   if (useIdx === -1) return;

   checkGals[useGal].options.index = useIdx;

   // open PhotoSwipe since valid index was found
   pswpGallery = new PhotoSwipe(checkGals[useGal].pswpElement, PhotoSwipeUI_Default, checkGals[useGal].items, checkGals[useGal].options);
   pswpGallery.init();
}

The "Load Galleries.html" file included in the download above and the following HTML listing demonstrates ways in which it can be used. I have also provided a demonstration of it in action in the "RoomArranger" section at the end of this page.

<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>PhotoSwipe implementation</title>

<link rel="stylesheet" href="path/to/photoswipe.css"> 
<link rel="stylesheet" href="path/to/default-skin/default-skin.css"> 
<script src="path/to/photoswipe.min.js"></script> 
<script src="path/to/photoswipe-ui-default.min.js"></script> 

<script src="GalleryLoad.js"></script>

<style>
.pswp__caption__center {
   text-align: center;     /* Why isn't this the default? */
   font-size: 1.5em;
   margin-top: 0.3em;
   margin-bottom: 1.5em;}

.pswp__counter {
   color: #ffff00;
   font-size: 1.5em;
   font-weight: bold;}
}
</style>
</head>

<body>
<h1>Example PhotoSwipe implementation</h1>

<div class="galleryA">

<h2>Gallery 1</h2>

<div class="mygrid">
    <div class="area1">
        <a href="images/pic1.jpg" data-size="964x1024" data-capt="Normal anchor with thumbnail">
            <img src="images/pic1small.jpg" alt="Pic1" height="60" width="60">
        </a>
    </div>
    <div class="area2">
        <a href="images/pic2.jpg" data-size="1024x683" data-capt="Thumbnail at lower level">
            <div class="something">
                <p>Some text</p>
                <img src="images/pic2small.jpg" alt="Pic2" height="60" width="60">
            </div>
        </a>
     </div>
</div>

<br><br>
<button type="button" onclick="galleryList[0].show()">Open gallery 1</button>

</div>

<div id="galleryB">

<h2>Gallery 2</h2>

<div data-src="images/pic3.jpg" data-size="1024x768" data-capt="DIV with thumbnail and PID" data-pid="CustomPid">
    <img src="images/pic3small.jpg" alt="Pic3" height="60" width="60">
</div>
<img src="images/pic4.jpg" alt="Pic4" height="60" width="60" data-size="1024x1024" data-capt="IMG link to gallery">

<a href="images/pic5.jpg" data-capt="No data-size so not in gallery">
    <img src="images/pic5small.jpg" alt="Pic5" height="60" width="60">
</a>

<br><br>
<button type="button" onclick="galleryList[1].show(1)">Open gallery 2 at pic 2</button>

</div>


<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
    <!-- Background of PhotoSwipe.
         It's a separate element, as animating opacity is faster than rgba(). -->
    <div class="pswp__bg"></div>
    <!-- Slides wrapper with overflow:hidden. -->
    <div class="pswp__scroll-wrap">
        <!-- Container that holds slides. PhotoSwipe keeps only 3 slides in DOM to save memory. -->
        <div class="pswp__container">
            <!-- don't modify these 3 pswp__item elements, data is added later on -->
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
        </div>
        <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
        <div class="pswp__ui pswp__ui--hidden">
            <div class="pswp__top-bar">
                <!--  Controls are self-explanatory. Order can be changed. -->
                <div class="pswp__counter"></div>
                <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
                <button class="pswp__button pswp__button--share" title="Share"></button>
                <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
                <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
                <!-- Preloader demo https://codepen.io/dimsemenov/pen/yyBWoR -->
                <!-- element will get class pswp__preloader--active when preloader is running -->
                <div class="pswp__preloader">
                    <div class="pswp__preloader__icn">
                      <div class="pswp__preloader__cut">
                        <div class="pswp__preloader__donut"></div>
                      </div>
                    </div>
                </div>
            </div>
            <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
                <div class="pswp__share-tooltip"></div>
            </div>
            <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
            </button>
            <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
            </button>
            <div class="pswp__caption">
                <div class="pswp__caption__center"></div>
            </div>
        </div>
    </div>
</div>


<script>

var galleryList = [];
galleryList[0] = new gallery('.galleryA');
galleryList[1] = new gallery('#galleryB');
checkIfUrlCall(galleryList);

</script>

</body>
</html>

Features it has so far:

Note that the gallery selector can be an ID or class. The checkIfUrlCall() function can be used to automatically open the gallery if the URL included the gallery number and picture number/name. It needs to check the URL parameters against all of the galleries on the page so takes an array as a parameter:

var galleryList = [];
galleryList[0] = new gallery('.galleryA');
galleryList[1] = new gallery('#galleryB');
checkIfUrlCall(galleryList);

An alternative way to do the same thing would be:

var galleryOne, galleryTwo;
galleryOne = new gallery('.galleryA');
galleryTwo = new gallery('#galleryB');
checkIfUrlCall( [galleryOne, galleryTwo] );

If there was just one gallery on the page this could be simplified to just:

var myGallery = new gallery('.galleryA');
checkIfUrlCall(myGallery);

As you can see from this the checkIfUrlCall() function accepts a single gallery variable as well as an array.

One big difference between my script and the Getting Started one is that I create the items array when the gallery is created rather than after a thumbnail has been clicked. This has the advantage that the array can be manipulated before the gallery is displayed. For example I have included functions to sort or shuffle the array:

myGallery.sortItemsByCapt();
myGallery.sortItemsByPid();
myGallery.shuffleItems();

You might want to use these if the web page showed the thumbnails in a random arrangement but you wanted the gallery to show the images in a more structured order. For example, the web page might show pictures of a house arranged to look appealing while the gallery should show the pictures room by room. If you use checkIfUrlCall() when loading the gallery you should sort or shuffle the items array first to get consistent results.

One issue with this approach is if the page is more dynamic with images being added after it has loaded. In this case the array initialisation can be delayed like this:

var myGallery = new gallery('.pic-galleryA', false);
     // ... something to add images ...
myGallery.initItems();
     // ... something now changes the images ...
myGallery.initItems(true);

The false parameter on the gallery() function call stops the items array being created. The true parameter on the second initItems() call tells it to clear out the previously loaded items array and create a new one.

RoomArranger

Since I wanted to demonstrate my PhotoSwipe script I thought I'd use some RoomArranger pictures as examples.

RoomArranger is a floor planning program that allows objects to be created by grouping together primitive shapes, or by importing them from VRML files. It aims for fast real time rendering so that the model can be explored in 3D interactively rather than having longer photorealistic renders from a set path or positions.

These pictures show some of the items I have created:

Click pictures to open gallery
Chair Kitchen
Spacer
Bathroom Bathroom

The Eames chair is made completely within RoomArranger using its built in shapes. The kitchen is also mainly RoomArranger objects, with only the oven handles and extractor fan being VRML. The two bathroom scenes however have many VRML objects: the sinks, bath, towels, lights, cupboard handles and window blinds.