dimsemenov / PhotoSwipe

JavaScript image gallery for mobile and desktop, modular, framework independent
http://photoswipe.com
MIT License
24.17k stars 3.31k forks source link

Suggestion for a standard gallery loading script #1631

Open Gnome16 opened 5 years ago

Gnome16 commented 5 years ago

Following my earlier message about the Getting Started page (https://github.com/dimsemenov/PhotoSwipe/issues/1626) I thought I'd look at the next level up: creating links dynamically using the example script provided. I got really confused as I didn't want to use Schema and I had trouble understanding and amending the script. I decided the best way to understand it was to try writing my own version.

Initially I wanted to write a simple example script, but it kept growing and now I think it forms a good starting point for a "standard" script when using this library. It's already quite flexible but I'm sure with input from others here we could improve it.

Edits [Version 1.1] The automatically created PIDs were confusing checkIfUrlCall() when the items array was sorted or shuffled. Since "fake" custom PIDs are created (via picPid in the source below) always use those.

I call the file GalleryLoad.js and load it with the other PhotoSwipe scripts in the [head] section:

/*
         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();
}

Features it has so far:

I've created an example page using this (but lacking any CSS to make it look pretty!) to demonstrate some of the options:

<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>More advanced example</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>More advanced example: Two galleries set dynamically</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>

Note that the gallery selector can be an ID or class. The checkIfUrlCall() function needs to check any URL parameters against all of the galleries on the page so takes an array as a parameter. 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);

Also note that the checkIfUrlCall() function accepts a single gallery variable as well as an array.

One big difference between my script and the example 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, for example, the web page showed the thumbnails in a random arrangement but you wanted the gallery to show the images in a more structured order. 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 to the page after the page 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 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.

I welcome any feedback or suggestions on this script. Please let me know what you think!

linkripper commented 5 years ago

@Gnome16 Hey! I just registered to tell you it's working perfectly, and I got my project on Grav CMS up and running really fast without any tinkering and with very little knowledge. I think you should make a repository with your code so it can help more people. Thank you!

Gnome16 commented 5 years ago

@tr-ansgression Thanks for letting me know you found it useful. You are right, I should probably set up a page in Github but in the meantime I have put it on my web page too: https://goneill.co.nz/photoswipe.php This also has a download for the examples I created when working on my original "Getting Started" post.

Elkaroui commented 4 years ago

it's work like magic, I can now make a call from any link or tags with that awesome JS, you make so easy, Thank you so much~