@homecode/ui 5.20.9 → 5.20.11

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.
@@ -1,5 +1,5 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { Component, createRef, useState, useRef, useLayoutEffect, useEffect } from 'react';
2
+ import { Component, createRef, useState, useRef, useLayoutEffect } from 'react';
3
3
  import { createStore } from 'justorm/react';
4
4
  import Time from 'timen';
5
5
  import compare from 'compareq';
@@ -39,13 +39,7 @@ function Arr({ className, size, icon, ...rest }) {
39
39
  return (jsx(Button, { className: cn(S.arr, className), size: size, variant: "clear", ...rest, children: jsx(Icon, { type: icon, size: size }) }));
40
40
  }
41
41
  function VideoItem({ src, size, isActive, }) {
42
- const wrapRef = useRef(null);
43
- useEffect(() => {
44
- if (isActive)
45
- return;
46
- wrapRef.current?.querySelector('video')?.pause();
47
- }, [isActive, src]);
48
- return (jsx("div", { ref: wrapRef, className: cn(S.item, S.videoItem), children: jsx(VideoPlayer, { src: src, size: size, storageKey: `Gallery:${src}`, className: S.video }) }));
42
+ return (jsx("div", { className: cn(S.item, S.videoItem), children: jsx(VideoPlayer, { src: src, size: size, storageKey: `Gallery:${src}`, isActive: isActive, className: S.video }) }));
49
43
  }
50
44
  function Item({ src, kind, size, isActive, }) {
51
45
  const [loaded, setLoaded] = useState(false);
@@ -167,6 +161,9 @@ class Gallery extends Component {
167
161
  }
168
162
  };
169
163
  onPointerDown = e => {
164
+ // Pointer on a video belongs to the player (seek, play, volume), not swipe.
165
+ if (e.target.closest?.(`.${S.video}`))
166
+ return;
170
167
  this.startX = e.pageX;
171
168
  };
