How to add a button to the Photo Gallery Popup to download images

Hello,

Here you have the option to make images used in the Photo Gallery available for download. I find this feature to be the default setting for the Photo Gallery. Unfortunately, nothing has changed at Elfsight since I first requested it in 2022 – which is a shame. I’ve spent several hours trying, and here’s the code that should be added to the Custom JS field on the Style tab of your widget’s settings:

(function () {
  /** 1) URL aus url("…") extrahieren */
  function extractUrl(str) {
    const m = /url\(["']?(.*?)["']?\)/.exec(str || '');
    return m ? m[1] : null;
  }

  /** 2) Aktives Slide finden und Bild-URL zurückgeben */
  function getCurrentImageUrl() {
    const holders = document.querySelectorAll('.fslightbox-source-holder');
    for (const h of holders) {
      if (h.style.transform.includes('translate(0px')) {
        // echtes <img> suchen
        const img = h.querySelector('img');
        if (img && img.src) return img.src;
        // fallback: CSS-background
        const wrapper = h.querySelector('.fslightbox-single-source-wrapper');
        if (wrapper) {
          const bg = getComputedStyle(wrapper).backgroundImage;
          return extractUrl(bg);
        }
      }
    }
    return null;
  }

  /** 3) Download per fetch→Blob starten */
  async function triggerDownload(url) {
    try {
      const resp = await fetch(url, { mode: 'cors' });
      if (!resp.ok) throw new Error('Status ' + resp.status);
      const blob = await resp.blob();
      const blobUrl = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = blobUrl;
      const parts = url.split('/');
      a.download = parts[parts.length - 1] || 'download.jpg';
      document.body.appendChild(a);
      a.click();
      a.remove();
      setTimeout(() => URL.revokeObjectURL(blobUrl), 60000);
    } catch (e) {
      console.error('Download-Fehler:', e);
      window.open(url, '_blank');
    }
  }

  /** 4) Button einmalig in die Toolbar einfügen */
  function injectButton() {
    if (document.getElementById('fs-dl-btn')) return;
    const toolbar = document.querySelector('.fslightbox-toolbar');
    if (!toolbar) return;

    const btn = document.createElement('div');
    btn.id = 'fs-dl-btn';
    btn.className = 'fslightbox-toolbar-button button-style';
    btn.title = 'Bild herunterladen';
    btn.innerHTML = `
      <svg class="fslightbox-svg-icon" viewBox="0 0 24 24" width="17" height="17">
        <!-- Pfeil nach unten -->
        <path d="M12 20l-6-6h4V4h4v10h4l-6 6z"/>
      </svg>
    `;

    btn.addEventListener('click', async () => {
      const url = getCurrentImageUrl();
      if (url) await triggerDownload(url);
      else console.warn('❌ Keine Bild-URL gefunden');
    });

    toolbar.insertBefore(btn, toolbar.firstChild);
  }

  /** 5) Button-State (sichtbar / ausgegraut) */
  function updateButtonState() {
    const btn = document.getElementById('fs-dl-btn');
    if (!btn) return;
    btn.style.opacity = getCurrentImageUrl() ? '1' : '0.3';
  }

  /** 6) Initialisierung + Observer + Fallback */
  injectButton();
  updateButtonState();

  const mo = new MutationObserver(() => {
    injectButton();
    updateButtonState();
  });
  mo.observe(document.body, { childList: true, subtree: true });

  setInterval(() => {
    injectButton();
    updateButtonState();
  }, 500);
})();

BG, Timo

Hey there, @Fri :waving_hand:

We’d love to release the new features faster, but given the high number of requests, we have to prioritize based on their complexity and number votes.

At the moment, we have a bunch of requests with the higher priority level and our devs focused on them. If your request gets more votes, it might be prioritized sooner, but it’s hard to give any predictions for now. If any news comes up, we’ll update you on the Wishlist thread :slightly_smiling_face:

However, that’s truly amazing that you’ve found a custom solution that works like a charm. A huge thanks for sharing it with us - that’s much appreciated :heart:

Isn’t working on Chrome - bar pops up but I get the circle of death

Welcome to the Community, @Clair_Catillaz :waving_hand:

I’ve just tested the script, and it’s working fine on our side. I also checked your setup, and it looks like the widget is installed on the Canva website.

The thing is, Canva uses iframes, which means file downloads aren’t supported in this environment. That’s why the script doesn’t work in this case.

At the moment, the only workaround is to redirect users to a page where the image is opened directly. From there, they’ll be able to download it using the right-click option. Here is a script for this implementation:

function extractUrl(str) {
  const m = /url\(["']?(.*?)["']?\)/.exec(str || '');
  return m ? m[1] : null;
}

function getCurrentImageUrl() {
  const holders = document.querySelectorAll('.fslightbox-source-holder');

  for (const h of holders) {
    if (h.style.transform.includes('translate(0px')) {
      const img = h.querySelector('img');

      if (img && img.src) {
        return img.src;
      }

      const wrapper = h.querySelector('.fslightbox-single-source-wrapper');

      if (wrapper) {
        const bg = getComputedStyle(wrapper).backgroundImage;
        return extractUrl(bg);
      }
    }
  }

  return null;
}

function openImage(url) {
  const a = document.createElement('a');
  a.href = url;
  a.target = '_blank';
  a.rel = 'noopener noreferrer';
  document.body.appendChild(a);
  a.click();
  a.remove();
}

function injectButton() {
  if (document.getElementById('fs-dl-btn')) return;

  const toolbar = document.querySelector('.fslightbox-toolbar');

  if (!toolbar) return;

  const btn = document.createElement('div');

  btn.id = 'fs-dl-btn';
  btn.className = 'fslightbox-toolbar-button button-style';
  btn.title = 'Open image';

  btn.innerHTML = `
    <svg class="fslightbox-svg-icon" viewBox="0 0 24 24" width="17" height="17">
      <path d="M12 20l-6-6h4V4h4v10h4l-6 6z"/>
    </svg>
  `;

  btn.addEventListener('click', () => {
    const url = getCurrentImageUrl();

    if (url) {
      openImage(url);
    }
  });

  toolbar.insertBefore(btn, toolbar.firstChild);
}

function updateButtonState() {
  const btn = document.getElementById('fs-dl-btn');

  if (!btn) return;

  btn.style.opacity = getCurrentImageUrl() ? '1' : '0.3';
  btn.style.cursor = getCurrentImageUrl() ? 'pointer' : 'default';
}

injectButton();
updateButtonState();

const observer = new MutationObserver(() => {
  injectButton();
  updateButtonState();
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

setInterval(() => {
  injectButton();
  updateButtonState();
}, 500);