hypnagaga/src/components/Video/Video.svelte
2025-03-24 09:57:10 -07:00

330 lines
10 KiB
Svelte

<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
<!-- @component `Video` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-multimedia-video--docs) -->
<script lang="ts">
import IntersectionObserver from 'svelte-intersection-observer';
import Controls from './Controls.svelte';
import GraphicBlock from '../GraphicBlock/GraphicBlock.svelte';
import type { ContainerWidth } from '../@types/global';
/// //////////////////////////////////
/// /////////// Props ////////////////
/// //////////////////////////////////
/**
* Video src
* @type {string}
* @required
*/
export let src: string;
/**
* Image to be shown while the video is downloading
*/
export let poster: string = '';
/**
* Whether to wrap the graphic with an aria hidden tag.
*/
export let hidden: boolean = true;
/**
* ARIA description, passed in as a markdown string.
* @type {string}
*/
export let ariaDescription: string | null = null;
/** Add extra classes to the block tag to target it with custom CSS. */
let cls: string = '';
export { cls as class };
/**
* Title of the graphic
* @type {string}
*/
export let title: string | null = null;
/**
* Notes to the graphic, passed in as a markdown string.
* @type {string}
*/
export let notes: string | null = null;
/**
* Description of the graphic, passed in as a markdown string.
* @type {string}
*/
export let description: string | null = null;
/**
* Width of the block within the article well.
* @type {string}
*/
export let width: ContainerWidth = 'normal';
type PreloadOptions = 'auto' | 'none' | 'metadata';
/**
* Set a different width for the text within the text well, for example,
* "normal" to keep the title, description and notes inline with the rest
* of the text well. Can't ever be wider than `width`.
* @type {string}
*/
export let textWidth: ContainerWidth | null = 'normal';
/**
* Preload options. `auto` is ignored if `autoplay` is true. Can also be `none` or `metadata`.
* @type {string}
*/
export let preloadVideo: PreloadOptions = 'auto';
/**
* Whether the video should loop.
*/
export let loopVideo: boolean = true;
/**
* Whether video should have sound or not.
*/
export let muteVideo: boolean = true;
export let allowSoundToAutoplay = false; // for video with sound, whether video should be allowed to autoplay if the user has previously interacted with DOM
export let playVideoWhenInView = true; // whether the video should play when it comes into view or just on page load
export let playVideoThreshold = 0.5; // if video plays with intersection observer, how much of it should be into view to start playing
export let possibleToPlayPause = true; // whether to have the option to pause and play video
export let showControls = true; // whetner to show the play / pause buttons
export let hoverToSeeControls = false; // whether you need to hover over the video to see the controls
export let separateReplayIcon = false; // whether to use a separate replay icon or use the play icon for replay as well
export let controlsColour = '#333'; // change the colour of the play/pause button
export let controlsOpacity = 0.5; // change the opacity of the play/pause button
$: interactiveControlsOpacity = 0;
export let controlsPosition = 'top left'; // have four options for controls position - top right, top left, bottom right, bottom left
/// //////////////////////////////////
/// /////// Internal Logic ///////////
/// //////////////////////////////////
// Internal props
let time = 0;
let duration;
let paused = true;
let clickedOnPauseBtn = false; // special variable to track if user clicked on 'pause' btn to help with audio logic
$: resetCondition = time >= duration; // - 0.1;
// Dimensions etc other useful things
let heightVideo;
let widthVideo;
let heightVideoContainer;
let widthVideoContainer;
const controlsBorderOffset = 50;
// For intersection observer
let intersecting;
let element;
let videoElement;
// For video with sound, check if there has been an interaction with the DOM
let interactedWithDom = false;
const setInteractedWithDom = () => {
interactedWithDom = true;
};
// Play the video (with no sound) if it's intersecting; pause when it's no longer intersecting
$: if (playVideoWhenInView && intersecting && muteVideo) paused = false;
$: if (playVideoWhenInView && !intersecting) paused = true;
// Special case for video with sound
// Only ff you've clicked on play button or interacted with DOM in any way previously, video with audio will play
$: if (
allowSoundToAutoplay &&
playVideoWhenInView &&
intersecting &&
!muteVideo &&
interactedWithDom &&
!clickedOnPauseBtn // so video doesn't autoplay when coming into view again if paused previously
) {
paused = false;
}
$: if (allowSoundToAutoplay && !muteVideo && !interactedWithDom) {
paused = true;
}
$: if (!possibleToPlayPause) showControls = true;
// To get the pause state passed up from the Controls
const pausePlayEvent = (e) => {
const fwdPaused = e.detail.paused;
const fwdClickedOnPauseBtn = e.detail.clickedOnPauseBtn;
paused = fwdPaused;
clickedOnPauseBtn = fwdClickedOnPauseBtn;
};
// Warning to missing aria attributes
if (hidden && !ariaDescription) {
console.warn(
'Must provide aria description for video components if hidden is true.'
);
}
</script>
<svelte:window
on:click={setInteractedWithDom}
on:touchstart={setInteractedWithDom}
/>
<GraphicBlock
{textWidth}
{title}
{description}
{notes}
{width}
class="video {cls}"
>
<div
role="figure"
on:mouseover={() => {
interactiveControlsOpacity = controlsOpacity;
}}
on:focus={() => {
interactiveControlsOpacity = controlsOpacity;
}}
on:mouseout={() => {
interactiveControlsOpacity = 0;
}}
on:blur={() => {
interactiveControlsOpacity = 0;
}}
>
{#if (hidden && ariaDescription) || !hidden}
{#if ariaDescription}
<p class="visually-hidden">{ariaDescription}</p>
{/if}
{#if playVideoWhenInView}
<!-- Video element with Intersection Observer -->
<IntersectionObserver
{element}
bind:intersecting
threshold={playVideoThreshold}
once={false}
>
<div
bind:this={element}
class="video-wrapper relative block"
aria-hidden={hidden}
bind:clientWidth={widthVideoContainer}
bind:clientHeight={heightVideoContainer}
>
{#if possibleToPlayPause}
{#if showControls}
<Controls
on:pausePlayEvent={pausePlayEvent}
{paused}
{clickedOnPauseBtn}
controlsOpacity={hoverToSeeControls ?
interactiveControlsOpacity
: controlsOpacity}
{controlsPosition}
{widthVideoContainer}
{heightVideoContainer}
{controlsBorderOffset}
{resetCondition}
{separateReplayIcon}
{controlsColour}
/>
{:else}
<button
class="border-0 m-0 p-0 bg-transparent absolute"
on:click={() => {
if (paused === true) {
paused = false;
} else {
paused = true;
}
}}
style="top: 0; left: 0; width: {widthVideoContainer}px; height: {heightVideoContainer}px;"
></button>
{/if}
{/if}
<video
bind:this={videoElement}
{src}
{poster}
class="pointer-events-none relative"
width="100%"
muted={muteVideo}
playsinline
preload={preloadVideo}
loop={loopVideo}
bind:currentTime={time}
bind:duration
bind:paused
bind:clientWidth={widthVideo}
bind:clientHeight={heightVideo}
>
<track kind="captions" />
</video>
</div>
</IntersectionObserver>
{:else}
<!-- Video element without Intersection observer -->
<div
class="video-wrapper relative"
aria-hidden={hidden}
bind:clientWidth={widthVideoContainer}
bind:clientHeight={heightVideoContainer}
>
{#if possibleToPlayPause}
{#if showControls}
<Controls
on:pausePlayEvent={pausePlayEvent}
{paused}
{clickedOnPauseBtn}
{controlsOpacity}
{controlsPosition}
{widthVideoContainer}
{heightVideoContainer}
{controlsBorderOffset}
{resetCondition}
{separateReplayIcon}
{controlsColour}
/>
{:else}
<button
class="border-0 m-0 p-0 bg-transparent absolute"
on:click={() => {
if (paused === true) {
paused = false;
} else {
paused = true;
}
}}
style="top: 0; left: 0; width: {widthVideoContainer}px; height: {heightVideoContainer}px;"
></button>
{/if}
{/if}
<video
bind:this={videoElement}
{src}
{poster}
class="pointer-events-none relative"
width="100%"
muted={muteVideo}
playsinline
preload={preloadVideo}
loop={loopVideo}
bind:currentTime={time}
bind:duration
bind:paused
autoplay
bind:clientWidth={widthVideo}
bind:clientHeight={heightVideo}
>
<track kind="captions" />
</video>
</div>
{/if}
{/if}
</div>
{#if $$slots.notes}
<!-- Custom notes and source slot -->
<slot name="notes" />
{/if}
</GraphicBlock>