UNPKG

wix-style-react

Version:
192 lines • 8.96 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, PauseFilled } from '@wix/wix-ui-icons-common'; 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 = 'metadata', webAudioAPI = false, onLoad, onLoadError, onPlay, onPause, onEnd, onSeek, autoplay, }, 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 && 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 _onLoad = useCallback(() => { if (autoplay) { setShowDuration(false); setPlaying(true); } onLoad && onLoad(); }, [onLoad, autoplay]); const { loadingState, duration, seek, setSeek } = useAudioManager({ src, format, preload, webAudioAPI, onLoad: _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 (React.createElement("span", { "data-hook": dataHooks.audioPlayerLoad }, React.createElement(Loader, { size: "tiny" }))); } return playing ? (React.createElement(PauseFilled, { "data-hook": dataHooks.audioPlayerPause })) : (React.createElement(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 (React.createElement("div", { className: st(classes.root, className), "data-hook": dataHook }, React.createElement(IconButton, { ref: playPauseButtonRef, size: "small", onClick: _togglePlayPause, dataHook: dataHooks.audioPlayerPlayPause, className: classes.playPauseButton }, _playPauseButtonContent), React.createElement("div", { "data-hook": dataHooks.audioPlayerSlider, className: classes.slider, style: { [vars['audio-player-position']]: `${_seekPercentage}%`, }, onMouseDown: _handleSliderMouseDown, onMouseMove: _handleSliderMouseMove }, React.createElement("div", { className: classes.track }), React.createElement("div", { className: classes.tooltip, style: { left: `${hoverPosition}%` } }, React.createElement(Tooltip, { content: `${_hoverISO}` }, React.createElement("div", { className: classes.tooltipTarget }))), React.createElement("div", { "data-hook": dataHooks.audioPlayerSliderHandle, className: st(classes.handle, { grow: isLoaded && (!isSliderLocked || Math.abs(_seekPercentage - hoverPosition) < handleSizeInPercentage), }), style: { left: `${_seekPercentage}%` } })), React.createElement(Heading, { size: "tiny", className: classes.timer, dataHook: dataHooks.audioTimeIndicator }, showDuration ? secondsToISO(duration, isLoaded, duration) : secondsToISO(seek, isLoaded, duration)))); })); 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']), /** * Start playback automatically when audio is loaded. */ autoplay: PropTypes.bool, /** * 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, }; export default AudioPlayer; //# sourceMappingURL=AudioPlayer.js.map