@unlk/keymaster 1.4.3 → 1.4.6
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/CHANGELOG.md +21 -0
- package/dist/css/keymaster.css +15 -8
- package/dist/css/keymaster.css.map +1 -1
- package/dist/css/keymaster.min.css +2 -2
- package/dist/css/keymaster.min.css.map +1 -1
- package/dist/js/keymaster.js +69 -53
- package/dist/js/keymaster.js.map +1 -1
- package/dist/js/keymaster.min.js +14 -14
- package/dist/js/keymaster.min.js.map +1 -1
- package/js/bootstrap.js +1 -0
- package/js/carousel-caption.js +5 -26
- package/js/carousel-height.js +56 -0
- package/js/datepicker.js +1 -1
- package/js/video-modal.js +18 -27
- package/package.json +16 -5
- package/react/index.js +4 -0
- package/react/use-carousel-caption.js +24 -0
- package/react/use-carousel-height.js +39 -0
- package/react/use-datepicker.js +33 -0
- package/react/use-video-modal.js +132 -0
- package/scss/theme/_carousel.scss +18 -14
- package/scss/theme/_version.scss +1 -1
package/js/bootstrap.js
CHANGED
|
@@ -12,4 +12,5 @@ export { default as Tab } from 'bootstrap/js/dist/tab';
|
|
|
12
12
|
export { default as Tooltip } from 'bootstrap/js/dist/tooltip';
|
|
13
13
|
export { default as Datepicker } from './datepicker';
|
|
14
14
|
export { default as CarouselCaption } from './carousel-caption';
|
|
15
|
+
export { default as CarouselHeight } from './carousel-height';
|
|
15
16
|
export { default as VideoModal } from './video-modal';
|
package/js/carousel-caption.js
CHANGED
|
@@ -4,7 +4,6 @@ import SelectorEngine from 'bootstrap/js/src/dom/selector-engine.js';
|
|
|
4
4
|
import { defineJQueryPlugin } from 'bootstrap/js/src/util/index.js';
|
|
5
5
|
|
|
6
6
|
const NAME = 'carouselCaption';
|
|
7
|
-
const DATA_KEY = 'bs.carouselCaption';
|
|
8
7
|
const EVENT_SLID = 'slid.bs.carousel';
|
|
9
8
|
const SELECTOR_CAROUSEL = '.carousel';
|
|
10
9
|
const SELECTOR_CAPTION = '.carousel-caption-container p';
|
|
@@ -12,31 +11,14 @@ const SELECTOR_CAPTION = '.carousel-caption-container p';
|
|
|
12
11
|
class CarouselCaption extends BaseComponent {
|
|
13
12
|
constructor(element) {
|
|
14
13
|
super(element);
|
|
15
|
-
|
|
16
14
|
this._captionEl = this._findCaptionEl();
|
|
17
|
-
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
this._updateCaption(); // Initialize
|
|
22
|
-
this._bindEvents();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
dispose() {
|
|
26
|
-
EventHandler.off(this._element, EVENT_SLID);
|
|
27
|
-
super.dispose();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
_bindEvents() {
|
|
31
|
-
EventHandler.on(this._element, EVENT_SLID, () => this._updateCaption());
|
|
15
|
+
this._updateCaption();
|
|
32
16
|
}
|
|
33
17
|
|
|
34
18
|
_updateCaption() {
|
|
19
|
+
if (!this._captionEl) return;
|
|
35
20
|
const activeItem = SelectorEngine.findOne('.carousel-item.active', this._element);
|
|
36
|
-
|
|
37
|
-
if (this._captionEl && newCaption) {
|
|
38
|
-
this._captionEl.textContent = newCaption || '';
|
|
39
|
-
}
|
|
21
|
+
this._captionEl.textContent = activeItem?.getAttribute('data-caption') ?? '';
|
|
40
22
|
}
|
|
41
23
|
|
|
42
24
|
_findCaptionEl() {
|
|
@@ -54,11 +36,8 @@ class CarouselCaption extends BaseComponent {
|
|
|
54
36
|
}
|
|
55
37
|
}
|
|
56
38
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
for (const el of SelectorEngine.find(SELECTOR_CAROUSEL)) {
|
|
60
|
-
CarouselCaption.getOrCreateInstance(el);
|
|
61
|
-
}
|
|
39
|
+
EventHandler.on(document, EVENT_SLID, SELECTOR_CAROUSEL, function () {
|
|
40
|
+
CarouselCaption.getOrCreateInstance(this)._updateCaption();
|
|
62
41
|
});
|
|
63
42
|
|
|
64
43
|
defineJQueryPlugin(CarouselCaption);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import BaseComponent from 'bootstrap/js/src/base-component.js';
|
|
2
|
+
import EventHandler from 'bootstrap/js/src/dom/event-handler.js';
|
|
3
|
+
import SelectorEngine from 'bootstrap/js/src/dom/selector-engine.js';
|
|
4
|
+
import { defineJQueryPlugin } from 'bootstrap/js/src/util/index.js';
|
|
5
|
+
|
|
6
|
+
const NAME = 'carouselHeight';
|
|
7
|
+
const EVENT_SLIDE = 'slide.bs.carousel';
|
|
8
|
+
const EVENT_SLID = 'slid.bs.carousel';
|
|
9
|
+
const SELECTOR_CAROUSEL = '.carousel';
|
|
10
|
+
const SELECTOR_INNER = '.carousel-inner';
|
|
11
|
+
|
|
12
|
+
class CarouselHeight extends BaseComponent {
|
|
13
|
+
constructor(element) {
|
|
14
|
+
super(element);
|
|
15
|
+
this._inner = SelectorEngine.findOne(SELECTOR_INNER, this._element);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_onSlide(e) {
|
|
19
|
+
if (!this._inner) return;
|
|
20
|
+
const nextSlide = e.relatedTarget;
|
|
21
|
+
if (!nextSlide) return;
|
|
22
|
+
|
|
23
|
+
this._inner.style.height = `${this._inner.offsetHeight}px`;
|
|
24
|
+
|
|
25
|
+
requestAnimationFrame(() => {
|
|
26
|
+
this._inner.style.height = `${nextSlide.offsetHeight}px`;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_onSlid() {
|
|
31
|
+
if (!this._inner) return;
|
|
32
|
+
this._inner.style.height = '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static get NAME() {
|
|
36
|
+
return NAME;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static jQueryInterface(config) {
|
|
40
|
+
return this.each(function () {
|
|
41
|
+
CarouselHeight.getOrCreateInstance(this, config);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
EventHandler.on(document, EVENT_SLIDE, SELECTOR_CAROUSEL, function (e) {
|
|
47
|
+
CarouselHeight.getOrCreateInstance(this)._onSlide(e);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
EventHandler.on(document, EVENT_SLID, SELECTOR_CAROUSEL, function () {
|
|
51
|
+
CarouselHeight.getOrCreateInstance(this)._onSlid();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
defineJQueryPlugin(CarouselHeight);
|
|
55
|
+
|
|
56
|
+
export default CarouselHeight;
|
package/js/datepicker.js
CHANGED
|
@@ -67,7 +67,7 @@ EventHandler.on(document, EVENT_FOCUS_DATA_API, SELECTOR_INPUT, (event) => {
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
function isTouchDevice() {
|
|
70
|
-
return 'ontouchstart' in
|
|
70
|
+
return 'ontouchstart' in globalThis || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
package/js/video-modal.js
CHANGED
|
@@ -4,24 +4,25 @@ import SelectorEngine from 'bootstrap/js/src/dom/selector-engine.js';
|
|
|
4
4
|
import { defineJQueryPlugin } from 'bootstrap/js/src/util/index.js';
|
|
5
5
|
|
|
6
6
|
const NAME = 'videoModal';
|
|
7
|
-
const DATA_KEY = 'bs.videoModal';
|
|
8
7
|
const EVENT_SHOW = 'show.bs.modal';
|
|
9
8
|
const EVENT_HIDDEN = 'hidden.bs.modal';
|
|
9
|
+
const SELECTOR_MODAL = '.modal';
|
|
10
10
|
const SELECTOR_DIALOG = '.modal-dialog-media';
|
|
11
11
|
const SELECTOR_RATIO = '.ratio';
|
|
12
12
|
|
|
13
13
|
// eslint-disable-next-line no-unused-vars
|
|
14
14
|
let youTubeAPIReady = false;
|
|
15
15
|
const youTubeAPIInitPromise = new Promise((resolve) => {
|
|
16
|
-
if (
|
|
16
|
+
if (globalThis.YT?.Player) {
|
|
17
17
|
youTubeAPIReady = true;
|
|
18
18
|
resolve();
|
|
19
19
|
} else {
|
|
20
|
-
const prev =
|
|
21
|
-
|
|
20
|
+
const prev = globalThis.onYouTubeIframeAPIReady;
|
|
21
|
+
globalThis.onYouTubeIframeAPIReady = () => {
|
|
22
22
|
try {
|
|
23
23
|
if (typeof prev === 'function') prev();
|
|
24
24
|
} catch {}
|
|
25
|
+
|
|
25
26
|
youTubeAPIReady = true;
|
|
26
27
|
resolve();
|
|
27
28
|
};
|
|
@@ -35,7 +36,7 @@ const youTubeAPIInitPromise = new Promise((resolve) => {
|
|
|
35
36
|
// eslint-disable-next-line no-unused-vars
|
|
36
37
|
let vimeoAPIReady = false;
|
|
37
38
|
const vimeoAPIInitPromise = new Promise((resolve) => {
|
|
38
|
-
if (
|
|
39
|
+
if (globalThis.Vimeo?.Player) {
|
|
39
40
|
vimeoAPIReady = true;
|
|
40
41
|
resolve();
|
|
41
42
|
} else {
|
|
@@ -56,18 +57,10 @@ class VideoModal extends BaseComponent {
|
|
|
56
57
|
|
|
57
58
|
this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);
|
|
58
59
|
this._container = SelectorEngine.findOne(SELECTOR_RATIO, this._element);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
this._youtubeSrc = this._container.getAttribute('data-src');
|
|
65
|
-
this._vimeoUrl = this._container.getAttribute('data-vimeo-url');
|
|
60
|
+
this._youtubeSrc = this._container?.getAttribute('data-src');
|
|
61
|
+
this._vimeoUrl = this._container?.getAttribute('data-vimeo-url');
|
|
66
62
|
this._youtubePlayer = null;
|
|
67
63
|
this._vimeoPlayer = null;
|
|
68
|
-
|
|
69
|
-
EventHandler.on(this._element, EVENT_SHOW, () => this._onShow());
|
|
70
|
-
EventHandler.on(this._element, EVENT_HIDDEN, () => this._onHide());
|
|
71
64
|
}
|
|
72
65
|
|
|
73
66
|
dispose() {
|
|
@@ -83,7 +76,7 @@ class VideoModal extends BaseComponent {
|
|
|
83
76
|
await vimeoAPIInitPromise;
|
|
84
77
|
const vimeoId = this._getVimeoId(this._vimeoUrl);
|
|
85
78
|
|
|
86
|
-
this._vimeoPlayer = new
|
|
79
|
+
this._vimeoPlayer = new globalThis.Vimeo.Player(this._container, {
|
|
87
80
|
id: vimeoId,
|
|
88
81
|
autoplay: true
|
|
89
82
|
});
|
|
@@ -99,7 +92,7 @@ class VideoModal extends BaseComponent {
|
|
|
99
92
|
const youtubeId = this._getYouTubeId(this._youtubeSrc);
|
|
100
93
|
const playerDiv = this._container.querySelector(`#${playerID}`);
|
|
101
94
|
|
|
102
|
-
this._youtubePlayer = new
|
|
95
|
+
this._youtubePlayer = new globalThis.YT.Player(playerDiv, {
|
|
103
96
|
videoId: youtubeId,
|
|
104
97
|
playerVars: { autoplay: 1, rel: 0 }
|
|
105
98
|
});
|
|
@@ -147,16 +140,14 @@ class VideoModal extends BaseComponent {
|
|
|
147
140
|
}
|
|
148
141
|
}
|
|
149
142
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
}
|
|
143
|
+
EventHandler.on(document, EVENT_SHOW, SELECTOR_MODAL, function () {
|
|
144
|
+
if (!SelectorEngine.findOne(SELECTOR_DIALOG, this)) return;
|
|
145
|
+
VideoModal.getOrCreateInstance(this)._onShow();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
EventHandler.on(document, EVENT_HIDDEN, SELECTOR_MODAL, function () {
|
|
149
|
+
if (!SelectorEngine.findOne(SELECTOR_DIALOG, this)) return;
|
|
150
|
+
VideoModal.getOrCreateInstance(this)._onHide();
|
|
160
151
|
});
|
|
161
152
|
|
|
162
153
|
defineJQueryPlugin(VideoModal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unlk/keymaster",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"bs": "browser-sync start --config build/browser-sync.config.js",
|
|
11
|
-
"css": "npm-run-all css-compile css-postcss css-minify",
|
|
11
|
+
"css": "npm-run-all css-lint css-compile css-postcss css-minify",
|
|
12
12
|
"css-compile": "sass --style expanded --source-map --embed-sources --no-error-css --quiet scss/keymaster.scss:dist/css/keymaster.css",
|
|
13
|
-
"css-lint": "stylelint \"**/*.{css
|
|
13
|
+
"css-lint": "stylelint \"**/*.{scss,css}\" --cache --cache-location .cache/.stylelintcache --rd",
|
|
14
14
|
"css-minify": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*rtl*.css\"",
|
|
15
15
|
"css-postcss": "postcss --config build/postcss.config.js --replace \"dist/css/*.css\" \"!dist/css/*.rtl*.css\" \"!dist/css/*.min.css\"",
|
|
16
|
-
"js": "npm-run-all js-compile js-minify",
|
|
16
|
+
"js": "npm-run-all js-lint js-compile js-minify",
|
|
17
17
|
"js-compile": "rollup --config build/rollup.config.js --sourcemap",
|
|
18
18
|
"js-lint": "eslint --cache --cache-location .cache/.eslintcache --report-unused-disable-directives --ext .html,.js .",
|
|
19
19
|
"js-minify": "terser --config-file build/terser.config.json --output dist/js/keymaster.min.js dist/js/keymaster.js",
|
|
@@ -28,10 +28,15 @@
|
|
|
28
28
|
"dist-clean": "node build/dist-clean.js",
|
|
29
29
|
"lint": "npm-run-all --aggregate-output --continue-on-error --parallel js-lint css-lint"
|
|
30
30
|
},
|
|
31
|
+
"exports": {
|
|
32
|
+
"./*": "./*",
|
|
33
|
+
"./react": "./react/index.js"
|
|
34
|
+
},
|
|
31
35
|
"files": [
|
|
32
36
|
"dist",
|
|
33
37
|
"scss",
|
|
34
38
|
"js",
|
|
39
|
+
"react",
|
|
35
40
|
"fonts",
|
|
36
41
|
"README.md",
|
|
37
42
|
"CHANGELOG.md"
|
|
@@ -51,7 +56,13 @@
|
|
|
51
56
|
"homepage": "https://keymaster.unlock.com",
|
|
52
57
|
"peerDependencies": {
|
|
53
58
|
"@popperjs/core": "^2.11.0",
|
|
54
|
-
"bootstrap": "^5.3.0"
|
|
59
|
+
"bootstrap": "^5.3.0",
|
|
60
|
+
"react": ">=18"
|
|
61
|
+
},
|
|
62
|
+
"peerDependenciesMeta": {
|
|
63
|
+
"react": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
55
66
|
},
|
|
56
67
|
"devDependencies": {
|
|
57
68
|
"@babel/core": "^7.27.1",
|
package/react/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Replicates carousel-caption.js behaviour in React.
|
|
5
|
+
*
|
|
6
|
+
* Tracks the active slide index and returns the caption for that slide,
|
|
7
|
+
* replacing the Bootstrap slid.bs.carousel event listener.
|
|
8
|
+
*
|
|
9
|
+
* @param {Array<{ caption: string }>} slides - Array of slide data objects
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* const { activeIndex, onSelect, caption } = useCarouselCaption(slides);
|
|
13
|
+
* <Carousel activeIndex={activeIndex} onSelect={onSelect}>...</Carousel>
|
|
14
|
+
* <div className="carousel-caption-container"><p>{caption}</p></div>
|
|
15
|
+
*/
|
|
16
|
+
export function useCarouselCaption(slides) {
|
|
17
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
18
|
+
|
|
19
|
+
const onSelect = useCallback((index) => setActiveIndex(index), []);
|
|
20
|
+
|
|
21
|
+
const caption = slides[activeIndex]?.caption ?? '';
|
|
22
|
+
|
|
23
|
+
return { activeIndex, onSelect, caption };
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useRef, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Replicates carousel-height.js behaviour in React.
|
|
5
|
+
*
|
|
6
|
+
* Animates .carousel-inner height between slides of different heights,
|
|
7
|
+
* replacing the Bootstrap slide.bs.carousel / slid.bs.carousel listeners.
|
|
8
|
+
*
|
|
9
|
+
* Attach `ref` to the wrapping element or the <Carousel> element,
|
|
10
|
+
* and pass onSlide / onSlid to the <Carousel> component.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const { ref, onSlide, onSlid } = useCarouselHeight();
|
|
14
|
+
* <Carousel ref={ref} onSlide={onSlide} onSlid={onSlid}>...</Carousel>
|
|
15
|
+
*/
|
|
16
|
+
export function useCarouselHeight() {
|
|
17
|
+
const ref = useRef(null);
|
|
18
|
+
|
|
19
|
+
const onSlide = useCallback(() => {
|
|
20
|
+
const el = ref.current;
|
|
21
|
+
if (!el) return;
|
|
22
|
+
const inner = el.querySelector('.carousel-inner');
|
|
23
|
+
const next = el.querySelector('.carousel-item-next, .carousel-item-prev');
|
|
24
|
+
if (!inner || !next) return;
|
|
25
|
+
inner.style.height = `${inner.offsetHeight}px`;
|
|
26
|
+
requestAnimationFrame(() => {
|
|
27
|
+
inner.style.height = `${next.offsetHeight}px`;
|
|
28
|
+
});
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const onSlid = useCallback(() => {
|
|
32
|
+
const el = ref.current;
|
|
33
|
+
if (!el) return;
|
|
34
|
+
const inner = el.querySelector('.carousel-inner');
|
|
35
|
+
if (inner) inner.style.height = '';
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return { ref, onSlide, onSlid };
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Replicates datepicker.js behaviour in React.
|
|
5
|
+
*
|
|
6
|
+
* Renders as type="text" (so the floating label placeholder shows),
|
|
7
|
+
* switches to type="date" on focus (opens the native date picker),
|
|
8
|
+
* and reverts on blur if no value is selected.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const dp = useDatepicker();
|
|
12
|
+
* <input type={dp.type} className={dp.hasValue ? 'has-value' : ''}
|
|
13
|
+
* onFocus={dp.onFocus} onBlur={dp.onBlur} onChange={dp.onChange} />
|
|
14
|
+
*/
|
|
15
|
+
export function useDatepicker() {
|
|
16
|
+
const [type, setType] = useState('text');
|
|
17
|
+
const [hasValue, setHasValue] = useState(false);
|
|
18
|
+
|
|
19
|
+
const onFocus = useCallback(() => setType('date'), []);
|
|
20
|
+
|
|
21
|
+
const onBlur = useCallback((e) => {
|
|
22
|
+
if (!e.target.value) {
|
|
23
|
+
setType('text');
|
|
24
|
+
setHasValue(false);
|
|
25
|
+
}
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const onChange = useCallback((e) => {
|
|
29
|
+
setHasValue(Boolean(e.target.value));
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
return { type, hasValue, onFocus, onBlur, onChange };
|
|
33
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useRef, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
// Module-level promises so the API scripts are only ever loaded once
|
|
4
|
+
let ytAPIPromise = null;
|
|
5
|
+
let vimeoAPIPromise = null;
|
|
6
|
+
|
|
7
|
+
function loadYouTubeAPI() {
|
|
8
|
+
if (ytAPIPromise) return ytAPIPromise;
|
|
9
|
+
|
|
10
|
+
ytAPIPromise = new Promise((resolve) => {
|
|
11
|
+
if (globalThis.YT?.Player) {
|
|
12
|
+
resolve();
|
|
13
|
+
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const prev = globalThis.onYouTubeIframeAPIReady;
|
|
18
|
+
|
|
19
|
+
globalThis.onYouTubeIframeAPIReady = () => {
|
|
20
|
+
try {
|
|
21
|
+
if (typeof prev === 'function') prev();
|
|
22
|
+
} catch {}
|
|
23
|
+
|
|
24
|
+
resolve();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const s = document.createElement('script');
|
|
28
|
+
|
|
29
|
+
s.src = 'https://www.youtube.com/iframe_api';
|
|
30
|
+
document.head.append(s);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return ytAPIPromise;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadVimeoAPI() {
|
|
37
|
+
if (vimeoAPIPromise) return vimeoAPIPromise;
|
|
38
|
+
|
|
39
|
+
vimeoAPIPromise = new Promise((resolve) => {
|
|
40
|
+
if (globalThis.Vimeo?.Player) {
|
|
41
|
+
resolve();
|
|
42
|
+
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const s = document.createElement('script');
|
|
47
|
+
|
|
48
|
+
s.src = 'https://player.vimeo.com/api/player.js';
|
|
49
|
+
s.addEventListener('load', resolve);
|
|
50
|
+
document.head.append(s);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return vimeoAPIPromise;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getYouTubeId(url) {
|
|
57
|
+
const m = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|v\/)|youtu\.be\/)([\w-]{11})/);
|
|
58
|
+
|
|
59
|
+
return m?.[1] ?? '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getVimeoId(url) {
|
|
63
|
+
const m = url.match(/vimeo\.com\/(\d+)/);
|
|
64
|
+
|
|
65
|
+
return m?.[1] ?? '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Replicates video-modal.js behaviour in React.
|
|
70
|
+
*
|
|
71
|
+
* Auto-initialises a YouTube or Vimeo player when the modal opens and
|
|
72
|
+
* destroys it when the modal closes, replacing the Bootstrap
|
|
73
|
+
* show.bs.modal / hidden.bs.modal event listeners.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} options
|
|
76
|
+
* @param {string} [options.youtubeSrc] - YouTube video URL
|
|
77
|
+
* @param {string} [options.vimeoUrl] - Vimeo video URL
|
|
78
|
+
*
|
|
79
|
+
* Usage:
|
|
80
|
+
* const { containerRef, onShow, onHide } = useVideoModal({ youtubeSrc: '...' });
|
|
81
|
+
* <Modal onShow={onShow} onHide={onHide}>
|
|
82
|
+
* <div className="ratio ratio-16x9" ref={containerRef}>
|
|
83
|
+
* <div data-yt-player /> ← YouTube mounts here
|
|
84
|
+
* </div>
|
|
85
|
+
* </Modal>
|
|
86
|
+
*/
|
|
87
|
+
export function useVideoModal({ youtubeSrc, vimeoUrl } = {}) {
|
|
88
|
+
const containerRef = useRef(null);
|
|
89
|
+
const players = useRef({ youtube: null, vimeo: null });
|
|
90
|
+
|
|
91
|
+
const onShow = useCallback(async () => {
|
|
92
|
+
const container = containerRef.current;
|
|
93
|
+
|
|
94
|
+
if (!container) return;
|
|
95
|
+
|
|
96
|
+
if (vimeoUrl) {
|
|
97
|
+
await loadVimeoAPI();
|
|
98
|
+
|
|
99
|
+
players.current.vimeo = new globalThis.Vimeo.Player(container, {
|
|
100
|
+
id: getVimeoId(vimeoUrl),
|
|
101
|
+
autoplay: true,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (youtubeSrc) {
|
|
108
|
+
await loadYouTubeAPI();
|
|
109
|
+
|
|
110
|
+
const playerDiv = container.querySelector('[data-yt-player]');
|
|
111
|
+
|
|
112
|
+
players.current.youtube = new globalThis.YT.Player(playerDiv, {
|
|
113
|
+
videoId: getYouTubeId(youtubeSrc),
|
|
114
|
+
playerVars: { autoplay: 1, rel: 0 },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}, [youtubeSrc, vimeoUrl]);
|
|
118
|
+
|
|
119
|
+
const onHide = useCallback(() => {
|
|
120
|
+
if (players.current.vimeo) {
|
|
121
|
+
players.current.vimeo.unload().catch(() => {});
|
|
122
|
+
players.current.vimeo = null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (players.current.youtube) {
|
|
126
|
+
players.current.youtube.destroy();
|
|
127
|
+
players.current.youtube = null;
|
|
128
|
+
}
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
return { containerRef, onShow, onHide };
|
|
132
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
.carousel-inner {
|
|
2
|
+
@include transition(height $carousel-transition-duration ease);
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
.carousel-item {
|
|
2
6
|
img {
|
|
3
7
|
@include border-radius($border-radius-xl);
|
|
@@ -22,19 +26,19 @@
|
|
|
22
26
|
@extend .mt-3;
|
|
23
27
|
display: flex;
|
|
24
28
|
justify-content: end;
|
|
25
|
-
|
|
26
|
-
.carousel-control-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
.carousel-control-prev,
|
|
30
|
+
.carousel-control-next {
|
|
31
|
+
position: unset;
|
|
32
|
+
width: fit-content;
|
|
33
|
+
padding: rfs-value(6px);
|
|
34
|
+
}
|
|
35
|
+
.carousel-indicators {
|
|
36
|
+
position: unset;
|
|
37
|
+
margin-bottom: 0;
|
|
38
|
+
margin-left: 0;
|
|
39
|
+
@include margin-right($spacer * 1.5);
|
|
40
|
+
[data-bs-target] {
|
|
41
|
+
@extend .rounded-circle;
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
}
|
package/scss/theme/_version.scss
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED FILE – do not edit manually
|
|
2
|
-
$km-version: "1.4.
|
|
2
|
+
$km-version: "1.4.6" !default;
|