peek-carousel 1.0.2 → 1.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.
@@ -4,1373 +4,9 @@
4
4
  * @license MIT
5
5
  * @author lledellebell
6
6
  */
7
- (function (global, factory) {
8
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
9
- typeof define === 'function' && define.amd ? define(factory) :
10
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.PeekCarousel = factory());
11
- })(this, (function () { 'use strict';
12
-
13
- const LAYOUT_MODES = Object.freeze({
14
- STACK: 'stack',
15
- RADIAL: 'radial',
16
- CLASSIC: 'classic'
17
- });
18
- const DEFAULT_OPTIONS = Object.freeze({
19
- startIndex: 1,
20
- layoutMode: LAYOUT_MODES.STACK,
21
- autoRotate: false,
22
- autoRotateInterval: 2500,
23
- preloadRange: 2,
24
- swipeThreshold: 50,
25
- dragThreshold: 80,
26
- enableKeyboard: true,
27
- enableWheel: true,
28
- enableTouch: true,
29
- enableMouse: true,
30
- showNavigation: true,
31
- showCounter: true,
32
- showIndicators: true,
33
- showAutoRotateButton: true
34
- });
35
- function validateOptions(options) {
36
- const validated = {
37
- ...DEFAULT_OPTIONS,
38
- ...options
39
- };
40
- if (validated.startIndex < 0) {
41
- console.warn('PeekCarousel: startIndex는 0 이상이어야 합니다. 기본값 1 사용');
42
- validated.startIndex = 1;
43
- }
44
- if (!Object.values(LAYOUT_MODES).includes(validated.layoutMode)) {
45
- console.warn(`PeekCarousel: 유효하지 않은 layoutMode "${validated.layoutMode}". 기본값 "stack" 사용`);
46
- validated.layoutMode = LAYOUT_MODES.STACK;
47
- }
48
- if (validated.autoRotateInterval < 100) {
49
- console.warn('PeekCarousel: autoRotateInterval은 100ms 이상이어야 합니다. 기본값 2500 사용');
50
- validated.autoRotateInterval = 2500;
51
- }
52
- if (validated.preloadRange < 0) {
53
- console.warn('PeekCarousel: preloadRange는 0 이상이어야 합니다. 기본값 2 사용');
54
- validated.preloadRange = 2;
55
- }
56
- return validated;
57
- }
58
-
59
- const CLASS_NAMES = Object.freeze({
60
- carousel: 'peek-carousel',
61
- track: 'peek-carousel__track',
62
- item: 'peek-carousel__item',
63
- itemActive: 'peek-carousel__item--active',
64
- itemPrev: 'peek-carousel__item--prev',
65
- itemNext: 'peek-carousel__item--next',
66
- itemCenter: 'peek-carousel__item--center',
67
- itemHidden: 'peek-carousel__item--hidden',
68
- itemDraggingLeft: 'peek-carousel__item--dragging-left',
69
- itemDraggingRight: 'peek-carousel__item--dragging-right',
70
- figure: 'peek-carousel__figure',
71
- image: 'peek-carousel__image',
72
- caption: 'peek-carousel__caption',
73
- nav: 'peek-carousel__nav',
74
- navBtn: 'nav-btn',
75
- btn: 'peek-carousel__btn',
76
- prevBtn: 'prev-btn',
77
- nextBtn: 'next-btn',
78
- autoRotateBtn: 'auto-rotate-btn',
79
- btnAutoRotate: 'peek-carousel__btn--auto-rotate',
80
- btnActive: 'peek-carousel__btn--active',
81
- controls: 'peek-carousel__controls',
82
- indicators: 'peek-carousel__indicators',
83
- indicator: 'indicator',
84
- indicatorPeek: 'peek-carousel__indicator',
85
- indicatorActive: 'peek-carousel__indicator--active',
86
- indicatorProgress: 'peek-carousel__indicator--progress',
87
- indicatorCompleted: 'peek-carousel__indicator--completed',
88
- counter: 'peek-carousel__counter',
89
- counterCurrent: 'peek-carousel__counter-current',
90
- counterSeparator: 'peek-carousel__counter-separator',
91
- counterTotal: 'peek-carousel__counter-total',
92
- playIcon: 'play-icon',
93
- pauseIcon: 'pause-icon'
94
- });
95
- const SELECTORS = Object.freeze({
96
- carousel: '.peek-carousel__track',
97
- item: '.peek-carousel__item',
98
- indicator: '.indicator',
99
- prevBtn: '.prev-btn',
100
- nextBtn: '.next-btn',
101
- autoRotateBtn: '.auto-rotate-btn',
102
- playIcon: '.play-icon',
103
- pauseIcon: '.pause-icon',
104
- image: 'img'
105
- });
106
- const ARIA = Object.freeze({
107
- current: 'aria-current',
108
- selected: 'aria-selected',
109
- pressed: 'aria-pressed',
110
- label: 'aria-label',
111
- tabindex: 'tabindex'
112
- });
113
- const BREAKPOINTS = Object.freeze({
114
- mobile: 768 // [개발참고] px
115
- });
116
- const DURATIONS = Object.freeze({
117
- transition: 500,
118
- // [개발참고] ms
119
- progressReset: 10 // [개발참고] ms
120
- });
121
- const KEYS = Object.freeze({
122
- arrowLeft: 'ArrowLeft',
123
- arrowRight: 'ArrowRight',
124
- home: 'Home',
125
- end: 'End',
126
- space: ' '
127
- });
128
-
129
- function getElement(selector) {
130
- if (typeof selector === 'string') {
131
- return document.querySelector(selector);
132
- }
133
- return selector instanceof HTMLElement ? selector : null;
134
- }
135
- function getElements(selector, parent = document) {
136
- if (typeof selector !== 'string') return [];
137
- return Array.from(parent.querySelectorAll(selector));
138
- }
139
- function addClass(element, ...classes) {
140
- if (element instanceof HTMLElement && classes.length > 0) {
141
- element.classList.add(...classes);
142
- }
143
- }
144
- function removeClass(element, ...classes) {
145
- if (element instanceof HTMLElement && classes.length > 0) {
146
- element.classList.remove(...classes);
147
- }
148
- }
149
- function setCSSVar(element, property, value) {
150
- if (element instanceof HTMLElement && property) {
151
- element.style.setProperty(property, value);
152
- }
153
- }
154
- function setAttribute(element, name, value) {
155
- if (element instanceof HTMLElement && name) {
156
- element.setAttribute(name, value);
157
- }
158
- }
159
-
160
- const loadingCache = new Map();
161
- function preloadImage(src) {
162
- return new Promise((resolve, reject) => {
163
- if (!src) {
164
- reject(new Error('이미지 소스가 제공되지 않았습니다'));
165
- return;
166
- }
167
- if (loadingCache.has(src)) {
168
- return loadingCache.get(src);
169
- }
170
- const img = new Image();
171
- const promise = new Promise((res, rej) => {
172
- img.onload = () => {
173
- loadingCache.delete(src);
174
- res(img);
175
- };
176
- img.onerror = () => {
177
- loadingCache.delete(src);
178
- rej(new Error(`이미지 로드 실패: ${src}`));
179
- };
180
- img.src = src;
181
- });
182
- loadingCache.set(src, promise);
183
- promise.then(resolve, reject);
184
- });
185
- }
186
- function preloadImages(sources) {
187
- if (!sources || sources.length === 0) {
188
- return Promise.resolve([]);
189
- }
190
- const uniqueSources = [...new Set(sources)];
191
- return Promise.all(uniqueSources.map(src => preloadImage(src)));
192
- }
193
- function preloadImagesInRange(items, currentIndex, range) {
194
- if (!items || items.length === 0 || range < 0) {
195
- return;
196
- }
197
- const totalItems = items.length;
198
- const imagesToPreload = new Set();
199
- for (let distance = 1; distance <= range; distance++) {
200
- const prevIndex = (currentIndex - distance + totalItems) % totalItems;
201
- const nextIndex = (currentIndex + distance) % totalItems;
202
- const prevImg = items[prevIndex]?.querySelector('img');
203
- const nextImg = items[nextIndex]?.querySelector('img');
204
- if (prevImg && prevImg.src && !prevImg.complete) {
205
- imagesToPreload.add(prevImg.src);
206
- }
207
- if (nextImg && nextImg.src && !nextImg.complete) {
208
- imagesToPreload.add(nextImg.src);
209
- }
210
- }
211
- if (imagesToPreload.size > 0) {
212
- preloadImages([...imagesToPreload]).catch(err => {
213
- console.warn('일부 이미지 프리로드 실패:', err);
214
- });
215
- }
216
- }
217
-
218
- const iconCache = new Map();
219
- function createSVGIcon(path, options = {}) {
220
- const {
221
- width = 24,
222
- height = 24,
223
- viewBox = '0 0 24 24',
224
- fill = 'none',
225
- stroke = 'currentColor',
226
- strokeWidth = 2,
227
- strokeLinecap = 'round',
228
- strokeLinejoin = 'round',
229
- className = ''
230
- } = options;
231
- const cacheKey = `${path}-${JSON.stringify(options)}`;
232
- if (iconCache.has(cacheKey)) {
233
- return iconCache.get(cacheKey);
234
- }
235
- const svg = `<svg width="${width}" height="${height}" viewBox="${viewBox}" fill="${fill}" xmlns="http://www.w3.org/2000/svg" class="${className}"><path d="${path}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-linecap="${strokeLinecap}" stroke-linejoin="${strokeLinejoin}"/></svg>`;
236
- iconCache.set(cacheKey, svg);
237
- return svg;
238
- }
239
- const ICONS = {
240
- prev: {
241
- path: 'M15 18L9 12L15 6',
242
- options: {}
243
- },
244
- next: {
245
- path: 'M9 18L15 12L9 6',
246
- options: {}
247
- },
248
- play: {
249
- path: 'M8 6.5v11l9-5.5z',
250
- options: {
251
- fill: 'currentColor',
252
- stroke: 'currentColor',
253
- strokeWidth: 0,
254
- strokeLinecap: 'round',
255
- strokeLinejoin: 'round'
256
- }
257
- },
258
- pause: {
259
- path: 'M7 5.5C7 5.22386 7.22386 5 7.5 5H9.5C9.77614 5 10 5.22386 10 5.5V18.5C10 18.7761 9.77614 19 9.5 19H7.5C7.22386 19 7 18.7761 7 18.5V5.5ZM14 5.5C14 5.22386 14.2239 5 14.5 5H16.5C16.7761 5 17 5.22386 17 5.5V18.5C17 18.7761 16.7761 19 16.5 19H14.5C14.2239 19 14 18.7761 14 18.5V5.5Z',
260
- options: {
261
- fill: 'currentColor',
262
- stroke: 'none'
263
- }
264
- }
265
- };
266
- function getIcon(iconName, customOptions = {}) {
267
- const icon = ICONS[iconName];
268
- if (!icon) {
269
- console.warn(`PeekCarousel: 아이콘 "${iconName}"을 찾을 수 없습니다`);
270
- return '';
271
- }
272
- const options = {
273
- ...icon.options,
274
- ...customOptions
275
- };
276
- return createSVGIcon(icon.path, options);
277
- }
278
- function injectIcon(button, iconName, options = {}) {
279
- if (!button || !(button instanceof HTMLElement)) return;
280
- if (button.querySelector('svg')) {
281
- return;
282
- }
283
- const iconHTML = getIcon(iconName, options);
284
- if (iconHTML) {
285
- button.innerHTML = iconHTML;
286
- }
287
- }
288
- function injectAutoRotateIcons(button) {
289
- if (!button || !(button instanceof HTMLElement)) return;
290
- if (button.querySelector('svg')) {
291
- return;
292
- }
293
- const playHTML = getIcon('play', {
294
- className: 'play-icon'
295
- });
296
- const pauseHTML = getIcon('pause', {
297
- className: 'pause-icon'
298
- });
299
- if (playHTML && pauseHTML) {
300
- button.innerHTML = playHTML + pauseHTML;
301
- }
302
- }
303
-
304
- function isMobile() {
305
- return window.innerWidth <= 768;
306
- }
307
- function normalizeIndex(index, length) {
308
- if (length <= 0) return 0;
309
- return (index % length + length) % length;
310
- }
311
-
312
- const PROXIMITY_THRESHOLD = 2;
313
- class Navigator {
314
- constructor(carousel) {
315
- this.carousel = carousel;
316
- }
317
- get currentIndex() {
318
- return this.carousel.state.currentIndex;
319
- }
320
- set currentIndex(value) {
321
- this.carousel.state.currentIndex = normalizeIndex(value, this.carousel.totalItems);
322
- }
323
- getShortestDistance(from, to) {
324
- const total = this.carousel.totalItems;
325
- const normalizedTo = normalizeIndex(to, total);
326
- const normalizedFrom = normalizeIndex(from, total);
327
- const forwardDist = (normalizedTo - normalizedFrom + total) % total;
328
- const backwardDist = (normalizedFrom - normalizedTo + total) % total;
329
- return forwardDist <= backwardDist ? forwardDist : -backwardDist;
330
- }
331
- isNearby(from, to) {
332
- const distance = Math.abs(this.getShortestDistance(from, to));
333
- return distance <= PROXIMITY_THRESHOLD;
334
- }
335
- updateAfterNavigation() {
336
- this.carousel.animator.updateCarousel();
337
- this.carousel.updateCounter();
338
- if (this.carousel.options.preloadRange > 0) {
339
- this.carousel.preloadImages();
340
- }
341
- }
342
- rotate(direction) {
343
- this.currentIndex = this.currentIndex + direction;
344
- this.updateAfterNavigation();
345
- }
346
- next() {
347
- this.rotate(1);
348
- }
349
- prev() {
350
- this.rotate(-1);
351
- }
352
- goTo(index) {
353
- const normalizedIndex = normalizeIndex(index, this.carousel.totalItems);
354
- if (normalizedIndex === this.currentIndex) return;
355
- this.currentIndex = normalizedIndex;
356
- this.updateAfterNavigation();
357
- }
358
- navigateIfDifferent(targetIndex, callback) {
359
- const normalizedIndex = normalizeIndex(targetIndex, this.carousel.totalItems);
360
- if (normalizedIndex === this.currentIndex) return false;
361
- callback(normalizedIndex);
362
- return true;
363
- }
364
- handleItemClick(index) {
365
- this.navigateIfDifferent(index, normalizedIndex => {
366
- const {
367
- layoutMode
368
- } = this.carousel.options;
369
- if (layoutMode === 'radial') {
370
- this.handleRadialItemClick(normalizedIndex);
371
- } else {
372
- this.handleStackItemClick(normalizedIndex);
373
- }
374
- });
375
- }
376
- handleRadialItemClick(normalizedIndex) {
377
- const shortestDist = this.getShortestDistance(this.currentIndex, normalizedIndex);
378
- if (Math.abs(shortestDist) > 1) {
379
- const direction = shortestDist > 0 ? 1 : -1;
380
- this.rotate(direction);
381
- } else {
382
- this.rotate(shortestDist);
383
- }
384
- }
385
- handleStackItemClick(normalizedIndex) {
386
- if (this.isNearby(this.currentIndex, normalizedIndex)) {
387
- const shortestDist = this.getShortestDistance(this.currentIndex, normalizedIndex);
388
- this.rotate(shortestDist);
389
- } else {
390
- this.goTo(normalizedIndex);
391
- }
392
- }
393
- handleIndicatorClick(index) {
394
- this.navigateIfDifferent(index, normalizedIndex => {
395
- const {
396
- layoutMode
397
- } = this.carousel.options;
398
- if (layoutMode === 'radial') {
399
- const shortestDist = this.getShortestDistance(this.currentIndex, normalizedIndex);
400
- this.rotate(shortestDist);
401
- } else {
402
- this.goTo(normalizedIndex);
403
- }
404
- });
405
- }
406
- }
407
-
408
- const RADIAL_RADIUS = 400;
409
- const CLASSIC_POSITIONS = Object.freeze({
410
- center: {
411
- x: 50,
412
- scale: 1
413
- },
414
- peek: {
415
- scale: 1
416
- },
417
- hidden: {
418
- scale: 0.85
419
- }
420
- });
421
- const CLASSIC_SPACING = Object.freeze({
422
- gapPercent: 5,
423
- additionalMobile: 40,
424
- additionalDesktop: 15,
425
- mobileBreakpoint: 768
426
- });
427
- const MOMENTUM_CONFIG = Object.freeze({
428
- friction: 0.92,
429
- minVelocity: 0.05,
430
- navigationThreshold: 1.5,
431
- dampingFactor: 0.6
432
- });
433
- const STACK_POSITION_CLASSES = [CLASS_NAMES.itemCenter, CLASS_NAMES.itemPrev, CLASS_NAMES.itemNext, CLASS_NAMES.itemHidden];
434
- class Animator {
435
- constructor(carousel) {
436
- this.carousel = carousel;
437
- this.momentumAnimation = null;
438
- this.isAnimating = false;
439
- }
440
- normalizeAngleDiff(diff) {
441
- return (diff + 180) % 360 - 180;
442
- }
443
- round(value, decimals = 2) {
444
- return Math.round(value * 10 ** decimals) / 10 ** decimals;
445
- }
446
- getAdjacentIndices(currentIndex) {
447
- return {
448
- prev: normalizeIndex(currentIndex - 1, this.carousel.totalItems),
449
- next: normalizeIndex(currentIndex + 1, this.carousel.totalItems)
450
- };
451
- }
452
- setCarouselRotation(angle) {
453
- const rounded = this.round(angle, 2);
454
- this.carousel.container.style.setProperty('--carousel-rotation', `${rounded}deg`);
455
- }
456
- setCSSVariables(element, variables) {
457
- for (const [key, value] of Object.entries(variables)) {
458
- element.style.setProperty(key, value);
459
- }
460
- }
461
- updateRadialRotation(currentIndex) {
462
- const targetAngle = -this.carousel.state.angleUnit * currentIndex;
463
- const currentRotation = this.carousel.container.style.getPropertyValue('--carousel-rotation');
464
- if (!currentRotation || currentRotation === '0deg') {
465
- this.setCarouselRotation(targetAngle);
466
- return;
467
- }
468
-
469
- // [개발참고] 최단 경로 계산: -180 ~ 180 범위로 정규화
470
- const currentAngle = parseFloat(currentRotation);
471
- const diff = this.normalizeAngleDiff(targetAngle - currentAngle);
472
- const finalAngle = currentAngle + diff;
473
- this.setCarouselRotation(finalAngle);
474
- }
475
- updateCarousel() {
476
- const {
477
- currentIndex
478
- } = this.carousel.state;
479
- const {
480
- layoutMode
481
- } = this.carousel.options;
482
- if (layoutMode === 'stack' || layoutMode === 'classic') {
483
- this.setCarouselRotation(0);
484
- } else if (layoutMode === 'radial') {
485
- this.updateRadialRotation(currentIndex);
486
- }
487
- this.updateActiveItem();
488
- }
489
- updateActiveItem() {
490
- const {
491
- currentIndex
492
- } = this.carousel.state;
493
- const {
494
- layoutMode
495
- } = this.carousel.options;
496
- this.carousel.ui.updateActiveStates(currentIndex);
497
- if (layoutMode === 'radial') {
498
- this.updateRadialPositions(currentIndex);
499
- } else if (layoutMode === 'classic') {
500
- this.updateClassicPositions(currentIndex);
501
- } else {
502
- this.updateStackPositions(currentIndex);
503
- }
504
- }
505
- updateRadialPositions(currentIndex) {
506
- const {
507
- angleUnit
508
- } = this.carousel.state;
509
- for (let i = 0; i < this.carousel.items.length; i++) {
510
- const item = this.carousel.items[i];
511
- const angle = angleUnit * i;
512
- this.setCSSVariables(item, {
513
- '--item-angle': `${this.round(angle, 2)}deg`,
514
- '--item-radius': `${RADIAL_RADIUS}px`
515
- });
516
- }
517
- const {
518
- prev,
519
- next
520
- } = this.getAdjacentIndices(currentIndex);
521
- this.carousel.ui.setPeekItems(prev, next);
522
- }
523
- updateStackPositions(currentIndex) {
524
- const {
525
- prev,
526
- next
527
- } = this.getAdjacentIndices(currentIndex);
528
- for (let i = 0; i < this.carousel.items.length; i++) {
529
- const item = this.carousel.items[i];
530
- item.classList.remove(...STACK_POSITION_CLASSES);
531
- if (i === currentIndex) {
532
- item.classList.add(CLASS_NAMES.itemCenter);
533
- } else if (i === prev) {
534
- item.classList.add(CLASS_NAMES.itemPrev);
535
- } else if (i === next) {
536
- item.classList.add(CLASS_NAMES.itemNext);
537
- } else {
538
- item.classList.add(CLASS_NAMES.itemHidden);
539
- }
540
- }
541
- }
542
- calculateClassicSpacing(containerWidth) {
543
- const itemWidth = Math.max(300, Math.min(containerWidth * 0.35, 500));
544
- const isMobile = containerWidth <= CLASSIC_SPACING.mobileBreakpoint;
545
- const itemHalfPercent = itemWidth / containerWidth * 50;
546
- const baseSpacing = itemHalfPercent + CLASSIC_SPACING.gapPercent;
547
- const additionalSpacing = isMobile ? CLASSIC_SPACING.additionalMobile : CLASSIC_SPACING.additionalDesktop;
548
- return baseSpacing + additionalSpacing;
549
- }
550
- getClassicItemPosition(itemIndex, currentIndex, itemSpacing) {
551
- const {
552
- prev,
553
- next
554
- } = this.getAdjacentIndices(currentIndex);
555
- if (itemIndex === currentIndex) {
556
- return {
557
- x: CLASSIC_POSITIONS.center.x,
558
- scale: CLASSIC_POSITIONS.center.scale
559
- };
560
- }
561
- if (itemIndex === prev) {
562
- return {
563
- x: CLASSIC_POSITIONS.center.x - itemSpacing,
564
- scale: CLASSIC_POSITIONS.peek.scale
565
- };
566
- }
567
- if (itemIndex === next) {
568
- return {
569
- x: CLASSIC_POSITIONS.center.x + itemSpacing,
570
- scale: CLASSIC_POSITIONS.peek.scale
571
- };
572
- }
573
- const distanceFromCurrent = itemIndex - currentIndex;
574
- return {
575
- x: distanceFromCurrent < 0 ? CLASSIC_POSITIONS.center.x - itemSpacing * 2 : CLASSIC_POSITIONS.center.x + itemSpacing * 2,
576
- scale: CLASSIC_POSITIONS.hidden.scale
577
- };
578
- }
579
- updateClassicPositions(currentIndex) {
580
- const {
581
- prev,
582
- next
583
- } = this.getAdjacentIndices(currentIndex);
584
- const containerWidth = this.carousel.container.offsetWidth;
585
- const itemSpacing = this.calculateClassicSpacing(containerWidth);
586
- for (let i = 0; i < this.carousel.items.length; i++) {
587
- const item = this.carousel.items[i];
588
- const {
589
- x,
590
- scale
591
- } = this.getClassicItemPosition(i, currentIndex, itemSpacing);
592
- this.setCSSVariables(item, {
593
- '--item-x': `${this.round(x, 2)}%`,
594
- '--item-scale': String(scale)
595
- });
596
- }
597
- this.carousel.ui.setPeekItems(prev, next);
598
- }
599
- startMomentum(velocity) {
600
- this.stopMomentum();
601
- let currentVelocity = velocity;
602
- const momentumStep = () => {
603
- currentVelocity *= MOMENTUM_CONFIG.friction;
604
- if (Math.abs(currentVelocity) < MOMENTUM_CONFIG.minVelocity) {
605
- this.stopMomentum();
606
- return;
607
- }
608
- if (Math.abs(currentVelocity) > MOMENTUM_CONFIG.navigationThreshold) {
609
- const direction = currentVelocity > 0 ? -1 : 1;
610
- this.carousel.navigator.rotate(direction);
611
- currentVelocity *= MOMENTUM_CONFIG.dampingFactor;
612
- }
613
- this.momentumAnimation = requestAnimationFrame(momentumStep);
614
- };
615
- this.isAnimating = true;
616
- this.momentumAnimation = requestAnimationFrame(momentumStep);
617
- }
618
- stopMomentum() {
619
- if (this.momentumAnimation) {
620
- cancelAnimationFrame(this.momentumAnimation);
621
- this.momentumAnimation = null;
622
- }
623
- this.isAnimating = false;
624
- }
625
- }
626
-
627
- class AutoRotate {
628
- constructor(carousel) {
629
- this.carousel = carousel;
630
- this.interval = null;
631
- this.isActive = false;
632
- }
633
- setActiveState(isActive) {
634
- this.isActive = isActive;
635
- this.carousel.ui.updateAutoRotateButton(isActive);
636
- }
637
- toggle() {
638
- this.isActive ? this.stop() : this.start();
639
- }
640
- start() {
641
- if (this.isActive) return;
642
- this.setActiveState(true);
643
- const rotateInterval = this.carousel.options.autoRotateInterval;
644
- this.interval = setInterval(() => {
645
- this.carousel.navigator.next();
646
- }, rotateInterval);
647
- }
648
- stop() {
649
- if (!this.isActive) return;
650
- this.setActiveState(false);
651
- if (this.interval) {
652
- clearInterval(this.interval);
653
- this.interval = null;
654
- }
655
- }
656
- destroy() {
657
- this.stop();
658
- this.carousel = null;
659
- }
660
- }
661
-
662
- const WHEEL_CONFIG = Object.freeze({
663
- threshold: 50,
664
- timeout: 150,
665
- cooldown: 100
666
- });
667
- const DRAG_CONFIG = Object.freeze({
668
- touchThreshold: 15,
669
- mouseThreshold: 10,
670
- velocityThreshold: 0.5
671
- });
672
- const RESIZE_DEBOUNCE = 100;
673
- class EventHandler {
674
- constructor(carousel) {
675
- this.carousel = carousel;
676
- this.boundHandlers = new Map();
677
- this.touch = {
678
- startX: 0,
679
- endX: 0
680
- };
681
- this.drag = {
682
- active: false,
683
- startX: 0,
684
- currentX: 0,
685
- lastX: 0,
686
- lastTime: 0,
687
- velocity: 0
688
- };
689
- this.wheel = {
690
- isScrolling: false,
691
- scrollTimeout: null,
692
- lastWheelTime: 0,
693
- accumulatedDelta: 0
694
- };
695
- }
696
- init() {
697
- this.initNavigationButtons();
698
- this.initKeyboard();
699
- this.initWheel();
700
- this.initItemClick();
701
- this.initIndicatorClick();
702
- this.initTouch();
703
- this.initMouse();
704
- this.initResize();
705
- }
706
- stopAutoRotateAndNavigate(navigationFn) {
707
- this.completeCurrentIndicator();
708
- this.carousel.autoRotate.stop();
709
- navigationFn();
710
- }
711
- completeCurrentIndicator() {
712
- const currentIndicator = this.carousel.indicators[this.carousel.state.currentIndex];
713
- if (currentIndicator && currentIndicator.classList.contains('peek-carousel__indicator--active')) {
714
- currentIndicator.classList.add('peek-carousel__indicator--completed');
715
- }
716
- }
717
- resetDragState(index) {
718
- this.carousel.ui.removeDraggingClass(index);
719
- this.carousel.ui.clearDragTransform();
720
- }
721
- updateDraggingClass(dragDistance, currentIndex, threshold) {
722
- if (dragDistance > threshold) {
723
- this.carousel.ui.addDraggingClass(currentIndex, 'right');
724
- } else if (dragDistance < -threshold) {
725
- this.carousel.ui.addDraggingClass(currentIndex, 'left');
726
- }
727
- }
728
- initDragState(clientX) {
729
- this.drag.active = true;
730
- this.drag.startX = clientX;
731
- this.drag.currentX = clientX;
732
- this.drag.lastX = clientX;
733
- this.drag.lastTime = Date.now();
734
- this.drag.velocity = 0;
735
- }
736
- resetMouseCursor() {
737
- this.carousel.elements.carousel.style.cursor = 'grab';
738
- }
739
- calculateWheelDelta(e) {
740
- const deltaX = Math.abs(e.deltaX);
741
- const deltaY = Math.abs(e.deltaY);
742
- const isHorizontal = deltaX > deltaY;
743
-
744
- // [개발참고] 수평: 왼쪽(-) = 다음, 오른쪽(+) = 이전
745
- // 수직: 아래(+) = 다음, 위(-) = 이전
746
- return isHorizontal ? -e.deltaX : e.deltaY;
747
- }
748
- resetWheelState() {
749
- this.wheel.isScrolling = false;
750
- this.wheel.accumulatedDelta = 0;
751
- }
752
- initNavigationButtons() {
753
- const {
754
- prevBtn,
755
- nextBtn,
756
- autoRotateBtn
757
- } = this.carousel.elements;
758
- if (prevBtn) {
759
- this.addHandler(prevBtn, 'click', () => {
760
- this.stopAutoRotateAndNavigate(() => this.carousel.navigator.prev());
761
- });
762
- }
763
- if (nextBtn) {
764
- this.addHandler(nextBtn, 'click', () => {
765
- this.stopAutoRotateAndNavigate(() => this.carousel.navigator.next());
766
- });
767
- }
768
- if (autoRotateBtn) {
769
- this.addHandler(autoRotateBtn, 'click', () => {
770
- this.carousel.autoRotate.toggle();
771
- });
772
- }
773
- }
774
- initKeyboard() {
775
- if (!this.carousel.options.enableKeyboard) return;
776
- const handler = e => {
777
- const {
778
- navigator,
779
- autoRotate,
780
- totalItems
781
- } = this.carousel;
782
- switch (e.key) {
783
- case KEYS.arrowLeft:
784
- autoRotate.stop();
785
- navigator.prev();
786
- break;
787
- case KEYS.arrowRight:
788
- autoRotate.stop();
789
- navigator.next();
790
- break;
791
- case KEYS.home:
792
- e.preventDefault();
793
- autoRotate.stop();
794
- navigator.goTo(0);
795
- break;
796
- case KEYS.end:
797
- e.preventDefault();
798
- autoRotate.stop();
799
- navigator.goTo(totalItems - 1);
800
- break;
801
- case KEYS.space:
802
- e.preventDefault();
803
- autoRotate.toggle();
804
- break;
805
- default:
806
- const numKey = parseInt(e.key);
807
- if (numKey >= 1 && numKey <= totalItems) {
808
- e.preventDefault();
809
- autoRotate.stop();
810
- navigator.goTo(numKey - 1);
811
- }
812
- }
813
- };
814
- this.addHandler(document, 'keydown', handler);
815
- }
816
- initWheel() {
817
- if (!this.carousel.options.enableWheel) return;
818
- const handler = e => {
819
- const deltaX = Math.abs(e.deltaX);
820
- const deltaY = Math.abs(e.deltaY);
821
- if (deltaX < 1 && deltaY < 1) {
822
- return;
823
- }
824
- if (deltaX === deltaY) {
825
- return;
826
- }
827
- e.preventDefault();
828
- const currentTime = Date.now();
829
- if (currentTime - this.wheel.lastWheelTime < WHEEL_CONFIG.cooldown) {
830
- return;
831
- }
832
- if (!this.wheel.isScrolling) {
833
- this.wheel.isScrolling = true;
834
- this.wheel.accumulatedDelta = 0;
835
- this.carousel.autoRotate.stop();
836
- this.carousel.animator.stopMomentum();
837
- }
838
- this.wheel.accumulatedDelta += this.calculateWheelDelta(e);
839
- if (Math.abs(this.wheel.accumulatedDelta) >= WHEEL_CONFIG.threshold) {
840
- const direction = this.wheel.accumulatedDelta > 0 ? 1 : -1;
841
- this.carousel.navigator.rotate(direction);
842
- this.wheel.accumulatedDelta = 0;
843
- this.wheel.lastWheelTime = currentTime;
844
- }
845
- clearTimeout(this.wheel.scrollTimeout);
846
- this.wheel.scrollTimeout = setTimeout(() => {
847
- this.resetWheelState();
848
- }, WHEEL_CONFIG.timeout);
849
- };
850
- this.addHandler(this.carousel.elements.carousel, 'wheel', handler, {
851
- passive: false
852
- });
853
- }
854
- initItemClick() {
855
- const {
856
- items
857
- } = this.carousel;
858
- for (let i = 0; i < items.length; i++) {
859
- this.addHandler(items[i], 'click', () => {
860
- this.carousel.autoRotate.stop();
861
- this.carousel.navigator.handleItemClick(i);
862
- });
863
- }
864
- }
865
- initIndicatorClick() {
866
- const {
867
- indicators
868
- } = this.carousel;
869
- for (let i = 0; i < indicators.length; i++) {
870
- this.addHandler(indicators[i], 'click', () => {
871
- this.carousel.autoRotate.stop();
872
- this.carousel.navigator.handleIndicatorClick(i);
873
- });
874
- }
875
- }
876
- initTouch() {
877
- if (!this.carousel.options.enableTouch) return;
878
- this.addHandler(this.carousel.elements.carousel, 'touchstart', e => {
879
- this.touch.startX = e.changedTouches[0].screenX;
880
- });
881
- this.addHandler(this.carousel.elements.carousel, 'touchmove', e => {
882
- const touchCurrentX = e.changedTouches[0].screenX;
883
- const dragDistance = touchCurrentX - this.touch.startX;
884
- const {
885
- currentIndex
886
- } = this.carousel.state;
887
- this.carousel.ui.updateDragTransform(dragDistance);
888
- this.updateDraggingClass(dragDistance, currentIndex, DRAG_CONFIG.touchThreshold);
889
- });
890
- this.addHandler(this.carousel.elements.carousel, 'touchend', e => {
891
- this.touch.endX = e.changedTouches[0].screenX;
892
- const swipeDistance = this.touch.endX - this.touch.startX;
893
- const {
894
- swipeThreshold
895
- } = this.carousel.options;
896
- const {
897
- currentIndex
898
- } = this.carousel.state;
899
- this.resetDragState(currentIndex);
900
- if (swipeDistance < -swipeThreshold) {
901
- this.carousel.autoRotate.stop();
902
- this.carousel.navigator.next();
903
- } else if (swipeDistance > swipeThreshold) {
904
- this.carousel.autoRotate.stop();
905
- this.carousel.navigator.prev();
906
- }
907
- });
908
- }
909
- initMouse() {
910
- if (!this.carousel.options.enableMouse) return;
911
- this.addHandler(this.carousel.elements.carousel, 'mousedown', e => {
912
- if (isMobile()) return;
913
- this.initDragState(e.clientX);
914
- this.carousel.autoRotate.stop();
915
- this.carousel.animator.stopMomentum();
916
- this.carousel.elements.carousel.style.cursor = 'grabbing';
917
- e.preventDefault();
918
- });
919
- this.addHandler(document, 'mousemove', e => {
920
- if (!this.drag.active) return;
921
- const currentTime = Date.now();
922
- const deltaTime = currentTime - this.drag.lastTime;
923
- const deltaX = e.clientX - this.drag.lastX;
924
- if (deltaTime > 0) {
925
- this.drag.velocity = deltaX / deltaTime;
926
- }
927
- this.drag.currentX = e.clientX;
928
- this.drag.lastX = e.clientX;
929
- this.drag.lastTime = currentTime;
930
- const dragDistance = this.drag.currentX - this.drag.startX;
931
- const {
932
- currentIndex
933
- } = this.carousel.state;
934
- this.carousel.ui.updateDragTransform(dragDistance);
935
- this.updateDraggingClass(dragDistance, currentIndex, DRAG_CONFIG.mouseThreshold);
936
- if (Math.abs(dragDistance) > this.carousel.options.dragThreshold) {
937
- const direction = dragDistance > 0 ? -1 : 1;
938
- this.carousel.navigator.rotate(direction);
939
- this.drag.startX = this.drag.currentX;
940
- this.resetDragState(currentIndex);
941
- }
942
- });
943
- this.addHandler(document, 'mouseup', () => {
944
- if (!this.drag.active) return;
945
- this.drag.active = false;
946
- this.resetMouseCursor();
947
- const {
948
- currentIndex
949
- } = this.carousel.state;
950
- this.resetDragState(currentIndex);
951
- if (Math.abs(this.drag.velocity) > DRAG_CONFIG.velocityThreshold) {
952
- this.carousel.animator.startMomentum(this.drag.velocity);
953
- }
954
- });
955
- this.addHandler(this.carousel.elements.carousel, 'mouseleave', () => {
956
- if (this.drag.active) {
957
- this.drag.active = false;
958
- this.resetMouseCursor();
959
- const {
960
- currentIndex
961
- } = this.carousel.state;
962
- this.resetDragState(currentIndex);
963
- }
964
- });
965
- if (window.innerWidth > BREAKPOINTS.mobile) {
966
- this.resetMouseCursor();
967
- }
968
- }
969
- initResize() {
970
- let resizeTimer;
971
- const handler = () => {
972
- clearTimeout(resizeTimer);
973
- resizeTimer = setTimeout(() => {
974
- this.carousel.animator.updateCarousel();
975
- }, RESIZE_DEBOUNCE);
976
- };
977
- this.addHandler(window, 'resize', handler);
978
- }
979
- addHandler(element, event, handler, options) {
980
- element.addEventListener(event, handler, options);
981
- const key = `${event}-${Date.now()}-${Math.random()}`;
982
- this.boundHandlers.set(key, {
983
- element,
984
- event,
985
- handler,
986
- options
987
- });
988
- }
989
- destroy() {
990
- if (this.wheel.scrollTimeout) {
991
- clearTimeout(this.wheel.scrollTimeout);
992
- this.wheel.scrollTimeout = null;
993
- }
994
- for (const {
995
- element,
996
- event,
997
- handler,
998
- options
999
- } of this.boundHandlers.values()) {
1000
- element.removeEventListener(event, handler, options);
1001
- }
1002
- this.boundHandlers.clear();
1003
- this.carousel = null;
1004
- }
1005
- }
1006
-
1007
- const DRAG_TRANSFORM_CONFIG = Object.freeze({
1008
- stack: {
1009
- maxDrag: 200,
1010
- offsetMultiplier: 100,
1011
- rotationMultiplier: 3
1012
- },
1013
- radial: {
1014
- rotationSensitivity: 0.2
1015
- },
1016
- classic: {
1017
- dragSensitivity: 0.5
1018
- }
1019
- });
1020
- class UIManager {
1021
- constructor(carousel) {
1022
- this.carousel = carousel;
1023
- }
1024
- updateActiveStates(currentIndex) {
1025
- for (let i = 0; i < this.carousel.items.length; i++) {
1026
- const item = this.carousel.items[i];
1027
- removeClass(item, CLASS_NAMES.itemActive, CLASS_NAMES.itemPrev, CLASS_NAMES.itemNext);
1028
- item.removeAttribute(ARIA.current);
1029
- }
1030
- for (let i = 0; i < this.carousel.indicators.length; i++) {
1031
- const indicator = this.carousel.indicators[i];
1032
- removeClass(indicator, CLASS_NAMES.indicatorActive, CLASS_NAMES.indicatorProgress);
1033
- setAttribute(indicator, ARIA.selected, 'false');
1034
- setAttribute(indicator, ARIA.tabindex, '-1');
1035
- }
1036
- const currentItem = this.carousel.items[currentIndex];
1037
- const currentIndicator = this.carousel.indicators[currentIndex];
1038
- if (currentItem) {
1039
- addClass(currentItem, CLASS_NAMES.itemActive);
1040
- setAttribute(currentItem, ARIA.current, 'true');
1041
- }
1042
- if (currentIndicator) {
1043
- removeClass(currentIndicator, CLASS_NAMES.indicatorCompleted);
1044
- addClass(currentIndicator, CLASS_NAMES.indicatorActive);
1045
- setAttribute(currentIndicator, ARIA.selected, 'true');
1046
- setAttribute(currentIndicator, ARIA.tabindex, '0');
1047
- if (this.carousel.autoRotate.isActive) {
1048
- this.updateIndicatorProgress(currentIndicator);
1049
- }
1050
- }
1051
- }
1052
- updateIndicatorProgress(indicator) {
1053
- setCSSVar(indicator, '--progress-duration', `${this.carousel.options.autoRotateInterval}ms`);
1054
- setTimeout(() => {
1055
- if (indicator) {
1056
- addClass(indicator, CLASS_NAMES.indicatorProgress);
1057
- }
1058
- }, DURATIONS.progressReset);
1059
- }
1060
- clearPeekItems() {
1061
- for (let i = 0; i < this.carousel.items.length; i++) {
1062
- const item = this.carousel.items[i];
1063
- removeClass(item, CLASS_NAMES.itemPrev, CLASS_NAMES.itemNext);
1064
- }
1065
- }
1066
- setPeekItems(prevIndex, nextIndex) {
1067
- const prevItem = this.carousel.items[prevIndex];
1068
- const nextItem = this.carousel.items[nextIndex];
1069
- if (prevItem) addClass(prevItem, CLASS_NAMES.itemPrev);
1070
- if (nextItem) addClass(nextItem, CLASS_NAMES.itemNext);
1071
- }
1072
- updateAutoRotateButton(isActive) {
1073
- const {
1074
- autoRotateBtn
1075
- } = this.carousel.elements;
1076
- if (!autoRotateBtn) return;
1077
- if (isActive) {
1078
- addClass(autoRotateBtn, CLASS_NAMES.btnActive);
1079
- setAttribute(autoRotateBtn, ARIA.pressed, 'true');
1080
- } else {
1081
- removeClass(autoRotateBtn, CLASS_NAMES.btnActive);
1082
- setAttribute(autoRotateBtn, ARIA.pressed, 'false');
1083
- }
1084
- }
1085
- addDraggingClass(index, direction) {
1086
- const item = this.carousel.items[index];
1087
- if (!item) return;
1088
- const leftClass = CLASS_NAMES.itemDraggingLeft;
1089
- const rightClass = CLASS_NAMES.itemDraggingRight;
1090
- removeClass(item, leftClass, rightClass);
1091
- if (direction === 'left') {
1092
- addClass(item, leftClass);
1093
- } else if (direction === 'right') {
1094
- addClass(item, rightClass);
1095
- }
1096
- }
1097
- removeDraggingClass(index) {
1098
- const item = this.carousel.items[index];
1099
- if (!item) return;
1100
- removeClass(item, CLASS_NAMES.itemDraggingLeft, CLASS_NAMES.itemDraggingRight);
1101
- }
1102
- round(value, decimals = 2) {
1103
- return Math.round(value * 10 ** decimals) / 10 ** decimals;
1104
- }
1105
- applyEasing(progress) {
1106
- return progress * (2 - Math.abs(progress));
1107
- }
1108
- updateDragTransform(dragDistance) {
1109
- const {
1110
- layoutMode
1111
- } = this.carousel.options;
1112
- if (layoutMode === 'stack') {
1113
- // [개발참고] Stack 모드: 탄성 효과 적용 (easeOutQuad)
1114
- const config = DRAG_TRANSFORM_CONFIG.stack;
1115
- const clampedDrag = Math.max(-config.maxDrag, Math.min(config.maxDrag, dragDistance));
1116
- const progress = clampedDrag / config.maxDrag;
1117
- const easedProgress = this.applyEasing(progress);
1118
- const dragOffset = this.round(easedProgress * config.offsetMultiplier);
1119
- const dragRotation = this.round(easedProgress * config.rotationMultiplier);
1120
- this.carousel.container.style.setProperty('--drag-offset', `${dragOffset}px`);
1121
- this.carousel.container.style.setProperty('--drag-rotation', `${dragRotation}deg`);
1122
- } else if (layoutMode === 'radial') {
1123
- const config = DRAG_TRANSFORM_CONFIG.radial;
1124
- const dragRotation = this.round(dragDistance * config.rotationSensitivity);
1125
- this.carousel.container.style.setProperty('--drag-rotation-y', `${dragRotation}deg`);
1126
- } else if (layoutMode === 'classic') {
1127
- const config = DRAG_TRANSFORM_CONFIG.classic;
1128
- const dragOffset = this.round(dragDistance * config.dragSensitivity);
1129
- this.carousel.container.style.setProperty('--drag-offset', `${dragOffset}px`);
1130
- }
1131
- }
1132
- clearDragTransform() {
1133
- this.carousel.container.style.setProperty('--drag-offset', '0px');
1134
- this.carousel.container.style.setProperty('--drag-rotation', '0deg');
1135
- this.carousel.container.style.setProperty('--drag-rotation-y', '0deg');
1136
- }
1137
- destroy() {}
1138
- }
1139
-
1140
- const FULL_CIRCLE_DEGREES = 360;
1141
- class PeekCarousel {
1142
- constructor(selector, options = {}) {
1143
- this.container = getElement(selector);
1144
- if (!this.container) {
1145
- throw new Error(`PeekCarousel: 셀렉터 "${selector}"에 해당하는 컨테이너를 찾을 수 없습니다`);
1146
- }
1147
- this.options = validateOptions(options);
1148
- this.initElements();
1149
- if (this.items.length === 0) {
1150
- throw new Error('PeekCarousel: 캐러셀 아이템을 찾을 수 없습니다');
1151
- }
1152
- this.state = {
1153
- currentIndex: this.options.startIndex,
1154
- angleUnit: FULL_CIRCLE_DEGREES / this.totalItems
1155
- };
1156
- this.initModules();
1157
- this.init();
1158
- }
1159
- initElements() {
1160
- this.elements = {
1161
- carousel: this.container.querySelector(SELECTORS.carousel),
1162
- prevBtn: null,
1163
- nextBtn: null,
1164
- autoRotateBtn: null,
1165
- controls: null,
1166
- nav: null
1167
- };
1168
- this.items = getElements(SELECTORS.item, this.container);
1169
- this.totalItems = this.items.length;
1170
- this.indicators = [];
1171
- }
1172
- initModules() {
1173
- this.navigator = new Navigator(this);
1174
- this.animator = new Animator(this);
1175
- this.autoRotate = new AutoRotate(this);
1176
- this.eventHandler = new EventHandler(this);
1177
- this.ui = new UIManager(this);
1178
- }
1179
- init() {
1180
- this.updateLayoutClass();
1181
- this.createNavigation();
1182
- this.createControls();
1183
- this.injectIcons();
1184
- this.createCounter();
1185
- this.setImageLoadingAttributes();
1186
- this.initCSSVariables();
1187
- this.eventHandler.init();
1188
- this.animator.updateCarousel();
1189
- if (this.options.autoRotate) {
1190
- this.autoRotate.start();
1191
- }
1192
- if (this.options.preloadRange > 0) {
1193
- this.preloadImages();
1194
- }
1195
- }
1196
- initCSSVariables() {
1197
- this.container.style.setProperty('--carousel-rotation', '0deg');
1198
- this.container.style.setProperty('--drag-offset', '0px');
1199
- this.container.style.setProperty('--drag-rotation', '0deg');
1200
- this.container.style.setProperty('--drag-rotation-y', '0deg');
1201
- }
1202
- createNavigation() {
1203
- if (!this.options.showNavigation) return;
1204
- const existingNav = this.container.querySelector(`.${CLASS_NAMES.nav}`);
1205
- if (existingNav) {
1206
- this.elements.nav = existingNav;
1207
- this.elements.prevBtn = existingNav.querySelector(SELECTORS.prevBtn);
1208
- this.elements.nextBtn = existingNav.querySelector(SELECTORS.nextBtn);
1209
- return;
1210
- }
1211
- const nav = document.createElement('div');
1212
- nav.className = CLASS_NAMES.nav;
1213
- const prevBtn = document.createElement('button');
1214
- prevBtn.className = `${CLASS_NAMES.navBtn} ${CLASS_NAMES.btn} ${CLASS_NAMES.prevBtn}`;
1215
- prevBtn.setAttribute('aria-label', 'Previous');
1216
- const nextBtn = document.createElement('button');
1217
- nextBtn.className = `${CLASS_NAMES.navBtn} ${CLASS_NAMES.btn} ${CLASS_NAMES.nextBtn}`;
1218
- nextBtn.setAttribute('aria-label', 'Next');
1219
- nav.appendChild(prevBtn);
1220
- nav.appendChild(nextBtn);
1221
- this.container.appendChild(nav);
1222
- this.elements.nav = nav;
1223
- this.elements.prevBtn = prevBtn;
1224
- this.elements.nextBtn = nextBtn;
1225
- }
1226
- createControls() {
1227
- if (!this.options.showIndicators && !this.options.showAutoRotateButton) return;
1228
- const existingControls = this.container.querySelector(`.${CLASS_NAMES.controls}`);
1229
- if (existingControls) {
1230
- this.elements.controls = existingControls;
1231
- const indicatorsWrapper = existingControls.querySelector(`.${CLASS_NAMES.indicators}`);
1232
- if (indicatorsWrapper && this.options.showIndicators) {
1233
- indicatorsWrapper.innerHTML = '';
1234
- this.createIndicators(indicatorsWrapper);
1235
- }
1236
- this.elements.autoRotateBtn = existingControls.querySelector(SELECTORS.autoRotateBtn);
1237
- return;
1238
- }
1239
- const controls = document.createElement('div');
1240
- controls.className = CLASS_NAMES.controls;
1241
- if (this.options.showIndicators) {
1242
- const indicatorsWrapper = document.createElement('div');
1243
- indicatorsWrapper.className = CLASS_NAMES.indicators;
1244
- this.createIndicators(indicatorsWrapper);
1245
- controls.appendChild(indicatorsWrapper);
1246
- }
1247
- if (this.options.showAutoRotateButton) {
1248
- const autoRotateBtn = document.createElement('button');
1249
- autoRotateBtn.className = `${CLASS_NAMES.autoRotateBtn} ${CLASS_NAMES.btn} ${CLASS_NAMES.btnAutoRotate}`;
1250
- autoRotateBtn.setAttribute('aria-label', 'Toggle auto-rotate');
1251
- autoRotateBtn.setAttribute('aria-pressed', 'false');
1252
- controls.appendChild(autoRotateBtn);
1253
- this.elements.autoRotateBtn = autoRotateBtn;
1254
- }
1255
- this.container.appendChild(controls);
1256
- this.elements.controls = controls;
1257
- }
1258
- createIndicators(wrapper) {
1259
- this.indicators = [];
1260
- for (let i = 0; i < this.totalItems; i++) {
1261
- const indicator = document.createElement('button');
1262
- const isActive = i === this.state.currentIndex;
1263
- indicator.className = CLASS_NAMES.indicator;
1264
- indicator.classList.add(CLASS_NAMES.indicatorPeek);
1265
- indicator.setAttribute('role', 'tab');
1266
- indicator.setAttribute('aria-label', `Image ${i + 1}`);
1267
- indicator.setAttribute('aria-selected', isActive ? 'true' : 'false');
1268
- indicator.setAttribute('tabindex', isActive ? '0' : '-1');
1269
- if (isActive) {
1270
- indicator.classList.add(CLASS_NAMES.indicatorActive);
1271
- }
1272
- wrapper.appendChild(indicator);
1273
- this.indicators.push(indicator);
1274
- }
1275
- }
1276
- injectIcons() {
1277
- const {
1278
- prevBtn,
1279
- nextBtn,
1280
- autoRotateBtn
1281
- } = this.elements;
1282
- if (prevBtn) injectIcon(prevBtn, 'prev');
1283
- if (nextBtn) injectIcon(nextBtn, 'next');
1284
- if (autoRotateBtn) injectAutoRotateIcons(autoRotateBtn);
1285
- }
1286
- createCounter() {
1287
- if (!this.options.showCounter) return;
1288
- const existingCounter = this.container.querySelector(`.${CLASS_NAMES.counter}`);
1289
- if (existingCounter) {
1290
- this.counterElement = existingCounter;
1291
- this.updateCounter();
1292
- return;
1293
- }
1294
- const counter = document.createElement('div');
1295
- counter.className = CLASS_NAMES.counter;
1296
- counter.setAttribute('aria-live', 'polite');
1297
- counter.setAttribute('aria-atomic', 'true');
1298
- counter.innerHTML = `
1299
- <span class="${CLASS_NAMES.counterCurrent}">${this.state.currentIndex + 1}</span>
1300
- <span class="${CLASS_NAMES.counterSeparator}">/</span>
1301
- <span class="${CLASS_NAMES.counterTotal}">${this.totalItems}</span>
1302
- `;
1303
- this.container.appendChild(counter);
1304
- this.counterElement = counter;
1305
- }
1306
- updateCounter() {
1307
- if (!this.counterElement) return;
1308
- const currentSpan = this.counterElement.querySelector(`.${CLASS_NAMES.counterCurrent}`);
1309
- if (currentSpan) {
1310
- currentSpan.textContent = this.state.currentIndex + 1;
1311
- }
1312
- }
1313
- setImageLoadingAttributes() {
1314
- const {
1315
- startIndex
1316
- } = this.options;
1317
- const preloadRange = this.options.preloadRange || 1;
1318
- for (let index = 0; index < this.items.length; index++) {
1319
- const item = this.items[index];
1320
- const img = item.querySelector(`.${CLASS_NAMES.image}`);
1321
- if (!img || img.hasAttribute('loading')) continue;
1322
- const distance = Math.abs(index - startIndex);
1323
- const isNearby = distance <= preloadRange;
1324
- img.setAttribute('loading', isNearby ? 'eager' : 'lazy');
1325
- }
1326
- }
1327
- updateLayoutClass() {
1328
- const currentMode = this.currentLayoutMode;
1329
- const newMode = this.options.layoutMode;
1330
- if (currentMode && currentMode !== newMode) {
1331
- this.container.classList.remove(`peek-carousel--${currentMode}`);
1332
- }
1333
- this.container.classList.add(`peek-carousel--${newMode}`);
1334
- this.currentLayoutMode = newMode;
1335
- }
1336
- preloadImages() {
1337
- preloadImagesInRange(this.items, this.state.currentIndex, this.options.preloadRange);
1338
- }
1339
-
1340
- // [개발참고] Public API
1341
- next() {
1342
- this.navigator.next();
1343
- }
1344
- prev() {
1345
- this.navigator.prev();
1346
- }
1347
- goTo(index) {
1348
- this.navigator.goTo(index);
1349
- }
1350
- startAutoRotate() {
1351
- this.autoRotate.start();
1352
- }
1353
- stopAutoRotate() {
1354
- this.autoRotate.stop();
1355
- }
1356
- toggleAutoRotate() {
1357
- this.autoRotate.toggle();
1358
- }
1359
- destroy() {
1360
- this.autoRotate.destroy();
1361
- this.animator.stopMomentum();
1362
- this.eventHandler.destroy();
1363
- this.ui.destroy();
1364
- }
1365
- get currentIndex() {
1366
- return this.state.currentIndex;
1367
- }
1368
- get isAutoRotating() {
1369
- return this.autoRotate.isActive;
1370
- }
1371
- }
1372
-
1373
- return PeekCarousel;
1374
-
1375
- }));
7
+ (function(v,b){typeof exports=="object"&&typeof module<"u"?module.exports=b():typeof define=="function"&&define.amd?define(b):(v=typeof globalThis<"u"?globalThis:v||self,v.PeekCarousel=b())})(this,(function(){"use strict";const v=Object.freeze({STACK:"stack",RADIAL:"radial",CLASSIC:"classic"}),b=Object.freeze({startIndex:1,layoutMode:v.STACK,autoRotate:!1,autoRotateInterval:2500,preloadRange:2,swipeThreshold:50,dragThreshold:80,enableKeyboard:!0,enableWheel:!0,enableTouch:!0,enableMouse:!0,showNavigation:!0,showCounter:!0,showIndicators:!0,showAutoRotateButton:!0});function L(a){const t={...b,...a};return t.startIndex<0&&(console.warn("PeekCarousel: startIndex는 0 이상이어야 합니다. 기본값 1 사용"),t.startIndex=1),Object.values(v).includes(t.layoutMode)||(console.warn(`PeekCarousel: 유효하지 않은 layoutMode "${t.layoutMode}". 기본값 "stack" 사용`),t.layoutMode=v.STACK),t.autoRotateInterval<100&&(console.warn("PeekCarousel: autoRotateInterval은 100ms 이상이어야 합니다. 기본값 2500 사용"),t.autoRotateInterval=2500),t.preloadRange<0&&(console.warn("PeekCarousel: preloadRange는 0 이상이어야 합니다. 기본값 2 사용"),t.preloadRange=2),t}const r=Object.freeze({carousel:"peek-carousel",track:"peek-carousel__track",item:"peek-carousel__item",itemActive:"peek-carousel__item--active",itemPrev:"peek-carousel__item--prev",itemNext:"peek-carousel__item--next",itemCenter:"peek-carousel__item--center",itemHidden:"peek-carousel__item--hidden",itemDraggingLeft:"peek-carousel__item--dragging-left",itemDraggingRight:"peek-carousel__item--dragging-right",figure:"peek-carousel__figure",image:"peek-carousel__image",caption:"peek-carousel__caption",nav:"peek-carousel__nav",navBtn:"nav-btn",btn:"peek-carousel__btn",prevBtn:"prev-btn",nextBtn:"next-btn",autoRotateBtn:"auto-rotate-btn",btnAutoRotate:"peek-carousel__btn--auto-rotate",btnActive:"peek-carousel__btn--active",controls:"peek-carousel__controls",indicators:"peek-carousel__indicators",indicator:"indicator",indicatorPeek:"peek-carousel__indicator",indicatorActive:"peek-carousel__indicator--active",indicatorProgress:"peek-carousel__indicator--progress",indicatorCompleted:"peek-carousel__indicator--completed",counter:"peek-carousel__counter",counterCurrent:"peek-carousel__counter-current",counterSeparator:"peek-carousel__counter-separator",counterTotal:"peek-carousel__counter-total",playIcon:"play-icon",pauseIcon:"pause-icon"}),A=Object.freeze({carousel:".peek-carousel__track",item:".peek-carousel__item",indicator:".indicator",prevBtn:".prev-btn",nextBtn:".next-btn",autoRotateBtn:".auto-rotate-btn",playIcon:".play-icon",pauseIcon:".pause-icon",image:"img"}),p=Object.freeze({current:"aria-current",selected:"aria-selected",pressed:"aria-pressed",label:"aria-label",tabindex:"tabindex"}),H=Object.freeze({mobile:768}),E=Object.freeze({transition:500,progressReset:10}),k=Object.freeze({arrowLeft:"ArrowLeft",arrowRight:"ArrowRight",home:"Home",end:"End",space:" "});function N(a){return typeof a=="string"?document.querySelector(a):a instanceof HTMLElement?a:null}function $(a,t=document){return typeof a!="string"?[]:Array.from(t.querySelectorAll(a))}function g(a,...t){a instanceof HTMLElement&&t.length>0&&a.classList.add(...t)}function I(a,...t){a instanceof HTMLElement&&t.length>0&&a.classList.remove(...t)}function B(a,t,e){a instanceof HTMLElement&&t&&a.style.setProperty(t,e)}function y(a,t,e){a instanceof HTMLElement&&t&&a.setAttribute(t,e)}const S=new Map;function O(a){return new Promise((t,e)=>{if(!a){e(new Error("이미지 소스가 제공되지 않았습니다"));return}if(S.has(a))return S.get(a);const s=new Image,i=new Promise((o,n)=>{s.onload=()=>{S.delete(a),o(s)},s.onerror=()=>{S.delete(a),n(new Error(`이미지 로드 실패: ${a}`))},s.src=a});S.set(a,i),i.then(t,e)})}function z(a){if(!a||a.length===0)return Promise.resolve([]);const t=[...new Set(a)];return Promise.all(t.map(e=>O(e)))}function X(a,t,e){if(!a||a.length===0||e<0)return;const s=a.length,i=new Set;for(let o=1;o<=e;o++){const n=(t-o+s)%s,l=(t+o)%s,u=a[n]?.querySelector("img"),d=a[l]?.querySelector("img");u&&u.src&&!u.complete&&i.add(u.src),d&&d.src&&!d.complete&&i.add(d.src)}i.size>0&&z([...i]).catch(o=>{console.warn("일부 이미지 프리로드 실패:",o)})}const T=new Map;function j(a,t={}){const{width:e=24,height:s=24,viewBox:i="0 0 24 24",fill:o="none",stroke:n="currentColor",strokeWidth:l=2,strokeLinecap:u="round",strokeLinejoin:d="round",className:h=""}=t,c=`${a}-${JSON.stringify(t)}`;if(T.has(c))return T.get(c);const f=`<svg width="${e}" height="${s}" viewBox="${i}" fill="${o}" xmlns="http://www.w3.org/2000/svg" class="${h}"><path d="${a}" stroke="${n}" stroke-width="${l}" stroke-linecap="${u}" stroke-linejoin="${d}"/></svg>`;return T.set(c,f),f}const W={prev:{path:"M15 18L9 12L15 6",options:{}},next:{path:"M9 18L15 12L9 6",options:{}},play:{path:"M8 6.5v11l9-5.5z",options:{fill:"currentColor",stroke:"currentColor",strokeWidth:0,strokeLinecap:"round",strokeLinejoin:"round"}},pause:{path:"M7 5.5C7 5.22386 7.22386 5 7.5 5H9.5C9.77614 5 10 5.22386 10 5.5V18.5C10 18.7761 9.77614 19 9.5 19H7.5C7.22386 19 7 18.7761 7 18.5V5.5ZM14 5.5C14 5.22386 14.2239 5 14.5 5H16.5C16.7761 5 17 5.22386 17 5.5V18.5C17 18.7761 16.7761 19 16.5 19H14.5C14.2239 19 14 18.7761 14 18.5V5.5Z",options:{fill:"currentColor",stroke:"none"}}};function w(a,t={}){const e=W[a];if(!e)return console.warn(`PeekCarousel: 아이콘 "${a}"을 찾을 수 없습니다`),"";const s={...e.options,...t};return j(e.path,s)}function P(a,t,e={}){if(!a||!(a instanceof HTMLElement)||a.querySelector("svg"))return;const s=w(t,e);s&&(a.innerHTML=s)}function q(a){if(!a||!(a instanceof HTMLElement)||a.querySelector("svg"))return;const t=w("play",{className:"play-icon"}),e=w("pause",{className:"pause-icon"});t&&e&&(a.innerHTML=t+e)}function F(){return window.innerWidth<=768}function C(a,t){return t<=0?0:(a%t+t)%t}const V=2;class K{constructor(t){this.carousel=t}get currentIndex(){return this.carousel.state.currentIndex}set currentIndex(t){this.carousel.state.currentIndex=C(t,this.carousel.totalItems)}getShortestDistance(t,e){const s=this.carousel.totalItems,i=C(e,s),o=C(t,s),n=(i-o+s)%s,l=(o-i+s)%s;return n<=l?n:-l}isNearby(t,e){return Math.abs(this.getShortestDistance(t,e))<=V}updateAfterNavigation(){this.carousel.animator.updateCarousel(),this.carousel.updateCounter(),this.carousel.options.preloadRange>0&&this.carousel.preloadImages()}rotate(t){this.currentIndex=this.currentIndex+t,this.updateAfterNavigation()}next(){this.rotate(1)}prev(){this.rotate(-1)}goTo(t){const e=C(t,this.carousel.totalItems);e!==this.currentIndex&&(this.currentIndex=e,this.updateAfterNavigation())}navigateIfDifferent(t,e){const s=C(t,this.carousel.totalItems);return s===this.currentIndex?!1:(e(s),!0)}handleItemClick(t){this.navigateIfDifferent(t,e=>{const{layoutMode:s}=this.carousel.options;s==="radial"?this.handleRadialItemClick(e):this.handleStackItemClick(e)})}handleRadialItemClick(t){const e=this.getShortestDistance(this.currentIndex,t);if(Math.abs(e)>1){const s=e>0?1:-1;this.rotate(s)}else this.rotate(e)}handleStackItemClick(t){if(this.isNearby(this.currentIndex,t)){const e=this.getShortestDistance(this.currentIndex,t);this.rotate(e)}else this.goTo(t)}handleIndicatorClick(t){this.navigateIfDifferent(t,e=>{const{layoutMode:s}=this.carousel.options;if(s==="radial"){const i=this.getShortestDistance(this.currentIndex,e);this.rotate(i)}else this.goTo(e)})}}const U=400,m=Object.freeze({center:{x:50,scale:1},peek:{scale:1},hidden:{scale:.85}}),R=Object.freeze({gapPercent:5,additionalMobile:40,additionalDesktop:15,mobileBreakpoint:768}),x=Object.freeze({friction:.92,minVelocity:.05,navigationThreshold:1.5,dampingFactor:.6}),G=[r.itemCenter,r.itemPrev,r.itemNext,r.itemHidden];class Y{constructor(t){this.carousel=t,this.momentumAnimation=null,this.isAnimating=!1,this.previousIndex=null}normalizeAngleDiff(t){return(t+180)%360-180}round(t,e=2){return Math.round(t*10**e)/10**e}getAdjacentIndices(t){return{prev:C(t-1,this.carousel.totalItems),next:C(t+1,this.carousel.totalItems)}}setCarouselRotation(t){const e=this.round(t,2);this.carousel.container.style.setProperty("--carousel-rotation",`${e}deg`)}setCSSVariables(t,e){for(const[s,i]of Object.entries(e))t.style.setProperty(s,i)}updateRadialRotation(t){const e=-this.carousel.state.angleUnit*t,s=this.carousel.container.style.getPropertyValue("--carousel-rotation");if(!s||s==="0deg"){this.setCarouselRotation(e);return}const i=parseFloat(s),o=this.normalizeAngleDiff(e-i),n=i+o;this.setCarouselRotation(n)}updateCarousel(){const{currentIndex:t}=this.carousel.state,{layoutMode:e}=this.carousel.options;e==="stack"||e==="classic"?this.setCarouselRotation(0):e==="radial"&&this.updateRadialRotation(t),this.updateActiveItem()}updateActiveItem(){const{currentIndex:t}=this.carousel.state,{layoutMode:e}=this.carousel.options;this.carousel.ui.updateActiveStates(t),e==="radial"?this.updateRadialPositions(t):e==="classic"?this.updateClassicPositions(t):this.updateStackPositions(t)}updateRadialPositions(t){const{angleUnit:e}=this.carousel.state;for(let o=0;o<this.carousel.items.length;o++){const n=this.carousel.items[o],l=e*o;this.setCSSVariables(n,{"--item-angle":`${this.round(l,2)}deg`,"--item-radius":`${U}px`})}const{prev:s,next:i}=this.getAdjacentIndices(t);this.carousel.ui.setPeekItems(s,i)}updateStackPositions(t){const{prev:e,next:s}=this.getAdjacentIndices(t);for(let i=0;i<this.carousel.items.length;i++){const o=this.carousel.items[i];o.classList.remove(...G),i===t?o.classList.add(r.itemCenter):i===e?o.classList.add(r.itemPrev):i===s?o.classList.add(r.itemNext):o.classList.add(r.itemHidden)}}calculateClassicSpacing(t){const e=Math.max(300,Math.min(t*.35,500)),s=t<=R.mobileBreakpoint,o=e/t*50+R.gapPercent,n=s?R.additionalMobile:R.additionalDesktop;return o+n}getWrapInfo(t,e){if(t===null)return{isWrap:!1,direction:0};const s=this.carousel.totalItems,i=t===s-1&&e===0,o=t===0&&e===s-1;return i?{isWrap:!0,direction:1}:o?{isWrap:!0,direction:-1}:{isWrap:!1,direction:e>t?1:-1}}getClassicItemPosition(t,e,s,i=0){const{prev:o,next:n}=this.getAdjacentIndices(e),l=this.carousel.totalItems;if(t===e)return{x:m.center.x,scale:m.center.scale,isCenter:!0};if(t===o)return{x:m.center.x-s,scale:m.peek.scale,isPrev:!0};if(t===n)return{x:m.center.x+s,scale:m.peek.scale,isNext:!0};const u=(t-e+l)%l,d=(e-t+l)%l;return{x:u<d?m.center.x+s*2:m.center.x-s*2,scale:m.hidden.scale,isHidden:!0}}updateClassicPositions(t){const{prev:e,next:s}=this.getAdjacentIndices(t),i=this.carousel.container.offsetWidth,o=this.calculateClassicSpacing(i),{isWrap:n,direction:l}=this.getWrapInfo(this.previousIndex,t),u=this.carousel.items,d=this.previousIndex!==null?this.getAdjacentIndices(this.previousIndex):{prev:null,next:null};if(n){const h=new Set([t,e,s,this.previousIndex,d.prev,d.next].filter(c=>c!==null));for(let c=0;c<u.length;c++)h.has(c)||(u[c].style.transition="none");this.carousel.container.offsetHeight}for(let h=0;h<u.length;h++){const c=u[h],f=this.getClassicItemPosition(h,t,o);this.setCSSVariables(c,{"--item-x":`${this.round(f.x,2)}%`,"--item-scale":String(f.scale)}),f.isCenter?(c.style.opacity="1",c.style.visibility="visible",c.style.zIndex="100"):f.isPrev||f.isNext?(c.style.opacity="0.6",c.style.visibility="visible",c.style.zIndex="50"):(c.style.opacity="0",c.style.visibility="hidden",c.style.zIndex="0")}n&&requestAnimationFrame(()=>{for(let h=0;h<u.length;h++)u[h].style.transition=""}),this.previousIndex=t,this.carousel.ui.setPeekItems(e,s)}startMomentum(t){this.stopMomentum();let e=t;const s=()=>{if(e*=x.friction,Math.abs(e)<x.minVelocity){this.stopMomentum();return}if(Math.abs(e)>x.navigationThreshold){const i=e>0?-1:1;this.carousel.navigator.rotate(i),e*=x.dampingFactor}this.momentumAnimation=requestAnimationFrame(s)};this.isAnimating=!0,this.momentumAnimation=requestAnimationFrame(s)}stopMomentum(){this.momentumAnimation&&(cancelAnimationFrame(this.momentumAnimation),this.momentumAnimation=null),this.isAnimating=!1}}class Z{constructor(t){this.carousel=t,this.interval=null,this.isActive=!1}setActiveState(t){this.isActive=t,this.carousel.ui.updateAutoRotateButton(t)}toggle(){this.isActive?this.stop():this.start()}start(){if(this.isActive)return;this.setActiveState(!0);const t=this.carousel.options.autoRotateInterval;this.interval=setInterval(()=>{this.carousel.navigator.next()},t)}stop(){this.isActive&&(this.setActiveState(!1),this.interval&&(clearInterval(this.interval),this.interval=null))}destroy(){this.stop(),this.carousel=null}}const M=Object.freeze({threshold:50,timeout:150,cooldown:100}),D=Object.freeze({touchThreshold:15,mouseThreshold:10,velocityThreshold:.5}),J=100;let Q=0;class tt{constructor(t){this.carousel=t,this.boundHandlers=new Map,this.resizeTimer=null,this.touch={startX:0,endX:0},this.drag={active:!1,startX:0,currentX:0,lastX:0,lastTime:0,velocity:0},this.wheel={isScrolling:!1,scrollTimeout:null,lastWheelTime:0,accumulatedDelta:0}}init(){this.initNavigationButtons(),this.initKeyboard(),this.initWheel(),this.initItemClick(),this.initIndicatorClick(),this.initTouch(),this.initMouse(),this.initResize()}stopAutoRotateAndNavigate(t){this.completeCurrentIndicator(),this.carousel.autoRotate.stop(),t()}completeCurrentIndicator(){const t=this.carousel.indicators[this.carousel.state.currentIndex];t&&t.classList.contains("peek-carousel__indicator--active")&&t.classList.add("peek-carousel__indicator--completed")}resetDragState(t){this.carousel.ui.removeDraggingClass(t),this.carousel.ui.clearDragTransform()}updateDraggingClass(t,e,s){t>s?this.carousel.ui.addDraggingClass(e,"right"):t<-s&&this.carousel.ui.addDraggingClass(e,"left")}initDragState(t){this.drag.active=!0,this.drag.startX=t,this.drag.currentX=t,this.drag.lastX=t,this.drag.lastTime=Date.now(),this.drag.velocity=0}resetMouseCursor(){this.carousel.elements.carousel.style.cursor="grab"}calculateWheelDelta(t){const e=Math.abs(t.deltaX),s=Math.abs(t.deltaY);return e>s?-t.deltaX:t.deltaY}resetWheelState(){this.wheel.isScrolling=!1,this.wheel.accumulatedDelta=0}initNavigationButtons(){const{prevBtn:t,nextBtn:e,autoRotateBtn:s}=this.carousel.elements;t&&this.addHandler(t,"click",()=>{this.stopAutoRotateAndNavigate(()=>this.carousel.navigator.prev())}),e&&this.addHandler(e,"click",()=>{this.stopAutoRotateAndNavigate(()=>this.carousel.navigator.next())}),s&&this.addHandler(s,"click",()=>{this.carousel.autoRotate.toggle()})}initKeyboard(){if(!this.carousel.options.enableKeyboard)return;const t=e=>{const{navigator:s,autoRotate:i,totalItems:o}=this.carousel;switch(e.key){case k.arrowLeft:i.stop(),s.prev();break;case k.arrowRight:i.stop(),s.next();break;case k.home:e.preventDefault(),i.stop(),s.goTo(0);break;case k.end:e.preventDefault(),i.stop(),s.goTo(o-1);break;case k.space:e.preventDefault(),i.toggle();break;default:const n=parseInt(e.key);n>=1&&n<=o&&(e.preventDefault(),i.stop(),s.goTo(n-1))}};this.addHandler(document,"keydown",t)}initWheel(){if(!this.carousel.options.enableWheel)return;const t=e=>{const s=Math.abs(e.deltaX),i=Math.abs(e.deltaY);if(s<1&&i<1||s===i)return;e.preventDefault();const o=Date.now();if(!(o-this.wheel.lastWheelTime<M.cooldown)){if(this.wheel.isScrolling||(this.wheel.isScrolling=!0,this.wheel.accumulatedDelta=0,this.carousel.autoRotate.stop(),this.carousel.animator.stopMomentum()),this.wheel.accumulatedDelta+=this.calculateWheelDelta(e),Math.abs(this.wheel.accumulatedDelta)>=M.threshold){const n=this.wheel.accumulatedDelta>0?1:-1;this.carousel.navigator.rotate(n),this.wheel.accumulatedDelta=0,this.wheel.lastWheelTime=o}clearTimeout(this.wheel.scrollTimeout),this.wheel.scrollTimeout=setTimeout(()=>{this.resetWheelState()},M.timeout)}};this.addHandler(this.carousel.elements.carousel,"wheel",t,{passive:!1})}initItemClick(){const{items:t}=this.carousel;for(let e=0;e<t.length;e++)this.addHandler(t[e],"click",()=>{this.carousel.autoRotate.stop(),this.carousel.navigator.handleItemClick(e)})}initIndicatorClick(){const{indicators:t}=this.carousel;for(let e=0;e<t.length;e++)this.addHandler(t[e],"click",()=>{this.carousel.autoRotate.stop(),this.carousel.navigator.handleIndicatorClick(e)})}initTouch(){this.carousel.options.enableTouch&&(this.addHandler(this.carousel.elements.carousel,"touchstart",t=>{this.touch.startX=t.changedTouches[0].screenX}),this.addHandler(this.carousel.elements.carousel,"touchmove",t=>{const s=t.changedTouches[0].screenX-this.touch.startX,{currentIndex:i}=this.carousel.state;this.carousel.ui.updateDragTransform(s),this.updateDraggingClass(s,i,D.touchThreshold)}),this.addHandler(this.carousel.elements.carousel,"touchend",t=>{this.touch.endX=t.changedTouches[0].screenX;const e=this.touch.endX-this.touch.startX,{swipeThreshold:s}=this.carousel.options,{currentIndex:i}=this.carousel.state;this.resetDragState(i),e<-s?(this.carousel.autoRotate.stop(),this.carousel.navigator.next()):e>s&&(this.carousel.autoRotate.stop(),this.carousel.navigator.prev())}))}initMouse(){this.carousel.options.enableMouse&&(this.addHandler(this.carousel.elements.carousel,"mousedown",t=>{F()||(this.initDragState(t.clientX),this.carousel.autoRotate.stop(),this.carousel.animator.stopMomentum(),this.carousel.elements.carousel.style.cursor="grabbing",t.preventDefault())}),this.addHandler(document,"mousemove",t=>{if(!this.drag.active)return;const e=Date.now(),s=e-this.drag.lastTime,i=t.clientX-this.drag.lastX;s>0&&(this.drag.velocity=i/s),this.drag.currentX=t.clientX,this.drag.lastX=t.clientX,this.drag.lastTime=e;const o=this.drag.currentX-this.drag.startX,{currentIndex:n}=this.carousel.state;if(this.carousel.ui.updateDragTransform(o),this.updateDraggingClass(o,n,D.mouseThreshold),Math.abs(o)>this.carousel.options.dragThreshold){const l=o>0?-1:1;this.carousel.navigator.rotate(l),this.drag.startX=this.drag.currentX,this.resetDragState(n)}}),this.addHandler(document,"mouseup",()=>{if(!this.drag.active)return;this.drag.active=!1,this.resetMouseCursor();const{currentIndex:t}=this.carousel.state;this.resetDragState(t),Math.abs(this.drag.velocity)>D.velocityThreshold&&this.carousel.animator.startMomentum(this.drag.velocity)}),this.addHandler(this.carousel.elements.carousel,"mouseleave",()=>{if(this.drag.active){this.drag.active=!1,this.resetMouseCursor();const{currentIndex:t}=this.carousel.state;this.resetDragState(t)}}),window.innerWidth>H.mobile&&this.resetMouseCursor())}initResize(){const t=()=>{clearTimeout(this.resizeTimer),this.resizeTimer=setTimeout(()=>{this.carousel&&this.carousel.animator.updateCarousel()},J)};this.addHandler(window,"resize",t)}addHandler(t,e,s,i){t.addEventListener(e,s,i);const o=`${e}-${++Q}`;this.boundHandlers.set(o,{element:t,event:e,handler:s,options:i})}destroy(){this.wheel.scrollTimeout&&(clearTimeout(this.wheel.scrollTimeout),this.wheel.scrollTimeout=null),this.resizeTimer&&(clearTimeout(this.resizeTimer),this.resizeTimer=null);for(const{element:t,event:e,handler:s,options:i}of this.boundHandlers.values())t.removeEventListener(e,s,i);this.boundHandlers.clear(),this.drag.active=!1,this.drag.velocity=0,this.wheel.isScrolling=!1,this.wheel.accumulatedDelta=0,this.carousel=null}}const _=Object.freeze({stack:{maxDrag:200,offsetMultiplier:100,rotationMultiplier:3},radial:{rotationSensitivity:.2},classic:{dragSensitivity:.5}});class et{constructor(t){this.carousel=t}updateActiveStates(t){for(let i=0;i<this.carousel.items.length;i++){const o=this.carousel.items[i];I(o,r.itemActive,r.itemPrev,r.itemNext),o.removeAttribute(p.current)}for(let i=0;i<this.carousel.indicators.length;i++){const o=this.carousel.indicators[i];I(o,r.indicatorActive,r.indicatorProgress),y(o,p.selected,"false"),y(o,p.tabindex,"-1")}const e=this.carousel.items[t],s=this.carousel.indicators[t];e&&(g(e,r.itemActive),y(e,p.current,"true")),s&&(I(s,r.indicatorCompleted),g(s,r.indicatorActive),y(s,p.selected,"true"),y(s,p.tabindex,"0"),this.carousel.autoRotate.isActive&&this.updateIndicatorProgress(s))}updateIndicatorProgress(t){B(t,"--progress-duration",`${this.carousel.options.autoRotateInterval}ms`),setTimeout(()=>{t&&g(t,r.indicatorProgress)},E.progressReset)}clearPeekItems(){for(let t=0;t<this.carousel.items.length;t++){const e=this.carousel.items[t];I(e,r.itemPrev,r.itemNext)}}setPeekItems(t,e){const s=this.carousel.items[t],i=this.carousel.items[e];s&&g(s,r.itemPrev),i&&g(i,r.itemNext)}updateAutoRotateButton(t){const{autoRotateBtn:e}=this.carousel.elements;e&&(t?(g(e,r.btnActive),y(e,p.pressed,"true")):(I(e,r.btnActive),y(e,p.pressed,"false")))}addDraggingClass(t,e){const s=this.carousel.items[t];if(!s)return;const i=r.itemDraggingLeft,o=r.itemDraggingRight;I(s,i,o),e==="left"?g(s,i):e==="right"&&g(s,o)}removeDraggingClass(t){const e=this.carousel.items[t];e&&I(e,r.itemDraggingLeft,r.itemDraggingRight)}round(t,e=2){return Math.round(t*10**e)/10**e}applyEasing(t){return t*(2-Math.abs(t))}updateDragTransform(t){const{layoutMode:e}=this.carousel.options;if(e==="stack"){const s=_.stack,o=Math.max(-s.maxDrag,Math.min(s.maxDrag,t))/s.maxDrag,n=this.applyEasing(o),l=this.round(n*s.offsetMultiplier),u=this.round(n*s.rotationMultiplier);this.carousel.container.style.setProperty("--drag-offset",`${l}px`),this.carousel.container.style.setProperty("--drag-rotation",`${u}deg`)}else if(e==="radial"){const s=_.radial,i=this.round(t*s.rotationSensitivity);this.carousel.container.style.setProperty("--drag-rotation-y",`${i}deg`)}else if(e==="classic"){const s=_.classic,i=this.round(t*s.dragSensitivity);this.carousel.container.style.setProperty("--drag-offset",`${i}px`)}}clearDragTransform(){this.carousel.container.style.setProperty("--drag-offset","0px"),this.carousel.container.style.setProperty("--drag-rotation","0deg"),this.carousel.container.style.setProperty("--drag-rotation-y","0deg")}destroy(){}}const st=360;class it{constructor(t,e={}){if(this.container=N(t),!this.container)throw new Error(`PeekCarousel: 셀렉터 "${t}"에 해당하는 컨테이너를 찾을 수 없습니다`);if(this.options=L(e),this.initElements(),this.items.length===0)throw new Error("PeekCarousel: 캐러셀 아이템을 찾을 수 없습니다");this.state={currentIndex:this.options.startIndex,angleUnit:st/this.totalItems},this.initModules(),this.init()}initElements(){this.elements={carousel:this.container.querySelector(A.carousel),prevBtn:null,nextBtn:null,autoRotateBtn:null,controls:null,nav:null},this.items=$(A.item,this.container),this.totalItems=this.items.length,this.indicators=[]}initModules(){this.navigator=new K(this),this.animator=new Y(this),this.autoRotate=new Z(this),this.eventHandler=new tt(this),this.ui=new et(this)}init(){this.updateLayoutClass(),this.createNavigation(),this.createControls(),this.injectIcons(),this.createCounter(),this.setImageLoadingAttributes(),this.initCSSVariables(),this.eventHandler.init(),this.animator.updateCarousel(),this.options.autoRotate&&this.autoRotate.start(),this.options.preloadRange>0&&this.preloadImages()}initCSSVariables(){this.container.style.setProperty("--carousel-rotation","0deg"),this.container.style.setProperty("--drag-offset","0px"),this.container.style.setProperty("--drag-rotation","0deg"),this.container.style.setProperty("--drag-rotation-y","0deg")}createNavigation(){if(!this.options.showNavigation)return;const t=this.container.querySelector(`.${r.nav}`);if(t){this.elements.nav=t,this.elements.prevBtn=t.querySelector(A.prevBtn),this.elements.nextBtn=t.querySelector(A.nextBtn);return}const e=document.createElement("div");e.className=r.nav;const s=document.createElement("button");s.className=`${r.navBtn} ${r.btn} ${r.prevBtn}`,s.setAttribute("aria-label","Previous");const i=document.createElement("button");i.className=`${r.navBtn} ${r.btn} ${r.nextBtn}`,i.setAttribute("aria-label","Next"),e.appendChild(s),e.appendChild(i),this.container.appendChild(e),this.elements.nav=e,this.elements.prevBtn=s,this.elements.nextBtn=i}createControls(){if(!this.options.showIndicators&&!this.options.showAutoRotateButton)return;const t=this.container.querySelector(`.${r.controls}`);if(t){this.elements.controls=t;const s=t.querySelector(`.${r.indicators}`);s&&this.options.showIndicators&&(s.innerHTML="",this.createIndicators(s)),this.elements.autoRotateBtn=t.querySelector(A.autoRotateBtn);return}const e=document.createElement("div");if(e.className=r.controls,this.options.showIndicators){const s=document.createElement("div");s.className=r.indicators,this.createIndicators(s),e.appendChild(s)}if(this.options.showAutoRotateButton){const s=document.createElement("button");s.className=`${r.autoRotateBtn} ${r.btn} ${r.btnAutoRotate}`,s.setAttribute("aria-label","Toggle auto-rotate"),s.setAttribute("aria-pressed","false"),e.appendChild(s),this.elements.autoRotateBtn=s}this.container.appendChild(e),this.elements.controls=e}createIndicators(t){this.indicators=[];for(let e=0;e<this.totalItems;e++){const s=document.createElement("button"),i=e===this.state.currentIndex;s.className=r.indicator,s.classList.add(r.indicatorPeek),s.setAttribute("role","tab"),s.setAttribute("aria-label",`Image ${e+1}`),s.setAttribute("aria-selected",i?"true":"false"),s.setAttribute("tabindex",i?"0":"-1"),i&&s.classList.add(r.indicatorActive),t.appendChild(s),this.indicators.push(s)}}injectIcons(){const{prevBtn:t,nextBtn:e,autoRotateBtn:s}=this.elements;t&&P(t,"prev"),e&&P(e,"next"),s&&q(s)}createCounter(){if(!this.options.showCounter)return;const t=this.container.querySelector(`.${r.counter}`);if(t){this.counterElement=t,this.updateCounter();return}const e=document.createElement("div");e.className=r.counter,e.setAttribute("aria-live","polite"),e.setAttribute("aria-atomic","true"),e.innerHTML=`
8
+ <span class="${r.counterCurrent}">${this.state.currentIndex+1}</span>
9
+ <span class="${r.counterSeparator}">/</span>
10
+ <span class="${r.counterTotal}">${this.totalItems}</span>
11
+ `,this.container.appendChild(e),this.counterElement=e}updateCounter(){if(!this.counterElement)return;const t=this.counterElement.querySelector(`.${r.counterCurrent}`);t&&(t.textContent=this.state.currentIndex+1)}setImageLoadingAttributes(){const{startIndex:t}=this.options,e=this.options.preloadRange||1;for(let s=0;s<this.items.length;s++){const o=this.items[s].querySelector(`.${r.image}`);if(!o||o.hasAttribute("loading"))continue;const l=Math.abs(s-t)<=e;o.setAttribute("loading",l?"eager":"lazy")}}updateLayoutClass(){const t=this.currentLayoutMode,e=this.options.layoutMode;t&&t!==e&&this.container.classList.remove(`peek-carousel--${t}`),this.container.classList.add(`peek-carousel--${e}`),this.currentLayoutMode=e}preloadImages(){X(this.items,this.state.currentIndex,this.options.preloadRange)}next(){this.navigator.next()}prev(){this.navigator.prev()}goTo(t){this.navigator.goTo(t)}startAutoRotate(){this.autoRotate.start()}stopAutoRotate(){this.autoRotate.stop()}toggleAutoRotate(){this.autoRotate.toggle()}destroy(){this.autoRotate.destroy(),this.animator.stopMomentum(),this.eventHandler.destroy(),this.ui.destroy()}get currentIndex(){return this.state.currentIndex}get isAutoRotating(){return this.autoRotate.isActive}}return it}));
1376
12
  //# sourceMappingURL=peek-carousel.js.map