Blogger用のライトボックスを導入【JavaScript・CSSのみ】

本サイトには、広告あるいはアフィリエイトプログラムによるアソシエイトリンクが含まれる場合があります。

本ブログはテーマテンプレートにJetThemeを使っていてBloggerの標準JavaScriptが基本的にオフになっているため、画像のライトボックス表示が使えません。

記事内画像にはaタグを付けておりリンク先にて拡大版を確認できますが、ページ遷移せずに手軽に拡大できると良いと思いました。

今回はBloggerブログ向けにJavaScriptとCSSのみで作成し導入してみました。

サンプル

以下の画像をクリック(タップ)してみてください。ページ遷移せずに拡大画像がポップアップ表示され、そのまま前後の画像へ遷移できるはずです。

画像:フリー素材ぱくたそ[ https://www.pakutaso.com

機能

  • aタグ内のリンク先URL画像をポップアップ表示(aタグのない画像は拡大しません)
  • Blogger内画像および、URL末尾が画像拡張子の外部サイト画像に対応
  • 記事内の前後画像をスライド表示(クリック操作、左右キー、フリックに対応)

以下のようなタグ構成の画像が対象です(Bloggerの投稿エディタから挿入すれば同様になります)。

<a href="拡大画像URL">
<img src="画像URL" />
</a>

対象の画像URLは以下のいずれかです。

  1. googleusercontent.com を含むもの
  2. bp.blogspot.com を含むもの
  3. URL末尾が[画像ファイル名.拡張子]で終わるもの

導入方法

Bloggerの標準JSを使用(b:js='true')しているブログでは、競合を避けるためBlogger本体のライトボックスをオフに設定してください(「設定」→「画像のライトボックス」をオフ)。

Blogger管理画面「設定」内の「画像のライトボックス」をオフに

Blogger管理画面「テーマ」→「HTMLを編集」よりテーマテンプレート編集画面にて以下のコード(JavaScriptおよびCSS)を導入します。

テンプレートファイルの編集を行うため、事前にバックアップすることを推奨します。なお、筆者はJavaScriptやCSSについては素人・勉強中です。筆者環境で動作確認はしていますが、コードに間違いや非効率的な部分などあるかもしれませんのでご了承ください。ご利用は自己責任でお願いします。

JavaScript

</body>の上に以下のコードを設置します。

<!--Lightbox-->  
<b:if cond='data:view.isSingleItem'> <!-- 投稿記事ページか個別ページでのみ読み込み -->
<script>
//<![CDATA[
(function() {
  let currentImages = [];
  let currentIndex = 0;
  let isAnimating = false;

  const isValidImageLink = (url) => {
    if (!url) return false;
    if (/\.(jpg|jpeg|png|gif|webp|bmp|avif)(?:\?.*)?$/i.test(url)) return true;
    if (/blogger\.googleusercontent\.com\/img\/[ab]\//i.test(url)) return true;
    if (/lh\d+\.googleusercontent\.com\//i.test(url)) return true;
    if (/bp\.blogspot\.com\//i.test(url)) return true;
    return false;
  };

  const initLightboxElement = () => {
    let el = document.getElementById('custom-lightbox');
    if (el) return el;
    
    el = document.createElement('div');
    el.id = 'custom-lightbox';
    
	el.innerHTML = `
      <div id="lightbox-close" class="lightbox-btn">
        <svg viewBox="0 0 21 21">
          <g stroke-linecap="round" stroke-linejoin="round" transform="translate(5 5)">
            <path d="m10.5 10.5-10-10z"/>
            <path d="m10.5.5-10 10"/>
          </g>
        </svg>
      </div>
      <div id="lightbox-prev" class="lightbox-btn lightbox-nav">
        <svg viewBox="0 0 21 21">
          <path stroke-linecap="round" stroke-linejoin="round" transform="translate(7 6)" d="m4.5 8.5-4-4 4-4"/>
        </svg>
      </div>
      <div id="lightbox-next" class="lightbox-btn lightbox-nav">
        <svg viewBox="0 0 21 21">
          < stroke-linecap="round" stroke-linejoin="round" transform="translate(9 6)" d="m.5 8.5 4-4-4-4"/>
        </svg>
      </div>
      <img src="" alt="Lightbox Image">
    `;
    document.body.appendChild(el);
    
    const img = el.querySelector('img');
    const prevBtn = el.querySelector('#lightbox-prev');
    const nextBtn = el.querySelector('#lightbox-next');
    const closeBtn = el.querySelector('#lightbox-close');

    const closeLightbox = () => {
      el.classList.remove('active');
      img.classList.remove('zoomed', 'loaded', 'slide-out-left', 'slide-out-right');
      el.style.alignItems = 'center';
      el.style.justifyContent = 'center';
      document.body.style.overflow = '';
      isAnimating = false;
      setTimeout(() => { img.src = ''; }, 300);
    };

    const updateImage = (newIndex) => {
      if (currentImages.length <= 1 || isAnimating) return; 

      const isNext = newIndex > currentIndex;

      if (newIndex < 0) newIndex = currentImages.length - 1;
      if (newIndex >= currentImages.length) newIndex = 0;
      
      const newHref = currentImages[newIndex].getAttribute('href') || '';
      const currentHref = img.getAttribute('src') || '';
      if (newHref === currentHref) {
        currentIndex = newIndex;
        return;
      }

      isAnimating = true; 
      currentIndex = newIndex; 

      img.classList.remove('loaded', 'slide-out-left', 'slide-out-right');
      img.classList.add(isNext ? 'slide-out-left' : 'slide-out-right');
      
      setTimeout(() => {
        img.style.transition = 'none';
        img.classList.remove('slide-out-left', 'slide-out-right', 'zoomed');
        void img.offsetWidth;
        img.style.transition = '';
        
        el.style.alignItems = 'center';
        el.style.justifyContent = 'center';
        el.classList.add('loading');
        
        img.src = newHref;
      }, 300);
    };

    closeBtn.onclick = (e) => { e.stopPropagation(); closeLightbox(); };
    prevBtn.onclick = (e) => { e.stopPropagation(); updateImage(currentIndex - 1); };
    nextBtn.onclick = (e) => { e.stopPropagation(); updateImage(currentIndex + 1); };

    el.onclick = (e) => {
      if (e.target.tagName !== 'IMG' && !e.target.closest('.lightbox-btn')) {
        closeLightbox();
      }
    };

    img.onclick = (e) => {
      e.stopPropagation();
      img.classList.toggle('zoomed');
      if (img.classList.contains('zoomed')) {
        el.style.alignItems = 'flex-start';
        el.style.justifyContent = 'flex-start';
      } else {
        el.style.alignItems = 'center';
        el.style.justifyContent = 'center';
      }
    };

    let touchStartX = 0;
    let touchStartY = 0;
    let touchEndX = 0;
    let touchEndY = 0;
    let isMultiTouch = false;
    
    el.addEventListener('touchstart', e => {
      if (e.touches.length > 1) {
        isMultiTouch = true;
        return;
      }
      isMultiTouch = false;
      touchStartX = e.changedTouches[0].screenX;
      touchStartY = e.changedTouches[0].screenY;
    }, { passive: true });

    el.addEventListener('touchend', e => {
      if (isMultiTouch) return;
      touchEndX = e.changedTouches[0].screenX;
      touchEndY = e.changedTouches[0].screenY;
      handleSwipe();
    }, { passive: true });

    const handleSwipe = () => {
      if (img.classList.contains('zoomed') || currentImages.length <= 1) return;
      const diffX = touchEndX - touchStartX;
      const diffY = touchEndY - touchStartY;
      
      if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 70) {
        if (diffX < 0) updateImage(currentIndex + 1);
        else updateImage(currentIndex - 1);
      }
    };

    document.addEventListener('keydown', (e) => {

      if (!el.classList.contains('active')) return;
      
      if (e.key === 'ArrowLeft') updateImage(currentIndex - 1);
      if (e.key === 'ArrowRight') updateImage(currentIndex + 1);
      if (e.key === 'Escape') closeLightbox();
    });
    return { el, img, prevBtn, nextBtn, closeLightbox };
  };

  const { el: lightbox, img: lightboxImg, prevBtn, nextBtn } = initLightboxElement();

  document.addEventListener('click', function(e) {
    const anchor = e.target.closest('a');
    if (!anchor) return;
    if (!anchor.querySelector('img')) return;

    const href = anchor.getAttribute('href') || '';

    if (isValidImageLink(href)) {
      const postArea = anchor.closest('#post-body, .post-body, .entry-text, .entry-content');
      
      if (postArea) {
        e.preventDefault();
        
        const candidateLinks = postArea.querySelectorAll(
          'a[href*=".jpg"], a[href*=".jpeg"], a[href*=".png"], a[href*=".gif"], a[href*=".webp"], a[href*=".bmp"], a[href*=".avif"], ' +
          'a[href*="googleusercontent.com"], a[href*="bp.blogspot.com"]'
        );
        
        currentImages = Array.from(candidateLinks).filter(a => 
		  isValidImageLink(a.getAttribute('href')) && a.querySelector('img')
		);
        currentIndex = currentImages.indexOf(anchor);

        if (currentImages.length > 1) {
          prevBtn.classList.remove('hidden');
          nextBtn.classList.remove('hidden');
        } else {
          prevBtn.classList.add('hidden');
          nextBtn.classList.add('hidden');
        }

        isAnimating = false;
        lightboxImg.classList.remove('loaded', 'zoomed', 'slide-out-left', 'slide-out-right');
        lightbox.style.alignItems = 'center';
        lightbox.style.justifyContent = 'center';
        lightbox.classList.add('active', 'loading');
        
        lightboxImg.onerror = () => {
          lightbox.classList.remove('loading');
          isAnimating = false;
        };

        lightboxImg.onload = () => {
          lightbox.classList.remove('loading');
          lightboxImg.classList.add('loaded');
          isAnimating = false; 
        };
        
        lightboxImg.src = currentImages[currentIndex].getAttribute('href');
        document.body.style.overflow = 'hidden';
      }
    }
  }, { capture: true });
})();
//]]>
</script>
</b:if>

CSS

CSSは以下のコードを設置します。JetThemeやQooQでは/*Your custom CSS is here*/*** 個別アイテム ***の下でOKです。

/* ライトボックス */
#custom-lightbox {
  display: none;
  position: fixed;
  z-index: 9999;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.85);
  cursor: pointer;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.3s ease;
  overflow: auto;
}

.lightbox-btn {
  position: fixed;
  width: 44px;
  height: 44px;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 50%;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10000;
  cursor: pointer;
  transition: background 0.2s;
}
.lightbox-btn:hover { background: rgba(0, 0, 0, 0.8); }
.lightbox-btn svg {
  width: 24px; height: 24px; stroke: currentColor; fill: none; stroke-width: 2;
}

#lightbox-close { top: 20px; right: 20px; }
#lightbox-prev { top: 50%; left: 20px; transform: translateY(-50%); }
#lightbox-next { top: 50%; right: 20px; transform: translateY(-50%); }

.lightbox-btn.hidden { display: none !important; }

#custom-lightbox::after {
  content: "";
  display: none;
  width: 40px; height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  position: absolute;
  top: calc(50% - 20px); left: calc(50% - 20px);
}
#custom-lightbox.loading::after { display: block; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }

