create-fesd-app 1.0.46 → 1.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fesd-app",
3
- "version": "1.0.46",
3
+ "version": "1.0.48",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-fesd-app": "bin/create-fesd-app.mjs"
@@ -19,11 +19,15 @@
19
19
  "dependencies": {
20
20
  "@tailwindcss/postcss": "^4.1.10",
21
21
  "@tailwindcss/vite": "^4.1.10",
22
- "@xwadex/fesd": "0.0.68",
22
+ "@xwadex/fesd": "0.0.70",
23
23
  "ansi-colors": "^4.1.3",
24
24
  "chalk": "^5.3.0",
25
25
  "clsx": "^2.1.1",
26
26
  "colorthief": "^2.4.0",
27
+ "embla-carousel": "^8.6.0",
28
+ "embla-carousel-autoplay": "^8.6.0",
29
+ "embla-carousel-class-names": "^8.6.0",
30
+ "embla-carousel-fade": "^8.6.0",
27
31
  "commander": "^12.1.0",
28
32
  "flatpickr": "^4.6.13",
29
33
  "gsap": "^3.12.5",
@@ -47,7 +51,7 @@
47
51
  "@vituum/vite-plugin-pug": "^1.1.0",
48
52
  "sass": "1.77.4",
49
53
  "sass-migrator": "^2.1.0",
50
- "vite": "^5.4.8",
54
+ "vite": "7.3.3",
51
55
  "vite-plugin-compression": "^0.5.1",
52
56
  "vite-plugin-imagemin": "^0.6.1"
53
57
  },
@@ -0,0 +1,100 @@
1
+ // 固定不改 可以用變數條整
2
+ // --slide-spacing: 0px
3
+ .embla
4
+ --slide-size: 30%
5
+ position: relative
6
+ overflow: hidden
7
+ width: 100%
8
+ .embla__viewport
9
+ overflow: hidden
10
+ width: 100%
11
+ .embla__container
12
+ display: flex
13
+ touch-action: pan-y pinch-zoom
14
+ margin-left: calc(var(--slide-spacing, 0) * -1)
15
+ .embla__slide
16
+ transform: translate3d(0, 0, 0)
17
+ flex: 0 0 var(--slide-size, 100%)
18
+ min-width: 0
19
+ padding-left: var(--slide-spacing, 0)
20
+ overflow: hidden
21
+ // no scroll
22
+ &.embla--noScrollable
23
+ .embla__container
24
+ transform: none !important
25
+ .embla__prev,
26
+ .embla__next,
27
+ .embla__dots
28
+ display: none !important
29
+ // parallax
30
+ .embla__parallax
31
+ height: 100%
32
+ overflow: hidden
33
+ .embla__parallax__layer
34
+ position: relative
35
+ height: 100%
36
+ width: 100%
37
+ display: flex
38
+ justify-content: center
39
+ .embla__parallax__img
40
+ max-width: none
41
+ flex: 0 0 calc(115% + (var(--slide-spacing, 0) * 2))
42
+ object-fit: cover
43
+
44
+
45
+ // 箭頭控制
46
+ .embla__controls
47
+ display: flex
48
+ justify-content: center
49
+ gap: 2.5rem
50
+ pointer-events: none
51
+ .embla__prev
52
+ >i
53
+ display: block
54
+ transform: rotate(180deg)
55
+ .embla__prev,
56
+ .embla__next
57
+ font-size: px(18)
58
+ pointer-events: auto
59
+ &[isHide]
60
+ opacity: 0.2
61
+ cursor: default
62
+ pointer-events: none
63
+
64
+ // 點點控制
65
+ .embla__dots
66
+ --bullet-size: 5px
67
+ --bullet-gap: 10px
68
+ --bullet-color-RGB: 0,0,0
69
+
70
+ display: flex
71
+ justify-content: center
72
+ align-items: center
73
+ gap: var(--bullet-gap)
74
+ .embla__dot
75
+ width: var(--bullet-size)
76
+ height: var(--bullet-size)
77
+ padding: 0
78
+ border-radius: 50%
79
+ background: rgba(var(--bullet-color-RGB), 0)
80
+ border: 1px solid rgba(var(--bullet-color-RGB), 1)
81
+ &.is-selected
82
+ background: rgba(var(--bullet-color-RGB),1)
83
+
84
+
85
+ // 滾動條
86
+ .embla__progress
87
+ border-radius: 1.875rem
88
+ background-color: rgba(0,0,0,0.1)
89
+ position: relative
90
+ height: 0.625rem
91
+ width: 200px
92
+ max-width: 90%
93
+ overflow: hidden
94
+ .embla__progressBar
95
+ background-color: rgba(0,0,0,.5)
96
+ height: 100%
97
+ width: 100%
98
+ transform: scaleX(0)
99
+ transform-origin: left
100
+ transition: transform 0.3s ease
@@ -1,3 +1,5 @@
1
+ @import ./_emblaStyle
2
+
1
3
  // Swiper
2
4
  .swiper
3
5
  width: 100%
@@ -288,16 +288,17 @@
288
288
  // 以下可修改
289
289
  font-size: 16px
290
290
  line-height: 1.2
291
- font-weight: 600
291
+ font-weight: 700
292
292
  tbody
293
293
  td
294
- &.lefthead
295
- font-size: 16px
296
- line-height: 1.2
297
- font-weight: 600
298
294
  // 以下可修改
299
295
  font-size: 16px
300
- line-height: 1.2
296
+ line-height: 1.6
297
+ font-weight: 400
298
+ letter-spacing: 0.5px
299
+ &.lefthead
300
+ line-height: 1.2
301
+ font-weight: 700
301
302
 
302
303
  %table_tipText
303
304
  // 以下可修改
@@ -291,5 +291,55 @@
291
291
  justify-content: center
292
292
  align-items: center
293
293
  cursor: pointer
