3e7cea4491
Changed: [themes/neon/assets/app.js](/Users/tyemeclifford/Documents/GH/blog/themes/neon/assets/app.js) Scans each single post’s .markdown content. Collects images, native videos, shortcode/video iframes, and direct image/video links. Opens media in a lightbox with Previous/Next, count, caption, Escape close, and arrow-key navigation. Clicking a displayed image advances to the next item. [themes/neon/assets/style.css](/Users/tyemeclifford/Documents/GH/blog/themes/neon/assets/style.css) Added clickable media styling, embed “View” buttons, and responsive lightbox layout. Works in dark and light modes. [CHANGELOG.md](/Users/tyemeclifford/Documents/GH/blog/CHANGELOG.md) Added the feature note.
327 lines
12 KiB
JavaScript
327 lines
12 KiB
JavaScript
(function () {
|
|
var root = document.documentElement;
|
|
var storedMode = localStorage.getItem('neon-mode') || 'dark';
|
|
var storedAccent = localStorage.getItem('neon-accent') || 'green';
|
|
var modeButton = document.querySelector('[data-mode-toggle]');
|
|
|
|
function applyMode(mode) {
|
|
root.dataset.mode = mode;
|
|
localStorage.setItem('neon-mode', mode);
|
|
if (modeButton) {
|
|
modeButton.textContent = mode === 'light' ? 'Dark' : 'Light';
|
|
modeButton.setAttribute('aria-label', mode === 'light' ? 'Use dark mode' : 'Use light mode');
|
|
}
|
|
}
|
|
|
|
function applyAccent(accent) {
|
|
root.dataset.accent = accent;
|
|
localStorage.setItem('neon-accent', accent);
|
|
}
|
|
|
|
applyMode(storedMode);
|
|
applyAccent(storedAccent);
|
|
|
|
if (modeButton) {
|
|
modeButton.addEventListener('click', function () {
|
|
applyMode(root.dataset.mode === 'light' ? 'dark' : 'light');
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('[data-accent]').forEach(function (button) {
|
|
button.addEventListener('click', function () {
|
|
applyAccent(button.dataset.accent || 'green');
|
|
});
|
|
});
|
|
|
|
function initLightbox() {
|
|
var article = document.querySelector('.article .markdown');
|
|
if (!article) {
|
|
return;
|
|
}
|
|
|
|
var imageExtensions = /\.(avif|gif|jpe?g|png|webp|bmp|svg)(?:[?#].*)?$/i;
|
|
var videoExtensions = /\.(m4v|mov|mp4|ogv|ogg|webm)(?:[?#].*)?$/i;
|
|
var entries = [];
|
|
var currentIndex = 0;
|
|
var lastFocus = null;
|
|
var lightbox = null;
|
|
var mediaFrame = null;
|
|
var titleNode = null;
|
|
var countNode = null;
|
|
var captionNode = null;
|
|
var closeButton = null;
|
|
|
|
function mediaTitle(element, fallback) {
|
|
return element.getAttribute('alt') ||
|
|
element.getAttribute('title') ||
|
|
element.getAttribute('aria-label') ||
|
|
fallback ||
|
|
'Media';
|
|
}
|
|
|
|
function isVideoIframe(iframe, src) {
|
|
return Boolean(iframe.closest('.shortcode-embed')) ||
|
|
/(?:youtube\.com|youtu\.be|vimeo\.com|player\.vimeo|lone-embed\.php)/i.test(src) ||
|
|
videoExtensions.test(src);
|
|
}
|
|
|
|
function addEntry(entry) {
|
|
if (!entry.src) {
|
|
return;
|
|
}
|
|
entries.push(entry);
|
|
}
|
|
|
|
article.querySelectorAll('img, video, iframe, a[href]').forEach(function (element) {
|
|
var tag = element.tagName.toLowerCase();
|
|
if (tag === 'img') {
|
|
addEntry({
|
|
type: 'image',
|
|
src: element.currentSrc || element.src || element.getAttribute('src'),
|
|
title: mediaTitle(element, 'Image'),
|
|
trigger: element
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (tag === 'video') {
|
|
var source = element.currentSrc || element.getAttribute('src');
|
|
var sourceElement = source ? null : element.querySelector('source[src]');
|
|
if (!source && sourceElement) {
|
|
source = sourceElement.getAttribute('src');
|
|
}
|
|
addEntry({
|
|
type: 'video',
|
|
src: source,
|
|
poster: element.getAttribute('poster') || '',
|
|
title: mediaTitle(element, 'Video'),
|
|
trigger: element
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (tag === 'iframe') {
|
|
var iframeSrc = element.getAttribute('src') || '';
|
|
if (isVideoIframe(element, iframeSrc)) {
|
|
addEntry({
|
|
type: 'iframe',
|
|
src: iframeSrc,
|
|
title: mediaTitle(element, 'Video'),
|
|
trigger: element
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (element.querySelector('img, video, iframe')) {
|
|
return;
|
|
}
|
|
|
|
var href = element.getAttribute('href') || '';
|
|
if (imageExtensions.test(href)) {
|
|
addEntry({
|
|
type: 'image',
|
|
src: href,
|
|
title: mediaTitle(element, element.textContent.trim() || 'Image'),
|
|
trigger: element
|
|
});
|
|
} else if (videoExtensions.test(href)) {
|
|
addEntry({
|
|
type: 'video',
|
|
src: href,
|
|
title: mediaTitle(element, element.textContent.trim() || 'Video'),
|
|
trigger: element
|
|
});
|
|
}
|
|
});
|
|
|
|
if (!entries.length) {
|
|
return;
|
|
}
|
|
|
|
function buildLightbox() {
|
|
lightbox = document.createElement('div');
|
|
lightbox.className = 'tcms-lightbox';
|
|
lightbox.hidden = true;
|
|
lightbox.setAttribute('role', 'dialog');
|
|
lightbox.setAttribute('aria-modal', 'true');
|
|
lightbox.setAttribute('aria-label', 'Post media slideshow');
|
|
|
|
lightbox.innerHTML = [
|
|
'<button class="tcms-lightbox__backdrop" type="button" data-lightbox-close aria-label="Close media viewer"></button>',
|
|
'<section class="tcms-lightbox__dialog" role="document">',
|
|
'<header class="tcms-lightbox__bar">',
|
|
'<strong class="tcms-lightbox__title"></strong>',
|
|
'<span class="tcms-lightbox__count"></span>',
|
|
'<button class="tcms-lightbox__close" type="button" data-lightbox-close aria-label="Close">Close</button>',
|
|
'</header>',
|
|
'<div class="tcms-lightbox__stage">',
|
|
'<button class="tcms-lightbox__nav tcms-lightbox__nav--previous" type="button" data-lightbox-previous aria-label="Previous media">Previous</button>',
|
|
'<div class="tcms-lightbox__media"></div>',
|
|
'<button class="tcms-lightbox__nav tcms-lightbox__nav--next" type="button" data-lightbox-next aria-label="Next media">Next</button>',
|
|
'</div>',
|
|
'<p class="tcms-lightbox__caption"></p>',
|
|
'</section>'
|
|
].join('');
|
|
|
|
document.body.appendChild(lightbox);
|
|
mediaFrame = lightbox.querySelector('.tcms-lightbox__media');
|
|
titleNode = lightbox.querySelector('.tcms-lightbox__title');
|
|
countNode = lightbox.querySelector('.tcms-lightbox__count');
|
|
captionNode = lightbox.querySelector('.tcms-lightbox__caption');
|
|
closeButton = lightbox.querySelector('.tcms-lightbox__close');
|
|
|
|
lightbox.querySelectorAll('[data-lightbox-close]').forEach(function (button) {
|
|
button.addEventListener('click', closeLightbox);
|
|
});
|
|
lightbox.querySelector('[data-lightbox-previous]').addEventListener('click', function () {
|
|
showLightboxItem(currentIndex - 1);
|
|
});
|
|
lightbox.querySelector('[data-lightbox-next]').addEventListener('click', function () {
|
|
showLightboxItem(currentIndex + 1);
|
|
});
|
|
}
|
|
|
|
function showLightboxItem(index) {
|
|
if (!lightbox) {
|
|
buildLightbox();
|
|
}
|
|
|
|
if (index < 0) {
|
|
index = entries.length - 1;
|
|
} else if (index >= entries.length) {
|
|
index = 0;
|
|
}
|
|
currentIndex = index;
|
|
|
|
var entry = entries[currentIndex];
|
|
mediaFrame.textContent = '';
|
|
titleNode.textContent = entry.title;
|
|
countNode.textContent = (currentIndex + 1) + ' of ' + entries.length;
|
|
captionNode.textContent = entry.title;
|
|
|
|
if (entry.type === 'image') {
|
|
var image = document.createElement('img');
|
|
image.src = entry.src;
|
|
image.alt = entry.title;
|
|
image.addEventListener('click', function () {
|
|
if (entries.length > 1) {
|
|
showLightboxItem(currentIndex + 1);
|
|
}
|
|
});
|
|
mediaFrame.appendChild(image);
|
|
return;
|
|
}
|
|
|
|
if (entry.type === 'video') {
|
|
var video = document.createElement('video');
|
|
video.src = entry.src;
|
|
video.controls = true;
|
|
video.playsInline = true;
|
|
video.preload = 'metadata';
|
|
if (entry.poster) {
|
|
video.poster = entry.poster;
|
|
}
|
|
mediaFrame.appendChild(video);
|
|
return;
|
|
}
|
|
|
|
var iframe = document.createElement('iframe');
|
|
iframe.src = entry.src;
|
|
iframe.title = entry.title;
|
|
iframe.loading = 'lazy';
|
|
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
|
|
iframe.allowFullscreen = true;
|
|
mediaFrame.appendChild(iframe);
|
|
}
|
|
|
|
function openLightbox(index) {
|
|
if (!lightbox) {
|
|
buildLightbox();
|
|
}
|
|
lastFocus = document.activeElement;
|
|
lightbox.hidden = false;
|
|
document.body.classList.add('tcms-lightbox-open');
|
|
showLightboxItem(index);
|
|
closeButton.focus();
|
|
}
|
|
|
|
function closeLightbox() {
|
|
if (!lightbox || lightbox.hidden) {
|
|
return;
|
|
}
|
|
lightbox.hidden = true;
|
|
mediaFrame.textContent = '';
|
|
document.body.classList.remove('tcms-lightbox-open');
|
|
if (lastFocus && typeof lastFocus.focus === 'function') {
|
|
lastFocus.focus();
|
|
}
|
|
}
|
|
|
|
function openFromEvent(event, index) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
openLightbox(index);
|
|
}
|
|
|
|
entries.forEach(function (entry, index) {
|
|
var trigger = entry.trigger;
|
|
var tag = trigger.tagName.toLowerCase();
|
|
var button;
|
|
|
|
if (tag === 'img' || tag === 'a') {
|
|
trigger.classList.add('tcms-lightboxable');
|
|
trigger.setAttribute('data-lightbox-index', String(index));
|
|
if (tag === 'img' && !trigger.hasAttribute('tabindex')) {
|
|
trigger.setAttribute('tabindex', '0');
|
|
}
|
|
if (tag === 'img') {
|
|
trigger.setAttribute('role', 'button');
|
|
trigger.setAttribute('aria-label', 'Open image viewer');
|
|
}
|
|
trigger.addEventListener('click', function (event) {
|
|
openFromEvent(event, index);
|
|
});
|
|
trigger.addEventListener('keydown', function (event) {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
openFromEvent(event, index);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
button = document.createElement('button');
|
|
button.className = 'tcms-lightbox-trigger';
|
|
button.type = 'button';
|
|
button.textContent = 'View';
|
|
button.setAttribute('aria-label', 'Open media viewer');
|
|
button.addEventListener('click', function (event) {
|
|
openFromEvent(event, index);
|
|
});
|
|
|
|
if (tag === 'iframe' && trigger.parentElement && trigger.parentElement.classList.contains('shortcode-embed')) {
|
|
trigger.parentElement.classList.add('tcms-lightbox-host');
|
|
trigger.parentElement.appendChild(button);
|
|
return;
|
|
}
|
|
|
|
trigger.insertAdjacentElement('afterend', button);
|
|
});
|
|
|
|
document.addEventListener('keydown', function (event) {
|
|
if (!lightbox || lightbox.hidden) {
|
|
return;
|
|
}
|
|
if (event.key === 'Escape') {
|
|
closeLightbox();
|
|
} else if (event.key === 'ArrowRight') {
|
|
showLightboxItem(currentIndex + 1);
|
|
} else if (event.key === 'ArrowLeft') {
|
|
showLightboxItem(currentIndex - 1);
|
|
}
|
|
});
|
|
}
|
|
|
|
initLightbox();
|
|
}());
|