mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-19 10:54:06 -06:00
Merge 778d08f39d into eb6cc6ff8e
This commit is contained in:
commit
3b47ee4890
50
docs/plugins/Carousel.md
Normal file
50
docs/plugins/Carousel.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
title: Carousel
|
||||||
|
---
|
||||||
|
|
||||||
|
The Carousel plugin transforms custom `<Carousel>` tags in your Markdown into interactive image galleries with navigation controls, dot indicators, and modal viewing capabilities.
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
<Carousel>
|
||||||
|
<img src="image1.jpg" alt="First image">
|
||||||
|
<img src="image2.jpg" alt="Second image">
|
||||||
|
<img src="image3.jpg" alt="Third image">
|
||||||
|
</Carousel>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const config: QuartzConfig = {
|
||||||
|
configuration: {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
transformers: [
|
||||||
|
// ...
|
||||||
|
Plugin.Carousel({
|
||||||
|
showDots: true, // Show dot indicators (default: true)
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
filters: [
|
||||||
|
// ...
|
||||||
|
],
|
||||||
|
emitters: [
|
||||||
|
// ...
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
The carousel automatically handles multiple images and provides intuitive navigation controls for users to browse through your image collections.
|
||||||
|
|
||||||
|
- Navigation: Click arrows or use keyboard (←/→ keys)
|
||||||
|
- Touch Support: Swipe gestures on mobile devices
|
||||||
|
- Modal View: Click images for full-screen viewing
|
||||||
|
- Responsive: Adapts to different screen sizes
|
||||||
|
- Accessibility: ARIA labels and keyboard navigation
|
||||||
|
- Theme Support: Works with light/dark modes
|
||||||
320
quartz/components/scripts/carousel.inline.ts
Normal file
320
quartz/components/scripts/carousel.inline.ts
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
interface CarouselInstance {
|
||||||
|
currentIndex: number
|
||||||
|
goToSlide: (index: number) => void
|
||||||
|
destroy: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache for initialized carousels to avoid re-processing
|
||||||
|
const initializedCarousels = new WeakSet<HTMLElement>()
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const contentArea =
|
||||||
|
document.querySelector("article") ||
|
||||||
|
document.querySelector("#quartz-body .center") ||
|
||||||
|
document.body
|
||||||
|
|
||||||
|
initAllCarousels(contentArea)
|
||||||
|
setupCarouselObserver(contentArea)
|
||||||
|
|
||||||
|
// Make initCarousel available globally
|
||||||
|
;(window as any).initCarousel = initCarousel
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initializes all carousels within the specified container
|
||||||
|
function initAllCarousels(container: Element): void {
|
||||||
|
const carousels = container.querySelectorAll<HTMLElement>(
|
||||||
|
'.quartz-carousel[data-needs-init="true"]',
|
||||||
|
)
|
||||||
|
carousels.forEach(initCarousel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup a MutationObserver to watch for new carousels being added to the DOM
|
||||||
|
function setupCarouselObserver(contentArea: Element): void {
|
||||||
|
let debounceTimer: number | null = null
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
const hasNewCarousels = mutations.some((mutation) =>
|
||||||
|
Array.from(mutation.addedNodes).some((node) => {
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE) return false
|
||||||
|
|
||||||
|
const element = node as HTMLElement
|
||||||
|
|
||||||
|
// Check if the node itself is a carousel
|
||||||
|
if (
|
||||||
|
element.classList?.contains("quartz-carousel") &&
|
||||||
|
element.getAttribute("data-needs-init") === "true"
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for nested carousels
|
||||||
|
return element.querySelectorAll?.('.quartz-carousel[data-needs-init="true"]').length > 0
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasNewCarousels) {
|
||||||
|
// Debounce to avoid multiple rapid initializations
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer)
|
||||||
|
debounceTimer = window.setTimeout(() => initAllCarousels(contentArea), 50)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(contentArea, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a modal for displaying images in full screen
|
||||||
|
function createImageModal(): HTMLElement {
|
||||||
|
const modal = document.createElement("div")
|
||||||
|
modal.className = "carousel-image-modal"
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="carousel-modal-overlay">
|
||||||
|
<div class="carousel-modal-content">
|
||||||
|
<button class="carousel-modal-close" aria-label="Close modal">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<img class="carousel-modal-image" src="" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
document.body.appendChild(modal)
|
||||||
|
return modal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the image in a modal when clicked
|
||||||
|
function showImageModal(img: HTMLImageElement): void {
|
||||||
|
let modal = document.querySelector(".carousel-image-modal") as HTMLElement
|
||||||
|
|
||||||
|
if (!modal) {
|
||||||
|
modal = createImageModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalImg = modal.querySelector(".carousel-modal-image") as HTMLImageElement
|
||||||
|
const closeBtn = modal.querySelector(".carousel-modal-close") as HTMLButtonElement
|
||||||
|
const overlay = modal.querySelector(".carousel-modal-overlay") as HTMLElement
|
||||||
|
|
||||||
|
// Set image source and alt
|
||||||
|
modalImg.src = img.src
|
||||||
|
modalImg.alt = img.alt
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
modal.style.display = "flex"
|
||||||
|
document.body.style.overflow = "hidden"
|
||||||
|
|
||||||
|
// Close handlers
|
||||||
|
const closeModal = () => {
|
||||||
|
modal.style.display = "none"
|
||||||
|
document.body.style.overflow = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove existing listeners to avoid duplicates
|
||||||
|
closeBtn.onclick = null
|
||||||
|
overlay.onclick = null
|
||||||
|
|
||||||
|
closeBtn.onclick = closeModal
|
||||||
|
overlay.onclick = (e) => {
|
||||||
|
if (e.target === overlay) {
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard handler (Escape key)
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
closeModal()
|
||||||
|
document.removeEventListener("keydown", handleKeyDown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a carousel and return an instance
|
||||||
|
function initCarousel(carousel: HTMLElement): CarouselInstance | null {
|
||||||
|
// Prevent re-initialization using WeakSet
|
||||||
|
if (initializedCarousels.has(carousel) || carousel.getAttribute("data-needs-init") !== "true") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as initialized
|
||||||
|
initializedCarousels.add(carousel)
|
||||||
|
carousel.removeAttribute("data-needs-init")
|
||||||
|
|
||||||
|
const slidesContainer = carousel.querySelector<HTMLElement>(".quartz-carousel-slides")
|
||||||
|
if (!slidesContainer) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const slides = slidesContainer.querySelectorAll<HTMLElement>(".quartz-carousel-slide")
|
||||||
|
const dotsContainer = carousel.querySelector<HTMLElement>(".quartz-carousel-dots")
|
||||||
|
const prevButton = carousel.querySelector<HTMLButtonElement>(".quartz-carousel-prev")
|
||||||
|
const nextButton = carousel.querySelector<HTMLButtonElement>(".quartz-carousel-next")
|
||||||
|
|
||||||
|
let currentIndex = 0
|
||||||
|
|
||||||
|
// Early return for single slide
|
||||||
|
if (slides.length <= 1) {
|
||||||
|
hideNavigationElements()
|
||||||
|
setupImageClickHandlers()
|
||||||
|
return createCarouselInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
setupDots()
|
||||||
|
setupNavigation()
|
||||||
|
setupKeyboardNavigation()
|
||||||
|
setupTouchNavigation()
|
||||||
|
setupImageClickHandlers()
|
||||||
|
|
||||||
|
// Initialize first slide
|
||||||
|
goToSlide(0)
|
||||||
|
|
||||||
|
function hideNavigationElements(): void {
|
||||||
|
prevButton?.style.setProperty("display", "none")
|
||||||
|
nextButton?.style.setProperty("display", "none")
|
||||||
|
dotsContainer?.style.setProperty("display", "none")
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupImageClickHandlers(): void {
|
||||||
|
slides.forEach((slide) => {
|
||||||
|
const img = slide.querySelector("img")
|
||||||
|
if (img) {
|
||||||
|
img.style.cursor = "pointer"
|
||||||
|
img.addEventListener(
|
||||||
|
"click",
|
||||||
|
(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
showImageModal(img)
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupDots(): void {
|
||||||
|
if (!dotsContainer) return
|
||||||
|
|
||||||
|
// Use DocumentFragment for better performance
|
||||||
|
const fragment = document.createDocumentFragment()
|
||||||
|
|
||||||
|
slides.forEach((_, index) => {
|
||||||
|
const dot = document.createElement("span")
|
||||||
|
dot.className = index === 0 ? "dot active" : "dot"
|
||||||
|
dot.addEventListener("click", () => goToSlide(index), { passive: true })
|
||||||
|
fragment.appendChild(dot)
|
||||||
|
})
|
||||||
|
|
||||||
|
dotsContainer.innerHTML = ""
|
||||||
|
dotsContainer.appendChild(fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupNavigation(): void {
|
||||||
|
const handlePrevClick = (e: Event): void => {
|
||||||
|
e.preventDefault()
|
||||||
|
goToSlide(currentIndex - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNextClick = (e: Event): void => {
|
||||||
|
e.preventDefault()
|
||||||
|
goToSlide(currentIndex + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevButton?.addEventListener("click", handlePrevClick, { passive: false })
|
||||||
|
nextButton?.addEventListener("click", handleNextClick, { passive: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup keyboard navigation (Arrow keys)
|
||||||
|
function setupKeyboardNavigation(): void {
|
||||||
|
carousel.setAttribute("tabindex", "0")
|
||||||
|
carousel.addEventListener(
|
||||||
|
"keydown",
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
switch (e.key) {
|
||||||
|
case "ArrowLeft":
|
||||||
|
e.preventDefault()
|
||||||
|
goToSlide(currentIndex - 1)
|
||||||
|
break
|
||||||
|
case "ArrowRight":
|
||||||
|
e.preventDefault()
|
||||||
|
goToSlide(currentIndex + 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup touch navigation (swipe gestures)
|
||||||
|
function setupTouchNavigation(): void {
|
||||||
|
let touchStartX = 0
|
||||||
|
let touchEndX = 0
|
||||||
|
|
||||||
|
carousel.addEventListener(
|
||||||
|
"touchstart",
|
||||||
|
(e: TouchEvent) => {
|
||||||
|
touchStartX = e.changedTouches[0].screenX
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
carousel.addEventListener(
|
||||||
|
"touchend",
|
||||||
|
(e: TouchEvent) => {
|
||||||
|
touchEndX = e.changedTouches[0].screenX
|
||||||
|
handleSwipe()
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
function handleSwipe(): void {
|
||||||
|
const minSwipeDistance = 50
|
||||||
|
const swipeDistance = touchEndX - touchStartX
|
||||||
|
|
||||||
|
if (Math.abs(swipeDistance) < minSwipeDistance) return
|
||||||
|
|
||||||
|
if (swipeDistance < 0) {
|
||||||
|
goToSlide(currentIndex + 1) // Swipe left -> next
|
||||||
|
} else {
|
||||||
|
goToSlide(currentIndex - 1) // Swipe right -> prev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to go to a specific slide
|
||||||
|
function goToSlide(index: number): void {
|
||||||
|
// Handle wrapping
|
||||||
|
currentIndex = ((index % slides.length) + slides.length) % slides.length
|
||||||
|
|
||||||
|
// Use transform for better performance
|
||||||
|
if (slidesContainer) {
|
||||||
|
slidesContainer.style.transform = `translateX(-${currentIndex * 100}%)`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update dots efficiently
|
||||||
|
if (dotsContainer) {
|
||||||
|
const dots = dotsContainer.querySelectorAll<HTMLElement>(".dot")
|
||||||
|
dots.forEach((dot, i) => {
|
||||||
|
dot.classList.toggle("active", i === currentIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and return the carousel instance that references the carousel element
|
||||||
|
function createCarouselInstance(): CarouselInstance {
|
||||||
|
return {
|
||||||
|
currentIndex,
|
||||||
|
goToSlide,
|
||||||
|
destroy: () => {
|
||||||
|
initializedCarousels.delete(carousel)
|
||||||
|
carousel.setAttribute("data-needs-init", "true")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createCarouselInstance()
|
||||||
|
}
|
||||||
234
quartz/components/styles/carousel.inline.scss
Normal file
234
quartz/components/styles/carousel.inline.scss
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
// Variables
|
||||||
|
$carousel-control-bg: #f0f0f0;
|
||||||
|
$carousel-control-color: #333;
|
||||||
|
$carousel-control-hover-bg: #ddd;
|
||||||
|
$carousel-control-size: 40px;
|
||||||
|
|
||||||
|
article .quartz-carousel {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
margin: 2rem 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.quartz-carousel-slides {
|
||||||
|
display: flex;
|
||||||
|
transition: transform 0.5s ease-in-out;
|
||||||
|
|
||||||
|
.quartz-carousel-slide {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 400px;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quartz-carousel-dots {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
margin: 0 5px;
|
||||||
|
background-color: #ddd;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quartz-carousel-prev,
|
||||||
|
.quartz-carousel-next {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background-color: $carousel-control-bg;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: $carousel-control-size;
|
||||||
|
height: $carousel-control-size;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
fill: $carousel-control-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $carousel-control-hover-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quartz-carousel-prev {
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quartz-carousel-next {
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image Modal Styles
|
||||||
|
.carousel-image-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
|
.carousel-modal-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-modal-content {
|
||||||
|
position: relative;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-modal-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: -50px;
|
||||||
|
right: -50px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
z-index: 10001;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode adjustments
|
||||||
|
html[saved-theme="dark"] article .quartz-carousel,
|
||||||
|
html[saved-theme="dark"] .carousel-image-modal {
|
||||||
|
.quartz-carousel-prev,
|
||||||
|
.quartz-carousel-next {
|
||||||
|
background-color: rgba(50, 50, 50, 0.8);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(70, 70, 70, 0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
background-color: #555;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-modal-close {
|
||||||
|
background-color: rgba(50, 50, 50, 0.9);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(70, 70, 70, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile responsiveness
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
article .quartz-carousel {
|
||||||
|
.quartz-carousel-prev,
|
||||||
|
.quartz-carousel-next {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-image-modal {
|
||||||
|
.carousel-modal-overlay {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-modal-close {
|
||||||
|
top: -35px;
|
||||||
|
right: -35px;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
quartz/plugins/transformers/carousel.ts
Normal file
79
quartz/plugins/transformers/carousel.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { QuartzTransformerPlugin } from "../types"
|
||||||
|
import { visit } from "unist-util-visit"
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import carouselScript from "../../components/scripts/carousel.inline"
|
||||||
|
import carouselStyle from "../../components/styles/carousel.inline.scss"
|
||||||
|
|
||||||
|
interface CarouselOptions {
|
||||||
|
showDots: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Carousel: QuartzTransformerPlugin<Partial<CarouselOptions>> = (opts) => {
|
||||||
|
const showDots = opts?.showDots ?? true
|
||||||
|
|
||||||
|
function carouselTransformer() {
|
||||||
|
return (tree: any) => {
|
||||||
|
visit(tree, "html", (node: any) => {
|
||||||
|
// Check if the node contains a carousel tag
|
||||||
|
const content = node.value as string
|
||||||
|
if (content.startsWith("<Carousel>") && content.endsWith("</Carousel>")) {
|
||||||
|
// Extract the content inside the carousel tag
|
||||||
|
const innerContent = content.slice("<Carousel>".length, -"</Carousel>".length).trim()
|
||||||
|
|
||||||
|
// Process images correctly by using a more reliable approach
|
||||||
|
const processedContent = innerContent
|
||||||
|
.split("<img")
|
||||||
|
.map((part, index) => {
|
||||||
|
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
|
||||||
|
node.type = "html"
|
||||||
|
node.value = `<div class="quartz-carousel" data-needs-init="true">
|
||||||
|
<div class="quartz-carousel-slides">
|
||||||
|
${processedContent}
|
||||||
|
</div>
|
||||||
|
${showDots ? '<div class="quartz-carousel-dots"></div>' : ""}
|
||||||
|
<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">
|
||||||
|
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="quartz-carousel-next" aria-label="Next slide">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "Carousel",
|
||||||
|
markdownPlugins() {
|
||||||
|
return [carouselTransformer]
|
||||||
|
},
|
||||||
|
externalResources() {
|
||||||
|
return {
|
||||||
|
css: [
|
||||||
|
{
|
||||||
|
content: carouselStyle,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
js: [
|
||||||
|
{
|
||||||
|
script: carouselScript,
|
||||||
|
loadTime: "afterDOMReady",
|
||||||
|
contentType: "inline",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,3 +11,4 @@ export { SyntaxHighlighting } from "./syntax"
|
|||||||
export { TableOfContents } from "./toc"
|
export { TableOfContents } from "./toc"
|
||||||
export { HardLineBreaks } from "./linebreaks"
|
export { HardLineBreaks } from "./linebreaks"
|
||||||
export { RoamFlavoredMarkdown } from "./roam"
|
export { RoamFlavoredMarkdown } from "./roam"
|
||||||
|
export { Carousel } from "./carousel"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user