lbbz example zoom featured meme

WordPress plugin: LightBox Bootstrap Zoom v1.0

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 *