IT Cooking

Success is just one script away

WordPress plugin: LightBox Bootstrap Zoom v1.0

6 min read
Yet another lightbox... for WordPress... But this one has a zoom! You can zoom in by scrolling, and clicking to advance through the gallery as usual.
lbbz example zoom featured meme

lbbz example zoom featured meme

Yet another lightbox… for WordPress… But this one has a zoom! You can zoom in by scrolling, and clicking simply advances through the gallery as usual.

What does lbbz WordPress Lightbox plugin do?

Very simple, it’s a lightbox in the form of 3 snippets. WordPress plugin release very soon. Click on those pictures to see what it can do:

portrait ginger girl 1
square, always pixelated
portrait taylor 1
portrait
coherentfacialexpressions3.8 openposegrid
super large landscape

Features:

  • based off Modal · Bootstrap v5.3 (getbootstrap.com)
  • open and close with a button or click outside image
  • scroll zoom in and out
  • disables zoom if image is inside the box dimensions
  • drag image
  • drag release if outside the box
  • pixelated at zoom scale 2x: disables resampling via css

TODO:

  • fix bugs: sometimes the sizes are not detected properly and a page refresh is needed
  • handle galleries: simply add rel=name to any of your images to make a gallery (a bit like foobox)
  • add navigation buttons
  • embed bootstrap if needed, currently my theme embeds it
  • wrap that shit up in a WP plugin and release it!
  • make money?

Code: github

How To Implement lbbz LightBox Zoom in WordPress?

Pretty simple, you need a PHP code snippet plugin, and one for CSS/JS, or one that does all 3. We recommend:

Code Snippets
Code Snippets: does PHP, html, CSS, JS. With a catch: all is PHP
Simple Custom CSS and JS
Simple Custom CSS and JS

The lbbz WordPress Code Snippets

lbbz PHP code

PHP code does two things:

  • inject a Bootstrap html modal in singular (posts and pasges)
  • add data sets to all images, that point to and trigger the Boostrap modal:
    • data-bs-toggle="modal" data-bs-target="#lightbox-modal"
// all img should have: data-bs-toggle="modal" data-bs-target="#lightbox-modal"
function get_lightbox_html($content){
if ( is_singular() && in_the_loop() && is_main_query() ) {
$modal = <<<HEREDOC
<div id="lightbox-modal" class="modal fade" tabindex="-1" aria-hidden="true">
    <div id="lightbox-modal-bg" class="modal-dialog modal-dialog-centered">
    <div id="lightbox-modal-content" class="modal-content lightbox_zoom_outer">
      <button id="lightbox-modal-close-btn" type="button" class="btn btn-secondary" data-bs-dismiss="modal">❌</button>
      <div id="lightbox-modal-body" class="modal-body">
      <img id="lightbox-modal-img" decoding="async" loading="lazy" />
      </div>
    </div>
    </div>
  </div>	
HEREDOC;
return $content.$modal;
}
}
add_filter('the_content', 'get_lightbox_html', 10);

// WordPress lightbox-modal data add to all images: data-bs-toggle="modal" data-bs-target="#lightbox-modal"
// noobs prefer the heavy DOM method... preg_replace is 100x faster
function add_lightbox_data_to_img( $content ) {
if ( is_singular() && in_the_loop() && is_main_query() ) {
  global $post;
  $pattern ="/<a (.*?)href=(.*?)><img (.*?)class=\"(.*?)\"(.*?)>/i";
  $replacement = '<a $1href=$2><img data-bs-toggle="modal" data-bs-target="#lightbox-modal" $3class="$4 img-fluid"$5>';
  $content = preg_replace($pattern, $replacement, $content);
  // $html = preg_replace( '/<img /', ' data-bs-toggle="modal" data-bs-target="#lightbox-modal"', $html );
  return $content;
  }
return $content;
}
add_filter( 'the_content', 'add_lightbox_data_to_img' );

 

lbbz JS code

It’s a bit rough, with lots of opportunities to debug, but it works. There is a Bootstrap modal eventListener, a size detection function, some onmouse scroll and move events, and heavy calculations to render the zoom user firendly (and not buggy).

// lightbox modal ///////////////////////////////////////////////

function isHrefImg(src) {
    let extensions = ["png", "jpg", "jpeg", "webp", "avif", "gif"];
  return extensions.some(ext => src.split('.').pop() == ext);
}

