After prettier

This commit is contained in:
Pìnei 2025-06-07 21:37:21 +00:00
parent f909b7a5aa
commit 7f2e119188
4 changed files with 290 additions and 263 deletions

View File

@ -1,69 +1,74 @@
interface CarouselInstance { interface CarouselInstance {
currentIndex: number; currentIndex: number
goToSlide: (index: number) => void; goToSlide: (index: number) => void
destroy: () => void; destroy: () => void
} }
// Cache for initialized carousels to avoid re-processing // Cache for initialized carousels to avoid re-processing
const initializedCarousels = new WeakSet<HTMLElement>(); const initializedCarousels = new WeakSet<HTMLElement>()
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
const contentArea = document.querySelector('article') const contentArea =
|| document.querySelector('#quartz-body .center') document.querySelector("article") ||
|| document.body; document.querySelector("#quartz-body .center") ||
document.body
initAllCarousels(contentArea); initAllCarousels(contentArea)
setupCarouselObserver(contentArea); setupCarouselObserver(contentArea)
// Make initCarousel available globally // Make initCarousel available globally
(window as any).initCarousel = initCarousel; ;(window as any).initCarousel = initCarousel
}); })
// Initializes all carousels within the specified container // Initializes all carousels within the specified container
function initAllCarousels(container: Element): void { function initAllCarousels(container: Element): void {
const carousels = container.querySelectorAll<HTMLElement>('.quartz-carousel[data-needs-init="true"]'); const carousels = container.querySelectorAll<HTMLElement>(
carousels.forEach(initCarousel); '.quartz-carousel[data-needs-init="true"]',
)
carousels.forEach(initCarousel)
} }
// Setup a MutationObserver to watch for new carousels being added to the DOM // Setup a MutationObserver to watch for new carousels being added to the DOM
function setupCarouselObserver(contentArea: Element): void { function setupCarouselObserver(contentArea: Element): void {
let debounceTimer: number | null = null; let debounceTimer: number | null = null
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
const hasNewCarousels = mutations.some((mutation) => const hasNewCarousels = mutations.some((mutation) =>
Array.from(mutation.addedNodes).some((node) => { Array.from(mutation.addedNodes).some((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return false; if (node.nodeType !== Node.ELEMENT_NODE) return false
const element = node as HTMLElement; const element = node as HTMLElement
// Check if the node itself is a carousel // Check if the node itself is a carousel
if (element.classList?.contains('quartz-carousel') && if (
element.getAttribute('data-needs-init') === 'true') { element.classList?.contains("quartz-carousel") &&
return true; element.getAttribute("data-needs-init") === "true"
) {
return true
} }
// Check for nested carousels // Check for nested carousels
return element.querySelectorAll?.('.quartz-carousel[data-needs-init="true"]').length > 0; return element.querySelectorAll?.('.quartz-carousel[data-needs-init="true"]').length > 0
}) }),
); )
if (hasNewCarousels) { if (hasNewCarousels) {
// Debounce to avoid multiple rapid initializations // Debounce to avoid multiple rapid initializations
if (debounceTimer) clearTimeout(debounceTimer); if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = window.setTimeout(() => initAllCarousels(contentArea), 50); debounceTimer = window.setTimeout(() => initAllCarousels(contentArea), 50)
} }
}); })
observer.observe(contentArea, { observer.observe(contentArea, {
childList: true, childList: true,
subtree: true subtree: true,
}); })
} }
// Create a modal for displaying images in full screen // Create a modal for displaying images in full screen
function createImageModal(): HTMLElement { function createImageModal(): HTMLElement {
const modal = document.createElement('div'); const modal = document.createElement("div")
modal.className = 'carousel-image-modal'; modal.className = "carousel-image-modal"
modal.innerHTML = ` modal.innerHTML = `
<div class="carousel-modal-overlay"> <div class="carousel-modal-overlay">
<div class="carousel-modal-content"> <div class="carousel-modal-content">
@ -75,191 +80,207 @@ function createImageModal(): HTMLElement {
<img class="carousel-modal-image" src="" alt="" /> <img class="carousel-modal-image" src="" alt="" />
</div> </div>
</div> </div>
`; `
document.body.appendChild(modal); document.body.appendChild(modal)
return modal; return modal
} }
// Show the image in a modal when clicked // Show the image in a modal when clicked
function showImageModal(img: HTMLImageElement): void { function showImageModal(img: HTMLImageElement): void {
let modal = document.querySelector('.carousel-image-modal') as HTMLElement; let modal = document.querySelector(".carousel-image-modal") as HTMLElement
if (!modal) { if (!modal) {
modal = createImageModal(); modal = createImageModal()
} }
const modalImg = modal.querySelector('.carousel-modal-image') as HTMLImageElement; const modalImg = modal.querySelector(".carousel-modal-image") as HTMLImageElement
const closeBtn = modal.querySelector('.carousel-modal-close') as HTMLButtonElement; const closeBtn = modal.querySelector(".carousel-modal-close") as HTMLButtonElement
const overlay = modal.querySelector('.carousel-modal-overlay') as HTMLElement; const overlay = modal.querySelector(".carousel-modal-overlay") as HTMLElement
// Set image source and alt // Set image source and alt
modalImg.src = img.src; modalImg.src = img.src
modalImg.alt = img.alt; modalImg.alt = img.alt
// Show modal // Show modal
modal.style.display = 'flex'; modal.style.display = "flex"
document.body.style.overflow = 'hidden'; document.body.style.overflow = "hidden"
// Close handlers // Close handlers
const closeModal = () => { const closeModal = () => {
modal.style.display = 'none'; modal.style.display = "none"
document.body.style.overflow = ''; document.body.style.overflow = ""
}; }
// Remove existing listeners to avoid duplicates // Remove existing listeners to avoid duplicates
closeBtn.onclick = null; closeBtn.onclick = null
overlay.onclick = null; overlay.onclick = null
closeBtn.onclick = closeModal; closeBtn.onclick = closeModal
overlay.onclick = (e) => { overlay.onclick = (e) => {
if (e.target === overlay) { if (e.target === overlay) {
closeModal(); closeModal()
}
} }
};
// Keyboard handler (Escape key) // Keyboard handler (Escape key)
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === "Escape") {
closeModal(); closeModal()
document.removeEventListener('keydown', handleKeyDown); document.removeEventListener("keydown", handleKeyDown)
}
} }
};
document.addEventListener('keydown', handleKeyDown); document.addEventListener("keydown", handleKeyDown)
} }
// Initialize a carousel and return an instance // Initialize a carousel and return an instance
function initCarousel(carousel: HTMLElement): CarouselInstance | null { function initCarousel(carousel: HTMLElement): CarouselInstance | null {
// Prevent re-initialization using WeakSet // Prevent re-initialization using WeakSet
if (initializedCarousels.has(carousel) || carousel.getAttribute('data-needs-init') !== 'true') { if (initializedCarousels.has(carousel) || carousel.getAttribute("data-needs-init") !== "true") {
return null; return null
} }
// Mark as initialized // Mark as initialized
initializedCarousels.add(carousel); initializedCarousels.add(carousel)
carousel.removeAttribute('data-needs-init'); carousel.removeAttribute("data-needs-init")
const slidesContainer = carousel.querySelector<HTMLElement>('.quartz-carousel-slides'); const slidesContainer = carousel.querySelector<HTMLElement>(".quartz-carousel-slides")
if (!slidesContainer) { if (!slidesContainer) {
return null; return null
} }
const slides = slidesContainer.querySelectorAll<HTMLElement>('.quartz-carousel-slide'); const slides = slidesContainer.querySelectorAll<HTMLElement>(".quartz-carousel-slide")
const dotsContainer = carousel.querySelector<HTMLElement>('.quartz-carousel-dots'); const dotsContainer = carousel.querySelector<HTMLElement>(".quartz-carousel-dots")
const prevButton = carousel.querySelector<HTMLButtonElement>('.quartz-carousel-prev'); const prevButton = carousel.querySelector<HTMLButtonElement>(".quartz-carousel-prev")
const nextButton = carousel.querySelector<HTMLButtonElement>('.quartz-carousel-next'); const nextButton = carousel.querySelector<HTMLButtonElement>(".quartz-carousel-next")
let currentIndex = 0; let currentIndex = 0
// Early return for single slide // Early return for single slide
if (slides.length <= 1) { if (slides.length <= 1) {
hideNavigationElements(); hideNavigationElements()
setupImageClickHandlers(); setupImageClickHandlers()
return createCarouselInstance(); return createCarouselInstance()
} }
setupDots(); setupDots()
setupNavigation(); setupNavigation()
setupKeyboardNavigation(); setupKeyboardNavigation()
setupTouchNavigation(); setupTouchNavigation()
setupImageClickHandlers(); setupImageClickHandlers()
// Initialize first slide // Initialize first slide
goToSlide(0); goToSlide(0)
function hideNavigationElements(): void { function hideNavigationElements(): void {
prevButton?.style.setProperty('display', 'none'); prevButton?.style.setProperty("display", "none")
nextButton?.style.setProperty('display', 'none'); nextButton?.style.setProperty("display", "none")
dotsContainer?.style.setProperty('display', 'none'); dotsContainer?.style.setProperty("display", "none")
} }
function setupImageClickHandlers(): void { function setupImageClickHandlers(): void {
slides.forEach(slide => { slides.forEach((slide) => {
const img = slide.querySelector('img'); const img = slide.querySelector("img")
if (img) { if (img) {
img.style.cursor = 'pointer'; img.style.cursor = "pointer"
img.addEventListener('click', (e) => { img.addEventListener(
e.stopPropagation(); "click",
showImageModal(img); (e) => {
}, { passive: true }); e.stopPropagation()
showImageModal(img)
},
{ passive: true },
)
} }
}); })
} }
function setupDots(): void { function setupDots(): void {
if (!dotsContainer) return; if (!dotsContainer) return
// Use DocumentFragment for better performance // Use DocumentFragment for better performance
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment()
slides.forEach((_, index) => { slides.forEach((_, index) => {
const dot = document.createElement('span'); const dot = document.createElement("span")
dot.className = index === 0 ? 'dot active' : 'dot'; dot.className = index === 0 ? "dot active" : "dot"
dot.addEventListener('click', () => goToSlide(index), { passive: true }); dot.addEventListener("click", () => goToSlide(index), { passive: true })
fragment.appendChild(dot); fragment.appendChild(dot)
}); })
dotsContainer.innerHTML = ''; dotsContainer.innerHTML = ""
dotsContainer.appendChild(fragment); dotsContainer.appendChild(fragment)
} }
function setupNavigation(): void { function setupNavigation(): void {
const handlePrevClick = (e: Event): void => { const handlePrevClick = (e: Event): void => {
e.preventDefault(); e.preventDefault()
goToSlide(currentIndex - 1); goToSlide(currentIndex - 1)
}; }
const handleNextClick = (e: Event): void => { const handleNextClick = (e: Event): void => {
e.preventDefault(); e.preventDefault()
goToSlide(currentIndex + 1); goToSlide(currentIndex + 1)
}; }
prevButton?.addEventListener('click', handlePrevClick, { passive: false }); prevButton?.addEventListener("click", handlePrevClick, { passive: false })
nextButton?.addEventListener('click', handleNextClick, { passive: false }); nextButton?.addEventListener("click", handleNextClick, { passive: false })
} }
// Setup keyboard navigation (Arrow keys) // Setup keyboard navigation (Arrow keys)
function setupKeyboardNavigation(): void { function setupKeyboardNavigation(): void {
carousel.setAttribute('tabindex', '0'); carousel.setAttribute("tabindex", "0")
carousel.addEventListener('keydown', (e: KeyboardEvent) => { carousel.addEventListener(
"keydown",
(e: KeyboardEvent) => {
switch (e.key) { switch (e.key) {
case 'ArrowLeft': case "ArrowLeft":
e.preventDefault(); e.preventDefault()
goToSlide(currentIndex - 1); goToSlide(currentIndex - 1)
break; break
case 'ArrowRight': case "ArrowRight":
e.preventDefault(); e.preventDefault()
goToSlide(currentIndex + 1); goToSlide(currentIndex + 1)
break; break
} }
}, { passive: false }); },
{ passive: false },
)
} }
// Setup touch navigation (swipe gestures) // Setup touch navigation (swipe gestures)
function setupTouchNavigation(): void { function setupTouchNavigation(): void {
let touchStartX = 0; let touchStartX = 0
let touchEndX = 0; let touchEndX = 0
carousel.addEventListener('touchstart', (e: TouchEvent) => { carousel.addEventListener(
touchStartX = e.changedTouches[0].screenX; "touchstart",
}, { passive: true }); (e: TouchEvent) => {
touchStartX = e.changedTouches[0].screenX
},
{ passive: true },
)
carousel.addEventListener('touchend', (e: TouchEvent) => { carousel.addEventListener(
touchEndX = e.changedTouches[0].screenX; "touchend",
handleSwipe(); (e: TouchEvent) => {
}, { passive: true }); touchEndX = e.changedTouches[0].screenX
handleSwipe()
},
{ passive: true },
)
function handleSwipe(): void { function handleSwipe(): void {
const minSwipeDistance = 50; const minSwipeDistance = 50
const swipeDistance = touchEndX - touchStartX; const swipeDistance = touchEndX - touchStartX
if (Math.abs(swipeDistance) < minSwipeDistance) return; if (Math.abs(swipeDistance) < minSwipeDistance) return
if (swipeDistance < 0) { if (swipeDistance < 0) {
goToSlide(currentIndex + 1); // Swipe left -> next goToSlide(currentIndex + 1) // Swipe left -> next
} else { } else {
goToSlide(currentIndex - 1); // Swipe right -> prev goToSlide(currentIndex - 1) // Swipe right -> prev
} }
} }
} }
@ -267,19 +288,19 @@ function initCarousel(carousel: HTMLElement): CarouselInstance | null {
// Function to go to a specific slide // Function to go to a specific slide
function goToSlide(index: number): void { function goToSlide(index: number): void {
// Handle wrapping // Handle wrapping
currentIndex = ((index % slides.length) + slides.length) % slides.length; currentIndex = ((index % slides.length) + slides.length) % slides.length
// Use transform for better performance // Use transform for better performance
if (slidesContainer) { if (slidesContainer) {
slidesContainer.style.transform = `translateX(-${currentIndex * 100}%)`; slidesContainer.style.transform = `translateX(-${currentIndex * 100}%)`
} }
// Update dots efficiently // Update dots efficiently
if (dotsContainer) { if (dotsContainer) {
const dots = dotsContainer.querySelectorAll<HTMLElement>('.dot'); const dots = dotsContainer.querySelectorAll<HTMLElement>(".dot")
dots.forEach((dot, i) => { dots.forEach((dot, i) => {
dot.classList.toggle('active', i === currentIndex); dot.classList.toggle("active", i === currentIndex)
}); })
} }
} }
@ -289,11 +310,11 @@ function initCarousel(carousel: HTMLElement): CarouselInstance | null {
currentIndex, currentIndex,
goToSlide, goToSlide,
destroy: () => { destroy: () => {
initializedCarousels.delete(carousel); initializedCarousels.delete(carousel)
carousel.setAttribute('data-needs-init', 'true'); carousel.setAttribute("data-needs-init", "true")
},
} }
};
} }
return createCarouselInstance(); return createCarouselInstance()
} }

