mtrl 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.typedocignore +11 -0
- package/DOCS.md +153 -0
- package/index.ts +18 -3
- package/package.json +7 -2
- package/src/components/badge/_styles.scss +174 -0
- package/src/components/badge/api.ts +292 -0
- package/src/components/badge/badge.ts +52 -0
- package/src/components/badge/config.ts +68 -0
- package/src/components/badge/constants.ts +30 -0
- package/src/components/badge/features.ts +185 -0
- package/src/components/badge/index.ts +4 -0
- package/src/components/badge/types.ts +105 -0
- package/src/components/button/types.ts +174 -29
- package/src/components/carousel/_styles.scss +645 -0
- package/src/components/carousel/api.ts +147 -0
- package/src/components/carousel/carousel.ts +178 -0
- package/src/components/carousel/config.ts +91 -0
- package/src/components/carousel/constants.ts +95 -0
- package/src/components/carousel/features/drag.ts +388 -0
- package/src/components/carousel/features/index.ts +8 -0
- package/src/components/carousel/features/slides.ts +682 -0
- package/src/components/carousel/index.ts +38 -0
- package/src/components/carousel/types.ts +327 -0
- package/src/components/dialog/_styles.scss +213 -0
- package/src/components/dialog/api.ts +283 -0
- package/src/components/dialog/config.ts +113 -0
- package/src/components/dialog/constants.ts +32 -0
- package/src/components/dialog/dialog.ts +56 -0
- package/src/components/dialog/features.ts +713 -0
- package/src/components/dialog/index.ts +15 -0
- package/src/components/dialog/types.ts +221 -0
- package/src/components/progress/_styles.scss +13 -1
- package/src/components/progress/api.ts +2 -2
- package/src/components/progress/progress.ts +2 -2
- package/src/components/progress/types.ts +3 -0
- package/src/components/radios/_styles.scss +232 -0
- package/src/components/radios/api.ts +100 -0
- package/src/components/radios/config.ts +60 -0
- package/src/components/radios/constants.ts +28 -0
- package/src/components/radios/index.ts +4 -0
- package/src/components/radios/radio.ts +269 -0
- package/src/components/radios/radios.ts +42 -0
- package/src/components/radios/types.ts +232 -0
- package/src/components/sheet/_styles.scss +236 -0
- package/src/components/sheet/api.ts +96 -0
- package/src/components/sheet/config.ts +66 -0
- package/src/components/sheet/constants.ts +20 -0
- package/src/components/sheet/features/content.ts +51 -0
- package/src/components/sheet/features/gestures.ts +177 -0
- package/src/components/sheet/features/index.ts +6 -0
- package/src/components/sheet/features/position.ts +42 -0
- package/src/components/sheet/features/state.ts +116 -0
- package/src/components/sheet/features/title.ts +86 -0
- package/src/components/sheet/index.ts +4 -0
- package/src/components/sheet/sheet.ts +57 -0
- package/src/components/sheet/types.ts +266 -0
- package/src/components/slider/_styles.scss +518 -0
- package/src/components/slider/api.ts +336 -0
- package/src/components/slider/config.ts +145 -0
- package/src/components/slider/constants.ts +28 -0
- package/src/components/slider/features/appearance.ts +140 -0
- package/src/components/slider/features/disabled.ts +43 -0
- package/src/components/slider/features/events.ts +164 -0
- package/src/components/slider/features/index.ts +5 -0
- package/src/components/slider/features/interactions.ts +256 -0
- package/src/components/slider/features/keyboard.ts +114 -0
- package/src/components/slider/features/slider.ts +336 -0
- package/src/components/slider/features/structure.ts +264 -0
- package/src/components/slider/features/ui.ts +518 -0
- package/src/components/slider/index.ts +9 -0
- package/src/components/slider/slider.ts +58 -0
- package/src/components/slider/types.ts +166 -0
- package/src/components/tabs/_styles.scss +224 -0
- package/src/components/tabs/api.ts +443 -0
- package/src/components/tabs/config.ts +80 -0
- package/src/components/tabs/constants.ts +12 -0
- package/src/components/tabs/index.ts +4 -0
- package/src/components/tabs/tabs.ts +52 -0
- package/src/components/tabs/types.ts +247 -0
- package/src/components/textfield/_styles.scss +97 -4
- package/src/components/tooltip/_styles.scss +241 -0
- package/src/components/tooltip/api.ts +411 -0
- package/src/components/tooltip/config.ts +78 -0
- package/src/components/tooltip/constants.ts +27 -0
- package/src/components/tooltip/index.ts +4 -0
- package/src/components/tooltip/tooltip.ts +60 -0
- package/src/components/tooltip/types.ts +178 -0
- package/src/core/build/_ripple.scss +79 -0
- package/src/core/build/constants.ts +48 -0
- package/src/core/build/icon.ts +137 -0
- package/src/core/build/ripple.ts +216 -0
- package/src/core/build/text.ts +91 -0
- package/src/index.ts +9 -1
- package/src/styles/abstract/_variables.scss +24 -12
- package/tsconfig.json +22 -0
- package/typedoc.json +28 -0
- package/typedoc.simple.json +14 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
// src/components/carousel/features/slides.ts
|
|
2
|
+
import { CAROUSEL_EVENTS, CAROUSEL_LAYOUTS, CAROUSEL_DEFAULTS, CAROUSEL_ITEM_SIZES } from '../constants';
|
|
3
|
+
import { CarouselConfig, CarouselSlide, CarouselLayout } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adds slide functionality to the carousel component with Material Design 3 layout types support
|
|
7
|
+
*
|
|
8
|
+
* @param {CarouselConfig} config - Carousel configuration
|
|
9
|
+
* @returns {Function} Higher-order function that adds slides feature
|
|
10
|
+
*/
|
|
11
|
+
export const withSlides = (config: CarouselConfig) => (component) => {
|
|
12
|
+
// Create the enhanced component to avoid circular references
|
|
13
|
+
const enhancedComponent = { ...component };
|
|
14
|
+
|
|
15
|
+
// Layout type from config (with default)
|
|
16
|
+
const layout: CarouselLayout = config.layout || CAROUSEL_DEFAULTS.LAYOUT;
|
|
17
|
+
|
|
18
|
+
// Create a wrapper for slides and "Show all" link
|
|
19
|
+
const createSlidesWrapper = () => {
|
|
20
|
+
const wrapper = document.createElement('div');
|
|
21
|
+
wrapper.className = `${component.getClass('carousel')}-wrapper`;
|
|
22
|
+
|
|
23
|
+
// Add a data attribute for the layout type
|
|
24
|
+
wrapper.dataset.layout = layout;
|
|
25
|
+
component.element.appendChild(wrapper);
|
|
26
|
+
return wrapper;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Container for all slides
|
|
30
|
+
const slidesContainer = document.createElement('div');
|
|
31
|
+
slidesContainer.className = `${component.getClass('carousel')}-slides`;
|
|
32
|
+
slidesContainer.setAttribute('role', 'list');
|
|
33
|
+
slidesContainer.setAttribute('aria-label', 'Carousel Slides');
|
|
34
|
+
|
|
35
|
+
// Set scroll behavior
|
|
36
|
+
if (config.scrollBehavior === 'snap') {
|
|
37
|
+
slidesContainer.dataset.snapScroll = 'true';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Set gap from config
|
|
41
|
+
slidesContainer.dataset.gap = `${config.gap || CAROUSEL_DEFAULTS.GAP}`;
|
|
42
|
+
|
|
43
|
+
// Create wrapper and add slides container first
|
|
44
|
+
const wrapper = createSlidesWrapper();
|
|
45
|
+
wrapper.appendChild(slidesContainer);
|
|
46
|
+
|
|
47
|
+
// Current slide index
|
|
48
|
+
let currentSlide = config.initialSlide || 0;
|
|
49
|
+
let slideCount = 0;
|
|
50
|
+
const slides: HTMLElement[] = [];
|
|
51
|
+
const slideData: CarouselSlide[] = [];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Determine the size class for a slide based on layout and position
|
|
55
|
+
*
|
|
56
|
+
* @param {number} index - Slide index
|
|
57
|
+
* @returns {'large' | 'medium' | 'small'} Size class
|
|
58
|
+
*/
|
|
59
|
+
function determineSlideSize(index: number): 'large' | 'medium' | 'small' {
|
|
60
|
+
// If the slide has a predefined size, use it
|
|
61
|
+
if (slideData[index]?.size) {
|
|
62
|
+
return slideData[index].size as 'large' | 'medium' | 'small';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Default sizing logic based on layout type
|
|
66
|
+
switch (layout) {
|
|
67
|
+
case 'multi-browse':
|
|
68
|
+
// First 2 slides are large, next is medium, rest are small
|
|
69
|
+
if (index === 0 || index === 1) return 'large';
|
|
70
|
+
if (index === 2) return 'medium';
|
|
71
|
+
return 'small';
|
|
72
|
+
|
|
73
|
+
case 'uncontained':
|
|
74
|
+
// All slides are the same size in uncontained layout
|
|
75
|
+
return 'large';
|
|
76
|
+
|
|
77
|
+
case 'hero':
|
|
78
|
+
// First slide is large, rest are small
|
|
79
|
+
return index === 0 ? 'large' : 'small';
|
|
80
|
+
|
|
81
|
+
case 'full-screen':
|
|
82
|
+
// All slides take full width in full-screen layout
|
|
83
|
+
return 'large';
|
|
84
|
+
|
|
85
|
+
default:
|
|
86
|
+
return 'large';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Update slide sizes based on layout, window size, and position
|
|
92
|
+
* This implements the responsive behavior where sizes adjust as window changes
|
|
93
|
+
*/
|
|
94
|
+
function updateSlideSizes() {
|
|
95
|
+
// Get the container width to calculate relative sizes
|
|
96
|
+
const containerWidth = slidesContainer.clientWidth;
|
|
97
|
+
|
|
98
|
+
// Default to the standard sizes from constants
|
|
99
|
+
let largeWidth = typeof CAROUSEL_DEFAULTS.ITEM_WIDTHS[layout].LARGE === 'number'
|
|
100
|
+
? CAROUSEL_DEFAULTS.ITEM_WIDTHS[layout].LARGE as number
|
|
101
|
+
: containerWidth;
|
|
102
|
+
|
|
103
|
+
let mediumWidth = typeof CAROUSEL_DEFAULTS.ITEM_WIDTHS[layout].MEDIUM === 'number'
|
|
104
|
+
? CAROUSEL_DEFAULTS.ITEM_WIDTHS[layout].MEDIUM as number
|
|
105
|
+
: containerWidth * 0.75;
|
|
106
|
+
|
|
107
|
+
let smallWidth = typeof CAROUSEL_DEFAULTS.ITEM_WIDTHS[layout].SMALL === 'number'
|
|
108
|
+
? CAROUSEL_DEFAULTS.ITEM_WIDTHS[layout].SMALL as number
|
|
109
|
+
: CAROUSEL_DEFAULTS.SMALL_ITEM_WIDTH;
|
|
110
|
+
|
|
111
|
+
// Apply custom large item max width if specified
|
|
112
|
+
if (config.largeItemMaxWidth && typeof config.largeItemMaxWidth === 'number') {
|
|
113
|
+
largeWidth = Math.min(largeWidth, config.largeItemMaxWidth);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Apply custom small item width if specified
|
|
117
|
+
if (config.smallItemWidth && typeof config.smallItemWidth === 'number') {
|
|
118
|
+
smallWidth = config.smallItemWidth;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Adjust for full-screen layout
|
|
122
|
+
if (layout === 'full-screen') {
|
|
123
|
+
largeWidth = containerWidth;
|
|
124
|
+
mediumWidth = containerWidth;
|
|
125
|
+
smallWidth = containerWidth;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Apply sizes to all slides
|
|
129
|
+
slides.forEach((slideEl, index) => {
|
|
130
|
+
const size = determineSlideSize(index);
|
|
131
|
+
|
|
132
|
+
// Set width based on size class
|
|
133
|
+
switch(size) {
|
|
134
|
+
case 'large':
|
|
135
|
+
slideEl.style.width = `${largeWidth}px`;
|
|
136
|
+
break;
|
|
137
|
+
case 'medium':
|
|
138
|
+
slideEl.style.width = `${mediumWidth}px`;
|
|
139
|
+
break;
|
|
140
|
+
case 'small':
|
|
141
|
+
slideEl.style.width = `${smallWidth}px`;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Add size class for styling
|
|
146
|
+
slideEl.className = `${component.getClass('carousel')}-slide ${component.getClass('carousel')}-slide--${size}`;
|
|
147
|
+
|
|
148
|
+
// Handle text visibility based on size
|
|
149
|
+
const titleEl = slideEl.querySelector(`.${component.getClass('carousel')}-slide-title`);
|
|
150
|
+
const descEl = slideEl.querySelector(`.${component.getClass('carousel')}-slide-description`);
|
|
151
|
+
|
|
152
|
+
if (titleEl) {
|
|
153
|
+
if (size === 'small' && layout !== 'uncontained') {
|
|
154
|
+
titleEl.classList.add('visually-hidden');
|
|
155
|
+
} else {
|
|
156
|
+
titleEl.classList.remove('visually-hidden');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (descEl) {
|
|
161
|
+
if (size === 'small' || (size === 'medium' && layout === 'multi-browse')) {
|
|
162
|
+
descEl.classList.add('visually-hidden');
|
|
163
|
+
} else {
|
|
164
|
+
descEl.classList.remove('visually-hidden');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create a new slide element from CarouselSlide data
|
|
172
|
+
*
|
|
173
|
+
* @param {CarouselSlide} slide - Slide data
|
|
174
|
+
* @param {number} index - Optional position to insert at
|
|
175
|
+
* @returns {HTMLElement} Created slide element
|
|
176
|
+
*/
|
|
177
|
+
function addSlideElement(slide: CarouselSlide, index?: number): HTMLElement {
|
|
178
|
+
const slideElement = document.createElement('div');
|
|
179
|
+
slideElement.className = `${component.getClass('carousel')}-slide`;
|
|
180
|
+
slideElement.setAttribute('role', 'listitem');
|
|
181
|
+
slideElement.setAttribute('aria-roledescription', 'slide');
|
|
182
|
+
|
|
183
|
+
// Set border radius from config
|
|
184
|
+
slideElement.dataset.borderRadius = `${config.borderRadius || CAROUSEL_DEFAULTS.BORDER_RADIUS}`;
|
|
185
|
+
|
|
186
|
+
// Create image container
|
|
187
|
+
const imageContainer = document.createElement('div');
|
|
188
|
+
imageContainer.className = `${component.getClass('carousel')}-slide-image`;
|
|
189
|
+
|
|
190
|
+
// Create image element
|
|
191
|
+
const image = document.createElement('img');
|
|
192
|
+
image.src = slide.image;
|
|
193
|
+
image.alt = slide.title || 'Carousel slide';
|
|
194
|
+
imageContainer.appendChild(image);
|
|
195
|
+
|
|
196
|
+
// Add overlay with accent color if provided
|
|
197
|
+
if (slide.accent) {
|
|
198
|
+
const overlay = document.createElement('div');
|
|
199
|
+
overlay.className = `${component.getClass('carousel')}-slide-overlay`;
|
|
200
|
+
if (slide.accent) {
|
|
201
|
+
overlay.style.backgroundColor = slide.accent;
|
|
202
|
+
}
|
|
203
|
+
imageContainer.appendChild(overlay);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Create content container
|
|
207
|
+
const contentContainer = document.createElement('div');
|
|
208
|
+
contentContainer.className = `${component.getClass('carousel')}-slide-content`;
|
|
209
|
+
|
|
210
|
+
// Add title if provided
|
|
211
|
+
if (slide.title) {
|
|
212
|
+
const title = document.createElement('div');
|
|
213
|
+
title.className = `${component.getClass('carousel')}-slide-title`;
|
|
214
|
+
title.textContent = slide.title;
|
|
215
|
+
|
|
216
|
+
// For full-screen layout, add title to content container
|
|
217
|
+
// For others, add directly to image container for overlay effect
|
|
218
|
+
if (layout === 'full-screen') {
|
|
219
|
+
contentContainer.appendChild(title);
|
|
220
|
+
} else {
|
|
221
|
+
imageContainer.appendChild(title);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Add description if provided
|
|
226
|
+
if (slide.description) {
|
|
227
|
+
const description = document.createElement('div');
|
|
228
|
+
description.className = `${component.getClass('carousel')}-slide-description`;
|
|
229
|
+
description.textContent = slide.description;
|
|
230
|
+
contentContainer.appendChild(description);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Add button if provided
|
|
234
|
+
if (slide.buttonText) {
|
|
235
|
+
const button = document.createElement('a');
|
|
236
|
+
button.className = `${component.getClass('carousel')}-slide-button`;
|
|
237
|
+
button.textContent = slide.buttonText;
|
|
238
|
+
|
|
239
|
+
if (slide.buttonUrl) {
|
|
240
|
+
button.href = slide.buttonUrl;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (slide.accent) {
|
|
244
|
+
button.style.backgroundColor = slide.accent;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
contentContainer.appendChild(button);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
slideElement.appendChild(imageContainer);
|
|
251
|
+
slideElement.appendChild(contentContainer);
|
|
252
|
+
|
|
253
|
+
// Store the slide data
|
|
254
|
+
if (index !== undefined && index >= 0 && index <= slideData.length) {
|
|
255
|
+
slideData.splice(index, 0, slide);
|
|
256
|
+
} else {
|
|
257
|
+
slideData.push(slide);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Insert at specific position or append
|
|
261
|
+
if (index !== undefined && index >= 0 && index <= slides.length) {
|
|
262
|
+
if (index < slides.length) {
|
|
263
|
+
slidesContainer.insertBefore(slideElement, slides[index]);
|
|
264
|
+
slides.splice(index, 0, slideElement);
|
|
265
|
+
} else {
|
|
266
|
+
slidesContainer.appendChild(slideElement);
|
|
267
|
+
slides.push(slideElement);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
slidesContainer.appendChild(slideElement);
|
|
271
|
+
slides.push(slideElement);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
slideCount++;
|
|
275
|
+
|
|
276
|
+
// Update sizes after adding a new slide
|
|
277
|
+
updateSlideSizes();
|
|
278
|
+
|
|
279
|
+
return slideElement;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Add "Show all" link after the slides
|
|
284
|
+
* Material Design 3 recommends this for accessibility on vertical scrolling pages
|
|
285
|
+
*/
|
|
286
|
+
const addShowAllLink = () => {
|
|
287
|
+
if (config.showAllLink === false) return;
|
|
288
|
+
|
|
289
|
+
const showAllCard = document.createElement('div');
|
|
290
|
+
showAllCard.className = `${component.getClass('carousel')}-show-all`;
|
|
291
|
+
showAllCard.setAttribute('role', 'button');
|
|
292
|
+
showAllCard.setAttribute('tabindex', '0');
|
|
293
|
+
showAllCard.setAttribute('aria-label', 'Show all items');
|
|
294
|
+
|
|
295
|
+
const showAllText = document.createElement('span');
|
|
296
|
+
showAllText.textContent = 'Show all';
|
|
297
|
+
|
|
298
|
+
showAllCard.appendChild(showAllText);
|
|
299
|
+
wrapper.appendChild(showAllCard); // Add to wrapper after slides
|
|
300
|
+
|
|
301
|
+
showAllCard.addEventListener('click', () => {
|
|
302
|
+
if (config.onShowAll && typeof config.onShowAll === 'function') {
|
|
303
|
+
config.onShowAll();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Add keyboard support
|
|
308
|
+
showAllCard.addEventListener('keydown', (event) => {
|
|
309
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
310
|
+
if (config.onShowAll && typeof config.onShowAll === 'function') {
|
|
311
|
+
config.onShowAll();
|
|
312
|
+
}
|
|
313
|
+
event.preventDefault();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Add initial slides if provided
|
|
319
|
+
if (config.slides && config.slides.length > 0) {
|
|
320
|
+
config.slides.forEach((slide) => {
|
|
321
|
+
addSlideElement(slide);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Add "Show all" link after all slides are added
|
|
326
|
+
// This is recommended by MD3 for accessibility on vertical scrolling pages
|
|
327
|
+
if (config.showAllLink !== false && layout !== 'full-screen') {
|
|
328
|
+
addShowAllLink();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Handle window resize events for responsive design
|
|
332
|
+
const handleResize = () => {
|
|
333
|
+
updateSlideSizes();
|
|
334
|
+
|
|
335
|
+
// Dispatch resize event
|
|
336
|
+
const resizeEvent = new CustomEvent(CAROUSEL_EVENTS.RESIZE, {
|
|
337
|
+
detail: { width: slidesContainer.clientWidth }
|
|
338
|
+
});
|
|
339
|
+
component.element.dispatchEvent(resizeEvent);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// Add resize listener
|
|
343
|
+
window.addEventListener('resize', handleResize);
|
|
344
|
+
|
|
345
|
+
// Run initial size update
|
|
346
|
+
setTimeout(updateSlideSizes, 0);
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Navigate to a specific slide
|
|
350
|
+
*
|
|
351
|
+
* @param {number} index - Target slide index
|
|
352
|
+
* @returns {Object} Enhanced component for chaining
|
|
353
|
+
*/
|
|
354
|
+
const goTo = (index: number) => {
|
|
355
|
+
// Validate index
|
|
356
|
+
if (index < 0 || index >= slideCount) {
|
|
357
|
+
if (config.loop) {
|
|
358
|
+
// Handle looping
|
|
359
|
+
if (index < 0) {
|
|
360
|
+
index = slideCount - 1;
|
|
361
|
+
} else {
|
|
362
|
+
index = 0;
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
// Don't change if out of bounds and not looping
|
|
366
|
+
return enhancedComponent;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Only trigger events if the slide actually changes
|
|
371
|
+
if (currentSlide !== index) {
|
|
372
|
+
// Dispatch event before changing
|
|
373
|
+
const beforeEvent = new CustomEvent(CAROUSEL_EVENTS.SLIDE_CHANGE, {
|
|
374
|
+
detail: { previousSlide: currentSlide, nextSlide: index }
|
|
375
|
+
});
|
|
376
|
+
component.element.dispatchEvent(beforeEvent);
|
|
377
|
+
|
|
378
|
+
// Update current slide
|
|
379
|
+
currentSlide = index;
|
|
380
|
+
|
|
381
|
+
// Apply active state to current slide
|
|
382
|
+
slides.forEach((slideEl, i) => {
|
|
383
|
+
if (i === currentSlide) {
|
|
384
|
+
slideEl.classList.add(`${component.getClass('carousel')}-slide--active`);
|
|
385
|
+
slideEl.setAttribute('aria-current', 'true');
|
|
386
|
+
} else {
|
|
387
|
+
slideEl.classList.remove(`${component.getClass('carousel')}-slide--active`);
|
|
388
|
+
slideEl.removeAttribute('aria-current');
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Scroll behavior based on config
|
|
393
|
+
const behavior = config.scrollBehavior === 'snap' ? 'smooth' : 'auto';
|
|
394
|
+
|
|
395
|
+
// Scroll to the slide
|
|
396
|
+
if (slides[index]) {
|
|
397
|
+
// For center-aligned hero layout, adjust scroll position
|
|
398
|
+
if (layout === 'hero' && config.centered) {
|
|
399
|
+
const slideWidth = slides[index].offsetWidth;
|
|
400
|
+
const containerWidth = slidesContainer.clientWidth;
|
|
401
|
+
const offsetLeft = slides[index].offsetLeft;
|
|
402
|
+
|
|
403
|
+
slidesContainer.scrollTo({
|
|
404
|
+
left: offsetLeft - (containerWidth / 2) + (slideWidth / 2),
|
|
405
|
+
behavior
|
|
406
|
+
});
|
|
407
|
+
} else {
|
|
408
|
+
// Standard scrolling
|
|
409
|
+
slides[index].scrollIntoView({
|
|
410
|
+
behavior,
|
|
411
|
+
block: 'nearest',
|
|
412
|
+
inline: 'start'
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Dispatch event after changing
|
|
418
|
+
const afterEvent = new CustomEvent(CAROUSEL_EVENTS.SLIDE_CHANGED, {
|
|
419
|
+
detail: { currentSlide }
|
|
420
|
+
});
|
|
421
|
+
component.element.dispatchEvent(afterEvent);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return enhancedComponent;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// Define next and prev functions that use goTo
|
|
428
|
+
const next = () => goTo(currentSlide + 1);
|
|
429
|
+
const prev = () => goTo(currentSlide - 1);
|
|
430
|
+
|
|
431
|
+
// Assign properties to the enhanced component
|
|
432
|
+
Object.assign(enhancedComponent, {
|
|
433
|
+
slides: {
|
|
434
|
+
addSlide: (slide: CarouselSlide, index?: number) => {
|
|
435
|
+
addSlideElement(slide, index);
|
|
436
|
+
return enhancedComponent.slides;
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
removeSlide: (index: number) => {
|
|
440
|
+
if (index >= 0 && index < slides.length) {
|
|
441
|
+
slidesContainer.removeChild(slides[index]);
|
|
442
|
+
slides.splice(index, 1);
|
|
443
|
+
slideData.splice(index, 1);
|
|
444
|
+
slideCount--;
|
|
445
|
+
|
|
446
|
+
// Adjust current slide index if needed
|
|
447
|
+
if (currentSlide >= slideCount) {
|
|
448
|
+
currentSlide = slideCount - 1;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (currentSlide < 0) {
|
|
452
|
+
currentSlide = 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Update sizes after removing a slide
|
|
456
|
+
updateSlideSizes();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return enhancedComponent.slides;
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
updateSlide: (index: number, slide: CarouselSlide) => {
|
|
463
|
+
if (index >= 0 && index < slides.length) {
|
|
464
|
+
// Update slide data
|
|
465
|
+
slideData[index] = slide;
|
|
466
|
+
|
|
467
|
+
// Update visual elements
|
|
468
|
+
const slideElement = slides[index];
|
|
469
|
+
|
|
470
|
+
// Update image
|
|
471
|
+
const imageElement = slideElement.querySelector('img');
|
|
472
|
+
if (imageElement) {
|
|
473
|
+
imageElement.src = slide.image;
|
|
474
|
+
imageElement.alt = slide.title || 'Carousel slide';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Update title
|
|
478
|
+
const titleElement = slideElement.querySelector(`.${component.getClass('carousel')}-slide-title`);
|
|
479
|
+
if (titleElement && slide.title) {
|
|
480
|
+
titleElement.textContent = slide.title;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Update description
|
|
484
|
+
const descriptionElement = slideElement.querySelector(`.${component.getClass('carousel')}-slide-description`);
|
|
485
|
+
if (descriptionElement && slide.description) {
|
|
486
|
+
descriptionElement.textContent = slide.description;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Update accent color
|
|
490
|
+
const overlayElement = slideElement.querySelector(`.${component.getClass('carousel')}-slide-overlay`);
|
|
491
|
+
if (overlayElement && slide.accent) {
|
|
492
|
+
overlayElement.style.backgroundColor = slide.accent;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Update button
|
|
496
|
+
const buttonElement = slideElement.querySelector(`.${component.getClass('carousel')}-slide-button`);
|
|
497
|
+
if (buttonElement) {
|
|
498
|
+
if (slide.buttonText) {
|
|
499
|
+
buttonElement.textContent = slide.buttonText;
|
|
500
|
+
|
|
501
|
+
if (slide.buttonUrl) {
|
|
502
|
+
(buttonElement as HTMLAnchorElement).href = slide.buttonUrl;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (slide.accent) {
|
|
506
|
+
buttonElement.style.backgroundColor = slide.accent;
|
|
507
|
+
}
|
|
508
|
+
} else {
|
|
509
|
+
// Remove button if no text provided
|
|
510
|
+
buttonElement.parentElement?.removeChild(buttonElement);
|
|
511
|
+
}
|
|
512
|
+
} else if (slide.buttonText) {
|
|
513
|
+
// Add button if not present but now needed
|
|
514
|
+
const contentContainer = slideElement.querySelector(`.${component.getClass('carousel')}-slide-content`);
|
|
515
|
+
if (contentContainer) {
|
|
516
|
+
const button = document.createElement('a');
|
|
517
|
+
button.className = `${component.getClass('carousel')}-slide-button`;
|
|
518
|
+
button.textContent = slide.buttonText;
|
|
519
|
+
|
|
520
|
+
if (slide.buttonUrl) {
|
|
521
|
+
button.href = slide.buttonUrl;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (slide.accent) {
|
|
525
|
+
button.style.backgroundColor = slide.accent;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
contentContainer.appendChild(button);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Update sizes if size property changed
|
|
533
|
+
if (slide.size) {
|
|
534
|
+
updateSlideSizes();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return enhancedComponent.slides;
|
|
539
|
+
},
|
|
540
|
+
|
|
541
|
+
getSlide: (index: number) => {
|
|
542
|
+
if (index >= 0 && index < slideData.length) {
|
|
543
|
+
return slideData[index];
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
getCount: () => slideCount,
|
|
549
|
+
|
|
550
|
+
getElements: () => [...slides]
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
slidesContainer,
|
|
554
|
+
slideData,
|
|
555
|
+
wrapper,
|
|
556
|
+
|
|
557
|
+
getCurrentSlide: () => currentSlide,
|
|
558
|
+
|
|
559
|
+
goTo,
|
|
560
|
+
next,
|
|
561
|
+
prev,
|
|
562
|
+
|
|
563
|
+
enableLoop: () => {
|
|
564
|
+
config.loop = true;
|
|
565
|
+
return enhancedComponent;
|
|
566
|
+
},
|
|
567
|
+
|
|
568
|
+
disableLoop: () => {
|
|
569
|
+
config.loop = false;
|
|
570
|
+
return enhancedComponent;
|
|
571
|
+
},
|
|
572
|
+
|
|
573
|
+
setBorderRadius: (radius: number) => {
|
|
574
|
+
config.borderRadius = radius;
|
|
575
|
+
slides.forEach(slide => {
|
|
576
|
+
slide.dataset.borderRadius = `${radius}`;
|
|
577
|
+
});
|
|
578
|
+
return enhancedComponent;
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
setGap: (gap: number) => {
|
|
582
|
+
config.gap = gap;
|
|
583
|
+
slidesContainer.dataset.gap = `${gap}`;
|
|
584
|
+
return enhancedComponent;
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
updateLayout: (newLayout: CarouselLayout) => {
|
|
588
|
+
// Update layout setting
|
|
589
|
+
config.layout = newLayout;
|
|
590
|
+
|
|
591
|
+
// Update data attributes
|
|
592
|
+
wrapper.dataset.layout = newLayout;
|
|
593
|
+
component.element.dataset.layout = newLayout;
|
|
594
|
+
|
|
595
|
+
// Remove previous layout classes
|
|
596
|
+
Object.values(CAROUSEL_LAYOUTS).forEach(layoutValue => {
|
|
597
|
+
component.element.classList.remove(`${component.getClass('carousel')}-layout--${layoutValue}`);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// Add new layout class
|
|
601
|
+
component.element.classList.add(`${component.getClass('carousel')}-layout--${newLayout}`);
|
|
602
|
+
|
|
603
|
+
// Update sizes based on new layout
|
|
604
|
+
updateSlideSizes();
|
|
605
|
+
|
|
606
|
+
return enhancedComponent;
|
|
607
|
+
},
|
|
608
|
+
|
|
609
|
+
// Add lifecycle cleanup for resize handler
|
|
610
|
+
lifecycle: {
|
|
611
|
+
...(component.lifecycle || {}),
|
|
612
|
+
destroy: () => {
|
|
613
|
+
// Remove resize event listener
|
|
614
|
+
window.removeEventListener('resize', handleResize);
|
|
615
|
+
|
|
616
|
+
// Call original destroy if it exists
|
|
617
|
+
if (component.lifecycle && component.lifecycle.destroy) {
|
|
618
|
+
component.lifecycle.destroy();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// Add necessary class for styling based on transition type
|
|
625
|
+
if (config.transition) {
|
|
626
|
+
component.element.classList.add(`${component.getClass('carousel')}-transition--${config.transition}`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Add scroll event listeners for detecting current slide when using snap-scroll
|
|
630
|
+
if (config.scrollBehavior === 'snap') {
|
|
631
|
+
slidesContainer.addEventListener('scroll', () => {
|
|
632
|
+
// Debounce the scroll event
|
|
633
|
+
clearTimeout(enhancedComponent['scrollTimeout']);
|
|
634
|
+
|
|
635
|
+
enhancedComponent['scrollTimeout'] = setTimeout(() => {
|
|
636
|
+
// Find the slide most in view
|
|
637
|
+
let closestSlide = 0;
|
|
638
|
+
let closestDistance = Infinity;
|
|
639
|
+
|
|
640
|
+
const containerLeft = slidesContainer.scrollLeft;
|
|
641
|
+
const containerWidth = slidesContainer.clientWidth;
|
|
642
|
+
|
|
643
|
+
slides.forEach((slide, index) => {
|
|
644
|
+
const slideLeft = slide.offsetLeft;
|
|
645
|
+
const slideCenter = slideLeft + (slide.offsetWidth / 2);
|
|
646
|
+
const containerCenter = containerLeft + (containerWidth / 2);
|
|
647
|
+
|
|
648
|
+
const distance = Math.abs(slideCenter - containerCenter);
|
|
649
|
+
|
|
650
|
+
if (distance < closestDistance) {
|
|
651
|
+
closestDistance = distance;
|
|
652
|
+
closestSlide = index;
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Only update if the slide has changed
|
|
657
|
+
if (closestSlide !== currentSlide) {
|
|
658
|
+
currentSlide = closestSlide;
|
|
659
|
+
|
|
660
|
+
// Update active class
|
|
661
|
+
slides.forEach((slideEl, i) => {
|
|
662
|
+
if (i === currentSlide) {
|
|
663
|
+
slideEl.classList.add(`${component.getClass('carousel')}-slide--active`);
|
|
664
|
+
slideEl.setAttribute('aria-current', 'true');
|
|
665
|
+
} else {
|
|
666
|
+
slideEl.classList.remove(`${component.getClass('carousel')}-slide--active`);
|
|
667
|
+
slideEl.removeAttribute('aria-current');
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Dispatch event
|
|
672
|
+
const event = new CustomEvent(CAROUSEL_EVENTS.SLIDE_CHANGED, {
|
|
673
|
+
detail: { currentSlide }
|
|
674
|
+
});
|
|
675
|
+
component.element.dispatchEvent(event);
|
|
676
|
+
}
|
|
677
|
+
}, 150); // Debounce time
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return enhancedComponent;
|
|
682
|
+
};
|