srcdev-nuxt-components 4.0.3 → 4.0.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.
|
@@ -61,6 +61,10 @@ const props = defineProps({
|
|
|
61
61
|
type: Boolean,
|
|
62
62
|
default: false,
|
|
63
63
|
},
|
|
64
|
+
returnToStart: {
|
|
65
|
+
type: Boolean,
|
|
66
|
+
default: false,
|
|
67
|
+
},
|
|
64
68
|
});
|
|
65
69
|
|
|
66
70
|
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
@@ -76,12 +80,17 @@ const itemCount = ref(props.carouselDataIds.length);
|
|
|
76
80
|
const offset = ref(0);
|
|
77
81
|
const transitionSpeedStr = props.transitionSpeed + 'ms';
|
|
78
82
|
const itemTransform = computed(() => {
|
|
79
|
-
return `translateX(calc(${offset.value} * (${itemWidth.value} + var(--
|
|
83
|
+
return `translateX(calc(${offset.value} * (${itemWidth.value} + var(--_carousel-item-track-gap))))`;
|
|
80
84
|
});
|
|
81
85
|
|
|
82
86
|
const itemWidth = ref('0px');
|
|
83
87
|
|
|
84
88
|
const actionPrevious = () => {
|
|
89
|
+
if (props.returnToStart && currentIndex.value === 0) {
|
|
90
|
+
offset.value = -itemCount.value;
|
|
91
|
+
doAction();
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
if (offset.value >= 0) {
|
|
86
95
|
return;
|
|
87
96
|
}
|
|
@@ -91,6 +100,12 @@ const actionPrevious = () => {
|
|
|
91
100
|
};
|
|
92
101
|
|
|
93
102
|
const actionNext = () => {
|
|
103
|
+
if (props.returnToStart && offset.value <= -1 * (itemCount.value - 1)) {
|
|
104
|
+
offset.value = 0;
|
|
105
|
+
doAction();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
94
109
|
if (offset.value <= -1 * (itemCount.value - 1)) {
|
|
95
110
|
return;
|
|
96
111
|
}
|
|
@@ -156,7 +171,7 @@ onMounted(() => {
|
|
|
156
171
|
|
|
157
172
|
<style lang="css">
|
|
158
173
|
.carousel-basic {
|
|
159
|
-
--
|
|
174
|
+
--_carousel-item-track-gap: 10px;
|
|
160
175
|
|
|
161
176
|
display: grid;
|
|
162
177
|
grid-template-columns: 1fr;
|
|
@@ -176,7 +191,7 @@ onMounted(() => {
|
|
|
176
191
|
|
|
177
192
|
.timeline-container {
|
|
178
193
|
display: flex;
|
|
179
|
-
gap: var(--
|
|
194
|
+
gap: var(--_carousel-item-track-gap);
|
|
180
195
|
overflow-x: hidden;
|
|
181
196
|
|
|
182
197
|
.timeline-item {
|
|
@@ -204,7 +219,7 @@ onMounted(() => {
|
|
|
204
219
|
|
|
205
220
|
.item-container {
|
|
206
221
|
display: flex;
|
|
207
|
-
gap: var(--
|
|
222
|
+
gap: var(--_carousel-item-track-gap);
|
|
208
223
|
overflow-x: hidden;
|
|
209
224
|
position: relative;
|
|
210
225
|
|
|
@@ -1,48 +1,57 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="carousel-flip" :class="[elementClasses
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
2
|
+
<section class="carousel-flip" :class="[elementClasses]" ref="carouselWrapperRef" role="region" aria-label="Image carousel">
|
|
3
|
+
<div aria-live="polite" aria-atomic="true" class="sr-only">Item {{ currentActiveIndex + 1 }} of {{ itemCount }}</div>
|
|
4
|
+
|
|
5
|
+
<LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-20']">
|
|
6
|
+
<div tabindex="0" class="item-container" :class="{ 'allow-overflow': allowCarouselOverflow }" ref="carouselContainerRef" role="group" aria-label="Carousel items">
|
|
7
|
+
<div
|
|
8
|
+
v-for="(item, index) in carouselDataIds"
|
|
9
|
+
:key="index"
|
|
10
|
+
class="item"
|
|
11
|
+
:class="{ loaded: carouselInitComplete && userHasInteracted }"
|
|
12
|
+
ref="carouselItems"
|
|
13
|
+
:data-id="item"
|
|
14
|
+
:aria-current="currentActiveIndex === index ? 'true' : 'false'"
|
|
15
|
+
>
|
|
16
|
+
<slot :name="item"></slot>
|
|
17
|
+
</div>
|
|
18
18
|
</div>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
</LayoutRow>
|
|
20
|
+
|
|
21
|
+
<LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-20']">
|
|
22
|
+
<div tabindex="0" class="controls-container" ref="controlsContainerRef">
|
|
23
|
+
<div class="markers-container">
|
|
24
|
+
<ul class="markers-list">
|
|
25
|
+
<li v-for="index in itemCount" :key="index" class="markers-item">
|
|
26
|
+
<button
|
|
27
|
+
@click.prevent="jumpToFrame(index - 1)"
|
|
28
|
+
class="btn-marker"
|
|
29
|
+
:class="[{ active: currentActiveIndex === getOffsetIndex(index - 1, circularOffsetBase, itemCount) }]"
|
|
30
|
+
:aria-label="`Jump to item ${Math.floor(index + 1)}`"
|
|
31
|
+
></button>
|
|
32
|
+
</li>
|
|
33
|
+
</ul>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="buttons-container">
|
|
36
|
+
<button type="button" @click.prevent="actionPrevious()" class="btn-action" aria-label="Go to previous item">
|
|
37
|
+
<Icon name="ic:outline-keyboard-arrow-left" class="arrows-icon" />
|
|
38
|
+
</button>
|
|
39
|
+
<button type="button" @click.prevent="actionNext()" class="btn-action" aria-label="Go to next item">
|
|
40
|
+
<Icon name="ic:outline-keyboard-arrow-right" class="arrows-icon" />
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
25
43
|
</div>
|
|
26
|
-
</
|
|
44
|
+
</LayoutRow>
|
|
27
45
|
</section>
|
|
28
46
|
</template>
|
|
29
47
|
|
|
30
48
|
<script setup lang="ts">
|
|
31
|
-
import
|
|
32
|
-
|
|
49
|
+
import { useEventListener, useResizeObserver, useSwipe } from '@vueuse/core';
|
|
50
|
+
|
|
33
51
|
const props = defineProps({
|
|
34
|
-
|
|
35
|
-
type:
|
|
36
|
-
default:
|
|
37
|
-
items: [],
|
|
38
|
-
total: 0,
|
|
39
|
-
skip: 0,
|
|
40
|
-
limit: 10
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
data: {
|
|
44
|
-
type: Object,
|
|
45
|
-
default: <ICarouselBasic>{}
|
|
52
|
+
carouselDataIds: {
|
|
53
|
+
type: Array as PropType<string[]>,
|
|
54
|
+
default: () => [],
|
|
46
55
|
},
|
|
47
56
|
styleClassPassthrough: {
|
|
48
57
|
type: Array as PropType<string[]>,
|
|
@@ -50,329 +59,403 @@ const props = defineProps({
|
|
|
50
59
|
},
|
|
51
60
|
transitionSpeed: {
|
|
52
61
|
type: Number,
|
|
53
|
-
default:
|
|
62
|
+
default: 200,
|
|
54
63
|
},
|
|
55
|
-
|
|
64
|
+
allowCarouselOverflow: {
|
|
56
65
|
type: Boolean,
|
|
57
|
-
default: false
|
|
58
|
-
}
|
|
66
|
+
default: false,
|
|
67
|
+
},
|
|
68
|
+
useFlipAnimation: {
|
|
69
|
+
type: Boolean,
|
|
70
|
+
default: true,
|
|
71
|
+
},
|
|
59
72
|
});
|
|
60
73
|
|
|
61
|
-
const { elementClasses
|
|
74
|
+
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
62
75
|
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
76
|
+
const carouselWrapperRef = ref<HTMLDivElement | null>(null);
|
|
77
|
+
const carouselContainerRef = ref<HTMLDivElement | null>(null);
|
|
78
|
+
const carouselItemsRef = useTemplateRef<HTMLDivElement[]>('carouselItems');
|
|
79
|
+
const controlsContainerRef = ref<HTMLDivElement | null>(null);
|
|
66
80
|
const carouselInitComplete = ref(false);
|
|
81
|
+
const userHasInteracted = ref(false);
|
|
67
82
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const actionPrevious = () => {
|
|
76
|
-
// if (transitionRunning.value) return;
|
|
83
|
+
const initialItemOffset = computed(() => {
|
|
84
|
+
return props.useFlipAnimation ? 1 : 2;
|
|
85
|
+
});
|
|
86
|
+
const circularOffsetBase = computed(() => {
|
|
87
|
+
return props.useFlipAnimation ? 1 : Math.floor(2 * initialItemOffset.value);
|
|
88
|
+
});
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
function getOffsetIndex(index: number, offset: number, itemCount: number): number {
|
|
91
|
+
return (index + offset) % itemCount;
|
|
80
92
|
}
|
|
81
93
|
|
|
82
|
-
const
|
|
83
|
-
|
|
94
|
+
const currentIndex = ref(0);
|
|
95
|
+
const itemCount = ref(props.carouselDataIds.length);
|
|
96
|
+
const transitionSpeedStr = props.transitionSpeed + 'ms';
|
|
84
97
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
const itemWidth = ref(0);
|
|
99
|
+
const itemWidthOffsetStr = computed(() => {
|
|
100
|
+
if (props.allowCarouselOverflow) {
|
|
101
|
+
if (props.useFlipAnimation) {
|
|
102
|
+
return `calc(-${initialItemOffset.value} * ${itemWidth.value}px - var(--_carousel-item-track-gap))`; // Good
|
|
103
|
+
} else {
|
|
104
|
+
return `calc(-${initialItemOffset.value} * ${itemWidth.value}px - (2 * var(--_carousel-item-track-gap)))`; // Good
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
if (props.useFlipAnimation) {
|
|
108
|
+
return `calc(-${initialItemOffset.value} * ${itemWidth.value}px - var(--_carousel-item-track-gap))`; // Goof
|
|
109
|
+
} else {
|
|
110
|
+
return `calc(-${initialItemOffset.value} * ${itemWidth.value}px - (2 * var(--_carousel-item-track-gap)))`; // Good
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
const currentActiveIndex = ref(0);
|
|
88
115
|
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
const updateItemOrder = (index: number, order: number, zIndex: number = 2) => {
|
|
117
|
+
if (carouselItemsRef?.value && carouselItemsRef.value[index]) {
|
|
118
|
+
carouselItemsRef.value[index].style.order = order.toString();
|
|
119
|
+
carouselItemsRef.value[index].style.zIndex = zIndex.toString();
|
|
93
120
|
}
|
|
94
121
|
};
|
|
95
122
|
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const thumbs = thumbnailItems.value;
|
|
123
|
+
function analyzeOffsets(offsets: number[]) {
|
|
124
|
+
const counts = new Map<number, number>();
|
|
99
125
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
item.style.order = String(index + 1);
|
|
103
|
-
// item.setAttribute('data-order', String(index + 1));
|
|
126
|
+
offsets.forEach((val) => {
|
|
127
|
+
counts.set(val, (counts.get(val) || 0) + 1);
|
|
104
128
|
});
|
|
105
|
-
thumbs?.forEach((thumb, index) => {
|
|
106
|
-
thumb.style.zIndex = index === 0 || index === itemCount.value - 1 ? '1' : '2';
|
|
107
|
-
thumb.style.order = String(index + 1);
|
|
108
|
-
// thumb.setAttribute('data-order', String(index + 1));
|
|
109
|
-
});
|
|
110
|
-
carouselInitComplete.value = true;
|
|
111
|
-
}
|
|
112
129
|
|
|
130
|
+
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
113
131
|
|
|
114
|
-
const
|
|
132
|
+
const majorityValue = sorted[0][0];
|
|
133
|
+
const minorityValue = sorted[sorted.length - 1][0];
|
|
134
|
+
const minorityIndex = offsets.findIndex((val) => val === minorityValue);
|
|
115
135
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
136
|
+
return {
|
|
137
|
+
majorityValue,
|
|
138
|
+
minorityValue,
|
|
139
|
+
minorityIndex,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
119
142
|
|
|
120
|
-
|
|
121
|
-
if (!
|
|
143
|
+
const reorderItems = (direction: 'next' | 'previous' | 'jump' = 'jump', skipAnimation: boolean = false) => {
|
|
144
|
+
if (!carouselItemsRef?.value) return;
|
|
122
145
|
|
|
123
|
-
//
|
|
124
|
-
const
|
|
125
|
-
const firstThumbRects = thumbs.map(el => el.getBoundingClientRect());
|
|
146
|
+
// Capture positions before reordering (only if we're going to animate)
|
|
147
|
+
const beforeRects = skipAnimation ? [] : carouselItemsRef.value.map((item) => item.getBoundingClientRect());
|
|
126
148
|
|
|
127
|
-
//
|
|
128
|
-
let
|
|
149
|
+
// Apply new order and z-index based on direction
|
|
150
|
+
let order = 1;
|
|
129
151
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
currentIndex.value = currentIndex.value === itemCount.value ? 1 : currentIndex.value + localOffset;
|
|
134
|
-
firstVisualElementIndex = currentIndex.value;
|
|
135
|
-
let order = 1;
|
|
152
|
+
// For items from currentActiveIndex to end
|
|
153
|
+
for (let i = currentActiveIndex.value; i < itemCount.value; i++) {
|
|
154
|
+
let zIndex = 2; // default normal z-index
|
|
136
155
|
|
|
137
|
-
|
|
138
|
-
|
|
156
|
+
if (i === currentActiveIndex.value) {
|
|
157
|
+
// The item becoming visible
|
|
158
|
+
if (direction === 'previous') {
|
|
159
|
+
// When going previous, the item moving to first position should go behind
|
|
160
|
+
zIndex = 1;
|
|
161
|
+
} else {
|
|
162
|
+
// Normal case - visible item gets highest z-index
|
|
163
|
+
zIndex = 3;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
139
166
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
currentIndex.value = currentIndex.value === 1 ? itemCount.value : currentIndex.value + localOffset;
|
|
143
|
-
firstVisualElementIndex = currentIndex.value;
|
|
144
|
-
let order = itemCount.value;
|
|
167
|
+
updateItemOrder(i, order++, zIndex);
|
|
168
|
+
}
|
|
145
169
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
170
|
+
// For items from 0 to currentActiveIndex
|
|
171
|
+
for (let i = 0; i < currentActiveIndex.value; i++) {
|
|
172
|
+
// Items that wrap around get lower z-index to slide behind
|
|
173
|
+
const zIndex = 1;
|
|
174
|
+
updateItemOrder(i, order++, zIndex);
|
|
175
|
+
}
|
|
150
176
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
177
|
+
// Skip animation if requested (for initial setup)
|
|
178
|
+
if (skipAnimation) {
|
|
179
|
+
return;
|
|
154
180
|
}
|
|
155
181
|
|
|
156
|
-
//
|
|
182
|
+
// Animate using FLIP technique
|
|
157
183
|
requestAnimationFrame(() => {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
const afterRects = carouselItemsRef.value!.map((item) => item.getBoundingClientRect());
|
|
185
|
+
|
|
186
|
+
// Calculate offset values
|
|
187
|
+
const offsetValues = beforeRects.map((beforeRect, index) => {
|
|
188
|
+
const afterRect = afterRects[index];
|
|
189
|
+
return beforeRect.left - afterRect.left;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const leftValues = analyzeOffsets(offsetValues);
|
|
193
|
+
|
|
194
|
+
carouselItemsRef.value!.forEach((item, index) => {
|
|
195
|
+
const deltaX = beforeRects[index].left - afterRects[index].left;
|
|
196
|
+
|
|
197
|
+
if (deltaX !== 0) {
|
|
198
|
+
item.style.transition = 'none';
|
|
199
|
+
item.style.transform = `translateX(${deltaX}px)`;
|
|
200
|
+
|
|
201
|
+
requestAnimationFrame(() => {
|
|
202
|
+
const shouldTransition = carouselInitComplete.value && userHasInteracted.value;
|
|
203
|
+
let transitionProperties = 'none';
|
|
204
|
+
|
|
205
|
+
if (shouldTransition) {
|
|
206
|
+
if (props.allowCarouselOverflow) {
|
|
207
|
+
if (props.useFlipAnimation) {
|
|
208
|
+
transitionProperties = `transform ${transitionSpeedStr} ease`;
|
|
209
|
+
} else {
|
|
210
|
+
if (leftValues.minorityIndex !== index) {
|
|
211
|
+
transitionProperties = `transform ${transitionSpeedStr} ease`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
if (props.useFlipAnimation) {
|
|
216
|
+
transitionProperties = `transform ${transitionSpeedStr} ease`;
|
|
217
|
+
} else {
|
|
218
|
+
if (leftValues.minorityIndex !== index) {
|
|
219
|
+
transitionProperties = `transform ${transitionSpeedStr} ease`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
182
223
|
}
|
|
183
|
-
};
|
|
184
224
|
|
|
185
|
-
|
|
186
|
-
|
|
225
|
+
item.style.transition = transitionProperties;
|
|
226
|
+
item.style.transform = 'translateX(0)';
|
|
227
|
+
|
|
228
|
+
// After animation completes, normalize z-index values
|
|
229
|
+
const handleTransitionEnd = (event: TransitionEvent) => {
|
|
230
|
+
if (event.propertyName === 'transform') {
|
|
231
|
+
// Set final z-index: current item gets highest, others get normal
|
|
232
|
+
const isCurrentlyVisible = index === currentActiveIndex.value;
|
|
233
|
+
item.style.zIndex = isCurrentlyVisible ? '3' : '2';
|
|
234
|
+
item.removeEventListener('transitionend', handleTransitionEnd);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
if (shouldTransition) {
|
|
239
|
+
item.addEventListener('transitionend', handleTransitionEnd);
|
|
240
|
+
} else {
|
|
241
|
+
// If no transition, immediately normalize z-index
|
|
242
|
+
const isCurrentlyVisible = index === currentActiveIndex.value;
|
|
243
|
+
item.style.zIndex = isCurrentlyVisible ? '3' : '2';
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
187
247
|
});
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const actionPrevious = () => {
|
|
252
|
+
if (!carouselInitComplete.value || !carouselItemsRef?.value) return;
|
|
253
|
+
|
|
254
|
+
userHasInteracted.value = true;
|
|
188
255
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
256
|
+
if (currentActiveIndex.value === 0) {
|
|
257
|
+
currentActiveIndex.value = itemCount.value - 1;
|
|
258
|
+
} else {
|
|
259
|
+
currentActiveIndex.value = currentActiveIndex.value === 0 ? itemCount.value - 1 : currentActiveIndex.value - 1;
|
|
260
|
+
}
|
|
193
261
|
|
|
194
|
-
|
|
195
|
-
|
|
262
|
+
reorderItems('previous');
|
|
263
|
+
currentIndex.value = currentActiveIndex.value;
|
|
264
|
+
};
|
|
196
265
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
thumb.style.transform = '';
|
|
266
|
+
const actionNext = () => {
|
|
267
|
+
if (!carouselInitComplete.value || !carouselItemsRef?.value) return;
|
|
200
268
|
|
|
201
|
-
|
|
202
|
-
const thumbIndex = i + 1; // Convert to 1-based index
|
|
203
|
-
const isActiveThumbnail = thumbIndex === firstVisualElementIndex;
|
|
269
|
+
userHasInteracted.value = true;
|
|
204
270
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
271
|
+
if (currentActiveIndex.value === itemCount.value - 1) {
|
|
272
|
+
currentActiveIndex.value = 0;
|
|
273
|
+
} else {
|
|
274
|
+
currentActiveIndex.value = currentActiveIndex.value === itemCount.value - 1 ? 0 : currentActiveIndex.value + 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
reorderItems('next');
|
|
278
|
+
currentIndex.value = currentActiveIndex.value;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const jumpToFrame = (index: number) => {
|
|
282
|
+
if (index >= 0 && index < itemCount.value) {
|
|
283
|
+
// Only mark as user interaction if carousel is already initialized
|
|
284
|
+
if (carouselInitComplete.value) {
|
|
285
|
+
userHasInteracted.value = true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
currentActiveIndex.value = getOffsetIndex(index, circularOffsetBase.value, itemCount.value);
|
|
289
|
+
|
|
290
|
+
// currentActiveIndex.value = index;
|
|
291
|
+
reorderItems('jump');
|
|
292
|
+
currentIndex.value = currentActiveIndex.value;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const checkAndMoveLastItem = () => {
|
|
297
|
+
if (props.allowCarouselOverflow || !props.useFlipAnimation) {
|
|
298
|
+
currentActiveIndex.value = itemCount.value - initialItemOffset.value;
|
|
299
|
+
reorderItems('jump', true); // Skip animation during initial setup
|
|
300
|
+
currentIndex.value = currentActiveIndex.value;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
212
303
|
|
|
213
|
-
|
|
214
|
-
|
|
304
|
+
const initialSetup = () => {
|
|
305
|
+
if (carouselItemsRef?.value && carouselItemsRef.value.length > 0 && carouselItemsRef.value[0]) {
|
|
306
|
+
itemWidth.value = carouselItemsRef.value[0].offsetWidth;
|
|
307
|
+
|
|
308
|
+
// Set initial order and z-index for all items
|
|
309
|
+
carouselItemsRef.value.forEach((item, index) => {
|
|
310
|
+
item.style.order = String(index + 1);
|
|
311
|
+
item.dataset.order = String(index + 1);
|
|
312
|
+
// First item gets higher z-index, others get normal z-index
|
|
313
|
+
item.style.zIndex = index === 0 ? '3' : '2';
|
|
215
314
|
});
|
|
216
|
-
}
|
|
315
|
+
}
|
|
217
316
|
|
|
218
317
|
carouselInitComplete.value = true;
|
|
318
|
+
checkAndMoveLastItem();
|
|
219
319
|
};
|
|
220
320
|
|
|
221
|
-
|
|
321
|
+
const { direction } = useSwipe(carouselContainerRef, {
|
|
322
|
+
passive: false,
|
|
323
|
+
onSwipeEnd() {
|
|
324
|
+
if (direction.value === 'left') {
|
|
325
|
+
actionNext();
|
|
326
|
+
} else if (direction.value === 'right') {
|
|
327
|
+
actionPrevious();
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
useEventListener(carouselContainerRef, 'keydown', (event: KeyboardEvent) => {
|
|
333
|
+
if (event.key === 'ArrowLeft') {
|
|
334
|
+
actionPrevious();
|
|
335
|
+
} else if (event.key === 'ArrowRight') {
|
|
336
|
+
actionNext();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
useEventListener(controlsContainerRef, 'keydown', (event: KeyboardEvent) => {
|
|
341
|
+
if (event.key === 'ArrowLeft') {
|
|
342
|
+
actionPrevious();
|
|
343
|
+
} else if (event.key === 'ArrowRight') {
|
|
344
|
+
actionNext();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
useResizeObserver(carouselWrapperRef, async () => {
|
|
222
349
|
initialSetup();
|
|
223
350
|
});
|
|
224
351
|
|
|
352
|
+
onMounted(() => {
|
|
353
|
+
initialSetup();
|
|
354
|
+
});
|
|
225
355
|
</script>
|
|
226
356
|
|
|
227
357
|
<style lang="css">
|
|
228
358
|
.carousel-flip {
|
|
359
|
+
--_carousel-item-track-gap: 10px;
|
|
229
360
|
|
|
230
361
|
display: grid;
|
|
231
362
|
grid-template-columns: 1fr;
|
|
232
363
|
gap: 10px;
|
|
233
364
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
grid-area: carousel-content;
|
|
245
|
-
z-index: 2;
|
|
246
|
-
height: fit-content;
|
|
247
|
-
align-self: flex-end;
|
|
248
|
-
}
|
|
365
|
+
.sr-only {
|
|
366
|
+
position: absolute;
|
|
367
|
+
width: 1px;
|
|
368
|
+
height: 1px;
|
|
369
|
+
padding: 0;
|
|
370
|
+
margin: -1px;
|
|
371
|
+
overflow: hidden;
|
|
372
|
+
clip: rect(0, 0, 0, 0);
|
|
373
|
+
white-space: nowrap;
|
|
374
|
+
border: 0;
|
|
249
375
|
}
|
|
250
376
|
|
|
251
377
|
.item-container {
|
|
252
378
|
display: flex;
|
|
253
|
-
gap:
|
|
254
|
-
overflow-x:
|
|
255
|
-
|
|
256
|
-
padding-inline: 10px;
|
|
257
|
-
outline: 1px solid light-dark(#00000090, #f00ff090);
|
|
379
|
+
gap: var(--_carousel-item-track-gap);
|
|
380
|
+
overflow-x: hidden;
|
|
381
|
+
position: relative;
|
|
258
382
|
|
|
259
|
-
|
|
383
|
+
max-inline-size: var(--_carousel-display-max-width);
|
|
384
|
+
margin-inline: auto;
|
|
260
385
|
|
|
261
|
-
|
|
262
|
-
|
|
386
|
+
&.allow-overflow {
|
|
387
|
+
overflow-x: initial;
|
|
388
|
+
}
|
|
263
389
|
|
|
264
390
|
.item {
|
|
265
391
|
display: flex;
|
|
266
|
-
flex
|
|
267
|
-
|
|
268
|
-
justify-content: center;
|
|
269
|
-
|
|
270
|
-
/* transition: transform v-bind(transitionSpeedStr) ease; */
|
|
271
|
-
/* For FLIP smoothness */
|
|
392
|
+
flex: 0 0 100%;
|
|
393
|
+
position: relative;
|
|
272
394
|
|
|
273
|
-
|
|
395
|
+
margin-inline: auto;
|
|
274
396
|
|
|
275
|
-
|
|
276
|
-
color: light-dar(#aaa, #333);
|
|
277
|
-
padding-block: 10px;
|
|
278
|
-
padding-inline: 10px;
|
|
279
|
-
border-radius: 4px;
|
|
280
|
-
outline: 1px solid light-dark(#00000090, #f00ff090);
|
|
397
|
+
max-inline-size: calc(var(--_carousel-container-width) + var(--_carousel-item-track-gap) - (2 * var(--_carousel-item-edge-preview-width)));
|
|
281
398
|
|
|
282
|
-
|
|
399
|
+
translate: calc(v-bind(itemWidthOffsetStr) - var(--_carousel-item-track-gap) + var(--_carousel-item-edge-preview-width)) 0;
|
|
283
400
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
&:nth-child(odd) {
|
|
287
|
-
background-color: light-dark(#00f, #f00);
|
|
401
|
+
&.loaded {
|
|
402
|
+
transition: transform v-bind(transitionSpeedStr) ease;
|
|
288
403
|
}
|
|
289
404
|
}
|
|
290
405
|
}
|
|
291
406
|
|
|
292
|
-
|
|
293
407
|
.controls-container {
|
|
294
|
-
|
|
295
408
|
display: flex;
|
|
296
|
-
|
|
297
|
-
|
|
409
|
+
align-items: center;
|
|
410
|
+
justify-content: flex-end;
|
|
411
|
+
max-inline-size: var(--_carousel-display-max-width);
|
|
412
|
+
margin-inline: auto;
|
|
298
413
|
|
|
414
|
+
.markers-container {
|
|
415
|
+
.markers-list {
|
|
416
|
+
display: flex;
|
|
417
|
+
flex-direction: row;
|
|
418
|
+
gap: 10px;
|
|
419
|
+
list-style-type: none;
|
|
420
|
+
margin: unset;
|
|
421
|
+
padding: unset;
|
|
422
|
+
|
|
423
|
+
.markers-item {
|
|
424
|
+
.btn-marker {
|
|
425
|
+
border: 1px solid transparent;
|
|
426
|
+
outline: 1px solid transparent;
|
|
427
|
+
box-shadow: none;
|
|
428
|
+
cursor: pointer;
|
|
429
|
+
transition: background-color v-bind(transitionSpeedStr) linear;
|
|
430
|
+
|
|
431
|
+
&.active {
|
|
432
|
+
background-color: light-dark(var(--gray-12), var(--gray-00));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
299
438
|
|
|
300
439
|
.buttons-container {
|
|
301
440
|
display: flex;
|
|
302
|
-
flex-grow: 1;
|
|
303
441
|
align-items: center;
|
|
304
442
|
justify-content: end;
|
|
305
443
|
gap: 20px;
|
|
306
444
|
|
|
307
|
-
|
|
308
445
|
.btn-action {
|
|
309
|
-
padding: 10px 20px;
|
|
310
|
-
border-radius: 4px;
|
|
311
|
-
background-color: light-dark(#000, #fff);
|
|
312
|
-
color: light-dark(#fff, #000);
|
|
313
|
-
border: none;
|
|
314
|
-
cursor: pointer;
|
|
315
|
-
height: fit-content;
|
|
316
|
-
|
|
317
|
-
transition: background-color 0.3s ease, color 0.3s ease;
|
|
318
|
-
|
|
319
|
-
&:hover {
|
|
320
|
-
background-color: light-dark(#0009, #fff9);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
&:active {
|
|
324
|
-
background-color: light-dark(#0009, #fff9);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
&:disabled {
|
|
328
|
-
background-color: light-dark(#0003, #fff3);
|
|
329
|
-
cursor: not-allowed;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
.thumbnail-container {
|
|
335
|
-
padding-block: 10px;
|
|
336
|
-
padding-inline: 10px;
|
|
337
|
-
outline: 1px solid light-dark(#00000090, #f00ff090);
|
|
338
|
-
max-inline-size: 40%;
|
|
339
|
-
|
|
340
|
-
.thumbnail-list {
|
|
341
446
|
display: flex;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
padding-block: 8px;
|
|
345
|
-
padding-inline: 8px;
|
|
346
|
-
margin-block: 0;
|
|
347
|
-
margin-inline: 0;
|
|
348
|
-
|
|
349
|
-
outline: 1px solid light-dark(#00000090, #f00ff090);
|
|
350
|
-
overflow-x: auto;
|
|
351
|
-
|
|
352
|
-
.thumbnail-item {
|
|
353
|
-
|
|
354
|
-
display: flex;
|
|
355
|
-
align-items: center;
|
|
356
|
-
justify-content: center;
|
|
447
|
+
align-items: center;
|
|
448
|
+
justify-content: center;
|
|
357
449
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
outline: 1px solid light-dark(#f00, #00f);
|
|
361
|
-
border-radius: 4px;
|
|
362
|
-
|
|
363
|
-
background-color: light-dark(#f00, #00f);
|
|
364
|
-
|
|
365
|
-
&:nth-child(odd) {
|
|
366
|
-
background-color: light-dark(#00f, #f00);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
.thumbnail-item_inner {}
|
|
450
|
+
cursor: pointer;
|
|
451
|
+
height: fit-content;
|
|
371
452
|
|
|
453
|
+
.arrows-icon {
|
|
454
|
+
width: 24px;
|
|
455
|
+
height: 24px;
|
|
372
456
|
}
|
|
373
457
|
}
|
|
374
458
|
}
|
|
375
459
|
}
|
|
376
|
-
|
|
377
460
|
}
|
|
378
461
|
</style>
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="carousel-infinite" :class="[elementClasses]" ref="carouselWrapperRef" role="region" aria-label="Image carousel">
|
|
3
|
+
<!-- Screen reader announcement for current item -->
|
|
4
|
+
<div aria-live="polite" aria-atomic="true" class="sr-only">Item {{ currentVisibleIndex + 1 }} of {{ itemCount }}</div>
|
|
5
|
+
|
|
6
|
+
<LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-20']">
|
|
7
|
+
<div tabindex="0" class="item-container" :class="{ 'allow-overflow': allowCarouselOverflow }" ref="carouselContainerRef" role="group" aria-label="Carousel items">
|
|
8
|
+
<div v-for="(item, index) in carouselDataIds" :key="index" class="item" ref="carouselItems" :aria-current="currentVisibleIndex === index ? 'true' : 'false'">
|
|
9
|
+
<slot :name="item"></slot>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</LayoutRow>
|
|
13
|
+
|
|
14
|
+
<LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-20']">
|
|
15
|
+
<div tabindex="0" class="controls-container" ref="controlsContainerRef">
|
|
16
|
+
<div class="markers-container">
|
|
17
|
+
<ul class="markers-list">
|
|
18
|
+
<li v-for="index in itemCount" :key="index" class="markers-item">
|
|
19
|
+
<button
|
|
20
|
+
@click.prevent="jumpToFrame(index - 1)"
|
|
21
|
+
class="btn-marker"
|
|
22
|
+
:class="[{ active: currentVisibleIndex === index - 1 }]"
|
|
23
|
+
:aria-label="`Jump to item ${Math.floor(index + 1)}`"
|
|
24
|
+
></button>
|
|
25
|
+
</li>
|
|
26
|
+
</ul>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="buttons-container">
|
|
29
|
+
<button type="button" @click.prevent="actionPrevious()" class="btn-action" aria-label="Go to previous item">
|
|
30
|
+
<Icon name="ic:outline-keyboard-arrow-left" class="arrows-icon" />
|
|
31
|
+
</button>
|
|
32
|
+
<button type="button" @click.prevent="actionNext()" class="btn-action" aria-label="Go to next item">
|
|
33
|
+
<Icon name="ic:outline-keyboard-arrow-right" class="arrows-icon" />
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</LayoutRow>
|
|
38
|
+
</section>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<script setup lang="ts">
|
|
42
|
+
import { useEventListener, useResizeObserver, useSwipe } from '@vueuse/core';
|
|
43
|
+
|
|
44
|
+
const props = defineProps({
|
|
45
|
+
carouselDataIds: {
|
|
46
|
+
type: Array as PropType<string[]>,
|
|
47
|
+
default: () => [],
|
|
48
|
+
},
|
|
49
|
+
styleClassPassthrough: {
|
|
50
|
+
type: Array as PropType<string[]>,
|
|
51
|
+
default: () => [],
|
|
52
|
+
},
|
|
53
|
+
transitionSpeed: {
|
|
54
|
+
type: Number,
|
|
55
|
+
default: 200,
|
|
56
|
+
},
|
|
57
|
+
allowCarouselOverflow: {
|
|
58
|
+
type: Boolean,
|
|
59
|
+
default: false,
|
|
60
|
+
},
|
|
61
|
+
returnToStart: {
|
|
62
|
+
type: Boolean,
|
|
63
|
+
default: false,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
68
|
+
|
|
69
|
+
const carouselWrapperRef = ref<HTMLDivElement | null>(null);
|
|
70
|
+
const carouselContainerRef = ref<HTMLDivElement | null>(null);
|
|
71
|
+
const carouselItemsRef = useTemplateRef<HTMLDivElement[]>('carouselItems');
|
|
72
|
+
const controlsContainerRef = ref<HTMLDivElement | null>(null);
|
|
73
|
+
const carouselInitComplete = ref(false);
|
|
74
|
+
|
|
75
|
+
const currentIndex = ref(0);
|
|
76
|
+
const itemCount = ref(props.carouselDataIds.length);
|
|
77
|
+
const transitionSpeedStr = props.transitionSpeed + 'ms';
|
|
78
|
+
|
|
79
|
+
const itemWidth = ref(0);
|
|
80
|
+
const itemWidthOffsetStr = computed(() => {
|
|
81
|
+
return `-${itemWidth.value}px`;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const currentVisibleIndex = ref(0);
|
|
85
|
+
|
|
86
|
+
const carouselContainerRefLeftPosition = computed(() => {
|
|
87
|
+
return carouselContainerRef.value ? carouselContainerRef.value.getBoundingClientRect().left : 0;
|
|
88
|
+
});
|
|
89
|
+
const fullScreenOffsset = computed(() => {
|
|
90
|
+
return `-${Math.floor(carouselContainerRefLeftPosition.value)}px`;
|
|
91
|
+
});
|
|
92
|
+
console.log('INIT: carouselContainerRefLeftPosition:', carouselContainerRefLeftPosition.value, 'fullScreenOffsset:', fullScreenOffsset.value);
|
|
93
|
+
|
|
94
|
+
const updateItemOrder = (index: number, order: number, zIndex: number = 2) => {
|
|
95
|
+
if (carouselItemsRef?.value && carouselItemsRef.value[index]) {
|
|
96
|
+
carouselItemsRef.value[index].style.order = order.toString();
|
|
97
|
+
carouselItemsRef.value[index].style.zIndex = zIndex.toString();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const reorderItems = (direction: 'next' | 'previous' | 'jump' = 'jump') => {
|
|
102
|
+
console.log(`Reordering items in direction: ${direction}`);
|
|
103
|
+
if (!carouselItemsRef?.value || !carouselInitComplete.value) return;
|
|
104
|
+
|
|
105
|
+
// Capture positions before reordering
|
|
106
|
+
const beforeRects = carouselItemsRef.value.map((item) => item.getBoundingClientRect());
|
|
107
|
+
|
|
108
|
+
// Apply new order and z-index based on direction
|
|
109
|
+
let order = 1;
|
|
110
|
+
|
|
111
|
+
// For items from currentVisibleIndex to end
|
|
112
|
+
for (let i = currentVisibleIndex.value; i < itemCount.value; i++) {
|
|
113
|
+
let zIndex = 2; // default normal z-index
|
|
114
|
+
|
|
115
|
+
if (i === currentVisibleIndex.value) {
|
|
116
|
+
// The item becoming visible
|
|
117
|
+
if (direction === 'previous') {
|
|
118
|
+
// When going previous, the item moving to first position should go behind
|
|
119
|
+
zIndex = 1;
|
|
120
|
+
} else {
|
|
121
|
+
// Normal case - visible item gets highest z-index
|
|
122
|
+
zIndex = 3;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
updateItemOrder(i, order++, zIndex);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// For items from 0 to currentVisibleIndex
|
|
130
|
+
for (let i = 0; i < currentVisibleIndex.value; i++) {
|
|
131
|
+
// Items that wrap around get lower z-index to slide behind
|
|
132
|
+
const zIndex = 1;
|
|
133
|
+
updateItemOrder(i, order++, zIndex);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const actionPrevious = () => {
|
|
138
|
+
if (!carouselInitComplete.value || !carouselItemsRef?.value) return;
|
|
139
|
+
|
|
140
|
+
if (props.returnToStart && currentVisibleIndex.value === 0) {
|
|
141
|
+
currentVisibleIndex.value = itemCount.value - 1;
|
|
142
|
+
} else {
|
|
143
|
+
currentVisibleIndex.value = currentVisibleIndex.value === 0 ? itemCount.value - 1 : currentVisibleIndex.value - 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
reorderItems('previous');
|
|
147
|
+
currentIndex.value = currentVisibleIndex.value;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const actionNext = () => {
|
|
151
|
+
if (!carouselInitComplete.value || !carouselItemsRef?.value) return;
|
|
152
|
+
|
|
153
|
+
if (props.returnToStart && currentVisibleIndex.value === itemCount.value - 1) {
|
|
154
|
+
currentVisibleIndex.value = 0;
|
|
155
|
+
} else {
|
|
156
|
+
currentVisibleIndex.value = currentVisibleIndex.value === itemCount.value - 1 ? 0 : currentVisibleIndex.value + 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
reorderItems('next');
|
|
160
|
+
currentIndex.value = currentVisibleIndex.value;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const jumpToFrame = (index: number) => {
|
|
164
|
+
if (index >= 0 && index < itemCount.value) {
|
|
165
|
+
currentVisibleIndex.value = index;
|
|
166
|
+
reorderItems('jump');
|
|
167
|
+
currentIndex.value = currentVisibleIndex.value;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const checkAndMoveLastItem = () => {
|
|
172
|
+
if (props.allowCarouselOverflow) {
|
|
173
|
+
const itemsFit = Math.floor(carouselContainerRefLeftPosition.value / itemWidth.value + 1);
|
|
174
|
+
jumpToFrame(itemCount.value - 1);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const initialSetup = () => {
|
|
179
|
+
if (carouselItemsRef?.value && carouselItemsRef.value.length > 0 && carouselItemsRef.value[0]) {
|
|
180
|
+
itemWidth.value = carouselItemsRef.value[0].offsetWidth;
|
|
181
|
+
|
|
182
|
+
// Set initial order and z-index for all items
|
|
183
|
+
carouselItemsRef.value.forEach((item, index) => {
|
|
184
|
+
item.style.order = String(index + 1);
|
|
185
|
+
// First item gets higher z-index, others get normal z-index
|
|
186
|
+
item.style.zIndex = index === 0 ? '3' : '2';
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
carouselInitComplete.value = true;
|
|
191
|
+
checkAndMoveLastItem();
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const { direction } = useSwipe(carouselContainerRef, {
|
|
195
|
+
passive: false,
|
|
196
|
+
onSwipeEnd() {
|
|
197
|
+
if (direction.value === 'left') {
|
|
198
|
+
actionNext();
|
|
199
|
+
} else if (direction.value === 'right') {
|
|
200
|
+
actionPrevious();
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
useEventListener(carouselContainerRef, 'keydown', (event: KeyboardEvent) => {
|
|
206
|
+
if (event.key === 'ArrowLeft') {
|
|
207
|
+
actionPrevious();
|
|
208
|
+
} else if (event.key === 'ArrowRight') {
|
|
209
|
+
actionNext();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
useEventListener(controlsContainerRef, 'keydown', (event: KeyboardEvent) => {
|
|
214
|
+
if (event.key === 'ArrowLeft') {
|
|
215
|
+
actionPrevious();
|
|
216
|
+
} else if (event.key === 'ArrowRight') {
|
|
217
|
+
actionNext();
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
useResizeObserver(carouselWrapperRef, async () => {
|
|
222
|
+
initialSetup();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
onMounted(() => {
|
|
226
|
+
initialSetup();
|
|
227
|
+
console.log('onMounted: carouselContainerRefLeftPosition:', carouselContainerRefLeftPosition.value, 'fullScreenOffsset:', fullScreenOffsset.value);
|
|
228
|
+
});
|
|
229
|
+
</script>
|
|
230
|
+
|
|
231
|
+
<style lang="css">
|
|
232
|
+
.carousel-infinite {
|
|
233
|
+
--_carousel-item-track-gap: 10px;
|
|
234
|
+
|
|
235
|
+
display: grid;
|
|
236
|
+
grid-template-columns: 1fr;
|
|
237
|
+
gap: 10px;
|
|
238
|
+
|
|
239
|
+
.sr-only {
|
|
240
|
+
position: absolute;
|
|
241
|
+
width: 1px;
|
|
242
|
+
height: 1px;
|
|
243
|
+
padding: 0;
|
|
244
|
+
margin: -1px;
|
|
245
|
+
overflow: hidden;
|
|
246
|
+
clip: rect(0, 0, 0, 0);
|
|
247
|
+
white-space: nowrap;
|
|
248
|
+
border: 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.item-container {
|
|
252
|
+
display: flex;
|
|
253
|
+
gap: var(--_carousel-item-track-gap);
|
|
254
|
+
overflow-x: hidden;
|
|
255
|
+
position: relative;
|
|
256
|
+
|
|
257
|
+
&.allow-overflow {
|
|
258
|
+
overflow-x: initial;
|
|
259
|
+
|
|
260
|
+
.item {
|
|
261
|
+
translate: calc(v-bind(itemWidthOffsetStr) - var(--_carousel-item-track-gap)) 0;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.item {
|
|
266
|
+
display: flex;
|
|
267
|
+
flex: 0 0 100%;
|
|
268
|
+
max-inline-size: 800px;
|
|
269
|
+
position: relative;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.controls-container {
|
|
274
|
+
display: flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
justify-content: flex-end;
|
|
277
|
+
|
|
278
|
+
.markers-container {
|
|
279
|
+
.markers-list {
|
|
280
|
+
display: flex;
|
|
281
|
+
flex-direction: row;
|
|
282
|
+
gap: 10px;
|
|
283
|
+
list-style-type: none;
|
|
284
|
+
margin: unset;
|
|
285
|
+
padding: unset;
|
|
286
|
+
|
|
287
|
+
.markers-item {
|
|
288
|
+
.btn-marker {
|
|
289
|
+
border: none;
|
|
290
|
+
outline: none;
|
|
291
|
+
box-shadow: none;
|
|
292
|
+
cursor: pointer;
|
|
293
|
+
transition: background-color v-bind(transitionSpeedStr) linear;
|
|
294
|
+
|
|
295
|
+
&.active {
|
|
296
|
+
background-color: red;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.buttons-container {
|
|
304
|
+
display: flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
justify-content: end;
|
|
307
|
+
gap: 20px;
|
|
308
|
+
|
|
309
|
+
.btn-action {
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
justify-content: center;
|
|
313
|
+
|
|
314
|
+
cursor: pointer;
|
|
315
|
+
height: fit-content;
|
|
316
|
+
|
|
317
|
+
.arrows-icon {
|
|
318
|
+
width: 24px;
|
|
319
|
+
height: 24px;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
</style>
|
package/package.json
CHANGED