hypnagaga/src/components/Video/Video.svelte
2023-07-29 16:54:59 +01:00

265 lines
9.3 KiB
Svelte

<!-- @component `Video` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-Video--default) -->
<script lang="ts">
import IntersectionObserver from 'svelte-intersection-observer';
import Controls from './Controls.svelte';
import Block from '../Block/Block.svelte';
import type { ContainerWidth } from '../@types/global';
/// //////////////////////////////////
/// /////////// Props ////////////////
/// //////////////////////////////////
/**
* Video src.
*/
export let src = '';
export let ariaHidden = true;
export let ariaDescription = null;
export let caption = '';
/**
* Width of the block within the article well.
* @type {string}
*/
export let width: ContainerWidth = 'normal';
type PreloadOptions = 'auto' | 'none' | 'metadata';
/**
* 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 (ariaHidden && !ariaDescription) {
console.warn(
'Must provide aria description for video components if ariaHidden is true.'
);
}
</script>
<svelte:window
on:click="{setInteractedWithDom}"
on:touchstart="{setInteractedWithDom}"
/>
<Block width="{width}" class="video-container">
<div
on:mouseover="{() => {
interactiveControlsOpacity = controlsOpacity;
}}"
on:focus="{() => {
interactiveControlsOpacity = controlsOpacity;
}}"
on:mouseout="{() => {
interactiveControlsOpacity = 0;
}}"
on:blur="{() => {
interactiveControlsOpacity = 0;
}}"
>
{#if (ariaHidden && ariaDescription) || !ariaHidden}
{#if ariaDescription}
<p class="visually-hidden">{ariaDescription}</p>
{/if}
{#if playVideoWhenInView}
<!-- Video element with Intersection Observer -->
<IntersectionObserver
element="{element}"
bind:intersecting="{intersecting}"
threshold="{playVideoThreshold}"
once="{false}"
>
<div
bind:this="{element}"
class="video-wrapper relative"
aria-hidden="{ariaHidden}"
bind:clientWidth="{widthVideoContainer}"
bind:clientHeight="{heightVideoContainer}"
>
{#if possibleToPlayPause}
{#if showControls}
<Controls
on:pausePlayEvent="{pausePlayEvent}"
paused="{paused}"
clickedOnPauseBtn="{clickedOnPauseBtn}"
controlsOpacity="{hoverToSeeControls
? interactiveControlsOpacity
: controlsOpacity}"
controlsPosition="{controlsPosition}"
widthVideoContainer="{widthVideoContainer}"
heightVideoContainer="{heightVideoContainer}"
controlsBorderOffset="{controlsBorderOffset}"
resetCondition="{resetCondition}"
separateReplayIcon="{separateReplayIcon}"
controlsColour="{controlsColour}"
/>
{:else}
<button
class="border-0 m-0 p-0 bg-transparent absolute"
on:click="{() => {
paused === true ? (paused = false) : (paused = true);
}}"
style="top: 0; left: 0; width: {widthVideoContainer}px; height: {heightVideoContainer}px;"
></button>
{/if}
{/if}
<video
bind:this="{videoElement}"
src="{src}"
class="pointer-events-none relative"
width="100%"
muted="{muteVideo}"
playsinline
preload="{preloadVideo}"
loop="{loopVideo}"
bind:currentTime="{time}"
bind:duration="{duration}"
bind:paused="{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="{ariaHidden}"
bind:clientWidth="{widthVideoContainer}"
bind:clientHeight="{heightVideoContainer}"
>
{#if possibleToPlayPause}
{#if showControls}
<Controls
on:pausePlayEvent="{pausePlayEvent}"
paused="{paused}"
clickedOnPauseBtn="{clickedOnPauseBtn}"
controlsOpacity="{controlsOpacity}"
controlsPosition="{controlsPosition}"
widthVideoContainer="{widthVideoContainer}"
heightVideoContainer="{heightVideoContainer}"
controlsBorderOffset="{controlsBorderOffset}"
resetCondition="{resetCondition}"
separateReplayIcon="{separateReplayIcon}"
controlsColour="{controlsColour}"
/>
{:else}
<button
class="border-0 m-0 p-0 bg-transparent absolute"
on:click="{() => {
paused === true ? (paused = false) : (paused = true);
}}"
style="top: 0; left: 0; width: {widthVideoContainer}px; height: {heightVideoContainer}px;"
></button>
{/if}
{/if}
<video
bind:this="{videoElement}"
src="{src}"
class="pointer-events-none relative"
width="100%"
muted="{muteVideo}"
playsinline
preload="{preloadVideo}"
loop="{loopVideo}"
bind:currentTime="{time}"
bind:duration="{duration}"
bind:paused="{paused}"
autoplay
bind:clientWidth="{widthVideo}"
bind:clientHeight="{heightVideo}"
>
<track kind="captions" />
</video>
</div>
{/if}
{#if caption}
<div class="caption text-secondary text-xs">{caption}</div>
{/if}
{/if}
</div>
</Block>