//window.addEventListener("load", function(){
jQuery(document).ready(function( $ ){
  
  // https://dev.to/stackfindover/zoom-image-point-with-mouse-wheel-11n3
  // this html addnon has moved into the php function that alters images
  // document.body.insertAdjacentHTML("beforeend", lightboxModalHtml);
  
  // https://getbootstrap.com/docs/5.3/components/modal/#methods
  // php will only add the modal if there is at least 1 image in post
  const lightboxModal = document.getElementById('lightbox-modal')
  if (lightboxModal) {
    // we cannot uselightbox-modal-body because we want to scroll wheel outside the img as well
    //lightbox_zoom = document.getElementById('lightbox-modal-body');
    var lightbox_zoom = document.getElementById('lightbox-modal-content');
    var modalBody = document.getElementById('lightbox-modal-body');
    var modalImg = document.querySelector('.modal-body img');
    var scale, minScale,
      scale_ratio = 1.1,
      clickhold = false,
      pointX = 0, pointY = 0,
      start = { x: 0, y: 0 },
      rect, boundW, boundH, boundPortrait,
      imgRect, imgW, imgH, naturalW, naturalH, imgPortrait,
      pixelated = false,
      rel = null, related=[];

    function resetSizes() {
      clickhold = false
      pointX = 0
      pointY = 0
      start = { x: 0, y: 0 }
      scale = 1
    }
  
    function detectDimensions(event) {
      //console.log('event',event)
      // naturalW naturalH = actual size of the image, we don;t care about those values since all is relative to the computed starting size
      naturalW = modalImg.naturalWidth
      naturalH = modalImg.naturalHeight

      //console.log(img w,h = ${naturalW}x${naturalH})

      // getBoundingClientRect(): computed x,y,right,bottom: start/end from top-left and actual width/height
      // boundW boundH = computed size of the modal-content
      rect = modalBody.getBoundingClientRect();
      //console.log('modalBody rect',rect)
      boundW = rect.width
      boundH = rect.height
      boundPortrait = (boundW > boundH) ? false : true;

      // getBoundingClientRect(): computed x,y,right,bottom: start/end from top-left and actual width/height
      //imgW imgH = computed size of the image onload starting at scale = 1
      imgRect = modalImg.getBoundingClientRect();
      imgW = imgRect.width;
      imgH = imgRect.height;
      imgPortrait = (imgRect.width > imgRect.height) ? false : true;

      //scale = imgRect.width / naturalW	// nope
      //scale = boundW / naturalW			// nope
      minScale = ((boundW / imgW) < (boundH / imgH)) ? boundW / imgW : boundH / imgH;

      // add pixelated class in Image CSS Class to force no sampling: pixelated
      // otherwise, it only gets pixelated above scale=2
      if (modalImg.classList.contains('pixelated')) { pixelated = true }

      resetSizes()
      //console.log(boundWxH=${boundW}x${boundH} imgWxH=${imgW}x${imgH} scale=${scale}=1 minScale=${minScale});
    }

    // event delegation but all img should have: data-bs-toggle="modal" data-bs-target="#lightbox-modal"
    // this has to be added with a PHP snippet unfortunately...
    // we could do it here as well...
    document.getElementsByTagName("article")[0].addEventListener('click', function(event) {
      if (event.target.tagName === 'IMG') {
        event.preventDefault();
        // Handle the click event for the <img> tag
        // console.log('Image clicked:', event.target.src);
        
        // https://getbootstrap.com/docs/5.3/components/modal/#via-javascript
        // we cannot just call the modal here because lightboxModal will not receive event.relatedTarget, and we cannot pass anything to the modal
        // const myModal = new bootstrap.Modal(lightboxModal, {src: event.target.src})
        // console.log('myModal', myModal);
        // myModal.toggle()
      }
    });

    lightboxModal.addEventListener('show.bs.modal', event => {
      //console.log('event',event); // element that triggered the modal
      //console.log('event.classList',event.relatedTarget.classList);

      
      // exit immediately if this is a gallery. Usually handled by other scripts like magnificPopup
      if (event.relatedTarget.closest('.gallery')) {
        // example: MagnificPopup gallery
        return event.preventDefault();

        // https://dimsemenov.com/plugins/magnific-popup/documentation.html#options

        // MagnificPopup has this structure:
        //	<div class="mfp-wrap mfp-gallery mfp-close-btn-in mfp-auto-cursor mfp-ready" tabindex="-1" style="overflow: hidden auto;">
        //		<div class="mfp-container mfp-image-holder mfp-s-ready">
        //			<div class="mfp-content">
        //				<div class="mfp-figure" style="visibility: visible;">
        //					<button title="Close (Esc)" type="button" class="mfp-close">×</button>
        //					<figure><img class="mfp-img" alt="alt" src="src" style="max-height: 588px;">
      }
      
      let parent = event.relatedTarget.parentNode	// parent should be a link if we clicked on an image that links to its full size

      //console.log('event.srcElement,type',event.srcElement,event.type);
      //console.log('modalImg:', modalImg);
      
      // extract target img if exist, if not, close modal
      if (parent.hasAttribute("href")) {
        let parentHref = parent.href;
        if (isHrefImg(parentHref)) {
          // console.log('parentHref img:', parentHref);
          
          // Extract rel for galleries
          // gallery: img parent = <a rel="rel"> and we shall cycle through them
          if (parent.hasAttribute("rel")) {
            rel = parent.getAttribute("rel");
            //console.log('rel:', rel);
            related = document.querySelectorAll('[rel="'+rel+'"]');
            //console.log('related:', related);
          }
          
          // reset transform style from the parent
          //modalImg.parentNode.removeAttribute("style")
          modalBody.removeAttribute("style")
        
          // load img only if needed
          if (!modalImg.hasAttribute("src")) {
            // first time load
            //console.log('first')
            modalImg.src = parentHref
          } else if (modalImg.src != parentHref) {
            // load new image
            //console.log('new')
            modalImg.src = parentHref
          } else {
            // reset dimensions anyway
            //console.log('reset')
            //detectDimensions(event);
            //scale = boundW / naturalW		// nope
            resetSizes()
          }

        } // isHrefImg(parentHref)
      } else {
        // interrupt modal, there is no link, nothing to zoom on
        // https://stackoverflow.com/questions/67513467/bootstrap-suppress-modal-from-within-show-bs-modal-event
        return event.preventDefault();
      } // parent.hasAttribute("href")


      // async Update the modal's img with full size img onload
      modalImg.onload = function(e) {
        detectDimensions(e);
      }

      // lightbox zoom ///////////////////////////////////////////////
      function setTransform(e) {
        console.log(pointX/Y=${pointX}/${pointY} scale=${scale} mouseX/Y=${e.x}/${e.y});

        // release mouse when dragging outside the modal.
        // If we don't do that, the image sticks to it and when back in modal a click is needed to release. Inconvenient.
        //console.log('rect',rect);
        if (e.x < rect.left || e.x > rect.right || e.y < rect.top || e.y > rect.bottom) {
          var evt = document.createEvent("MouseEvents"); evt.initEvent("mouseup", true, true); lightbox_zoom.dispatchEvent(evt);
        }
        // pointX and pointY are the exact position of the image from the top-left corner of modal-content
        // scale IS RELATIVE TO THE MODAL SIZE - that means larger images downsized to fit have scale = 1
        // it makes no fucking sense but that's how this whole shit works
        modalBody.style.transform = "translate(" + pointX + "px, " + pointY + "px) scale(" + scale + ")";

        //detectDimensions(e);
      }

      lightbox_zoom.onmousedown = function (e) {
        e.preventDefault();
        //modalBody.classList.add('notransition');
        // e.clientX/Y = e.x/y = cursor position from top-left corner
        start = { x: e.x - pointX, y: e.y - pointY };
        //console.log('e',e);
        //console.log('e.x, e.y',e.x,e.y);
        //console.log('pointX, pointY',pointX,pointY);
        //console.log('startX, startY',start.x, start.y);
        clickhold = true;
      }

      lightbox_zoom.onmouseup = function (e) {
        clickhold = false;
      }

      lightbox_zoom.onmousemove = function (e) {
        e.preventDefault();
        if (!clickhold) {
          return;
        }
        pointX = (e.x - start.x);
        pointY = (e.y - start.y);
        setTransform(e);
      }

      lightbox_zoom.onwheel = function (e) {
        // e.x = e.clientX = where you click relative to top-left corner of the view screen
        // pointX and pointY are the exact position of the image from the top-left corner of modal-content
        e.preventDefault();
        if (!rect) {
          rect = modalBody.getBoundingClientRect();	// in certain cases, the margin will prevent rect detection when scrolling close to it
          //console.log('rect was null',rect)
        }
        let xs = Math.round((e.clientX - pointX - modalImg.x ) / scale),
          ys = Math.round((e.clientY - pointY - modalImg.y ) / scale),
          delta = (e.wheelDelta ? e.wheelDelta : -e.deltaY),
          previous_scale = scale;
        // we rely on modalImg.x/y because only an img can give us its x/y position
        //console.log(xs = Math.round((${e.clientX} - ${pointX} - ${modalImg.x} ) / ${scale}));
        (delta > 0) ? (scale *= scale_ratio) : (scale /= scale_ratio);

        // Constrain zoom to rect modal dimensions by adjusting scale
        //if ((scale < 1) && (naturalW*scale <= boundW) && (naturalH*scale <= boundH)) { // nope that's real_scale which we don't care about
        if ((scale <= 1) && (imgW*scale <= boundW) && (imgH*scale <= boundH)) {
          //console.log(if (${scale} < 1) && (${imgW*scale} <= ${boundW}) && (${imgH*scale} <= ${boundH})))
          // force adjust smallest scale that fits when both W and H are smaller then box boundaries
          scale = minScale;
          
          if (!boundPortrait) { pointY = 0 } else pointX = 0;
          pointX = (pointX < 0) ? 0 : pointX;	// make sure we stay inbound left
          pointX = ((pointX + imgW*scale) > boundW) ? (boundW - imgW*scale) : pointX; // make sure we stay inbound right
          pointY = (pointY < 0) ? 0 : pointY; // make sure we stay top
          pointY = ((pointY + imgH*scale) > boundH) ? (boundH - imgH*scale) : pointY; // make sure we stay inbound bottom

        } else {
          pointX = Math.round(e.clientX - xs * scale) - modalImg.x;
          pointY = Math.round(e.clientY - ys * scale) - modalImg.y;
          //console.log(pointX = ${pointX} = Math.round(${e.clientX} - ${xs} * ${scale}) - ${modalImg.x});
        }

        if (!pixelated) { // always pixelated
          if ((previous_scale <= 2) && (scale > 2)) {	// pixelated from scale=2
            //console.log(previous_scale ${previous_scale} <=1 scale=${scale})
            modalImg.classList.toggle('pixelated');	// we zoom for a reason: see the details
          } else if ((previous_scale > 2) && (scale <= 2)) {
            //console.log(previous_scale ${previous_scale} <=1 scale=${scale})
            modalImg.classList.toggle('pixelated');	// we dezoom and want sampling applied
          }
        }
        
        setTransform(e);

      } // onwheel
      // lightbox zoom ///////////////////////////////////////////////
      

    }); //addEventListener
  
  } // if (lightboxModal)

}); // on load


 

