UNPKG

wix-style-react

Version:
310 lines (268 loc) • 9.18 kB
import React, { memo, useCallback, useMemo, useState, useEffect, forwardRef, useRef, useImperativeHandle, } from 'react'; import PropTypes from 'prop-types'; import { st, classes, vars } from './AudioPlayer.st.css'; import Tooltip from '../Tooltip'; import IconButton from '../IconButton'; import Loader from '../Loader'; import Heading from '../Heading'; import PlayFilled from 'wix-ui-icons-common/PlayFilled'; import PauseFilled from 'wix-ui-icons-common/PauseFilled'; import { dataHooks } from './constants'; import { useAudioManager } from './AudioManager/AudioManager'; import { positionToSeconds, secondsToISO, secondsToPosition } from './utils'; /** AudioPlayer */ const AudioPlayer = memo( forwardRef( ( { dataHook, className, src, format, preload, webAudioAPI, onLoad, onLoadError, onPlay, onPause, onEnd, onSeek, }, ref, ) => { const [isSliderLocked, setIsSliderLocked] = useState(true); const [hoverPosition, setHoverPosition] = useState(0); const [handleSizeInPercentage, setHandleSizeInPercentage] = useState(0); const [playing, setPlaying] = useState(false); const [showDuration, setShowDuration] = useState(true); const playPauseButtonRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => playPauseButtonRef?.current?.focus(), })); const _onDestroy = useCallback(() => { setPlaying(false); setShowDuration(true); }, [setPlaying, setShowDuration]); const _onEnd = useCallback(() => { setShowDuration(true); setPlaying(false); onEnd && onEnd(); }, [onEnd, setShowDuration, setPlaying]); const { loadingState, duration, seek, setSeek } = useAudioManager({ src, format, preload, webAudioAPI, onLoad, onLoadError, onPlay, onSeek, onPause, playing, onDestroy: _onDestroy, onEnd: _onEnd, allowSeekLoop: isSliderLocked, }); const isLoaded = loadingState === 'loaded'; const _hoverISO = useMemo(() => { if (!isLoaded) { return secondsToISO(0, false, duration); } return secondsToISO( positionToSeconds(hoverPosition, duration), true, duration, ); }, [isLoaded, hoverPosition, duration]); // takes the current seek (in seconds) and converts it to slider position. const _seekPercentage = useMemo(() => { if (!isLoaded) { return 0; } return secondsToPosition(seek, duration); }, [isLoaded, seek, duration]); const _togglePlayPause = useCallback(() => { setShowDuration(false); if (playing) { setPlaying(false); } else { setPlaying(true); } }, [playing, setPlaying, setShowDuration]); const _playPauseButtonContent = useMemo(() => { if (loadingState === 'loading') { return ( <span data-hook={dataHooks.audioPlayerLoad}> <Loader size="tiny" /> </span> ); } return playing ? ( <PauseFilled data-hook={dataHooks.audioPlayerPause} /> ) : ( <PlayFilled data-hook={dataHooks.audioPlayerPlay} /> ); }, [loadingState, playing]); const _setSliderPositions = useCallback( (x, width, clickX) => { const positionInPixels = ((clickX - x) / width) * 100; const position = Math.min(Math.max(positionInPixels, 0), 100); setHandleSizeInPercentage((12 / width) * 100); setHoverPosition(position); }, [setHoverPosition], ); const _handleSliderMouseDown = useCallback( event => { const { clientX, currentTarget } = event; const { x, width } = currentTarget.getBoundingClientRect(); setIsSliderLocked(false); _setSliderPositions(x, width, clientX); }, [_setSliderPositions, setIsSliderLocked], ); const _handleSliderMouseMove = useCallback( event => { const { clientX, currentTarget } = event; const { x, width } = currentTarget.getBoundingClientRect(); _setSliderPositions(x, width, clientX); }, [_setSliderPositions], ); const _handleSliderMouseUp = useCallback(() => { setIsSliderLocked(true); }, [setIsSliderLocked]); useEffect(() => { window.addEventListener('mouseup', _handleSliderMouseUp); return () => window.removeEventListener('mouseup', _handleSliderMouseUp); }, [_handleSliderMouseUp]); // seek audio file to the slider location when dragged. useEffect(() => { if (!isSliderLocked) { setSeek(positionToSeconds(hoverPosition, duration)); setShowDuration(false); } }, [duration, hoverPosition, isSliderLocked, setSeek]); return ( <div className={st(classes.root, className)} data-hook={dataHook}> <IconButton ref={playPauseButtonRef} size="small" onClick={_togglePlayPause} dataHook={dataHooks.audioPlayerPlayPause} className={classes.playPauseButton} > {_playPauseButtonContent} </IconButton> <div data-hook={dataHooks.audioPlayerSlider} className={classes.slider} style={{ [vars['audio-player-position']]: `${_seekPercentage}%`, }} onMouseDown={_handleSliderMouseDown} onMouseMove={_handleSliderMouseMove} > <div className={classes.track} /> <div className={classes.tooltip} style={{ left: `${hoverPosition}%` }} > <Tooltip content={`${_hoverISO}`}> <div className={classes.tooltipTarget} /> </Tooltip> </div> <div data-hook={dataHooks.audioPlayerSliderHandle} className={st(classes.handle, { grow: isLoaded && (!isSliderLocked || Math.abs(_seekPercentage - hoverPosition) < handleSizeInPercentage), })} style={{ left: `${_seekPercentage}%` }} /> </div> <Heading appearance="H5" className={classes.timer} dataHook={dataHooks.audioTimeIndicator} > {showDuration ? secondsToISO(duration, isLoaded, duration) : secondsToISO(seek, isLoaded, duration)} </Heading> </div> ); }, ), ); AudioPlayer.displayName = 'AudioPlayer'; AudioPlayer.propTypes = { /** Applies a data-hook HTML attribute that can be used in the tests. */ dataHook: PropTypes.string, /** Specifies a CSS class name to be appended to the component’s root element. */ className: PropTypes.string, /** * Specifies a link to the source of the track to be loaded for the sound (URL or base64 data URI). * If a file has no extension, you will need to specify the extension using the format property. */ src: PropTypes.string.isRequired, /** * Specifies a file format in situations where extraction does not work (such as a SoundCloud stream).<br/> * By default, AudioPlayer detects your file format from the extension. */ format: PropTypes.string, /** * Determines what to download when the component is rendered: full file, its metadata or nothing at all. * When webAudioAPI = true you can only set it to either 'auto' or 'none'. * When webAudioAPI = false you can set it to 'auto', 'metadata' or 'none'. */ preload: PropTypes.oneOf(['auto', 'metadata', 'none']), /** * Specifies whether to force web audio API. Use it for relatively small audio files only because you have to wait for the full file * to be downloaded and decoded before playing. Web Audio API allows advanced capabilities as described in * [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API). */ webAudioAPI: PropTypes.bool, /** * Defines a callback function which is called when audio is loaded. */ onLoad: PropTypes.func, /** * Defines a callback function which is called every time audio fails to load. */ onLoadError: PropTypes.func, /** * Defines a callback function which is called when audio is played. */ onPlay: PropTypes.func, /** * Defines a callback function which is called when audio is paused. */ onPause: PropTypes.func, /** Will be called when audio is ended. */ onEnd: PropTypes.func, /** * Defines a callback function which is called when audio is seeked explicitly (i.e. when user drags the slider). */ onSeek: PropTypes.func, }; AudioPlayer.defaultProps = { preload: 'metadata', webAudioAPI: false, }; export default AudioPlayer;