#custom-lightbox.active { display: flex; opacity: 1; }

#custom-lightbox img {
  max-width: 90vw; max-height: 90vh;
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
  transform: scale(0.9);
  transition: transform 0.3s ease, opacity 0.3s ease;
  opacity: 0;
  cursor: zoom-in;
  margin: auto;
}
#custom-lightbox img.loaded { opacity: 1; transform: scale(1); }
#custom-lightbox img.zoomed { max-width: none; max-height: none; cursor: zoom-out; }

#custom-lightbox img.slide-out-left {
  opacity: 0;
  transform: scale(1) translateX(-50px);
}
#custom-lightbox img.slide-out-right {
  opacity: 0;
  transform: scale(1) translateX(50px);
}

カスタマイズ

JetThemeで使う場合

ライトボックスの「閉じるボタン」「進む/戻るボタン」は汎用的なSVGコードで作っていますが、JetThemeの場合はJavaScriptの一部コードを以下のように書き換えることでテーマ内に格納されているSVGスプライトを再利用できます。

修正前
	el.innerHTML = `
      <div id="lightbox-close" class="lightbox-btn">
        <svg viewBox="0 0 21 21">
          <g stroke-linecap="round" stroke-linejoin="round" transform="translate(5 5)">
            <path d="m10.5 10.5-10-10z"/>
            <path d="m10.5.5-10 10"/>
          </g>
        </svg>
      </div>
      <div id="lightbox-prev" class="lightbox-btn lightbox-nav">
        <svg viewBox="0 0 21 21">
          <path stroke-linecap="round" stroke-linejoin="round" transform="translate(7 6)" d="m4.5 8.5-4-4 4-4"/>
        </svg>
      </div>
      <div id="lightbox-next" class="lightbox-btn lightbox-nav">
        <svg viewBox="0 0 21 21">
          <path stroke-linecap="round" stroke-linejoin="round" transform="translate(9 6)" d="m.5 8.5 4-4-4-4"/>
        </svg>
      </div>
      <img src="" alt="Lightbox Image">
    `;
