- The public blog theme now has a post lightbox slideshow.
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.
This commit is contained in:
@@ -32,4 +32,295 @@
|
||||
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();
|
||||
}());
|
||||
|
||||
Reference in New Issue
Block a user