@c4h/chuci 0.1.0

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.
@@ -0,0 +1,362 @@
1
+ import { ChuciElement } from '@/utils/base-element'
2
+ import Swiper from 'swiper'
3
+ import { Navigation, Pagination, Scrollbar, Autoplay, Thumbs, Keyboard } from 'swiper/modules'
4
+
5
+ // Import Swiper styles as strings to inject into shadow DOM
6
+ import swiperStyles from './swiper-styles.css?inline'
7
+
8
+ export class CcSwiper extends ChuciElement {
9
+ private slider?: Swiper
10
+ private divContainer?: HTMLDivElement
11
+ private divSlides?: HTMLDivElement
12
+ private divGallery?: HTMLDivElement
13
+ private divPagination?: HTMLDivElement
14
+ private divPrevious?: HTMLDivElement
15
+ private divNext?: HTMLDivElement
16
+
17
+ static get observedAttributes() {
18
+ return ['has-thumb', 'autoplay']
19
+ }
20
+
21
+ get hasThumb() {
22
+ return this.hasAttribute('has-thumb')
23
+ }
24
+
25
+ get autoplay() {
26
+ return this.hasAttribute('autoplay')
27
+ }
28
+
29
+ get slides() {
30
+ return [
31
+ ...Array.from(this.querySelectorAll('cc-swiper-slide')),
32
+ ...Array.from(this.divSlides?.querySelectorAll('cc-swiper-slide') ?? [])
33
+ ]
34
+ }
35
+
36
+ async openViewer(imageUrl: string, imageType: string, slideIndex?: number) {
37
+ let ccView = document.querySelector("cc-viewer")
38
+ if (!ccView) {
39
+ const viewerElement = document.createElement("cc-viewer")
40
+ document.body.appendChild(viewerElement)
41
+
42
+ // Wait for custom element to be defined and connected
43
+ await customElements.whenDefined('cc-viewer')
44
+
45
+ // Use a small timeout to ensure the element is fully initialized
46
+ ccView = await new Promise((res) => {
47
+ setTimeout(() => {
48
+ res(document.querySelector("cc-viewer"))
49
+ }, 100)
50
+ })
51
+ }
52
+
53
+ // Store current swiper reference and slide index in viewer
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ (ccView as any).setSwiper(this);
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ (ccView as any).setCurrentSlideIndex(slideIndex ?? this.slider?.activeIndex ?? 0);
58
+
59
+ // Get the slide element to extract attributes
60
+ const slide = this.slides[slideIndex ?? this.slider?.activeIndex ?? 0];
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ const attributes: Record<string, any> = {};
63
+
64
+ // Check for viewer-specific attributes
65
+ if (slide?.hasAttribute('fit-to-container')) {
66
+ attributes.fitToContainer = true;
67
+ }
68
+ if (slide?.hasAttribute('debug-mode')) {
69
+ attributes.debugMode = true;
70
+ }
71
+ if (slide?.hasAttribute('camera-position')) {
72
+ attributes.cameraPosition = slide.getAttribute('camera-position');
73
+ }
74
+ if (slide?.hasAttribute('camera-target')) {
75
+ attributes.cameraTarget = slide.getAttribute('camera-target');
76
+ }
77
+ if (slide?.hasAttribute('show-texture')) {
78
+ attributes.showTexture = slide.getAttribute('show-texture') === 'true';
79
+ }
80
+
81
+ // For 3D models, pass material-url as attribute
82
+ if (imageType === '3dmodel' && slide?.hasAttribute('material-url')) {
83
+ attributes.materialUrl = slide.getAttribute('material-url');
84
+ }
85
+
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ ;(ccView as any).open(imageUrl, imageType, attributes)
88
+ }
89
+
90
+ protected firstUpdated() {
91
+ // Initialization is now done in render method after DOM update
92
+ }
93
+
94
+ protected render() {
95
+ // Inject Swiper styles
96
+ const swiperStyleTag = `
97
+ <style>
98
+ ${swiperStyles}
99
+ </style>
100
+ `
101
+
102
+ const styles = this.css`
103
+ :host {
104
+ display: block;
105
+ height: 100%;
106
+ width: 100%;
107
+ --swiper-theme-color: var(--cc-slider-theme-color, #007aff);
108
+ --swiper-navigation-color: var(--cc-slider-navigation-color, #007aff);
109
+ --swiper-gallery-height: 0px;
110
+ --swiper-slider-margin-bottom: 0px;
111
+ --swiper-navigation-size: 44px;
112
+ }
113
+
114
+ :host([has-thumb]) {
115
+ --swiper-slider-margin-bottom: 10px;
116
+ --swiper-gallery-height: calc(100px - var(--swiper-slider-margin-bottom));
117
+ }
118
+
119
+ #divContainer {
120
+ height: calc(100% - var(--swiper-gallery-height) - var(--swiper-slider-margin-bottom));
121
+ margin-bottom: var(--swiper-slider-margin-bottom);
122
+ }
123
+
124
+ .swiper {
125
+ height: 100%;
126
+ }
127
+
128
+ #divGallery {
129
+ height: var(--swiper-gallery-height);
130
+ }
131
+
132
+ .gallery-thumbs .swiper-slide {
133
+ height: 100%;
134
+ opacity: 0.25;
135
+ transition: 200ms;
136
+ cursor: pointer;
137
+ }
138
+
139
+ .gallery-thumbs .swiper-slide-thumb-active {
140
+ opacity: 1;
141
+ }
142
+
143
+ .gallery-thumb {
144
+ background-position: center !important;
145
+ background-repeat: no-repeat !important;
146
+ background-size: cover !important;
147
+ }
148
+
149
+ .swiper-wrapper {
150
+ text-align: center;
151
+ }
152
+
153
+ .swiper-slide {
154
+ background-color: white;
155
+ height: 100%;
156
+ }
157
+
158
+ img.viewer {
159
+ object-fit: contain;
160
+ height: 100%;
161
+ width: 100%;
162
+ cursor: pointer;
163
+ pointer-events: auto !important;
164
+ user-select: none;
165
+ }
166
+
167
+ img.viewer.w-caption {
168
+ height: calc(100% - 10px - 1.5rem);
169
+ }
170
+
171
+ .slider-caption {
172
+ padding: 5px;
173
+ margin: 0;
174
+ line-height: 1.5em;
175
+ background: #000000;
176
+ color: #ffffff;
177
+ font-size: 0.6rem;
178
+ font-weight: 700;
179
+ position: absolute;
180
+ bottom: 0;
181
+ left: 0;
182
+ right: 0;
183
+ z-index: 10;
184
+ }
185
+
186
+ /* Adjust pagination position when caption exists */
187
+ .swiper-pagination {
188
+ bottom: 10px !important;
189
+ }
190
+
191
+ /* When captions exist, move pagination up */
192
+ #divContainer.has-captions .swiper-pagination {
193
+ bottom: calc(1.5rem + 20px) !important;
194
+ }
195
+
196
+ /* Navigation button styles with SVG icons */
197
+ .swiper-button-prev,
198
+ .swiper-button-next {
199
+ color: var(--swiper-navigation-color);
200
+ font-size: 0; /* Hide text */
201
+ width: var(--swiper-navigation-size);
202
+ height: var(--swiper-navigation-size);
203
+ }
204
+
205
+ .swiper-button-prev:after {
206
+ content: '';
207
+ display: block;
208
+ width: var(--swiper-navigation-size);
209
+ height: var(--swiper-navigation-size);
210
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23007aff'%3E%3Cpath d='M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z'/%3E%3C/svg%3E");
211
+ background-size: contain;
212
+ background-repeat: no-repeat;
213
+ background-position: center;
214
+ }
215
+
216
+ .swiper-button-next:after {
217
+ content: '';
218
+ display: block;
219
+ width: var(--swiper-navigation-size);
220
+ height: var(--swiper-navigation-size);
221
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23007aff'%3E%3Cpath d='M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z'/%3E%3C/svg%3E");
222
+ background-size: contain;
223
+ background-repeat: no-repeat;
224
+ background-position: center;
225
+ }
226
+ `
227
+
228
+ const slidesHtml = this.slides.map((slide, index) => {
229
+ const thumbnailUrl = slide.getAttribute('thumbnail-url') || ''
230
+ const imageUrl = slide.getAttribute('image-url') || ''
231
+ const imageType = slide.getAttribute('image-type') || 'image'
232
+ const caption = slide.getAttribute('caption') || ''
233
+
234
+ return `
235
+ <div class='swiper-slide'>
236
+ <img src="${thumbnailUrl}" data-image-url="${imageUrl}" data-image-type="${imageType}" data-index="${index}" class="viewer${caption !== "" ? ` w-caption` : ""}">
237
+ ${caption !== "" ? `<p class="slider-caption">${caption}</p>` : ""}
238
+ </div>
239
+ `
240
+ }).join('')
241
+
242
+ const galleryHtml = this.slides.map((slide, index) => {
243
+ const thumbnailUrl = slide.getAttribute('thumbnail-url') || ''
244
+ return `
245
+ <div class='swiper-slide gallery-thumb' data-index="${index}" style="background-image: url('${thumbnailUrl}')"></div>
246
+ `
247
+ }).join('')
248
+
249
+ const html = `
250
+ ${swiperStyleTag}
251
+ ${styles}
252
+ <div id='divContainer' class='swiper gallery-top'>
253
+ <div id='divSlides' class='swiper-wrapper'>
254
+ ${slidesHtml}
255
+ </div>
256
+
257
+ <div id='divPagination' class='swiper-pagination'></div>
258
+ <div id='divPrevious' class='swiper-button-prev'></div>
259
+ <div id='divNext' class='swiper-button-next'></div>
260
+ </div>
261
+ <div id='divGallery' class='swiper gallery-thumbs'>
262
+ <div class='swiper-wrapper'>
263
+ ${galleryHtml}
264
+ </div>
265
+ </div>
266
+ `
267
+
268
+ this.updateShadowRoot(html)
269
+
270
+ // Initialize Swiper after DOM update
271
+ setTimeout(() => {
272
+ this.initializeSwiper()
273
+
274
+ // Add click handlers for gallery thumbs
275
+ this.queryAll('.gallery-thumb').forEach((thumb, index) => {
276
+ thumb.addEventListener('click', () => this.slider?.slideTo(index))
277
+ })
278
+
279
+ // Add click handlers for viewer images
280
+ this.queryAll('img.viewer').forEach((img) => {
281
+ // Prevent default image behavior
282
+ img.addEventListener('dragstart', (e) => e.preventDefault())
283
+
284
+ img.addEventListener('click', (e) => {
285
+ e.preventDefault()
286
+ e.stopPropagation()
287
+ e.stopImmediatePropagation()
288
+ const target = e.target as HTMLImageElement
289
+ const imageUrl = target.getAttribute('data-image-url') || ''
290
+ const imageType = target.getAttribute('data-image-type') || 'image'
291
+ const index = parseInt(target.getAttribute('data-index') || '0', 10)
292
+ this.openViewer(imageUrl, imageType, index)
293
+ return false
294
+ }, true)
295
+ })
296
+ }, 0)
297
+ }
298
+
299
+ private initializeSwiper() {
300
+ this.divContainer = this.query('#divContainer') ?? undefined
301
+ this.divSlides = this.query('#divSlides') ?? undefined
302
+ this.divGallery = this.query('#divGallery') ?? undefined
303
+ this.divPagination = this.query('#divPagination') ?? undefined
304
+ this.divPrevious = this.query('#divPrevious') ?? undefined
305
+ this.divNext = this.query('#divNext') ?? undefined
306
+
307
+ // Check if any slides have captions
308
+ const hasCaptions = this.slides.some(slide => slide.getAttribute('caption'))
309
+ if (hasCaptions && this.divContainer) {
310
+ this.divContainer.classList.add('has-captions')
311
+ }
312
+
313
+ // Core library features at https://swiperjs.com/api/#custom-build
314
+ const slidesLoop = this.slides.length >= 2
315
+ if (!this.divContainer) return
316
+
317
+ // Destroy existing slider if any
318
+ if (this.slider) {
319
+ this.slider.destroy()
320
+ }
321
+
322
+ this.slider = new Swiper(this.divContainer, {
323
+ modules: [Navigation, Pagination, Scrollbar, Autoplay, Thumbs, Keyboard],
324
+ navigation: {
325
+ prevEl: this.divPrevious,
326
+ nextEl: this.divNext,
327
+ },
328
+ pagination: this.hasThumb ? {} : {
329
+ el: this.divPagination
330
+ },
331
+ autoplay: this.autoplay ? {
332
+ delay: 5000,
333
+ disableOnInteraction: false,
334
+ reverseDirection: false,
335
+ stopOnLastSlide: false,
336
+ waitForTransition: true,
337
+ } : false,
338
+ thumbs: this.hasThumb && this.divGallery ? {
339
+ swiper: new Swiper(this.divGallery, {
340
+ spaceBetween: 10,
341
+ slidesPerView: Math.min(Math.max(4, this.slides.length), 8),
342
+ watchSlidesProgress: true,
343
+ }),
344
+ } : {},
345
+ preventClicks: false,
346
+ preventClicksPropagation: false,
347
+ simulateTouch: true,
348
+ allowTouchMove: true,
349
+ loop: slidesLoop
350
+ })
351
+ }
352
+ }
353
+
354
+ if (!customElements.get('cc-swiper')) {
355
+ customElements.define('cc-swiper', CcSwiper)
356
+ }
357
+
358
+ declare global {
359
+ interface HTMLElementTagNameMap {
360
+ 'cc-swiper': CcSwiper
361
+ }
362
+ }
@@ -0,0 +1,5 @@
1
+ /* Import all Swiper styles */
2
+ @import 'swiper/css';
3
+ @import 'swiper/css/navigation';
4
+ @import 'swiper/css/pagination';
5
+ @import 'swiper/css/scrollbar';