Skip to content
quarry Artifact [97a5402454]
Login

Artifact [97a5402454]

Artifact 97a54024544e93c55401d80ca7df07e4d770edfc99c00fce45b0ef6d7b76d069:


// attach.js is the attachment lightbox — the one enhancement the
// attachment section has, and the only thing on the page that needs
// script at all.
//
// Without it every image is a plain link to its inline view, which
// opens the picture. With it the picture opens in place, with its name
// and size, and the arrow keys walk the page's other images. That is
// the whole contract: nothing here is load-bearing.
(function () {
  'use strict';

  // Every image on the page that an attachment route serves: the cards'
  // previews and the inline images a body resolved from attach:. They
  // are collected in document order, which is the order prev/next walk.
  function shots() {
    var out = [];
    document.querySelectorAll('.attach-card[data-kind="image"] .attach-prev').forEach(function (a) {
      var img = a.querySelector('img');
      out.push({
        src: a.getAttribute('href'),
        name: a.getAttribute('data-attach-name') || (img && img.alt) || '',
        sub: a.getAttribute('data-attach-sub') || '',
        dl: a.getAttribute('data-attach-dl') || a.getAttribute('href'),
        from: a
      });
    });
    document.querySelectorAll('article img, .tkt-body img, .f-post-b img').forEach(function (img) {
      var src = img.getAttribute('src') || '';
      if (!/\/attachment\/[^/]+\/view$/.test(src)) return;
      out.push({
        src: src,
        name: img.getAttribute('alt') || '',
        sub: '',
        dl: src.replace(/\/view$/, ''),
        from: img
      });
    });
    return out;
  }

  var all = shots();
  if (!all.length) return;

  var dlg = document.createElement('dialog');
  dlg.className = 'attach-lightbox';
  dlg.innerHTML =
    '<figure><img alt=""></figure>' +
    '<div class="attach-lb-bar">' +
    '<button type="button" class="attach-lb-prev" aria-label="previous image">&#8249;</button>' +
    '<span class="attach-lb-meta"><span class="attach-lb-name"></span> <span class="attach-lb-sub"></span></span>' +
    '<a class="attach-lb-dl" download>Download</a>' +
    '<button type="button" class="attach-lb-next" aria-label="next image">&#8250;</button>' +
    '<button type="button" class="attach-lb-close" aria-label="close">&#215;</button>' +
    '</div>';
  document.body.appendChild(dlg);

  var img = dlg.querySelector('img');
  var nameEl = dlg.querySelector('.attach-lb-name');
  var subEl = dlg.querySelector('.attach-lb-sub');
  var dlEl = dlg.querySelector('.attach-lb-dl');
  var at = 0;
  var opener = null;

  function show(i) {
    if (i < 0) i = all.length - 1;
    if (i >= all.length) i = 0;
    at = i;
    var s = all[at];
    img.setAttribute('src', s.src);
    img.setAttribute('alt', s.name);
    nameEl.textContent = s.name;
    subEl.textContent = s.sub;
    dlEl.setAttribute('href', s.dl);
    // One image is not a carousel; hide the arrows rather than offering
    // two controls that do nothing.
    var many = all.length > 1;
    dlg.querySelector('.attach-lb-prev').hidden = !many;
    dlg.querySelector('.attach-lb-next').hidden = !many;
  }

  function open(i, from) {
    opener = from || null;
    show(i);
    if (typeof dlg.showModal === 'function') dlg.showModal();
    else dlg.setAttribute('open', '');
    dlg.querySelector('.attach-lb-close').focus();
  }

  // Focus goes back where it came from. A reader who opened the fourth
  // card with the keyboard lands on the fourth card again, not at the
  // top of the document.
  dlg.addEventListener('close', function () {
    if (opener && typeof opener.focus === 'function') opener.focus();
    opener = null;
  });

  all.forEach(function (s, i) {
    var target = s.from.tagName === 'IMG' && s.from.parentElement &&
      s.from.parentElement.tagName === 'A' ? s.from.parentElement : s.from;
    target.addEventListener('click', function (e) {
      if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
      e.preventDefault();
      open(i, target);
    });
  });

  dlg.querySelector('.attach-lb-prev').addEventListener('click', function () { show(at - 1); });
  dlg.querySelector('.attach-lb-next').addEventListener('click', function () { show(at + 1); });
  dlg.querySelector('.attach-lb-close').addEventListener('click', function () { dlg.close(); });
  // The backdrop is the dialog itself: a click that lands on the
  // element rather than on its contents is a click outside the picture.
  dlg.addEventListener('click', function (e) { if (e.target === dlg) dlg.close(); });
  dlg.addEventListener('keydown', function (e) {
    if (e.key === 'ArrowLeft') { e.preventDefault(); show(at - 1); }
    else if (e.key === 'ArrowRight') { e.preventDefault(); show(at + 1); }
  });
})();