@orangesk/orange-design-system 2.0.0-beta.3 → 2.0.0-beta.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/build/components/Accordion/tsconfig.tsbuildinfo +1 -1
- package/build/components/Alert/tsconfig.tsbuildinfo +1 -1
- package/build/components/AnchorNavigation/tsconfig.tsbuildinfo +1 -1
- package/build/components/Bar/tsconfig.tsbuildinfo +1 -1
- package/build/components/BlockAction/tsconfig.tsbuildinfo +1 -1
- package/build/components/BodyBanner/tsconfig.tsbuildinfo +1 -1
- package/build/components/Breadcrumbs/tsconfig.tsbuildinfo +1 -1
- package/build/components/Button/tsconfig.tsbuildinfo +1 -1
- package/build/components/Buttons/tsconfig.tsbuildinfo +1 -1
- package/build/components/Card/tsconfig.tsbuildinfo +1 -1
- package/build/components/Carousel/tsconfig.tsbuildinfo +1 -1
- package/build/components/CarouselHero/index.js +16 -0
- package/build/components/CarouselHero/index.js.map +1 -0
- package/build/components/CarouselHero/tsconfig.tsbuildinfo +1 -0
- package/build/components/CarouselPromotions/tsconfig.tsbuildinfo +1 -1
- package/build/components/CartTable/tsconfig.tsbuildinfo +1 -1
- package/build/components/Code/tsconfig.tsbuildinfo +1 -1
- package/build/components/Container/tsconfig.tsbuildinfo +1 -1
- package/build/components/Controls/tsconfig.tsbuildinfo +1 -1
- package/build/components/Cover/tsconfig.tsbuildinfo +1 -1
- package/build/components/Divider/tsconfig.tsbuildinfo +1 -1
- package/build/components/DocumentationSidebar/index.js +1 -1
- package/build/components/DocumentationSidebar/tsconfig.tsbuildinfo +1 -1
- package/build/components/Dropdown/tsconfig.tsbuildinfo +1 -1
- package/build/components/Expander/tsconfig.tsbuildinfo +1 -1
- package/build/components/FeatureAccordion/tsconfig.tsbuildinfo +1 -1
- package/build/components/Footer/tsconfig.tsbuildinfo +1 -1
- package/build/components/Forms/tsconfig.tsbuildinfo +1 -1
- package/build/components/Gauge/tsconfig.tsbuildinfo +1 -1
- package/build/components/Grid/tsconfig.tsbuildinfo +1 -1
- package/build/components/Hero/tsconfig.tsbuildinfo +1 -1
- package/build/components/Icon/tsconfig.tsbuildinfo +1 -1
- package/build/components/IconList/tsconfig.tsbuildinfo +1 -1
- package/build/components/Image/tsconfig.tsbuildinfo +1 -1
- package/build/components/Link/tsconfig.tsbuildinfo +1 -1
- package/build/components/List/tsconfig.tsbuildinfo +1 -1
- package/build/components/Loader/tsconfig.tsbuildinfo +1 -1
- package/build/components/Megamenu/tsconfig.tsbuildinfo +1 -1
- package/build/components/Modal/tsconfig.tsbuildinfo +1 -1
- package/build/components/Pagination/tsconfig.tsbuildinfo +1 -1
- package/build/components/Pill/tsconfig.tsbuildinfo +1 -1
- package/build/components/Preview/tsconfig.tsbuildinfo +1 -1
- package/build/components/Progress/tsconfig.tsbuildinfo +1 -1
- package/build/components/PromoBanner/tsconfig.tsbuildinfo +1 -1
- package/build/components/PromotionCard/tsconfig.tsbuildinfo +1 -1
- package/build/components/Section/tsconfig.tsbuildinfo +1 -1
- package/build/components/Skeleton/tsconfig.tsbuildinfo +1 -1
- package/build/components/SkipLink/tsconfig.tsbuildinfo +1 -1
- package/build/components/Stepbar/tsconfig.tsbuildinfo +1 -1
- package/build/components/Sticker/tsconfig.tsbuildinfo +1 -1
- package/build/components/Table/tsconfig.tsbuildinfo +1 -1
- package/build/components/Tabs/tsconfig.tsbuildinfo +1 -1
- package/build/components/Tag/tsconfig.tsbuildinfo +1 -1
- package/build/components/Testimonial/tsconfig.tsbuildinfo +1 -1
- package/build/components/Tile/tsconfig.tsbuildinfo +1 -1
- package/build/components/Tooltip/tsconfig.tsbuildinfo +1 -1
- package/build/components/index.js +6 -6
- package/build/components/index.js.map +1 -1
- package/build/components/static.js +4 -4
- package/build/components/static.js.map +1 -1
- package/build/components/tsconfig.tsbuildinfo +1 -1
- package/build/components/types/src/components/CarouselHero/CarouselHero.d.ts +18 -0
- package/build/components/types/src/components/CarouselHero/CarouselHero.static.d.ts +47 -0
- package/build/components/types/src/components/CarouselHero/CarouselHeroItem.d.ts +9 -0
- package/build/components/types/src/components/CarouselHero/constants.d.ts +34 -0
- package/build/components/types/src/components/CarouselHero/index.d.ts +2 -0
- package/build/components/types/src/components/index.d.ts +2 -1
- package/build/components/types/src/scripts/index.d.ts +5 -0
- package/build/lib/components.css +1 -1
- package/build/lib/components.css.map +1 -1
- package/build/lib/scripts.js +4 -4
- package/build/lib/scripts.js.map +1 -1
- package/build/lib/style.css +1 -1
- package/build/lib/style.css.map +1 -1
- package/package.json +1 -1
- package/src/components/CarouselHero/CarouselHero.static.ts +528 -0
- package/src/components/CarouselHero/CarouselHero.tsx +148 -0
- package/src/components/CarouselHero/CarouselHeroItem.tsx +41 -0
- package/src/components/CarouselHero/constants.ts +37 -0
- package/src/components/CarouselHero/index.ts +2 -0
- package/src/components/CarouselHero/styles/config.scss +54 -0
- package/src/components/CarouselHero/styles/mixins.scss +289 -0
- package/src/components/CarouselHero/styles/style.scss +67 -0
- package/src/components/CarouselHero/tests/CarouselHero.conformance.test.js +148 -0
- package/src/components/CarouselHero/tests/CarouselHero.unit.test.js +289 -0
- package/src/components/CarouselHero/tests/CarouselHeroItem.conformance.test.js +142 -0
- package/src/components/CarouselHero/tests/CarouselHeroItem.unit.test.js +210 -0
- package/src/components/Controls/styles/config.scss +2 -2
- package/src/components/index.ts +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import type { SwiperOptions } from "swiper/types";
|
|
2
|
+
import {
|
|
3
|
+
Navigation,
|
|
4
|
+
Pagination,
|
|
5
|
+
A11y,
|
|
6
|
+
Keyboard,
|
|
7
|
+
Autoplay,
|
|
8
|
+
} from "swiper/modules";
|
|
9
|
+
import { Swiper } from "swiper";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
CLASS_SLIDE,
|
|
13
|
+
CLASS_TRACK,
|
|
14
|
+
CLASS_ACTIVE,
|
|
15
|
+
CLASS_PLAYING,
|
|
16
|
+
CLASS_PAUSED,
|
|
17
|
+
SELECTOR_VIEWPORT,
|
|
18
|
+
SELECTOR_PREV,
|
|
19
|
+
SELECTOR_NEXT,
|
|
20
|
+
SELECTOR_PLAY_PAUSE,
|
|
21
|
+
SELECTOR_TABS,
|
|
22
|
+
SELECTOR_TAB,
|
|
23
|
+
SELECTOR_PAGINATION,
|
|
24
|
+
CLASS_PAGINATION_ITEM,
|
|
25
|
+
CLASS_PAGINATION_SVG,
|
|
26
|
+
CLASS_PAGINATION_CIRCLE,
|
|
27
|
+
} from "./constants";
|
|
28
|
+
|
|
29
|
+
export const defaultConfig: SwiperOptions = {
|
|
30
|
+
pagination: {
|
|
31
|
+
clickable: true,
|
|
32
|
+
bulletClass: CLASS_PAGINATION_ITEM,
|
|
33
|
+
bulletActiveClass: "is-active",
|
|
34
|
+
renderBullet: function (index: number, className: string) {
|
|
35
|
+
return `<button type="button" class="${className}" aria-label="Prejsť na snímok ${index + 1}"></button>`;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
slidesPerView: 1,
|
|
39
|
+
loop: false,
|
|
40
|
+
a11y: {
|
|
41
|
+
enabled: true,
|
|
42
|
+
prevSlideMessage: "Predchádzajúci snímok",
|
|
43
|
+
nextSlideMessage: "Nasledujúci snímok",
|
|
44
|
+
containerMessage: "Hero carousel so snímkami",
|
|
45
|
+
containerRoleDescriptionMessage: "carousel",
|
|
46
|
+
itemRoleDescriptionMessage: "snímok",
|
|
47
|
+
firstSlideMessage: "Prvý snímok",
|
|
48
|
+
lastSlideMessage: "Posledný snímok",
|
|
49
|
+
slideLabelMessage: "Snímok",
|
|
50
|
+
},
|
|
51
|
+
wrapperClass: CLASS_TRACK,
|
|
52
|
+
slideClass: CLASS_SLIDE,
|
|
53
|
+
slideActiveClass: CLASS_ACTIVE,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default class CarouselHero {
|
|
57
|
+
element: HTMLElement;
|
|
58
|
+
config: SwiperOptions;
|
|
59
|
+
viewport!: HTMLElement;
|
|
60
|
+
instance!: Swiper;
|
|
61
|
+
tabs: HTMLElement[] = [];
|
|
62
|
+
_dotAnimationJustStarted: boolean = false;
|
|
63
|
+
_isDragging: boolean = false;
|
|
64
|
+
|
|
65
|
+
private _boundTabClick!: (e: Event) => void;
|
|
66
|
+
private _boundPrevClick!: () => void;
|
|
67
|
+
private _boundNextClick!: () => void;
|
|
68
|
+
private _boundPlayPauseClick!: () => void;
|
|
69
|
+
|
|
70
|
+
constructor(element: HTMLElement, config?: Partial<SwiperOptions>) {
|
|
71
|
+
this.element = element;
|
|
72
|
+
this.config = { ...defaultConfig, ...config };
|
|
73
|
+
(this.element as any).ODS_CarouselHero = this;
|
|
74
|
+
this._boundTabClick = this._onTabClick.bind(this);
|
|
75
|
+
this._boundPrevClick = this._onUserNavigation.bind(this);
|
|
76
|
+
this._boundNextClick = this._onUserNavigation.bind(this);
|
|
77
|
+
this._boundPlayPauseClick = this._onPlayPauseClick.bind(this);
|
|
78
|
+
this.init();
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
init() {
|
|
83
|
+
this.getElements();
|
|
84
|
+
this.setupConfig();
|
|
85
|
+
this.createSwiper();
|
|
86
|
+
this.setupEventListeners();
|
|
87
|
+
this.renderPaginationDots();
|
|
88
|
+
this.updateStates();
|
|
89
|
+
this.updatePlayPauseIcon();
|
|
90
|
+
|
|
91
|
+
if (this.hasAutoplay() && this.isAutoplayRunning()) {
|
|
92
|
+
this.startDotAnimation();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
getElements() {
|
|
96
|
+
this.viewport = this.element.querySelector(SELECTOR_VIEWPORT)!;
|
|
97
|
+
const tabsContainer = this.element.querySelector(SELECTOR_TABS);
|
|
98
|
+
if (tabsContainer) {
|
|
99
|
+
this.tabs = Array.from(tabsContainer.querySelectorAll(SELECTOR_TAB));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setupConfig() {
|
|
104
|
+
const interval = this.element.hasAttribute("data-interval")
|
|
105
|
+
? parseInt(this.element.getAttribute("data-interval")!) || 0
|
|
106
|
+
: 0;
|
|
107
|
+
|
|
108
|
+
if (this.element.hasAttribute("data-swiper-options")) {
|
|
109
|
+
try {
|
|
110
|
+
const customOptions = JSON.parse(
|
|
111
|
+
this.element.getAttribute("data-swiper-options")!,
|
|
112
|
+
);
|
|
113
|
+
this.config = { ...this.config, ...customOptions };
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.warn("Invalid swiper options:", error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Enable loop for smoother autoplay behavior
|
|
120
|
+
this.config.loop = true;
|
|
121
|
+
|
|
122
|
+
// Setup autoplay if interval is specified
|
|
123
|
+
if (interval >= 1000) {
|
|
124
|
+
this.config.autoplay = {
|
|
125
|
+
delay: interval,
|
|
126
|
+
disableOnInteraction: false,
|
|
127
|
+
pauseOnMouseEnter: false,
|
|
128
|
+
waitForTransition: true,
|
|
129
|
+
stopOnLastSlide: false,
|
|
130
|
+
} as NonNullable<SwiperOptions["autoplay"]>;
|
|
131
|
+
this.element.classList.add(CLASS_PLAYING);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
createSwiper() {
|
|
136
|
+
const slides = this.viewport.querySelectorAll(`.${CLASS_SLIDE}`);
|
|
137
|
+
const hasPagination = slides.length > 1;
|
|
138
|
+
|
|
139
|
+
this.instance = new Swiper(this.viewport, {
|
|
140
|
+
...this.config,
|
|
141
|
+
modules: [Navigation, Pagination, A11y, Keyboard, Autoplay],
|
|
142
|
+
navigation: {
|
|
143
|
+
nextEl: this.element.querySelector(SELECTOR_NEXT) as HTMLElement,
|
|
144
|
+
prevEl: this.element.querySelector(SELECTOR_PREV) as HTMLElement,
|
|
145
|
+
},
|
|
146
|
+
pagination: hasPagination
|
|
147
|
+
? {
|
|
148
|
+
...(this.config.pagination as object),
|
|
149
|
+
el: this.element.querySelector(SELECTOR_PAGINATION) as HTMLElement,
|
|
150
|
+
}
|
|
151
|
+
: false,
|
|
152
|
+
on: {
|
|
153
|
+
slideChange: () => {
|
|
154
|
+
this.updateStates();
|
|
155
|
+
if (this.isAutoplayRunning()) {
|
|
156
|
+
this.restartDotAnimation();
|
|
157
|
+
} else {
|
|
158
|
+
this.stopDotAnimation();
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
autoplayStart: () => {
|
|
162
|
+
this._onAutoplayStart();
|
|
163
|
+
},
|
|
164
|
+
autoplayStop: () => {
|
|
165
|
+
this._onAutoplayStop();
|
|
166
|
+
},
|
|
167
|
+
touchStart: () => {
|
|
168
|
+
this._isDragging = true;
|
|
169
|
+
this._stopAutoplayIfRunning();
|
|
170
|
+
},
|
|
171
|
+
touchEnd: () => {
|
|
172
|
+
this._isDragging = false;
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
this.updatePlayPauseIcon();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setupEventListeners() {
|
|
181
|
+
this.tabs.forEach((tab) => {
|
|
182
|
+
tab.removeEventListener("click", this._boundTabClick);
|
|
183
|
+
tab.addEventListener("click", this._boundTabClick);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const playPauseButton = this.element.querySelector(SELECTOR_PLAY_PAUSE);
|
|
187
|
+
if (playPauseButton) {
|
|
188
|
+
playPauseButton.removeEventListener("click", this._boundPlayPauseClick);
|
|
189
|
+
playPauseButton.addEventListener("click", this._boundPlayPauseClick);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const prevButton = this.element.querySelector(SELECTOR_PREV);
|
|
193
|
+
const nextButton = this.element.querySelector(SELECTOR_NEXT);
|
|
194
|
+
if (prevButton) {
|
|
195
|
+
prevButton.removeEventListener("click", this._boundPrevClick);
|
|
196
|
+
prevButton.addEventListener("click", this._boundPrevClick);
|
|
197
|
+
}
|
|
198
|
+
if (nextButton) {
|
|
199
|
+
nextButton.removeEventListener("click", this._boundNextClick);
|
|
200
|
+
nextButton.addEventListener("click", this._boundNextClick);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private _onUserNavigation() {
|
|
205
|
+
this._stopAutoplayIfRunning();
|
|
206
|
+
this.stopDotAnimation();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_onTabClick(e: Event) {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
const tab = e.currentTarget as HTMLElement;
|
|
212
|
+
const index = this.tabs.indexOf(tab);
|
|
213
|
+
if (index !== -1 && index !== this.instance?.activeIndex) {
|
|
214
|
+
this._onUserNavigation();
|
|
215
|
+
this.goToSlide(index);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private _onPlayPauseClick() {
|
|
220
|
+
this.toggleAutoplay();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ====== Autoplay helpers ======
|
|
224
|
+
private isAutoplayRunning(): boolean {
|
|
225
|
+
return !!(this.instance?.autoplay && this.instance.autoplay.running);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private _stopAutoplayIfRunning() {
|
|
229
|
+
if (this.isAutoplayRunning()) {
|
|
230
|
+
this.instance.autoplay.stop();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private _onAutoplayStart() {
|
|
235
|
+
this.element.classList.add(CLASS_PLAYING);
|
|
236
|
+
this.element.classList.remove(CLASS_PAUSED);
|
|
237
|
+
this.updatePlayPauseIcon();
|
|
238
|
+
this.startDotAnimation();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private _onAutoplayStop() {
|
|
242
|
+
this.element.classList.remove(CLASS_PLAYING);
|
|
243
|
+
this.element.classList.add(CLASS_PAUSED);
|
|
244
|
+
this.updatePlayPauseIcon();
|
|
245
|
+
this.stopDotAnimation();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
toggleAutoplay() {
|
|
249
|
+
if (!this.instance || !this.instance.autoplay) return;
|
|
250
|
+
|
|
251
|
+
if (this.isAutoplayRunning()) {
|
|
252
|
+
this.instance.autoplay.stop();
|
|
253
|
+
} else {
|
|
254
|
+
// Force Swiper to be on the correct slide before starting autoplay
|
|
255
|
+
const currentRealIndex = this.instance.realIndex;
|
|
256
|
+
|
|
257
|
+
// Ensure we're on the right slide (accounting for loop mode)
|
|
258
|
+
if (
|
|
259
|
+
this.instance.activeIndex !==
|
|
260
|
+
this.instance.slides.length + currentRealIndex
|
|
261
|
+
) {
|
|
262
|
+
this.instance.slideTo(
|
|
263
|
+
this.instance.slides.length + currentRealIndex,
|
|
264
|
+
0,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Small delay to ensure slide position is stable before starting autoplay
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
if (this.instance && this.instance.autoplay) {
|
|
271
|
+
this.instance.autoplay.start();
|
|
272
|
+
}
|
|
273
|
+
}, 10);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
goToSlide(index: number) {
|
|
278
|
+
if (!this.instance || this.instance.activeIndex === index) return;
|
|
279
|
+
this.instance.slideTo(index);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
updateStates() {
|
|
283
|
+
if (!this.instance) return;
|
|
284
|
+
|
|
285
|
+
// Use realIndex for loop mode, fallback to activeIndex
|
|
286
|
+
const realIndex = this.instance.realIndex ?? this.instance.activeIndex;
|
|
287
|
+
|
|
288
|
+
// Update tab states
|
|
289
|
+
this.tabs.forEach((tab, index) => {
|
|
290
|
+
const isActive = index === realIndex;
|
|
291
|
+
tab.classList.toggle(CLASS_ACTIVE, isActive);
|
|
292
|
+
tab.setAttribute("aria-selected", String(isActive));
|
|
293
|
+
tab.setAttribute("tabindex", isActive ? "0" : "-1");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
this.scrollActiveTabIntoView(realIndex);
|
|
297
|
+
|
|
298
|
+
// Manually update pagination bullets
|
|
299
|
+
this.updatePaginationBullets(realIndex);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private updatePaginationBullets(activeIndex: number) {
|
|
303
|
+
const paginationEl = this.element.querySelector(SELECTOR_PAGINATION);
|
|
304
|
+
if (!paginationEl) return;
|
|
305
|
+
|
|
306
|
+
const bullets = paginationEl.querySelectorAll(`.${CLASS_PAGINATION_ITEM}`);
|
|
307
|
+
|
|
308
|
+
// With loop enabled, we need to get the real slide index
|
|
309
|
+
const realIndex = this.instance?.realIndex ?? activeIndex;
|
|
310
|
+
|
|
311
|
+
bullets.forEach((bullet, index) => {
|
|
312
|
+
const isActive = index === realIndex;
|
|
313
|
+
bullet.classList.toggle("is-active", isActive);
|
|
314
|
+
|
|
315
|
+
// For carousels without autoplay, manually show/hide the SVG
|
|
316
|
+
if (!this.hasAutoplay()) {
|
|
317
|
+
const svg = bullet.querySelector(
|
|
318
|
+
`.${CLASS_PAGINATION_SVG}`,
|
|
319
|
+
) as HTMLElement;
|
|
320
|
+
if (svg) {
|
|
321
|
+
svg.style.display = isActive ? "block" : "none";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
scrollActiveTabIntoView(activeIndex: number) {
|
|
327
|
+
if (!this.tabs.length || activeIndex < 0 || activeIndex >= this.tabs.length)
|
|
328
|
+
return;
|
|
329
|
+
|
|
330
|
+
const activeTab = this.tabs[activeIndex];
|
|
331
|
+
const tabsContainer = this.element.querySelector(
|
|
332
|
+
SELECTOR_TABS,
|
|
333
|
+
) as HTMLElement;
|
|
334
|
+
if (!tabsContainer || !activeTab) return;
|
|
335
|
+
|
|
336
|
+
const containerRect = tabsContainer.getBoundingClientRect();
|
|
337
|
+
const tabRect = activeTab.getBoundingClientRect();
|
|
338
|
+
const fadeWidth = 56;
|
|
339
|
+
const containerVisibleWidth = containerRect.width - fadeWidth;
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
tabRect.right > containerRect.left + containerVisibleWidth ||
|
|
343
|
+
tabRect.left < containerRect.left
|
|
344
|
+
) {
|
|
345
|
+
const scrollLeft =
|
|
346
|
+
tabRect.left -
|
|
347
|
+
containerRect.left -
|
|
348
|
+
containerVisibleWidth / 2 +
|
|
349
|
+
tabRect.width / 2;
|
|
350
|
+
tabsContainer.scrollBy({ left: scrollLeft, behavior: "smooth" });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
updatePlayPauseIcon() {
|
|
355
|
+
const playPauseButton = this.element.querySelector(SELECTOR_PLAY_PAUSE);
|
|
356
|
+
if (!playPauseButton) return;
|
|
357
|
+
|
|
358
|
+
const useElement = playPauseButton.querySelector("use");
|
|
359
|
+
if (useElement) {
|
|
360
|
+
const isPlaying = this.isAutoplayRunning();
|
|
361
|
+
const newIcon = isPlaying ? "pause" : "play";
|
|
362
|
+
const currentHref = useElement.getAttribute("xlink:href") || "";
|
|
363
|
+
if (!currentHref.endsWith(`#${newIcon}`)) {
|
|
364
|
+
const newHref = currentHref.replace(/#[\w-]+$/, `#${newIcon}`);
|
|
365
|
+
useElement.setAttribute("xlink:href", newHref);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
renderPaginationDots() {
|
|
371
|
+
const paginationButtons = this._getPaginationButtons();
|
|
372
|
+
|
|
373
|
+
paginationButtons.forEach((button) => {
|
|
374
|
+
const svg = this.createSvgDot();
|
|
375
|
+
button.innerHTML = "";
|
|
376
|
+
button.appendChild(svg);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
startDotAnimation(force: boolean = false) {
|
|
381
|
+
if (!this.hasAutoplay()) return;
|
|
382
|
+
if (!force && !this.isAutoplayRunning()) return;
|
|
383
|
+
if (this._dotAnimationJustStarted) return;
|
|
384
|
+
|
|
385
|
+
this._dotAnimationJustStarted = true;
|
|
386
|
+
|
|
387
|
+
const activeButton = this._getActiveBulletButton();
|
|
388
|
+
if (!activeButton) {
|
|
389
|
+
this._dotAnimationJustStarted = false;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const svg = activeButton.querySelector(
|
|
394
|
+
`.${CLASS_PAGINATION_SVG}`,
|
|
395
|
+
) as SVGSVGElement | null;
|
|
396
|
+
if (!svg) {
|
|
397
|
+
this._dotAnimationJustStarted = false;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const circle = svg.querySelector("circle") as SVGCircleElement | null;
|
|
402
|
+
if (!circle) {
|
|
403
|
+
this._dotAnimationJustStarted = false;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const duration = this.getAnimationDuration();
|
|
408
|
+
circle.style.animation = "none";
|
|
409
|
+
// Force reflow
|
|
410
|
+
void activeButton.offsetHeight;
|
|
411
|
+
circle.style.animation = `countdown linear ${duration}ms 1 forwards`;
|
|
412
|
+
this._dotAnimationJustStarted = false;
|
|
413
|
+
}
|
|
414
|
+
restartDotAnimation() {
|
|
415
|
+
this.stopDotAnimation();
|
|
416
|
+
setTimeout(() => {
|
|
417
|
+
this.startDotAnimation();
|
|
418
|
+
}, 50);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
stopDotAnimation() {
|
|
422
|
+
const paginationButtons = this._getPaginationButtons();
|
|
423
|
+
paginationButtons.forEach((button) => {
|
|
424
|
+
const svg = button.querySelector(
|
|
425
|
+
`.${CLASS_PAGINATION_SVG}`,
|
|
426
|
+
) as SVGSVGElement | null;
|
|
427
|
+
if (!svg) return;
|
|
428
|
+
const circle = svg.querySelector("circle") as SVGCircleElement | null;
|
|
429
|
+
if (!circle) return;
|
|
430
|
+
circle.style.animation = "none";
|
|
431
|
+
});
|
|
432
|
+
this._dotAnimationJustStarted = false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private _getPaginationButtons(): HTMLElement[] {
|
|
436
|
+
return Array.from(
|
|
437
|
+
this.element.querySelectorAll(`${SELECTOR_PAGINATION} > *`),
|
|
438
|
+
) as HTMLElement[];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private _getActiveBulletClass(): string {
|
|
442
|
+
const pagination = (this.instance?.params as any)?.pagination;
|
|
443
|
+
if (
|
|
444
|
+
pagination &&
|
|
445
|
+
typeof pagination === "object" &&
|
|
446
|
+
pagination.bulletActiveClass
|
|
447
|
+
) {
|
|
448
|
+
return String(pagination.bulletActiveClass);
|
|
449
|
+
}
|
|
450
|
+
const cfgPagination = this.config.pagination as any;
|
|
451
|
+
if (
|
|
452
|
+
cfgPagination &&
|
|
453
|
+
typeof cfgPagination === "object" &&
|
|
454
|
+
cfgPagination.bulletActiveClass
|
|
455
|
+
) {
|
|
456
|
+
return String(cfgPagination.bulletActiveClass);
|
|
457
|
+
}
|
|
458
|
+
return CLASS_ACTIVE;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private _getActiveBulletButton(): HTMLElement | null {
|
|
462
|
+
const activeClass = this._getActiveBulletClass();
|
|
463
|
+
return this.element.querySelector(
|
|
464
|
+
`${SELECTOR_PAGINATION} .${activeClass}`,
|
|
465
|
+
) as HTMLElement | null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
createSvgDot(): SVGSVGElement {
|
|
469
|
+
const svgNS = "http://www.w3.org/2000/svg";
|
|
470
|
+
const svg = document.createElementNS(svgNS, "svg");
|
|
471
|
+
svg.setAttribute("class", CLASS_PAGINATION_SVG);
|
|
472
|
+
svg.setAttribute("width", "12");
|
|
473
|
+
svg.setAttribute("height", "12");
|
|
474
|
+
svg.setAttribute("viewBox", "0 0 12 12");
|
|
475
|
+
|
|
476
|
+
const circle = document.createElementNS(svgNS, "circle");
|
|
477
|
+
circle.setAttribute("class", CLASS_PAGINATION_CIRCLE);
|
|
478
|
+
circle.setAttribute("r", "5");
|
|
479
|
+
circle.setAttribute("cx", "6");
|
|
480
|
+
circle.setAttribute("cy", "6");
|
|
481
|
+
|
|
482
|
+
svg.appendChild(circle);
|
|
483
|
+
return svg;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ====== Autoplay config helpers ======
|
|
487
|
+
hasAutoplay(): boolean {
|
|
488
|
+
return !!(
|
|
489
|
+
this.config.autoplay &&
|
|
490
|
+
typeof this.config.autoplay === "object" &&
|
|
491
|
+
(this.config.autoplay as any).delay >= 1000
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
getAnimationDuration(): number {
|
|
496
|
+
return (this.config.autoplay as any)?.delay || 0;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
destroy() {
|
|
500
|
+
this.tabs.forEach((tab) => {
|
|
501
|
+
tab.removeEventListener("click", this._boundTabClick);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const playPauseButton = this.element.querySelector(SELECTOR_PLAY_PAUSE);
|
|
505
|
+
if (playPauseButton) {
|
|
506
|
+
playPauseButton.removeEventListener("click", this._boundPlayPauseClick);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const prevButton = this.element.querySelector(SELECTOR_PREV);
|
|
510
|
+
const nextButton = this.element.querySelector(SELECTOR_NEXT);
|
|
511
|
+
if (prevButton) {
|
|
512
|
+
prevButton.removeEventListener("click", this._boundPrevClick);
|
|
513
|
+
}
|
|
514
|
+
if (nextButton) {
|
|
515
|
+
nextButton.removeEventListener("click", this._boundNextClick);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (this.instance) {
|
|
519
|
+
this.instance.destroy(true, true);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
delete (this.element as any).ODS_CarouselHero;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
static getInstance(el: HTMLElement): CarouselHero | null {
|
|
526
|
+
return (el as any).ODS_CarouselHero || null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { ReactNode } from "react";
|
|
4
|
+
import cx from "classnames";
|
|
5
|
+
|
|
6
|
+
import { Controls } from "../Controls";
|
|
7
|
+
import { CarouselHeroItem } from "./CarouselHeroItem";
|
|
8
|
+
import CarouselHeroStatic from "./CarouselHero.static";
|
|
9
|
+
import {
|
|
10
|
+
CLASS_ROOT,
|
|
11
|
+
CLASS_VIEWPORT_WRAPPER,
|
|
12
|
+
CLASS_VIEWPORT,
|
|
13
|
+
CLASS_PREV,
|
|
14
|
+
CLASS_NEXT,
|
|
15
|
+
CLASS_PLAY_PAUSE,
|
|
16
|
+
CLASS_TABS,
|
|
17
|
+
CLASS_TAB,
|
|
18
|
+
CLASS_PAGINATION,
|
|
19
|
+
CLASS_CONTROLS,
|
|
20
|
+
CLASS_TRACK,
|
|
21
|
+
CLASS_NAVIGATION,
|
|
22
|
+
} from "./constants";
|
|
23
|
+
import { useStatic } from "@/utils/hooks";
|
|
24
|
+
|
|
25
|
+
interface TabItem {
|
|
26
|
+
label: string;
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CarouselHeroProps {
|
|
31
|
+
className?: string;
|
|
32
|
+
swiperOptions?: Record<string, any>;
|
|
33
|
+
colorScheme?: "light" | "dark";
|
|
34
|
+
children?: ReactNode;
|
|
35
|
+
tabs?: TabItem[];
|
|
36
|
+
interval?: number;
|
|
37
|
+
[key: string]: any;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CarouselHero: React.FC<CarouselHeroProps> = ({
|
|
41
|
+
className,
|
|
42
|
+
swiperOptions,
|
|
43
|
+
colorScheme,
|
|
44
|
+
children,
|
|
45
|
+
tabs = [],
|
|
46
|
+
interval,
|
|
47
|
+
...other
|
|
48
|
+
}) => {
|
|
49
|
+
const [carouselRef] = useStatic(CarouselHeroStatic);
|
|
50
|
+
|
|
51
|
+
const classes = cx(CLASS_ROOT, className, {
|
|
52
|
+
"is-light": colorScheme === "light",
|
|
53
|
+
"is-dark": colorScheme === "dark",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const elementClasses = {
|
|
57
|
+
prev: cx(CLASS_PREV),
|
|
58
|
+
next: cx(CLASS_NEXT),
|
|
59
|
+
playPause: cx(CLASS_PLAY_PAUSE),
|
|
60
|
+
tabs: cx(CLASS_TABS),
|
|
61
|
+
tab: cx(CLASS_TAB),
|
|
62
|
+
pagination: cx(CLASS_PAGINATION),
|
|
63
|
+
controls: cx(CLASS_CONTROLS),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const hasAutoplay =
|
|
67
|
+
(swiperOptions?.autoplay &&
|
|
68
|
+
typeof swiperOptions.autoplay === "object" &&
|
|
69
|
+
(swiperOptions.autoplay as any).delay >= 1000) ||
|
|
70
|
+
(interval && interval >= 1000);
|
|
71
|
+
|
|
72
|
+
const playPauseIcon = "pause";
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
className={classes}
|
|
77
|
+
ref={carouselRef}
|
|
78
|
+
data-carousel-hero
|
|
79
|
+
{...(interval && interval >= 1000 ? { "data-interval": interval } : {})}
|
|
80
|
+
{...(swiperOptions
|
|
81
|
+
? { "data-swiper-options": JSON.stringify(swiperOptions) }
|
|
82
|
+
: {})}
|
|
83
|
+
{...other}
|
|
84
|
+
>
|
|
85
|
+
<div className={CLASS_VIEWPORT_WRAPPER}>
|
|
86
|
+
<div className={CLASS_VIEWPORT}>
|
|
87
|
+
<div className={CLASS_TRACK}>{children}</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Controls container */}
|
|
92
|
+
<div className={elementClasses.controls}>
|
|
93
|
+
{/* Tab navigation */}
|
|
94
|
+
{tabs.length > 0 && (
|
|
95
|
+
<div className={elementClasses.tabs} role="tablist">
|
|
96
|
+
{tabs.map((tab, index) => (
|
|
97
|
+
<button
|
|
98
|
+
key={index}
|
|
99
|
+
type="button"
|
|
100
|
+
className={elementClasses.tab}
|
|
101
|
+
role="tab"
|
|
102
|
+
aria-selected={index === 0 ? "true" : "false"}
|
|
103
|
+
tabIndex={index === 0 ? 0 : -1}
|
|
104
|
+
>
|
|
105
|
+
{tab.label}
|
|
106
|
+
</button>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{/* Pagination dots for mobile */}
|
|
112
|
+
<div role="tablist" className={elementClasses.pagination} />
|
|
113
|
+
|
|
114
|
+
{/* Navigation controls */}
|
|
115
|
+
<div className={CLASS_NAVIGATION}>
|
|
116
|
+
<Controls
|
|
117
|
+
className={elementClasses.prev}
|
|
118
|
+
icon="chevron-left"
|
|
119
|
+
colorScheme={colorScheme}
|
|
120
|
+
aria-label="Predchádzajúci snímok"
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<Controls
|
|
124
|
+
className={elementClasses.next}
|
|
125
|
+
icon="chevron-right"
|
|
126
|
+
colorScheme={colorScheme}
|
|
127
|
+
aria-label="Nasledujúci snímok"
|
|
128
|
+
/>
|
|
129
|
+
|
|
130
|
+
{/* Only show play/pause button if autoplay is configured */}
|
|
131
|
+
{hasAutoplay && (
|
|
132
|
+
<Controls
|
|
133
|
+
className={elementClasses.playPause}
|
|
134
|
+
icon={playPauseIcon}
|
|
135
|
+
colorScheme={colorScheme}
|
|
136
|
+
aria-label="Pozastaviť/Spustiť automatické prehrávanie"
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
CarouselHero.displayName = "CarouselHero";
|
|
146
|
+
|
|
147
|
+
export { CarouselHero, CarouselHeroItem };
|
|
148
|
+
export type { CarouselHeroProps, TabItem };
|