View File

@ -199,7 +199,6 @@ html[saved-theme="dark"] .carousel-image-modal {
} }
} }
// Mobile responsiveness // Mobile responsiveness
@media (max-width: 768px) { @media (max-width: 768px) {
article .quartz-carousel { article .quartz-carousel {

View File

@ -1,5 +1,5 @@
import { QuartzTransformerPlugin } from "../types" import { QuartzTransformerPlugin } from "../types"
import { visit } from "unist-util-visit"; import { visit } from "unist-util-visit"
// @ts-ignore // @ts-ignore
import carouselScript from "../../components/scripts/carousel.inline" import carouselScript from "../../components/scripts/carousel.inline"
@ -14,26 +14,29 @@ export const Carousel: QuartzTransformerPlugin<Partial<CarouselOptions>> = (opts
function carouselTransformer() { function carouselTransformer() {
return (tree: any) => { return (tree: any) => {
visit(tree, 'html', (node: any) => { visit(tree, "html", (node: any) => {
// Check if the node contains a carousel tag // Check if the node contains a carousel tag
const content = node.value as string const content = node.value as string
if (content.startsWith('<Carousel>') && content.endsWith('</Carousel>')) { if (content.startsWith("<Carousel>") && content.endsWith("</Carousel>")) {
// Extract the content inside the carousel tag // Extract the content inside the carousel tag
const innerContent = content.slice('<Carousel>'.length, -'</Carousel>'.length).trim() const innerContent = content.slice("<Carousel>".length, -"</Carousel>".length).trim()
// Process images correctly by using a more reliable approach // Process images correctly by using a more reliable approach
const processedContent = innerContent.split('<img').map((part, index) => { const processedContent = innerContent
if (index === 0) return ''; // Skip the first part before any img tag .split("<img")
return `<div class="quartz-carousel-slide"><img${part}</div>`; .map((part, index) => {
}).join(''); if (index === 0) return "" // Skip the first part before any img tag
return `<div class="quartz-carousel-slide"><img${part}</div>`
})
.join("")
// Replace the node with a div that has a carousel class // Replace the node with a div that has a carousel class
node.type = 'html' node.type = "html"
node.value = `<div class="quartz-carousel" data-needs-init="true"> node.value = `<div class="quartz-carousel" data-needs-init="true">
<div class="quartz-carousel-slides"> <div class="quartz-carousel-slides">
${processedContent} ${processedContent}
</div> </div>
${showDots ? '<div class="quartz-carousel-dots"></div>' : ''} ${showDots ? '<div class="quartz-carousel-dots"></div>' : ""}
<button class="quartz-carousel-prev" aria-label="Previous slide"> <button class="quartz-carousel-prev" aria-label="Previous slide">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path> <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
@ -57,15 +60,19 @@ export const Carousel: QuartzTransformerPlugin<Partial<CarouselOptions>> = (opts
}, },
externalResources() { externalResources() {
return { return {
css: [{ css: [
{
content: carouselStyle, content: carouselStyle,
inline: true inline: true,
}], },
js: [{ ],
js: [
{
script: carouselScript, script: carouselScript,
loadTime: "afterDOMReady", loadTime: "afterDOMReady",
contentType: "inline", contentType: "inline",
}], },
],
} }
}, },
} }