172
169
  onPointerMove = e => {
@@ -225,6 +222,7 @@ class Gallery extends Component {
225
222
  const { items, movingDirection, isDragging } = this.store;
226
223
  const dirName = DIR_NAME[movingDirection];
227
224
  const isSingle = this.isSingle();
225
+ const isVideoActive = items[isSingle ? 0 : 1]?.kind === 'video';
228
226
  const props = omit(rest, ['items', 'onChange', 'animation', 'startIndex']);
229
227
  const classes = cn(S.root, isSingle && S.single, cover && S.cover, className);
230
228
  const innerClasses = cn(S.inner, initialBounce && S.initialBounce, S[dirName]);
@@ -236,7 +234,7 @@ class Gallery extends Component {
236
234
  props.onPointerLeave = this.onPointerUp;
237
235
  }
238
236
  }
239
- return (jsxs("div", { className: classes, ...props, children: [jsx("div", { className: innerClasses, ref: this.innerRef, children: items.map((item, i) => (jsx(Item, { src: item.src, kind: item.kind, size: size, isActive: isSingle ? i === 0 : i === 1 }, `${i}_${item.src}_${item.kind}`))) }), !isSingle && showArrows && !isDragging && (jsxs(Fragment, { children: [jsx(Arr, { className: S.left, size: size, icon: "chevronLeft", onClick: () => this.move(1) }), jsx(Arr, { className: S.right, size: size, icon: "chevronRight", onClick: () => this.move(-1) })] })), showDots && (jsx(Lazy, { hideSpinner: true, loader: () => import('./Dots/Dots.js'),
237
+ return (jsxs("div", { className: classes, ...props, children: [jsx("div", { className: innerClasses, ref: this.innerRef, children: items.map((item, i) => (jsx(Item, { src: item.src, kind: item.kind, size: size, isActive: isSingle ? i === 0 : i === 1 }, `${i}_${item.src}_${item.kind}`))) }), !isSingle && showArrows && !isDragging && !isVideoActive && (jsxs(Fragment, { children: [jsx(Arr, { className: S.left, size: size, icon: "chevronLeft", onClick: () => this.move(1) }), jsx(Arr, { className: S.right, size: size, icon: "chevronRight", onClick: () => this.move(-1) })] })), showDots && (jsx(Lazy, { hideSpinner: true, loader: () => import('./Dots/Dots.js'),
240
238
  // @ts-ignore
241
239
  index: this.index % this.props.items.length, count: this.props.items.length }))] }));
242
240
  }
@@ -5,7 +5,7 @@ import cn from 'classnames';
5
5
  import { Icon } from '../../Icon/Icon.js';
6
6
  import Context from '../context.js';
7
7
  import S from './Link.styl.js';
8
- import { isRootRelativeHref, isNativeDocumentHref } from './linkHref.js';
8
+ import { isExternalHref, isNativeDocumentHref } from './linkHref.js';
9
9
 
10
10
  const isStartFromDoubleSlash = href => /^\/\//.test(href);
11
11
  const isHttpHref = (href) => /^https?:\/\//i.test(href);
@@ -33,13 +33,7 @@ const Link = ({ className, exactClassName, children, href: hrefProp, isClear = f
33
33
  const isExternal = useMemo(() => {
34
34
  if (internalHref)
35
35
  return false;
36
- // A dot in `?filePath=a.pdf` is not a hostname. `http://` + `/api/…`
37
- // is parsed as host `api`.
38
- if (isRootRelativeHref(hrefProp))
39
- return false;
40
- if (isHttpHref(hrefProp))
41
- return true;
42
- return /\./.test(hrefProp);
36
+ return isExternalHref(hrefProp);
43
37
  }, [hrefProp, internalHref]);
44
38
  const nativeDocumentProp = isNativeDocumentHref(internalHref ?? hrefProp);
45
39
  const rootPath = useMemo(() => {
@@ -14,6 +14,18 @@ function isNativeDocumentHref(href) {
14
14
  if (path === '/api' || path.startsWith('/api/'))
15
15
  return true;
16
16
  return path.includes('.');
17
+ }
18
+ /**
19
+ * Opens in a new tab. A bare `example.com` is a host, but a dot only in the
20
+ * query (`//storage?file=a.mp4`) is not.
21
+ */
22
+ function isExternalHref(href) {
23
+ if (isRootRelativeHref(href))
24
+ return false;
25
+ if (/^https?:\/\//i.test(href))
26
+ return true;
27
+ const path = href.split(/[?#]/)[0] ?? '';
28
+ return path.includes('.');
17
29
  }
18
30
 
19
- export { isNativeDocumentHref, isRootRelativeHref };
31
+ export { isExternalHref, isNativeDocumentHref, isRootRelativeHref };
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
- import { useRef, useMemo, useState, useCallback, useEffect } from 'react';
2
+ import { useRef, useId, useState, useMemo, useCallback, useEffect } from 'react';
3
3
  import cn from 'classnames';
4
4
  import { Button } from '../Button/Button.js';
5
5
  import { Icon } from '../Icon/Icon.js';
@@ -12,7 +12,7 @@ import S from './VideoPlayer.styl.js';
12
12
  import { TIME_PERSIST_DEBOUNCE_MS, saveStorage, formatTime, VOLUME_POPUP_HIDE_MS, clamp, leadingDebounce, isEditableTarget, loadStorage, getBufferedPercent, DOUBLE_TAP_MS, getStorageKey, timeToProgressPct, KEYBOARD_DEBOUNCE_MS, getSeekDelta, VOLUME_STEP } from './VideoPlayer.helpers.js';
13
13
 
14
14
  let activePlayerId = null;
15
- function VideoPlayer({ src, poster, storageKey, size = 'm', autoPlay = false, muted = false, loop = false, className, onPlayChange, onTimeUpdate, }) {
15
+ function VideoPlayer({ src, poster, storageKey, size = 'm', autoPlay = false, muted = false, loop = false, isActive = true, className, onPlayChange, onTimeUpdate, }) {
16
16
  const rootRef = useRef(null);
17
17
  const videoRef = useRef(null);
18
18
  const barRef = useRef(null);
@@ -25,7 +25,10 @@ function VideoPlayer({ src, poster, storageKey, size = 'm', autoPlay = false, mu
25
25
  const volumeHideTimerRef = useRef(0);
26
26
  const lastTapRef = useRef(0);
27
27
  const singleClickTimerRef = useRef(0);
28
- const playerId = useMemo(() => storageKey ?? src ?? Math.random().toString(36).slice(2), [storageKey, src]);
28
+ const isActiveRef = useRef(isActive);
29
+ isActiveRef.current = isActive;
30
+ // Per-instance id: two players may share a src/storageKey (e.g. Gallery slots).
31
+ const playerId = useId();
29
32
  const lsKey = getStorageKey(src, storageKey);
30
33
  const [isReady, setIsReady] = useState(false);
31
34
  const [isPlaying, setIsPlaying] = useState(false);
@@ -56,8 +59,9 @@ function VideoPlayer({ src, poster, storageKey, size = 'm', autoPlay = false, mu
56
59
  label.textContent = formatTime(time);
57
60
  }, []);
58
61
  const claimKeyboard = useCallback(() => {
59
- activePlayerId = playerId;
60
- }, [playerId]);
62
+ if (isActive)
63
+ activePlayerId = playerId;
64
+ }, [playerId, isActive]);
61
65
  const clearVolumeHideTimer = useCallback(() => {
62
66
  window.clearTimeout(volumeHideTimerRef.current);
63
67
  }, []);
@@ -157,7 +161,7 @@ function VideoPlayer({ src, poster, storageKey, size = 'm', autoPlay = false, mu
157
161
  const debouncedVolumeUp = useMemo(() => leadingDebounce(() => applyVolume(volumeRef.current + VOLUME_STEP, { flashPopup: true }), KEYBOARD_DEBOUNCE_MS), [applyVolume]);
158
162
  const debouncedVolumeDown = useMemo(() => leadingDebounce(() => applyVolume(volumeRef.current - VOLUME_STEP, { flashPopup: true }), KEYBOARD_DEBOUNCE_MS), [applyVolume]);
159
163
  const onKeyDown = useCallback((e) => {
160
- if (activePlayerId !== playerId)
164
+ if (!isActive || activePlayerId !== playerId)
161
165
  return;
162
166
  if (isEditableTarget(e.target))
163
167
  return;
@@ -184,6 +188,7 @@ function VideoPlayer({ src, poster, storageKey, size = 'm', autoPlay = false, mu
184
188
  break;
185
189
  }
186
190
  }, [
191
+ isActive,
187
192
  playerId,
188
193
  togglePlay,
189
194
  debouncedSeekLeft,
@@ -196,6 +201,15 @@ function VideoPlayer({ src, poster, storageKey, size = 'm', autoPlay = false, mu
196
201
  callback: onKeyDown,
197
202
  isActive: true,
198
203
  });
204
+ useEffect(() => {
205
+ if (isActive) {
206
+ activePlayerId = playerId;
207
+ return;
208
+ }
209
+ if (activePlayerId === playerId)
210
+ activePlayerId = null;
211
+ videoRef.current?.pause();
212
+ }, [isActive, playerId]);
199
213
  useEffect(() => {
200
214
  const onFullscreenChange = () => {
201
215
  setIsFullscreen(document.fullscreenElement === rootRef.current);
@@ -242,7 +256,8 @@ function VideoPlayer({ src, poster, storageKey, size = 'm', autoPlay = false, mu
242
256
  setLoadProgress(0);
243
257
  setDuration(0);
244
258
  durationRef.current = 0;
245
- activePlayerId = playerId;
259
+ if (isActiveRef.current)
260
+ activePlayerId = playerId;
246
261
  syncProgressDom(0);
247
262
  const stored = loadStorage(lsKey);
248
263
  if (typeof stored.volume === 'number') {
@@ -6,3 +6,8 @@ export declare function isRootRelativeHref(href: string): boolean;
6
6
  * an in-app route.
7
7
  */
8
8
  export declare function isNativeDocumentHref(href: string): boolean;
9
+ /**
10
+ * Opens in a new tab. A bare `example.com` is a host, but a dot only in the
11
+ * query (`//storage?file=a.mp4`) is not.
12
+ */
13
+ export declare function isExternalHref(href: string): boolean;
@@ -1,3 +1,3 @@
1
1
  import * as T from './VideoPlayer.types';
2
2
  export type VideoPlayerProps = T.Props;
3
- export declare function VideoPlayer({ src, poster, storageKey, size, autoPlay, muted, loop, className, onPlayChange, onTimeUpdate, }: T.Props): import("react").JSX.Element;
3
+ export declare function VideoPlayer({ src, poster, storageKey, size, autoPlay, muted, loop, isActive, className, onPlayChange, onTimeUpdate, }: T.Props): import("react").JSX.Element;
@@ -7,6 +7,7 @@ export type Props = ComponentType & {
7
7
  autoPlay?: boolean;
8
8
  muted?: boolean;
9
9
  loop?: boolean;
10
+ isActive?: boolean;
10
11
  onPlayChange?: (playing: boolean) => void;
11
12
  onTimeUpdate?: (currentTime: number, duration: number) => void;
12
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homecode/ui",
3
- "version": "5.20.9",
3
+ "version": "5.20.11",
4
4
  "description": "React UI components library",
5
5
  "scripts": {
6
6
  "tests": "jest",