修正後
    el.innerHTML = `
      <div id="lightbox-close" class="lightbox-btn">
        <svg viewBox="0 0 24 24"><use href="#i-close"></use></svg>
      </div>
      <div id="lightbox-prev" class="lightbox-btn lightbox-nav">
        <svg viewBox="0 0 24 24"><use href="#i-arrow-l"></use></svg>
      </div>
      <div id="lightbox-next" class="lightbox-btn lightbox-nav">
        <svg viewBox="0 0 24 24"><use href="#i-arrow-r"></use></svg>
      </div>
      <img src="" alt="Lightbox Image">
    `;

ライトボックスを読み込むページの条件を変更

投稿記事や個別ページでのみライトボックスが実行されるように<b:if cond='data:view.isSingleItem'>...</b:if>でスクリプトを囲んでいます。

投稿記事のみで表示したい場合はisSingleItem部分をisPostに、逆に個別ページのみの場合はisPageに書き換えてください。

対象をBlogger内画像のみにする場合

URL末尾に画像拡張子(.jpgなど)を持つ外部サイトの画像の拡大にも対応していますが、これが不要の場合、すなわち、Blogger内画像(URLにgoogleusercontent.combp.blogspot.comを含むもの)だけをライトボックス表示の対象としたい場合、JavaScript内の以下の2行を削除(またはコメントアウト)してください。

