mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(packages): add gesture bindings to default skins and presets (#1310)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { listen } from '@videojs/utils/dom';
|
||||
import { isInteractiveTarget, listen } from '@videojs/utils/dom';
|
||||
|
||||
import type { GestureBinding, GestureMatchResult, GestureRecognizer, GestureRegion, GestureType } from './gesture';
|
||||
import { resolveRegion } from './region';
|
||||
@@ -15,6 +15,10 @@ export class GestureCoordinator {
|
||||
this.#target = target;
|
||||
}
|
||||
|
||||
get bindings(): readonly GestureBinding[] {
|
||||
return this.#bindings;
|
||||
}
|
||||
|
||||
add(binding: GestureBinding): () => void {
|
||||
this.#bindings.push(binding);
|
||||
this.#recognizers.add(binding.recognizer);
|
||||
@@ -55,6 +59,7 @@ export class GestureCoordinator {
|
||||
'pointerup',
|
||||
(event) => {
|
||||
if (Date.now() - pointerDownTime > TAP_THRESHOLD) return;
|
||||
if (isInteractiveTarget(event)) return;
|
||||
|
||||
const pointerType = event.pointerType;
|
||||
const clientX = event.clientX;
|
||||
|
||||
@@ -35,6 +35,7 @@ export function createTapGesture(
|
||||
pointer: options?.pointer,
|
||||
region: options?.region,
|
||||
disabled: options?.disabled,
|
||||
action: options?.action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,5 +61,6 @@ export function createDoubleTapGesture(
|
||||
pointer: options?.pointer,
|
||||
region: options?.region,
|
||||
disabled: options?.disabled,
|
||||
action: options?.action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface GestureOptions {
|
||||
pointer?: GesturePointerType | undefined;
|
||||
region?: GestureRegion | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
action?: string | undefined;
|
||||
}
|
||||
|
||||
export interface GestureBinding {
|
||||
@@ -17,6 +18,7 @@ export interface GestureBinding {
|
||||
pointer?: GesturePointerType | undefined;
|
||||
region?: GestureRegion | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
action?: string | undefined;
|
||||
}
|
||||
|
||||
export interface GestureRecognizer {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isNull } from '@videojs/utils/predicate';
|
||||
|
||||
import type { MediaControlsState } from '../../../core/media/state';
|
||||
import { definePlayerFeature } from '../../feature';
|
||||
import { findGestureCoordinator } from '../../gesture/coordinator';
|
||||
|
||||
const IDLE_DELAY = 2000;
|
||||
const TAP_THRESHOLD = 250;
|
||||
@@ -80,7 +81,17 @@ export const controlsFeature = definePlayerFeature({
|
||||
|
||||
function onPointerUp(event: PointerEvent) {
|
||||
if (event.pointerType === 'touch' && Date.now() - pointerDownTime < TAP_THRESHOLD) {
|
||||
// If the event target is in the controls don't set inactive because that sets pointer-events: none in CSS.
|
||||
// When a toggleControls touch tap gesture is registered, it handles toggle — skip inline handler.
|
||||
const coordinator = findGestureCoordinator(container as HTMLElement);
|
||||
if (
|
||||
coordinator?.bindings.some(
|
||||
(b) => b.type === 'tap' && b.action === 'toggleControls' && (!b.pointer || b.pointer === 'touch')
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Inline touch tap-to-toggle for standalone use (no gestures).
|
||||
const isMediaOrContainer = [media, container].includes(event.target as HTMLElement);
|
||||
if (get().controlsVisible && isMediaOrContainer) {
|
||||
setInactive();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// without creating a skin element. Use this entry when building an ejected
|
||||
// (light DOM) player layout.
|
||||
import { MediaContainerElement } from '../../media/container-element';
|
||||
import { GestureElement } from '../../ui/gesture/gesture-element';
|
||||
import { HotkeyElement } from '../../ui/hotkey/hotkey-element';
|
||||
import { MuteButtonElement } from '../../ui/mute-button/mute-button-element';
|
||||
import { PlayButtonElement } from '../../ui/play-button/play-button-element';
|
||||
@@ -28,6 +29,7 @@ defineVolumeSlider();
|
||||
defineTime();
|
||||
|
||||
// Standalone elements.
|
||||
safeDefine(GestureElement);
|
||||
safeDefine(HotkeyElement);
|
||||
safeDefine(MuteButtonElement);
|
||||
safeDefine(PlayButtonElement);
|
||||
|
||||
@@ -149,6 +149,13 @@ function getTemplateHTML() {
|
||||
<media-hotkey keys="End" action="seekToPercent" value="100"></media-hotkey>
|
||||
<media-hotkey keys=">" action="speedUp"></media-hotkey>
|
||||
<media-hotkey keys="<" action="speedDown"></media-hotkey>
|
||||
|
||||
<!-- Gestures -->
|
||||
<media-gesture type="tap" action="togglePaused" pointer="mouse" region="center"></media-gesture>
|
||||
<media-gesture type="tap" action="toggleControls" pointer="touch"></media-gesture>
|
||||
<media-gesture type="doubletap" action="seekStep" value="-10" region="left"></media-gesture>
|
||||
<media-gesture type="doubletap" action="toggleFullscreen" region="center"></media-gesture>
|
||||
<media-gesture type="doubletap" action="seekStep" value="10" region="right"></media-gesture>
|
||||
</media-container>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MediaContainerElement } from '../../media/container-element';
|
||||
import { BufferingIndicatorElement } from '../../ui/buffering-indicator/buffering-indicator-element';
|
||||
import { CaptionsButtonElement } from '../../ui/captions-button/captions-button-element';
|
||||
import { FullscreenButtonElement } from '../../ui/fullscreen-button/fullscreen-button-element';
|
||||
import { GestureElement } from '../../ui/gesture/gesture-element';
|
||||
import { HotkeyElement } from '../../ui/hotkey/hotkey-element';
|
||||
import { MuteButtonElement } from '../../ui/mute-button/mute-button-element';
|
||||
import { PiPButtonElement } from '../../ui/pip-button/pip-button-element';
|
||||
@@ -37,6 +38,7 @@ defineTime();
|
||||
safeDefine(BufferingIndicatorElement);
|
||||
safeDefine(CaptionsButtonElement);
|
||||
safeDefine(FullscreenButtonElement);
|
||||
safeDefine(GestureElement);
|
||||
safeDefine(HotkeyElement);
|
||||
safeDefine(MuteButtonElement);
|
||||
safeDefine(PiPButtonElement);
|
||||
|
||||
@@ -79,6 +79,7 @@ export class GestureElement extends MediaElement {
|
||||
pointer: this.pointer,
|
||||
region: this.region,
|
||||
disabled: this.disabled,
|
||||
action: this.action,
|
||||
};
|
||||
|
||||
if (this.type === 'doubletap') {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { CaptionsButton } from '@/ui/captions-button';
|
||||
import { Controls } from '@/ui/controls';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { FullscreenButton } from '@/ui/fullscreen-button';
|
||||
import { MediaGesture } from '@/ui/gesture/media-gesture';
|
||||
import { MediaHotkey } from '@/ui/hotkey/media-hotkey';
|
||||
import { MuteButton } from '@/ui/mute-button';
|
||||
import { PiPButton } from '@/ui/pip-button';
|
||||
@@ -249,6 +250,13 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
|
||||
<MediaHotkey keys="End" action="seekToPercent" value={100} />
|
||||
<MediaHotkey keys=">" action="speedUp" />
|
||||
<MediaHotkey keys="<" action="speedDown" />
|
||||
|
||||
{/* Gestures */}
|
||||
<MediaGesture type="tap" action="togglePaused" pointer="mouse" region="center" />
|
||||
<MediaGesture type="tap" action="toggleControls" pointer="touch" />
|
||||
<MediaGesture type="doubletap" action="seekStep" value={-10} region="left" />
|
||||
<MediaGesture type="doubletap" action="toggleFullscreen" region="center" />
|
||||
<MediaGesture type="doubletap" action="seekStep" value={10} region="right" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function MediaGesture({ type, action, value, pointer, region, disabled }:
|
||||
resolver({ store, value, event });
|
||||
};
|
||||
|
||||
const options = { pointer, region };
|
||||
const options = { pointer, region, action };
|
||||
|
||||
if (type === 'doubletap') {
|
||||
return createDoubleTapGesture(container, onActivate, options);
|
||||
|
||||
@@ -12,6 +12,7 @@ export {
|
||||
isHTMLAudioElement,
|
||||
isHTMLMediaElement,
|
||||
isHTMLVideoElement,
|
||||
isInteractiveTarget,
|
||||
} from './predicates';
|
||||
export { type RafThrottled, rafThrottle } from './raf-throttle';
|
||||
export {
|
||||
|
||||
@@ -33,3 +33,12 @@ export function isEditableTarget(event: KeyboardEvent): boolean {
|
||||
const target = resolveEventTarget(event);
|
||||
return target instanceof Element && isEditableElement(target);
|
||||
}
|
||||
|
||||
const INTERACTIVE_SELECTOR = 'button, input, select, textarea, [role="button"], [role="slider"]';
|
||||
|
||||
/** Whether the event originated from an interactive control (button, slider, etc). */
|
||||
export function isInteractiveTarget(event: Event): boolean {
|
||||
const target = resolveEventTarget(event);
|
||||
if (!(target instanceof Element)) return false;
|
||||
return target.closest(INTERACTIVE_SELECTOR) !== null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user