lbbz CSS code

You wouldn’t believe how difficult it was to get a result that is pleasant to the eye, and not buggy.

/********************* lightbox-modal *********************/
#lightbox-modal-bg {
    max-width: 90%;
    max-height: 90%;
    height: fit-content;
    width: fit-content;
  /*display: flex;*/
}

#lightbox-modal-content {
  position: relative;
  border-color: #e0e0e0;
  border-width: 1em;
  position: relative;
  overflow: hidden;
  height: 90%;
  cursor: grab;
  /*display: flex;*/
}

.notransition {
    transition: none !important;
}
#lightbox-modal-body {
  max-height: calc(100vh - 143px); /* no idea why 143px but it works */
  padding: 0;
  cursor: grab;
  transition: all 0.2s ease-in-out;
  transition-delay: -50ms;
  /*display: flex;*/
}
/* https://dev.to/stackfindover/zoom-image-point-with-mouse-wheel-11n3 */
#lightbox-modal-body {
  width: 100%;
  height: 100%;
  transform-origin: 0px 0px;
  transform: scale(1) translate(0px, 0px);
}
div#lightbox-modal-body > img {
  width: 100%;
  height: auto;
}

.pixelated {
    image-rendering: optimizeSpeed;             /* STOP SMOOTHING, GIVE ME SPEED  */
    image-rendering: -moz-crisp-edges;          /* Firefox                        */
    image-rendering: -o-crisp-edges;            /* Opera                          */
    image-rendering: -webkit-optimize-contrast; /* Chrome (and eventually Safari) */
    image-rendering: pixelated;                 /* Universal support since 2021   */
    image-rendering: optimize-contrast;         /* CSS3 Proposed                  */
    -ms-interpolation-mode: nearest-neighbor;   /* IE8+                           */
}
/* https://dev.to/stackfindover/zoom-image-point-with-mouse-wheel-11n3 */

#lightbox-modal-close-btn {
  position: absolute;
  right: 0.5em;
  top: 0.5em;
  cursor: pointer;
  padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x) !important;
  color: #666;
  z-index: 2;
}

/*#lightbox-modal-close-btn:hover {
  color: #333;
  border-color: #959595;
}*/

/********************* lightbox-modal *********************/

That’s all, folks!

 

Leave a Reply

Your email address will not be published. Required fields are marked *