以下の各行を削除
if (/\.(jpg|jpeg|png|gif|webp|bmp|avif)(?:\?.*)?$/i.test(url)) return true;
'a[href*=".jpg"], a[href*=".jpeg"], a[href*=".png"], a[href*=".gif"], a[href*=".webp"], a[href*=".bmp"], a[href*=".avif"], ' +

修正後のイメージは以下のようになります。

// ...省略
  const isValidImageLink = (url) => {
    if (!url) return false;
    // この行を削除  if (/\.(jpg|jpeg|png|gif|webp|bmp|avif)(?:\?.*)?$/i.test(url)) return true;
    if (/blogger\.googleusercontent\.com\/img\/[ab]\//i.test(url)) return true;
    if (/lh\d+\.googleusercontent\.com\//i.test(url)) return true;
    if (/bp\.blogspot\.com\//i.test(url)) return true;
    return false;
  };

// ...省略

      if (postArea) {
        e.preventDefault();
        
        const candidateLinks = postArea.querySelectorAll(
          // この行を削除  'a[href*=".jpg"], a[href*=".jpeg"], a[href*=".png"], a[href*=".gif"], a[href*=".webp"], a[href*=".bmp"], a[href*=".avif"], ' +
          'a[href*="googleusercontent.com"], a[href*="bp.blogspot.com"]'
        );
// ...以下省略

記事本文のクラスやIDが異なる場合

Bloggerテーマによって記事本文が入る部分のclass, idが異なる場合は以下の部分に書き足し(あるいは不要なものを削除し入れ替え)てください。

const postArea = anchor.closest('#post-body, .post-body, .entry-text, .entry-content');

QooQやJetTheme、公式テーマのContempoでは上記コードのままで動くはずです。もしご自身のテーマでライトボックスが動かない場合は記事本文部分のclassidを確認してみてください。

【AD】弓木奈於さんを応援しています
Previous
まだコメントはありません
コメントする
コメントの投稿者IDについて:匿名でもOKですが、名前/URLで何らかの名前を設定していただくと他の匿名さんと区別がつきありがたいです。URLは空欄で構いません。
comment url