@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/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';
@@ -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
- if (!this._captionEl) {
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
- const newCaption = activeItem?.getAttribute('data-caption');
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
- // Auto-init on DOMContentLoaded
58
- document.addEventListener('DOMContentLoaded', () => {
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 window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
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 (window.YT?.Player) {
16
+ if (globalThis.YT?.Player) {
17
17
  youTubeAPIReady = true;
18
18
  resolve();
19
19
  } else {
20
- const prev = window.onYouTubeIframeAPIReady;
21
- window.onYouTubeIframeAPIReady = () => {
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 (window.Vimeo?.Player) {
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
- if (!this._dialog || !this._container) {
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 window.Vimeo.Player(this._container, {
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 window.YT.Player(playerDiv, {
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
- // Auto-init
151
- document.addEventListener('DOMContentLoaded', () => {
152
- const modals = SelectorEngine.find('[data-bs-toggle="modal"][data-bs-target]');
153
- for (const btn of modals) {
154
- const targetSelector = btn.getAttribute('data-bs-target');
155
- const modal = document.querySelector(targetSelector);
156
- if (modal?.querySelector(SELECTOR_DIALOG)) {
157
- VideoModal.getOrCreateInstance(modal);
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",
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,scss}\" --cache --cache-location .cache/.stylelintcache --rd",
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,4 @@
1
+ export { useDatepicker } from './use-datepicker.js';
2
+ export { useCarouselCaption } from './use-carousel-caption.js';
3
+ export { useCarouselHeight } from './use-carousel-height.js';
4
+ export { useVideoModal } from './use-video-modal.js';
@@ -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-prev,
27
- .carousel-control-next {
28
- position: unset;
29
- width: fit-content;
30
- padding: rfs-value(6px);
31
- }
32
- .carousel-indicators {
33
- position: unset;
34
- margin-bottom: 0;
35
- margin-left: 0;
36
- @include margin-right($spacer * 1.5);
37
- [data-bs-target] {
38
- @extend .rounded-circle;
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
  }
@@ -1,2 +1,2 @@
1
1
  // GENERATED FILE – do not edit manually
2
- $km-version: "1.4.3" !default;
2
+ $km-version: "1.4.6" !default;