Contents

Image Test

!
Images have a similar syntax to links but include a preceding exclamation point. ❕
![Alt text](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")

The Stormtroopocat
The Stormtroopocat

![Alt text](https://octodex.github.com/images/stormtroopocat.jpg)

The Stormtroopocat

Click to fold

The Stormtroopocat
The Stormtroopocat

{{< admonition type=info title="Click to fold" open=true >}}
![The Stormtroopocat](./images/stormtroopocat.jpg "The Stormtroopocat")
{{< /admonition >}}

image shortcode is an alternative to figure shortcode. image shortcode can take full advantage of the dependent libraries of lazysizes and lightgallery.js.

{{< image src="images/stormtroopocat.jpg" caption="The Stormtroopocat" linked=true >}}

The rendered output looks like this:

The Stormtroopocat
{{< admonition type=info title="Click to fold" open=true >}}
{{< image src="images/stormtroopocat.jpg" caption="The Stormtroopocat" height="676" width="704" linked=true >}}
{{< /admonition >}}

The rendered output looks like this:

Click to fold
The Stormtroopocat

Question
I ran into a strange problem that I’m not sure how to resolve for the time being. Images shortcode does not function properly in admonition shortcode and only displays a link to the image (see below). Surprisingly, only one computer (my laptop) has this issue. That is, somehow because Scale and Layout of :(fab fa-windows): Windows Display is set to 150 %, therefore the symptom occurs globally in all browsers, instead of just one. Fortunately, images load normally on other devices (desktop PC, phones, and tablets). :(far fa-question-circle):

The image :(fas fa-image): below only shows a link, but changing the zooming level of the browser forces it to show, however.

error


(function () {
  function isMeasurable(el) {
    // Visible and non-zero rendered size
    const rect = el.getBoundingClientRect();
    return rect.width > 0 && rect.height > 0 && el.offsetParent !== null;
  }

  function clamp(v, min, max) {
    return Math.min(Math.max(v, min), max);
  }

  // --- New helpers for symbol + toggle integration ---
  const SYMBOL_CLASS = 'symbol';
  const TOGGLE_BTN_CLASS = 'note-toggle-btn';
  const SYMBOL_W = 22;
  const SYMBOL_H = 22;

  function disposeTooltips(nodes) {
    nodes.forEach(el => {
      // Bootstrap 5 API: get existing instance and dispose if present
      if (window.bootstrap && bootstrap.Tooltip) {
        const inst = bootstrap.Tooltip.getInstance(el);
        if (inst) inst.dispose();
      }
    });
  }

  function clearSymbolsAndButton(container) {
    const symbols = Array.from(container.querySelectorAll(`.${SYMBOL_CLASS}`));
    disposeTooltips(symbols);
    symbols.forEach(el => el.remove());
    const btn = container.querySelector(`.${TOGGLE_BTN_CLASS}`);
    if (btn) btn.remove();
  }

  function makeSymbol(container, text, styleObj) {
    const el = document.createElement('div');
    el.className = SYMBOL_CLASS;
    el.setAttribute('title', text || '');
    el.setAttribute('data-bs-toggle', 'tooltip');
    el.setAttribute('data-bs-placement', 'top');
    Object.assign(el.style, { position: 'absolute', ...styleObj });
    container.appendChild(el);
    if (window.bootstrap && bootstrap.Tooltip) {
      new bootstrap.Tooltip(el);
    }
    // Prevent default navigation on clicks, matching second script
    el.addEventListener('click', (e) => { e.preventDefault(); });
    return el;
  }

  function createToggle(container, onClick) {
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = `btn btn-outline-success ${TOGGLE_BTN_CLASS}`;
    btn.textContent = 'Toggle';
    Object.assign(btn.style, {
      position: 'absolute',
      bottom: '10px',
      right: '10px',
      zIndex: '101',
    });
    btn.addEventListener('click', (e) => {
      e.preventDefault();
      onClick();
    });
    container.appendChild(btn);
    return btn;
  }

  function setupHoverUI(container) {
    const hoverTooltip = container.getAttribute('hover-tooltip');
    // Always clean first to be idempotent
    clearSymbolsAndButton(container);

    if (hoverTooltip !== 'true') {
      // Base: show original notes; remove symbols and button
      container.querySelectorAll('.note-box.movable').forEach(n => { n.style.display = ''; });
      return;
    }

    if (!isMeasurable(container)) return;

    const screenWidth = window.innerWidth || document.documentElement.clientWidth;
    const notes = Array.from(container.querySelectorAll('.note-box.movable'));

    // Utility for percent clamping like second script
    function clampPercent(v) {
      return Math.min(Math.max(v, 0), 100);
    }

    // Small: <= 600px — show symbols only (notes hidden), symbols placed by percent
    if (screenWidth <= 600) {
      notes.forEach(note => { note.style.display = 'none'; });
      notes.forEach(note => {
        const px = parseFloat(note.getAttribute('data-initial-x')) || 0;
        const py = parseFloat(note.getAttribute('data-initial-y')) || 0;
        const xPct = clampPercent(px);
        const yPct = clampPercent(py);
        makeSymbol(container, note.textContent || '', {
          left: `${xPct}%`,
          top: `${yPct}%`,
          zIndex: '100',
        });
      });
      // Prevent default on container clicks, similar to second script
      container.addEventListener('click', (e) => { e.preventDefault(); }, { passive: false });
      return;
    }

    // Medium: <= 1200px — symbols only by default + a Toggle button to switch
    if (screenWidth <= 1200) {
      notes.forEach(note => { note.style.display = 'none'; });
      notes.forEach(note => {
        const px = parseFloat(note.getAttribute('data-initial-x')) || 0;
        const py = parseFloat(note.getAttribute('data-initial-y')) || 0;
        const xPct = clampPercent(px);
        const yPct = clampPercent(py);
        makeSymbol(container, note.textContent || '', {
          left: `${xPct}%`,
          top: `${yPct}%`,
          zIndex: '100',
        });
      });
      const toggle = createToggle(container, () => {
        const anyNoteVisible = notes.some(n => n.style.display !== 'none');
        if (anyNoteVisible) {
          // Hide notes, show symbols
          notes.forEach(n => { n.style.display = 'none'; });
          container.querySelectorAll(`.${SYMBOL_CLASS}`).forEach(s => { s.style.display = ''; });
        } else {
          // Show notes, hide symbols
          notes.forEach(n => { n.style.display = ''; });
          container.querySelectorAll(`.${SYMBOL_CLASS}`).forEach(s => { s.style.display = 'none'; });
        }
      });
      container.addEventListener('click', (e) => { e.preventDefault(); }, { passive: false });
      return;
    }

    // Large: > 1200px — show notes by default, also create centered symbols hidden initially + Toggle
    notes.forEach(note => { note.style.display = ''; });

    const cpos = container.getBoundingClientRect();
    notes.forEach(note => {
      const npos = note.getBoundingClientRect();
      const centerLeft = (npos.left - cpos.left) + (npos.width / 2) - (SYMBOL_W / 2);
      const centerTop = (npos.top - cpos.top) + (npos.height / 2) - (SYMBOL_H / 2);
      const sym = makeSymbol(container, note.textContent || '', {
        left: `${centerLeft}px`,
        top: `${centerTop}px`,
        zIndex: '100',
      });
      // Hidden initially on large
      sym.style.display = 'none';
    });

    createToggle(container, () => {
      const symbols = Array.from(container.querySelectorAll(`.${SYMBOL_CLASS}`));
      const visible = symbols.some(s => s.style.display !== 'none');
      if (visible) {
        // Hide symbols, show notes
        symbols.forEach(s => { s.style.display = 'none'; });
        notes.forEach(n => { n.style.display = ''; });
      } else {
        // Show symbols, hide notes
        symbols.forEach(s => { s.style.display = ''; });
        notes.forEach(n => { n.style.display = 'none'; });
      }
    });

    container.addEventListener('click', (e) => { e.preventDefault(); }, { passive: false });
  }
  // --- End new helpers ---

  function layout(container) {
    // Ensure container is the positioning context
    const computed = getComputedStyle(container);
    if (computed.position === 'static') {
      container.style.position = 'relative';
    }

    // Require measurable size
    if (!isMeasurable(container)) return;

    const crect = container.getBoundingClientRect(); // rendered size
    const cw = crect.width;
    const ch = crect.height;

    // Place each note from % to px, then keep pixel math for drag
    const notes = container.querySelectorAll('.note-box.movable');
    notes.forEach(note => {
      // Make sure note is absolutely positioned above the image
      note.style.position = 'absolute';
      note.style.zIndex = '100';
      note.style.cursor = 'move';
      note.style.touchAction = 'none'; // prevent touch panning during drag

      const px = parseFloat(note.getAttribute('data-initial-x')) || 0;
      const py = parseFloat(note.getAttribute('data-initial-y')) || 0;

      // First pass: place near target to measure size
      let left = (px / 100) * cw;
      let top  = (py / 100) * ch;
      note.style.left = `${left}px`;
      note.style.top  = `${top}px`;

      // Measure outer size after initial placement
      const nrect = note.getBoundingClientRect();
      const nw = nrect.width;
      const nh = nrect.height;

      // Clamp so the entire note remains visible
      left = clamp(left, 0, Math.max(cw - nw, 0));
      top  = clamp(top, 0, Math.max(ch - nh, 0));
      note.style.left = `${left}px`;
      note.style.top  = `${top}px`;
    });
  }

  function enableDrag(container) {
    const notes = container.querySelectorAll('.note-box.movable');

    notes.forEach(note => {
      // Remove previous handlers by replacing element listeners via pointer events
      let dragging = false;
      let startX = 0, startY = 0, startLeft = 0, startTop = 0;

      const onPointerDown = (e) => {
        // Only start if container is measurable
        if (!isMeasurable(container)) return;

        dragging = true;
        note.setPointerCapture(e.pointerId);
        const npos = note.getBoundingClientRect();
        const cpos = container.getBoundingClientRect();
        startLeft = npos.left - cpos.left;
        startTop  = npos.top  - cpos.top;
        startX = e.clientX;
        startY = e.clientY;
        e.preventDefault();
      };

      const onPointerMove = (e) => {
        if (!dragging) return;
        const cpos = container.getBoundingClientRect();
        const dx = e.clientX - startX;
        const dy = e.clientY - startY;

        // Use rendered sizes for clamping
        const nrect = note.getBoundingClientRect();
        const cw = cpos.width;
        const ch = cpos.height;
        const nw = nrect.width;
        const nh = nrect.height;

        const nextLeft = clamp(startLeft + dx, 0, Math.max(cw - nw, 0));
        const nextTop  = clamp(startTop  + dy, 0, Math.max(ch - nh, 0));
        note.style.left = `${nextLeft}px`;
        note.style.top  = `${nextTop}px`;
        e.preventDefault();
      };

      const onPointerUp = (e) => {
        if (!dragging) return;
        dragging = false;
        try { note.releasePointerCapture(e.pointerId); } catch (_) {}
        e.preventDefault();
      };

      // Replace any prior listeners
      note.onpointerdown = onPointerDown;
      note.onpointermove = onPointerMove;
      note.onpointerup = onPointerUp;
      note.onpointercancel = onPointerUp;
    });
  }

  function initContainer(container) {
    // Try to layout now
    layout(container);
    enableDrag(container);
    setupHoverUI(container);

    // Observe size changes for relayout
    if ('ResizeObserver' in window) {
      const ro = new ResizeObserver(() => {
        layout(container);
        enableDrag(container);
        setupHoverUI(container);
      });
      ro.observe(container);
      // If an <img> exists, observe it as well (covers lazy/eager reloads)
      const img = container.querySelector('img');
      if (img) ro.observe(img);
      // Also respond to viewport size changes (breakpoints)
      window.addEventListener('resize', () => {
        layout(container);
        enableDrag(container);
        setupHoverUI(container);
      }, { passive: true });
    } else {
      // Fallback: re-check a few times while becoming visible
      let tries = 0;
      const id = setInterval(() => {
        layout(container);
        enableDrag(container);
        setupHoverUI(container);
        if (++tries >= 20) clearInterval(id);
      }, 100);
      window.addEventListener('resize', () => {
        layout(container);
        enableDrag(container);
        setupHoverUI(container);
      }, { passive: true });
    }

    // Also relayout on image load for browsers that delay sizing until onload
    const img = container.querySelector('img');
    if (img && !(img.complete && img.naturalWidth > 0)) {
      img.addEventListener('load', () => {
        layout(container);
        enableDrag(container);
        setupHoverUI(container);
      }, { once: true });
    }

    // If content is inside a collapsible section, react to visibility changes
    const mo = new MutationObserver(() => {
      if (isMeasurable(container)) {
        layout(container);
        enableDrag(container);
        setupHoverUI(container);
      }
    });
    mo.observe(container, { attributes: true, attributeFilter: ['style', 'class'] });
    const parent = container.parentElement;
    if (parent) {
      mo.observe(parent, { attributes: true, attributeFilter: ['style', 'class', 'open', 'hidden'] });
    }
  }

  document.addEventListener('DOMContentLoaded', function () {
    document.querySelectorAll('.image-container').forEach(initContainer);
  });
})();
{{< image2 src="images/stormtroopocat.jpg" caption="The Stormtroopocat" height="676" width="704" linked="true" data-initial-x="50" data-initial-y="50" textbox_content="This is the <b>first line</b> | This is the second line." >}}

This is the first line.
This is the second line.
The Stormtroopocat