294
+
295
+ section.section6
296
+ .container
297
+ padding-block: 30px
298
+ // +baseSpace(10)
299
+ // +rwdmax(1200)
300
+ // +baseSpace(1)
301
+ .titleBox
302
+ margin-bottom: px(20)
303
+ .sub-title,
304
+ .main-title-M
305
+ display: block
306
+ .embla-video-demo
307
+ --slide-size: 100%
308
+ .video-demo-card
309
+ position: relative
310
+ overflow: hidden
311
+ border-radius: 5px
312
+ background: #000
313
+ aspect-ratio: 16 / 9
314
+ .video-demo-label
315
+ position: absolute
316
+ top: px(20)
317
+ left: px(20)
318
+ z-index: 2
319
+ padding: px(10) px(14)
320
+ color: #fff
321
+ background: rgba(#000, .65)
322
+ border-radius: 5px
323
+ pointer-events: none
324
+ span,
325
+ small
326
+ display: block
327
+ small
328
+ margin-top: 2px
329
+ opacity: .7
330
+ .mediaBox,
331
+ .youtube-player,
332
+ .youku-player,
333
+ iframe
334
+ width: 100%
335
+ height: 100%
336
+ iframe
337
+ display: block
338
+ border: 0
339
+ video
340
+ width: 100%
341
+ height: 100%
342
+ object-fit: contain
343
+
294
344
  [video-cover="off"]
295
345
  padding: 0 !important
@@ -21,25 +21,30 @@ import {
21
21
  // flatpickr
22
22
  import { Mandarin } from "flatpickr/dist/l10n/zh.js"
23
23
  // swiper
24
- import { SwiperVer11, cn } from "@/plugins";
24
+ import { SwiperVer11, SwiperEmbla, cn } from "@/plugins";
25
25
  import { bannerConfig } from "@/configs"
26
-
26
+ const {
27
+ Embla,
28
+ setupVideo: setupVideoEmbla,
29
+ setupYt: setupYtEmbla,
30
+ setupYouku: setupYoukuEmbla
31
+ } = SwiperEmbla
27
32
  // swiper
28
- const { SwiperV11 } = SwiperVer11
33
+ const {
34
+ SwiperV11,
35
+ Autoplay,
36
+ setupVideo: setupVideoSwiper,
37
+ setupYt: setupYtSwiper,
38
+ setupYouku: setupYoukuSwiper,
39
+ } = SwiperVer11
29
40
  const swiperHandler = {};
30
41
 
31
42
  swiperHandler.banner = function () {
32
43
  const bannerEvents = {
33
44
  ...bannerConfig,
34
45
  autoplay: false,
35
- init() {
36
- console.log('swiper init!!');
37
- },
38
- slideChangeTransitionStart(swiper) {
39
- isVideo(swiper);
40
- },
41
46
  };
42
- const banner = new SwiperV11('.swiper', bannerEvents);
47
+ const banner = new SwiperV11('.banner .swiper', bannerEvents);
43
48
  };
44
49
 
45
50
  swiperHandler.all = function () {
@@ -125,19 +130,35 @@ const s4Handler = function () {
125
130
  };
126
131
 
127
132
  const s6Handler = function () {
128
- // 簡易版
129
- methods.collapseEvent('[data-collapse-click]', '[data-collapse]', true, true)
130
- // 公版
131
- collapse: new Collapse4('[partnerCollapse-wrapper]', {
132
- defaultOpen: false,
133
- single: true,
134
- targetStopPropagation: true,
133
+ const options = {
134
+ modules: [Autoplay],
135
+ speed: 1200,
136
+ autoplay: {
137
+ delay: 5000,
138
+ },
139
+ loop: true,
140
+ slidesPerView: 1,
135
141
  on: {
136
- afterCollapse() {
137
- console.log('afterCollapse');
138
- }
142
+ init(swiper) {
143
+ setupVideoSwiper(swiper);
144
+ setupYtSwiper(swiper, "swiper-video-demo");
145
+ setupYoukuSwiper(swiper);
146
+ },
147
+ }
148
+ }
149
+ const swiperTest = new SwiperV11('.swiper-video-demo', options)
150
+
151
+ const emblaTest = Embla({
152
+ selector: '.embla-video-demo',
153
+ autoplay: true,
154
+ loop: true,
155
+ onInit: (embla) => {
156
+ setupVideoEmbla(embla)
157
+ setupYtEmbla(embla, "embla-video-demo")
158
+ setupYoukuEmbla(embla)
139
159
  },
140
160
  })
161
+
141
162
  };
142
163
 
143
164
  const s8Handler = function () {
@@ -0,0 +1,603 @@
1
+ import EmblaCarousel from "embla-carousel";
2
+ import Autoplay from "embla-carousel-autoplay";
3
+ import Fade from "embla-carousel-fade";
4
+ import ClassNames from "embla-carousel-class-names";
5
+
6
+ export const Embla = (options) => {
7
+ const {
8
+ selector,
9
+ duration = 40,
10
+ loop = false,
11
+ hasButtons = false,
12
+ hasDots = false,
13
+ dotsDynamic = true,
14
+ dotsClickAble = false,
15
+ autoplay = false,
16
+ autoplayDelay = 6000,
17
+ autoplayStopOnHover = false,
18
+ useFade = false,
19
+ useClassNames = true,
20
+ useProgressBar = false,
21
+ usePageNumber = false,
22
+ classNamesOptions = {
23
+ selected: "is-selected",
24
+ snapped: "is-snapped",
25
+ visible: "is-visible",
26
+ },
27
+ emblaOptions = {},
28
+ onInit = () => { },
29
+ onReInit = () => { },
30
+ onSelect = () => { },
31
+ onDestroy = () => { },
32
+ onPointerDown = () => { },
33
+ onPointerUp = () => { },
34
+ } = options;
35
+
36
+ const core = createEmblaCore({
37
+ selector,
38
+ loop,
39
+ duration,
40
+ useFade,
41
+ useClassNames,
42
+ classNamesOptions,
43
+ autoplay,
44
+ autoplayDelay,
45
+ autoplayStopOnHover,
46
+ emblaOptions,
47
+ });
48
+
49
+ if (!core) return null;
50
+
51
+ const { embla, autoplayPlugin, root } = core;
52
+
53
+ function updateScrollableState() {
54
+ const scrollSnaps = embla.scrollSnapList();
55
+ const isScrollable = scrollSnaps.length > 1;
56
+
57
+ root.classList.toggle("embla--noScrollable", !isScrollable);
58
+ if (autoplay && !isScrollable) autoplayPlugin?.stop();
59
+ }
60
+
61
+ updateScrollableState();
62
+ embla.on("init", updateScrollableState);
63
+ embla.on("reInit", updateScrollableState);
64
+
65
+ if (hasButtons) addPrevNextBtnsClickHandlers(embla, root, autoplayPlugin);
66
+ if (hasDots)
67
+ addDotBtnsAndClickHandlers(
68
+ embla,
69
+ root,
70
+ autoplayPlugin,
71
+ dotsClickAble,
72
+ dotsDynamic,
73
+ );
74
+
75
+ if (useProgressBar) setupProgressBar(embla, root);
76
+ if (usePageNumber) setupPageNumber(embla, root);
77
+
78
+ // 個別定義事件
79
+ if (onInit) embla.on("init", () => onInit(embla));
80
+ if (onReInit) embla.on("reInit", () => onReInit(embla));
81
+ if (onSelect) embla.on("select", () => onSelect(embla));
82
+ if (onDestroy) embla.on("destroy", () => onDestroy(embla));
83
+ if (onPointerDown) embla.on("pointerDown", () => onPointerDown(embla));
84
+ if (onPointerUp) embla.on("pointerUp", () => onPointerUp(embla));
85
+
86
+ return embla;
87
+ };
88
+
89
+ const createEmblaCore = ({
90
+ selector,
91
+ loop,
92
+ duration,
93
+ autoplay,
94
+ autoplayDelay,
95
+ autoplayStopOnHover,
96
+ useFade,
97
+ useClassNames,
98
+ classNamesOptions,
99
+ emblaOptions,
100
+ }) => {
101
+ const root = document.querySelector(selector);
102
+ if (!root || root.classList.contains("embla--active")) return null;
103
+
104
+ const viewport = root.querySelector(".embla__viewport");
105
+ if (!viewport) return null;
106
+
107
+ const plugins = [];
108
+ const autoplayPlugin = autoplay
109
+ ? Autoplay({
110
+ delay: autoplayDelay,
111
+ stopOnInteraction: false,
112
+ stopOnMouseEnter: autoplayStopOnHover,
113
+ })
114
+ : null;
115
+
116
+ if (autoplayPlugin) plugins.push(autoplayPlugin);
117
+ if (useFade) plugins.push(Fade());
118
+ if (useClassNames) plugins.push(ClassNames(classNamesOptions));
119
+
120
+ const embla = EmblaCarousel(
121
+ viewport,
122
+ { loop, duration, ...emblaOptions },
123
+ plugins,
124
+ );
125
+
126
+ root.classList.add("embla--active");
127
+ return { embla, autoplayPlugin, root };
128
+ };
129
+
130
+ const addPrevNextBtnsClickHandlers = (embla, root, autoplayPlugin) => {
131
+ const prevBtn = root.querySelector(".embla__prev");
132
+ const nextBtn = root.querySelector(".embla__next");
133
+ if (!prevBtn || !nextBtn) return;
134
+
135
+ const scrollPrev = () => {
136
+ embla.scrollPrev();
137
+ autoplayPlugin?.reset();
138
+ };
139
+ const scrollNext = () => {
140
+ embla.scrollNext();
141
+ autoplayPlugin?.reset();
142
+ };
143
+
144
+ prevBtn.addEventListener("click", scrollPrev);
145
+ nextBtn.addEventListener("click", scrollNext);
146
+
147
+ const toggleButtons = () => {
148
+ embla.canScrollPrev()
149
+ ? prevBtn.removeAttribute("isHide")
150
+ : prevBtn.setAttribute("isHide", true);
151
+ embla.canScrollNext()
152
+ ? nextBtn.removeAttribute("isHide")
153
+ : nextBtn.setAttribute("isHide", true);
154
+ };
155
+
156
+ embla.on("init", toggleButtons);
157
+ embla.on("reInit", toggleButtons);
158
+ embla.on("select", toggleButtons);
159
+ };
160
+
161
+ // 輪播點點
162
+ const addDotBtnsAndClickHandlers = (
163
+ embla,
164
+ root,
165
+ autoplayPlugin,
166
+ dotsClickAble,
167
+ dotsDynamic,
168
+ ) => {
169
+ const dotsContainer = root.querySelector(".embla__dots");
170
+ if (!dotsContainer) return;
171
+
172
+ let track = dotsContainer;
173
+ if (dotsDynamic) {
174
+ const wrapper = dotsContainer.querySelector(".embla__dots__track__wrapper");
175
+ if (wrapper) {
176
+ const dotsTrack = wrapper.querySelector(".embla__dots__track");
177
+ if (dotsTrack) {
178
+ track = dotsTrack;
179
+ } else {
180
+ track = dotsContainer;
181
+ }
182
+ }
183
+ }
184
+
185
+ if (!track) return;
186
+
187
+ const containerStyle = getComputedStyle(dotsContainer);
188
+ const dotsWidth =
189
+ parseFloat(containerStyle.getPropertyValue("--bullet-size")) || 0;
190
+ const maxDots =
191
+ parseInt(containerStyle.getPropertyValue("--max-bullet-count")) || 5;
192
+ const inactiveScale =
193
+ parseFloat(containerStyle.getPropertyValue("--bullet-inactive-scale")) || 1;
194
+
195
+ let dotNodes = [];
196
+
197
+ function dotsBindClick(dot, index) {
198
+ if (!dotsClickAble) return;
199
+ dot.style.cursor = "pointer";
200
+ dot.addEventListener("click", () => {
201
+ embla.scrollTo(index);
202
+ autoplayPlugin?.reset();
203
+ });
204
+ }
205
+
206
+ const createDots = () => {
207
+ track.innerHTML = "";
208
+ dotNodes = [];
209
+
210
+ dotNodes = embla.scrollSnapList().map((_, index) => {
211
+ const dot = document.createElement("button");
212
+ dot.className = "embla__dot";
213
+ dot.type = "button";
214
+
215
+ dotsBindClick(dot, index);
216
+ track.appendChild(dot);
217
+ return dot;
218
+ });
219
+ };
220
+
221
+ const updateDots = () => {
222
+ const selected = embla.selectedScrollSnap();
223
+ // 預設點點
224
+ if (!dotsDynamic) {
225
+ dotNodes.forEach((dot, i) =>
226
+ dot.classList.toggle("is-selected", i === selected),
227
+ );
228
+ return;
229
+ }
230
+
231
+ // 動態點點
232
+ const total = dotNodes.length;
233
+ const half = Math.floor(maxDots / 2);
234
+
235
+ dotNodes.forEach((dot, i) => {
236
+ const distance = Math.abs(i - selected);
237
+ dot.classList.toggle("is-selected", i === selected);
238
+
239
+ const scale = distance === 0 ? 1 : inactiveScale;
240
+ dot.style.transform = `scale(${scale})`;
241
+ });
242
+
243
+ let offset = selected - half;
244
+ if (offset < 0) offset = 0;
245
+ if (offset > total - maxDots) offset = Math.max(total - maxDots, 0);
246
+
247
+ const trackStyle = getComputedStyle(track);
248
+ const gap = parseFloat(trackStyle.gap) || 0;
249
+
250
+ const translate = -(dotsWidth + gap) * offset;
251
+ track.style.transform = `translateX(${translate}px)`;
252
+ };
253
+
254
+ embla.on("init", () => {
255
+ createDots();
256
+ updateDots();
257
+ });
258
+ embla.on("reInit", () => {
259
+ createDots();
260
+ updateDots();
261
+ });
262
+ embla.on("select", updateDots);
263
+ };
264
+
265
+ const setupPageNumber = (embla, root) => {
266
+ const snapDisplay = root.querySelector(".embla__selected-snap-display");
267
+ if (!snapDisplay) return;
268
+ const updateSnapDisplay = (embla) => {
269
+ const selectedSnap = embla.selectedScrollSnap();
270
+ const snapCount = embla.scrollSnapList().length;
271
+ snapDisplay.innerHTML = `${selectedSnap + 1} / ${snapCount}`;
272
+ };
273
+ embla.on("select", updateSnapDisplay);
274
+ updateSnapDisplay(embla);
275
+ };
276
+
277
+ const setupProgressBar = (embla, root) => {
278
+ const bar = root.querySelector(".embla__progressBar");
279
+ if (!bar) return;
280
+
281
+ const applyProgress = () => {
282
+ const progress = Math.max(0, Math.min(1, embla.scrollProgress()));
283
+ bar.style.transform = `scaleX(${progress})`;
284
+ };
285
+
286
+ embla.on("init", applyProgress);
287
+ embla.on("reInit", applyProgress);
288
+ embla.on("scroll", applyProgress);
289
+ embla.on("select", applyProgress);
290
+ };
291
+
292
+ const stopAutoplayNextTick = (autoplayPlugin) => {
293
+ if (!autoplayPlugin) return;
294
+
295
+ autoplayPlugin.stop();
296
+ setTimeout(() => {
297
+ autoplayPlugin.stop();
298
+ }, 100);
299
+ };
300
+
301
+ // 串接
302
+ export const setupThumb = (
303
+ mainEmbla,
304
+ thumbEmbla,
305
+ activeClass = "is-selected",
306
+ ) => {
307
+ if (!mainEmbla || !thumbEmbla) return;
308
+ if (thumbEmbla.slideNodes().length !== mainEmbla.slideNodes().length)
309
+ console.warn("[setupThumbSync] slide not match");
310
+
311
+ const addThumbClickHandlers = () => {
312
+ thumbEmbla.slideNodes().forEach((thumb, index) => {
313
+ if (thumb.dataset.emblaThumbClickBound === "true") return;
314
+
315
+ thumb.dataset.emblaThumbClickBound = "true";
316
+ thumb.addEventListener("click", () => {
317
+ mainEmbla.scrollTo(index);
318
+ });
319
+ });
320
+ };
321
+ const updateThumbSelection = () => {
322
+ const selectedIndex = mainEmbla.selectedScrollSnap();
323
+ thumbEmbla.slideNodes().forEach((thumb, i) => {
324
+ thumb.classList.toggle(activeClass, i === selectedIndex);
325
+ });
326
+ thumbEmbla.scrollTo(selectedIndex);
327
+ };
328
+
329
+ updateThumbSelection();
330
+ addThumbClickHandlers();
331
+
332
+ mainEmbla.on("select", updateThumbSelection);
333
+ thumbEmbla.on("reInit", addThumbClickHandlers);
334
+ };
335
+
336
+ // 切換
337
+ export const switchTheme = (
338
+ embla,
339
+ containerSelectors = [],
340
+ selectorWithinSlide = ".slideContent",
341
+ attributeName = "data-theme",
342
+ ) => {
343
+ const targets = (
344
+ Array.isArray(containerSelectors)
345
+ ? containerSelectors
346
+ : [containerSelectors]
347
+ )
348
+ .map((selector) => document.querySelector(selector))
349
+ .filter(Boolean);
350
+
351
+ const slides = embla.slideNodes();
352
+ if (!targets.length) return;
353
+
354
+ const applyTheme = () => {
355
+ const index = embla.selectedScrollSnap();
356
+ const activeSlide = slides[index];
357
+ const el = activeSlide?.querySelector(selectorWithinSlide);
358
+ const theme = el?.getAttribute(attributeName);
359
+
360
+ if (theme) {
361
+ targets.forEach((target) => {
362
+ target.setAttribute(attributeName, theme);
363
+ });
364
+ }
365
+ };
366
+
367
+ embla.on("select", applyTheme);
368
+ applyTheme();
369
+ };
370
+
371
+ // video
372
+ export const setupVideo = (embla) => {
373
+ const slides = embla.slideNodes();
374
+ const autoplayPlugin = embla.plugins()?.autoplay;
375
+ if (!slides || slides.length === 1 || !autoplayPlugin) return;
376
+ let timer = null;
377
+
378
+ const clearVideoTimer = () => {
379
+ if (!timer) return;
380
+ clearTimeout(timer);
381
+ timer = null;
382
+ };
383
+
384
+ const pauseVideo = () => {
385
+ clearVideoTimer();
386
+ slides.forEach((slide) => {
387
+ const video = slide.querySelector("video");
388
+ if (!video) return;
389
+ video.removeAttribute("loop");
390
+ video.pause();
391
+ video.currentTime = 0;
392
+ video.onended = null;
393
+ });
394
+ };
395
+ const control = () => {
396
+ pauseVideo();
397
+ const index = embla.selectedScrollSnap();
398
+ const currentSlide = slides[index];
399
+ const video = currentSlide?.querySelector("video");
400
+ if (video) {
401
+ timer = setTimeout(() => {
402
+ if (autoplayPlugin.isPlaying()) autoplayPlugin.stop();
403
+ }, 150);
404
+ video.onended = () => {
405
+ if (embla.autoPlayPause) return;
406
+ embla.canScrollNext() ? embla.scrollNext() : embla.scrollTo(0);
407
+ if (!autoplayPlugin.isPlaying()) autoplayPlugin.play();
408
+ };
409
+ video.play()
410
+ }
411
+ };
412
+ control();
413
+ embla.on("select", control);
414
+ embla.on("destroy", clearVideoTimer);
415
+ };
416
+
417
+ // yt
418
+ export const setupYt = (embla, ID) => {
419
+ const slides = embla.slideNodes();
420
+ const autoplayPlugin = embla.plugins()?.autoplay;
421
+ let isDestroyed = false;
422
+ if (!slides || slides.length === 1) return;
423
+
424
+ const onYouTubeIframeAPIReady = (playerID, videoID) => {
425
+ if (!window.YT?.ready || !window.YT?.Player) return;
426
+
427
+ window.YT.ready(function () {
428
+ if (isDestroyed || !document.getElementById(playerID)) return;
429
+
430
+ new window.YT.Player(playerID, {
431
+ height: "100%",
432
+ width: "100%",
433
+ videoId: videoID,
434
+ playerVars: {
435
+ autoplay: 1, // 在讀取時自動播放影片
436
+ controls: 1, // 在播放器顯示暫停/播放按鈕
437
+ disablekb: 1,
438
+ modestbranding: 1, // 隱藏 YouTube Logo
439
+ fs: 1, // 隱藏全螢幕按鈕
440
+ iv_load_policy: 3, // 隱藏影片註解
441
+ autohide: 1,
442
+ showinfo: 0,
443
+ rel: 0,
444
+ playsinline: 1,
445
+ enablejsapi: 1,
446
+ cc_lang_pref: "zh",
447
+ cc_load_policy: 0,
448
+ mute: 1,
449
+ },
450
+ events: {
451
+ onReady: onPlayerReady,
452
+ onStateChange: onPlayerStateChange,
453
+ },
454
+ });
455
+ });
456
+ };
457
+ const onPlayerReady = (event) => {
458
+ event.target.mute();
459
+ const state = event.target.playerInfo.playerState;
460
+ switch (state) {
461
+ case 0:
462
+ if (embla.autoPlayPause) return;
463
+ embla.canScrollNext() ? embla.scrollNext() : embla.scrollTo(0);
464
+ if (autoplayPlugin && !autoplayPlugin.isPlaying()) autoplayPlugin.play();
465
+ break;
466
+ case 1:
467
+ if (autoplayPlugin) autoplayPlugin.stop();
468
+ break;
469
+ }
470
+ };
471
+ const onPlayerStateChange = (event) => {
472
+ const state = event.data;
473
+ switch (state) {
474
+ case 0:
475
+ if (embla.autoPlayPause) return;
476
+ embla.canScrollNext() ? embla.scrollNext() : embla.scrollTo(0);
477
+ if (autoplayPlugin && !autoplayPlugin.isPlaying()) autoplayPlugin.play();
478
+ break;
479
+ case 1:
480
+ if (autoplayPlugin) autoplayPlugin.stop();
481
+ break;
482
+ }
483
+ };
484
+
485
+ const getCurrentYtSlide = () => {
486
+ const activeSlide = slides[embla.selectedScrollSnap()];
487
+ return activeSlide?.querySelector("[data-type='youtube'][data-videoID]");
488
+ };
489
+
490
+ const createYtIframe = (ytVideo) => {
491
+ const videoID = ytVideo.getAttribute("data-videoID");
492
+ const playerWrap = document.createElement("div");
493
+ const playerID = `youtube-player__${ID}__${embla.selectedScrollSnap()}__${videoID}`;
494
+
495
+ playerWrap.id = playerID;
496
+ playerWrap.className = "youtube-player";
497
+ const oldPlayer = ytVideo.querySelector(".youtube-player");
498
+ if (oldPlayer) oldPlayer.remove();
499
+ ytVideo.appendChild(playerWrap);
500
+ requestAnimationFrame(() => onYouTubeIframeAPIReady(playerID, videoID));
501
+ };
502
+
503
+ const removeYtPlayers = () => {
504
+ slides.forEach((slide) => {
505
+ const players = slide.querySelectorAll(".youtube-player");
506
+ players.forEach((player) => player.remove());
507
+ });
508
+ };
509
+
510
+ const handleSelect = () => {
511
+ const ytVideo = getCurrentYtSlide();
512
+ removeYtPlayers();
513
+
514
+ if (ytVideo) {
515
+ stopAutoplayNextTick(autoplayPlugin);
516
+ if (isDestroyed || getCurrentYtSlide() !== ytVideo) return;
517
+ createYtIframe(ytVideo);
518
+ stopAutoplayNextTick(autoplayPlugin);
519
+ } else if (autoplayPlugin && !autoplayPlugin.isPlaying()) {
520
+ autoplayPlugin.play();
521
+ }
522
+ };
523
+ embla.on("select", handleSelect);
524
+ embla.on("destroy", () => {
525
+ isDestroyed = true;
526
+ removeYtPlayers();
527
+ });
528
+ handleSelect();
529
+ };
530
+
531
+ // youku
532
+ export const setupYouku = (embla) => {
533
+ const autoplayPlugin = embla.plugins()?.autoplay;
534
+ const slides = embla.slideNodes();
535
+
536
+ let youkuTimer = null;
537
+
538
+ const clearYoukuTimer = () => {
539
+ if (!youkuTimer) return;
540
+
541
+ clearTimeout(youkuTimer);
542
+ youkuTimer = null;
543
+ };
544
+
545
+ const getCurrentYoukuSlide = () => {
546
+ const activeSlide = slides[embla.selectedScrollSnap()];
547
+ return activeSlide?.querySelector("[data-type='youku'][data-videoID]");
548
+ };
549
+
550
+ const getYoukuDuration = (youkuVideo) => {
551
+ const duration = Number(youkuVideo.getAttribute("data-duration"));
552
+ return Number.isFinite(duration) && duration > 0 ? duration * 1000 : 30000;
553
+ };
554
+
555
+ const createYoukuIframe = (youkuVideo) => {
556
+ const videoID = youkuVideo.getAttribute("data-videoID");
557
+ const playerWrap = document.createElement("div");
558
+ const youkuIframe = document.createElement("iframe");
559
+
560
+ playerWrap.className = "youku-player";
561
+ youkuIframe.src = `https://player.youku.com/embed/${videoID}?rel=0&autoplay=1&muted=1`;
562
+ const oldPlayer = youkuVideo.querySelector(".youku-player");
563
+ if (oldPlayer) oldPlayer.remove();
564
+ playerWrap.appendChild(youkuIframe);
565
+ youkuVideo.appendChild(playerWrap);
566
+ };
567
+
568
+ const removeYoukuPlayers = () => {
569
+ slides.forEach((slide) => {
570
+ const players = slide.querySelectorAll(".youku-player iframe");
571
+ players.forEach((iframe) => {
572
+ iframe.src = "";
573
+ iframe.remove();
574
+ });
575
+ });
576
+ };
577
+
578
+ const handleSelect = () => {
579
+ const youkuVideo = getCurrentYoukuSlide();
580
+ clearYoukuTimer();
581
+ removeYoukuPlayers();
582
+ if (youkuVideo) {
583
+ createYoukuIframe(youkuVideo);
584
+ if (autoplayPlugin) {
585
+ stopAutoplayNextTick(autoplayPlugin);
586
+ youkuTimer = setTimeout(() => {
587
+ embla.scrollNext();
588
+ autoplayPlugin.play();
589
+ }, getYoukuDuration(youkuVideo));
590
+ }
591
+ } else if (autoplayPlugin && !autoplayPlugin.isPlaying()) {
592
+ autoplayPlugin.play();
593
+ }
594
+ };
595
+
596
+ embla.on("select", handleSelect);
597
+ embla.on("destroy", () => {
598
+ clearYoukuTimer();
599
+ removeYoukuPlayers();
600
+ });
601
+
602
+ handleSelect();
603
+ };
@@ -1,5 +1,6 @@
1
1
  export * as SwiperVer8 from "./swiperV8.plugin"
2
2
  export * as SwiperVer11 from "./swiperV11.plugin"
3
+ export * as SwiperEmbla from "./embla.plugin";
3
4
  export { default as gsap } from "./gsap.plugin"
4
5
  export * from "./lazyLoad.plugin"
5
6
  export * from "./tailwind.plugin"
@@ -24,3 +24,278 @@ export {
24
24
  EffectCreative,
25
25
  EffectCards,
26
26
  } from "swiperv11/modules"
27
+
28
+
29
+ const getSlides = (swiper) => Array.from(swiper?.slides || [])
30
+
31
+ const getActiveSlide = (swiper) => {
32
+ const slides = getSlides(swiper)
33
+ return slides[swiper?.activeIndex] || slides[swiper?.realIndex] || null
34
+ }
35
+
36
+ const stopAutoplayNextTick = (swiper) => {
37
+ const autoplay = swiper?.autoplay
38
+ if (!autoplay) return
39
+
40
+ autoplay.stop()
41
+ setTimeout(() => {
42
+ autoplay.stop()
43
+ }, 100)
44
+ }
45
+
46
+ const startAutoplay = (swiper) => {
47
+ const autoplay = swiper?.autoplay
48
+ if (!autoplay || autoplay.running) return
49
+
50
+ autoplay.start()
51
+ }
52
+
53
+ const slideNextOrFirst = (swiper) => {
54
+ if (!swiper) return
55
+
56
+ if (!swiper.params?.loop && swiper.isEnd) {
57
+ swiper.slideTo(0)
58
+ return
59
+ }
60
+
61
+ swiper.slideNext()
62
+ }
63
+
64
+ // video
65
+ export const setupVideo = (swiper) => {
66
+ const slides = getSlides(swiper)
67
+ if (!swiper || slides.length <= 1) return null
68
+
69
+ let timer = null
70
+
71
+ const clearVideoTimer = () => {
72
+ if (!timer) return
73
+ clearTimeout(timer)
74
+ timer = null
75
+ }
76
+
77
+ const pauseVideo = () => {
78
+ clearVideoTimer()
79
+ slides.forEach((slide) => {
80
+ const video = slide.querySelector("video")
81
+ if (!video) return
82
+ video.removeAttribute("loop")
83
+ video.pause()
84
+ video.currentTime = 0
85
+ video.onended = null
86
+ })
87
+ }
88
+
89
+ const control = () => {
90
+ pauseVideo()
91
+ const currentSlide = getActiveSlide(swiper)
92
+ const video = currentSlide?.querySelector("video")
93
+ if (!video) {
94
+ startAutoplay(swiper)
95
+ return
96
+ }
97
+
98
+ timer = setTimeout(() => {
99
+ if (swiper.autoplay?.running) swiper.autoplay.stop()
100
+ }, 150)
101
+
102
+ video.onended = () => {
103
+ if (swiper.autoPlayPause) return
104
+ slideNextOrFirst(swiper)
105
+ startAutoplay(swiper)
106
+ }
107
+
108
+ video.play()
109
+ }
110
+
111
+ control()
112
+ swiper.on("slideChange", control)
113
+ swiper.on("destroy", clearVideoTimer)
114
+ }
115
+
116
+ // yt
117
+ export const setupYt = (swiper, ID) => {
118
+ const slides = getSlides(swiper)
119
+ if (!swiper || slides.length <= 1) return null
120
+
121
+ let isDestroyed = false
122
+ let youtubePlayer = null
123
+
124
+ const getCurrentYtSlide = () => {
125
+ const activeSlide = getActiveSlide(swiper)
126
+ return activeSlide?.querySelector("[data-type='youtube'][data-videoID]")
127
+ }
128
+
129
+ const destroyYoutubePlayer = () => {
130
+ if (youtubePlayer?.destroy) youtubePlayer.destroy()
131
+ youtubePlayer = null
132
+
133
+ slides.forEach((slide) => {
134
+ const players = slide.querySelectorAll(".youtube-player")
135
+ players.forEach((player) => player.remove())
136
+ })
137
+ }
138
+
139
+ const handlePlayerStateChange = (event) => {
140
+ const state = event.data
141
+
142
+ switch (state) {
143
+ case 0:
144
+ if (swiper.autoPlayPause) return
145
+
146
+ slideNextOrFirst(swiper)
147
+ startAutoplay(swiper)
148
+ break
149
+ case 1:
150
+ stopAutoplayNextTick(swiper)
151
+ break
152
+ }
153
+ }
154
+
155
+ const createYtIframe = (ytVideo) => {
156
+ if (!window.YT?.ready || !window.YT?.Player) return
157
+
158
+ const videoID = ytVideo.getAttribute("data-videoID")
159
+ const playerWrap = document.createElement("div")
160
+ const playerID = `youtube-player__${ID}__${swiper.activeIndex}__${videoID}`
161
+
162
+ playerWrap.id = playerID
163
+ playerWrap.className = "youtube-player"
164
+
165
+ destroyYoutubePlayer()
166
+ ytVideo.appendChild(playerWrap)
167
+
168
+ window.YT.ready(() => {
169
+ if (isDestroyed || !document.getElementById(playerID)) return
170
+
171
+ youtubePlayer = new window.YT.Player(playerID, {
172
+ height: "100%",
173
+ width: "100%",
174
+ videoId: videoID,
175
+ playerVars: {
176
+ autoplay: 1,
177
+ controls: 1,
178
+ disablekb: 1,
179
+ modestbranding: 1,
180
+ fs: 1,
181
+ iv_load_policy: 3,
182
+ autohide: 1,
183
+ showinfo: 0,
184
+ rel: 0,
185
+ playsinline: 1,
186
+ enablejsapi: 1,
187
+ cc_lang_pref: "zh",
188
+ cc_load_policy: 0,
189
+ mute: 1,
190
+ },
191
+ events: {
192
+ onReady: (event) => {
193
+ event.target.mute()
194
+ event.target.playVideo()
195
+ swiper.youtubePlayer = event.target
196
+ console.log(swiper.youtubePlayer);
197
+ },
198
+ onStateChange: handlePlayerStateChange,
199
+ },
200
+ })
201
+ })
202
+ }
203
+
204
+ const handleSelect = () => {
205
+ const ytVideo = getCurrentYtSlide()
206
+ destroyYoutubePlayer()
207
+
208
+ if (ytVideo) {
209
+ stopAutoplayNextTick(swiper)
210
+ if (isDestroyed || getCurrentYtSlide() !== ytVideo) return
211
+
212
+ createYtIframe(ytVideo)
213
+ stopAutoplayNextTick(swiper)
214
+ } else {
215
+ startAutoplay(swiper)
216
+ }
217
+ }
218
+
219
+ swiper.on("slideChange", handleSelect)
220
+ swiper.on("destroy", () => {
221
+ isDestroyed = true
222
+ destroyYoutubePlayer()
223
+ })
224
+
225
+ handleSelect()
226
+ }
227
+
228
+ // youku
229
+ export const setupYouku = (swiper) => {
230
+ const slides = getSlides(swiper)
231
+ if (!swiper || slides.length <= 1) return null
232
+
233
+ let youkuTimer = null
234
+
235
+ const clearYoukuTimer = () => {
236
+ if (!youkuTimer) return
237
+
238
+ clearTimeout(youkuTimer)
239
+ youkuTimer = null
240
+ }
241
+
242
+ const getCurrentYoukuSlide = () => {
243
+ const activeSlide = getActiveSlide(swiper)
244
+ return activeSlide?.querySelector("[data-type='youku'][data-videoID]")
245
+ }
246
+
247
+ const createYoukuIframe = (youkuVideo) => {
248
+ const videoID = youkuVideo.getAttribute("data-videoID")
249
+ const playerWrap = document.createElement("div")
250
+ const youkuIframe = document.createElement("iframe")
251
+ playerWrap.className = "youku-player"
252
+ youkuIframe.src = `https://player.youku.com/embed/${videoID}?rel=0&autoplay=1&muted=1`
253
+ const oldPlayer = youkuVideo.querySelector(".youku-player")
254
+ if (oldPlayer) oldPlayer.remove()
255
+ playerWrap.appendChild(youkuIframe)
256
+ youkuVideo.appendChild(playerWrap)
257
+ }
258
+
259
+ const removeYoukuPlayers = () => {
260
+ slides.forEach((slide) => {
261
+ const players = slide.querySelectorAll(".youku-player")
262
+ players.forEach((player) => {
263
+ const iframe = player.querySelector("iframe")
264
+ if (iframe) iframe.src = ""
265
+ player.remove()
266
+ })
267
+ })
268
+ }
269
+
270
+ const handleSelect = () => {
271
+ const youkuVideo = getCurrentYoukuSlide()
272
+
273
+ clearYoukuTimer()
274
+ removeYoukuPlayers()
275
+
276
+ if (youkuVideo) {
277
+ const number = Number(youkuVideo.getAttribute("data-duration"))
278
+ const duration = number > 0 ? number * 1000 : 30000
279
+
280
+ createYoukuIframe(youkuVideo)
281
+ stopAutoplayNextTick(swiper)
282
+
283
+ youkuTimer = setTimeout(() => {
284
+ if (swiper.autoPlayPause) return
285
+
286
+ slideNextOrFirst(swiper)
287
+ startAutoplay(swiper)
288
+ }, duration)
289
+ } else {
290
+ startAutoplay(swiper)
291
+ }
292
+ }
293
+
294
+ swiper.on("slideChange", handleSelect)
295
+ swiper.on("destroy", () => {
296
+ clearYoukuTimer()
297
+ removeYoukuPlayers()
298
+ })
299
+
300
+ handleSelect()
301
+ }
@@ -26,6 +26,8 @@ html(lang="zh-Hant-TW" data-overlayscrollbars-initialize)
26
26
  block stylesheet
27
27
  // 主頁面 CSS
28
28
  link(rel="stylesheet", href=`/src/assets/css/pages/${css}.sass`)
29
+ // YT-API
30
+ script(src="https://www.youtube.com/iframe_api")
29
31
  // 共用 JS
30
32
  script(defer type="module" src=`/src/assets/js/apps/${page}/page.js`)
31
33
  // 若 video4 有開 Ig reels, 引入以下網址
@@ -290,76 +290,64 @@ block content
290
290
  li 藥品服務
291
291
  dropdown-el.dropdown4.disabled(d4-placeholder="請先選擇服務分類")
292
292
  section.section6
293
+ -
294
+ const videoDemoData = [
295
+ {
296
+ type: 'youtube',
297
+ title: 'YouTube Demo',
298
+ videoID: 'zKYNITx17Mw',
299
+ },
300
+ {
301
+ type: 'youku',
302
+ title: 'Youku Demo',
303
+ videoID: 'XNjQ5NDk1NTc4OA',
304
+ duration: 3,
305
+ },
306
+ {
307
+ type: 'video',
308
+ title: 'video Demo',
309
+ videoID: 'https://cdn.wdd.idv.tw/video/chappie_Video1.mp4',
310
+ },
311
+ ]
293
312
  .container
294
- h3 Collapse 👇
313
+ h3 VIDEO 👇
295
314
  .wrap
296
- .collapseItem(partnerCollapse-wrapper)
297
- .collapseTitle(collapse-block)
298
- p 以下是公版套件
299
- i.arrow
300
- .collapseContent(collapse-target)
301
- .innerBox
302
- .content
303
- p aaaaa
304
- p aaaaa
305
- p aaaaa
306
- p aaaaa
307
- .collapseItem(partnerCollapse-wrapper)
308
- .collapseTitle(collapse-block)
309
- p BBBBB
310
- i.arrow
311
- .collapseContent(collapse-target)
312
- .innerBox
313
- .content
314
- p bbbbb
315
- p bbbbb
316
- p bbbbb
317
- p bbbbb
318
- .collapseItem(partnerCollapse-wrapper)
319
- .collapseTitle(collapse-block)
320
- p CCCCC
321
- i.arrow
322
- .collapseContent(collapse-target)
323
- .innerBox
324
- .content
325
- p ccccc
326
- p ccccc
327
- p ccccc
328
- p ccccc
315
+ //
316
+ .titleBox
317
+ .heading
318
+ .sub-title EMBLA
319
+ .embla.embla-video-demo
320
+ .embla__viewport
321
+ .embla__container
322
+ each item in videoDemoData
323
+ .embla__slide
324
+ .video-demo-card
325
+ .video-demo-label
326
+ span= item.title
327
+ small= item.videoID
328
+ if item.type === 'video'
329
+ .mediaBox
330
+ video(src=item.videoID muted playsinline)
331
+ else
332
+ .mediaBox(data-type=item.type data-videoID=item.videoID data-duration=item.duration?item.duration:false)
329
333
  .wrap
330
- .collapseItem(data-collapse)
331
- .collapseTitle(data-collapse-click)
332
- p 以下是簡易版
333
- i.arrow
334
- .collapseBox(data-collapse-content)
335
- .innerBox
336
- .content
337
- p aaaaa
338
- p aaaaa
339
- p aaaaa
340
- p aaaaa
341
- .collapseItem(data-collapse)
342
- .collapseTitle(data-collapse-click)
343
- p BBBBB
344
- i.arrow
345
- .collapseBox(data-collapse-content)
346
- .innerBox
347
- .content
348
- p bbbbb
349
- p bbbbb
350
- p bbbbb
351
- p bbbbb
352
- .collapseItem(data-collapse)
353
- .collapseTitle(data-collapse-click)
354
- p CCCCC
355
- i.arrow
356
- .collapseBox(data-collapse-content)
357
- .innerBox
358
- .content
359
- p ccccc
360
- p ccccc
361
- p ccccc
362
- p ccccc
334
+ .titleBox
335
+ .heading
336
+ .sub-title SWIPER
337
+ .swiper.swiper-video-demo
338
+ .swiper-wrapper
339
+ each item in videoDemoData
340
+ .swiper-slide
341
+ .video-demo-card
342
+ .video-demo-label
343
+ span= item.title
344
+ small= item.videoID
345
+ if item.type === 'video'
346
+ .mediaBox
347
+ video(src=item.videoID muted playsinline)
348
+ else
349
+ .mediaBox(data-type=item.type data-videoID=item.videoID data-duration=item.duration?item.duration:false)
350
+
363
351
  section.section7
364
352
  .container
365
353
  h3 Ripple4 👇