Skip to content

Commit 6115126

Browse files
kgellerMpdreamz
andauthored
Adding support to the docs for an image carousel (#1396)
* who knows * closing tags fix * simplify carousel block code * more cleanup * adding fixed height setting and removing sample carousel addition * prettier updates * adding to the documentation * use existing pics in the carousel docs * dropping id * removed options for controls and indicators * updated fixed-height * cleanup and tests * cleanup * Update main.ts - fixing conflict resolution * formatted * fixing bad conflict resolution --------- Co-authored-by: Martijn Laarman <[email protected]>
1 parent d720ff5 commit 6115126

File tree

11 files changed

+888
-3
lines changed

11 files changed

+888
-3
lines changed

docs/syntax/images.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,42 @@ image::images/metrics-alert-filters-and-group.png[Metric threshold filter and gr
120120
```asciidoc
121121
image::images/synthetics-get-started-projects.png[]
122122
```
123+
124+
## Image carousel
125+
126+
The image carousel directive builds upon the image directive.
127+
128+
```markdown
129+
::::{carousel}
130+
131+
:id: nested-carousel-example
132+
:fixed-height: small ## small, medium, auto (auto is default if fixed-height is not specified)
133+
134+
:::{image} images/apm.png
135+
:alt: First image description
136+
:title: First image title
137+
:::
138+
139+
:::{image} images/applies.png
140+
:alt: Second image description
141+
:title: Second image title
142+
:::
143+
144+
::::
145+
```
146+
::::{carousel}
147+
148+
:id: nested-carousel-example
149+
:fixed-height: small
150+
151+
:::{image} images/apm.png
152+
:alt: First image description
153+
:title: First image title
154+
:::
155+
156+
:::{image} images/applies.png
157+
:alt: Second image description
158+
:title: Second image title
159+
:::
160+
161+
::::
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
class ImageCarousel {
2+
private container: HTMLElement
3+
private slides: HTMLElement[]
4+
private indicators: HTMLElement[]
5+
private prevButton: HTMLElement | null
6+
private nextButton: HTMLElement | null
7+
private currentIndex: number = 0
8+
private touchStartX: number = 0
9+
private touchEndX: number = 0
10+
11+
constructor(container: HTMLElement) {
12+
this.container = container
13+
if (!this.container) {
14+
console.warn('Carousel container element is null or undefined')
15+
return
16+
}
17+
18+
this.slides = Array.from(
19+
this.container.querySelectorAll('.carousel-slide')
20+
)
21+
this.indicators = Array.from(
22+
this.container.querySelectorAll('.carousel-indicator')
23+
)
24+
this.prevButton = this.container.querySelector('.carousel-prev')
25+
this.nextButton = this.container.querySelector('.carousel-next')
26+
27+
this.initializeSlides()
28+
this.setupEventListeners()
29+
}
30+
31+
private initializeSlides(): void {
32+
// Initialize all slides as inactive
33+
this.slides.forEach((slide, index) => {
34+
this.setSlideState(slide, index === 0)
35+
})
36+
37+
// Initialize indicators
38+
this.indicators.forEach((indicator, index) => {
39+
this.setIndicatorState(indicator, index === 0)
40+
})
41+
}
42+
43+
private setSlideState(slide: HTMLElement, isActive: boolean): void {
44+
slide.setAttribute('data-active', isActive.toString())
45+
slide.style.display = isActive ? 'block' : 'none'
46+
slide.style.opacity = isActive ? '1' : '0'
47+
}
48+
49+
private setIndicatorState(indicator: HTMLElement, isActive: boolean): void {
50+
indicator.setAttribute('data-active', isActive.toString())
51+
}
52+
53+
private setupEventListeners(): void {
54+
// Navigation controls
55+
this.prevButton?.addEventListener('click', () => this.prevSlide())
56+
this.nextButton?.addEventListener('click', () => this.nextSlide())
57+
58+
// Indicators
59+
this.indicators.forEach((indicator, index) => {
60+
indicator.addEventListener('click', () => this.goToSlide(index))
61+
})
62+
63+
// Keyboard navigation
64+
document.addEventListener('keydown', (e) => {
65+
if (!this.isInViewport()) return
66+
67+
if (e.key === 'ArrowLeft') this.prevSlide()
68+
else if (e.key === 'ArrowRight') this.nextSlide()
69+
})
70+
71+
// Touch events
72+
this.container.addEventListener('touchstart', (e) => {
73+
this.touchStartX = e.changedTouches[0].screenX
74+
})
75+
76+
this.container.addEventListener('touchend', (e) => {
77+
this.touchEndX = e.changedTouches[0].screenX
78+
this.handleSwipe()
79+
})
80+
}
81+
82+
private prevSlide(): void {
83+
const newIndex =
84+
(this.currentIndex - 1 + this.slides.length) % this.slides.length
85+
this.goToSlide(newIndex)
86+
}
87+
88+
private nextSlide(): void {
89+
const newIndex = (this.currentIndex + 1) % this.slides.length
90+
this.goToSlide(newIndex)
91+
}
92+
93+
private goToSlide(index: number): void {
94+
// Update slides
95+
this.setSlideState(this.slides[this.currentIndex], false)
96+
this.setSlideState(this.slides[index], true)
97+
98+
// Update indicators
99+
if (this.indicators.length > 0) {
100+
this.setIndicatorState(this.indicators[this.currentIndex], false)
101+
this.setIndicatorState(this.indicators[index], true)
102+
}
103+
104+
this.currentIndex = index
105+
}
106+
107+
private handleSwipe(): void {
108+
const threshold = 50
109+
const diff = this.touchStartX - this.touchEndX
110+
111+
if (Math.abs(diff) < threshold) return
112+
113+
if (diff > 0) this.nextSlide()
114+
else this.prevSlide()
115+
}
116+
117+
private isInViewport(): boolean {
118+
const rect = this.container.getBoundingClientRect()
119+
return (
120+
rect.top >= 0 &&
121+
rect.left >= 0 &&
122+
rect.bottom <=
123+
(window.innerHeight || document.documentElement.clientHeight) &&
124+
rect.right <=
125+
(window.innerWidth || document.documentElement.clientWidth)
126+
)
127+
}
128+
}
129+
130+
// Export function to initialize carousels
131+
export function initImageCarousel(): void {
132+
// Find all carousel containers
133+
const carousels = document.querySelectorAll('.carousel-container')
134+
135+
// Process each carousel
136+
carousels.forEach((carouselElement) => {
137+
const carousel = carouselElement as HTMLElement
138+
139+
// Get the existing track
140+
let track = carousel.querySelector('.carousel-track')
141+
if (!track) {
142+
track = document.createElement('div')
143+
track.className = 'carousel-track'
144+
carousel.appendChild(track)
145+
}
146+
147+
// Clean up any existing slides - this prevents duplicates
148+
const existingSlides = Array.from(
149+
track.querySelectorAll('.carousel-slide')
150+
)
151+
152+
// Find all image links that might be related to this carousel
153+
const section = findSectionForCarousel(carousel)
154+
if (!section) return
155+
156+
// First, collect all images we want in the carousel
157+
const allImageLinks = Array.from(
158+
section.querySelectorAll('a[href*="epr.elastic.co"]')
159+
)
160+
161+
// Track URLs to prevent duplicates
162+
const processedUrls = new Set()
163+
164+
// Process the existing slides first
165+
existingSlides.forEach((slide) => {
166+
const imageRef = slide.querySelector('a.carousel-image-reference')
167+
if (imageRef && imageRef instanceof HTMLAnchorElement) {
168+
processedUrls.add(imageRef.href)
169+
}
170+
})
171+
172+
// Find standalone images (not already in carousel slides)
173+
const standaloneImages = allImageLinks.filter((img) => {
174+
if (processedUrls.has(img.href)) {
175+
return false // Skip if already processed
176+
}
177+
178+
// Don't count images already in carousel slides
179+
const isInCarousel = img.closest('.carousel-slide') !== null
180+
if (isInCarousel) {
181+
processedUrls.add(img.href)
182+
return false
183+
}
184+
185+
processedUrls.add(img.href)
186+
return true
187+
})
188+
189+
// Add the standalone images to the carousel
190+
let slideIndex = existingSlides.length
191+
standaloneImages.forEach((imgLink) => {
192+
// Find container to hide
193+
const imgContainer = findClosestContainer(imgLink, carousel)
194+
195+
// Create a new slide
196+
const slide = document.createElement('div')
197+
slide.className = 'carousel-slide'
198+
slide.setAttribute('data-index', slideIndex.toString())
199+
if (slideIndex === 0 && existingSlides.length === 0) {
200+
slide.setAttribute('data-active', 'true')
201+
}
202+
203+
// Create a proper carousel image reference wrapper
204+
const imageRef = document.createElement('a')
205+
imageRef.className = 'carousel-image-reference'
206+
imageRef.href = imgLink.href
207+
imageRef.target = '_blank'
208+
209+
// Clone the image
210+
const img = imgLink.querySelector('img')
211+
if (img) {
212+
imageRef.appendChild(img.cloneNode(true))
213+
}
214+
215+
slide.appendChild(imageRef)
216+
track.appendChild(slide)
217+
218+
// Hide the original container properly
219+
if (imgContainer) {
220+
try {
221+
// Find the parent element that might be a paragraph or div containing the image
222+
let parent = imgContainer
223+
let maxAttempts = 3 // Don't go too far up the tree
224+
225+
while (
226+
maxAttempts > 0 &&
227+
parent &&
228+
parent !== document.body
229+
) {
230+
// If this is one of these elements, hide it
231+
if (
232+
parent.tagName === 'P' ||
233+
(parent.tagName === 'DIV' &&
234+
!parent.classList.contains(
235+
'carousel-container'
236+
))
237+
) {
238+
parent.style.display = 'none'
239+
break
240+
}
241+
parent = parent.parentElement
242+
maxAttempts--
243+
}
244+
245+
// If we couldn't find a suitable parent, just hide the container itself
246+
if (maxAttempts === 0) {
247+
imgContainer.style.display = 'none'
248+
}
249+
} catch (e) {
250+
console.error('Failed to hide original image:', e)
251+
}
252+
}
253+
254+
slideIndex++
255+
})
256+
257+
// Only set up controls if we have multiple slides
258+
const totalSlides = track.querySelectorAll('.carousel-slide').length
259+
if (totalSlides > 1) {
260+
// Add controls if they don't exist
261+
if (!carousel.querySelector('.carousel-prev')) {
262+
const prevButton = document.createElement('button')
263+
prevButton.type = 'button'
264+
prevButton.className = 'carousel-control carousel-prev'
265+
prevButton.setAttribute('aria-label', 'Previous slide')
266+
prevButton.innerHTML = '<span aria-hidden="true">←</span>'
267+
carousel.appendChild(prevButton)
268+
}
269+
270+
if (!carousel.querySelector('.carousel-next')) {
271+
const nextButton = document.createElement('button')
272+
nextButton.type = 'button'
273+
nextButton.className = 'carousel-control carousel-next'
274+
nextButton.setAttribute('aria-label', 'Next slide')
275+
nextButton.innerHTML = '<span aria-hidden="true">→</span>'
276+
carousel.appendChild(nextButton)
277+
}
278+
279+
// Add or update indicators
280+
let indicators = carousel.querySelector('.carousel-indicators')
281+
if (!indicators) {
282+
indicators = document.createElement('div')
283+
indicators.className = 'carousel-indicators'
284+
carousel.appendChild(indicators)
285+
} else {
286+
indicators.innerHTML = '' // Clear existing indicators
287+
}
288+
289+
for (let i = 0; i < totalSlides; i++) {
290+
const indicator = document.createElement('button')
291+
indicator.type = 'button'
292+
indicator.className = 'carousel-indicator'
293+
indicator.setAttribute('data-index', i.toString())
294+
if (i === 0) {
295+
indicator.setAttribute('data-active', 'true')
296+
}
297+
indicator.setAttribute('aria-label', `Go to slide ${i + 1}`)
298+
indicators.appendChild(indicator)
299+
}
300+
}
301+
302+
// Initialize this carousel
303+
new ImageCarousel(carousel)
304+
})
305+
}
306+
307+
// Helper to find a suitable container for an image
308+
function findClosestContainer(
309+
element: Element,
310+
carousel: Element
311+
): Element | null {
312+
let current = element
313+
while (
314+
current &&
315+
!current.contains(carousel) &&
316+
current !== document.body
317+
) {
318+
// Stop at these elements
319+
if (
320+
current.tagName === 'P' ||
321+
current.tagName === 'DIV' ||
322+
current.classList.contains('carousel-container')
323+
) {
324+
return current
325+
}
326+
current = current.parentElement!
327+
}
328+
return element
329+
}
330+
331+
// Helper to find the section containing a carousel
332+
function findSectionForCarousel(carousel: Element): Element | null {
333+
// Look for containing section, article, or main element
334+
let section = carousel.closest(
335+
'section, article, main, div.markdown-content'
336+
)
337+
if (!section) {
338+
// Fallback to parent element
339+
section = carousel.parentElement
340+
}
341+
return section
342+
}
343+
344+
// Initialize all carousels when DOM is loaded
345+
document.addEventListener('DOMContentLoaded', () => {
346+
initImageCarousel()
347+
})

0 commit comments

Comments
 (0)