UNPKG

@react-av/editor

Version:

Editor Timeline Components built on React AV.

108 lines 8.84 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import * as Media from '@react-av/core'; import { useState, useMemo, cloneElement } from 'react'; import { toTimestampString as toTimestampStringShort } from "@react-av/controls"; import { toTimestampString } from '@react-av/vtt-core'; import useResizeObserver from 'use-resize-observer'; import { TimelineOverflowContainer } from './TimelineOverflowContainer'; import { useTimelineEditorContext } from './TimelineEditor'; function parseTimestampString(timestamp) { const [hms, ms] = timestamp.split("."); if (!hms) { if (ms) { const msInt = parseInt(ms); if (Number.isNaN(msInt)) { return 0; } return msInt / 1000; } return 0; } let timeUnits = hms.split(":"); if (timeUnits.length > 3) { // use only the last 3 timeUnits = timeUnits.slice(timeUnits.length - 3); } if (timeUnits.length < 3) { // pad with 0s timeUnits = new Array(3 - timeUnits.length).fill("0").concat(timeUnits); } const [hours, minutes, seconds] = timeUnits.map((unit) => parseInt(unit)); const msInt = ms ? parseInt(ms) : 0; if (Number.isNaN(hours) || Number.isNaN(minutes) || Number.isNaN(seconds) || Number.isNaN(msInt)) return 0; return hours * 3600 + minutes * 60 + seconds + msInt / 1000; } export function TimelineHeader({ styling }) { const { timelineInterval: interval } = useTimelineEditorContext(); const [currentTime, setCurrentTime] = Media.useMediaCurrentTimeFine(); const [internalTime, setInternalTime] = useState(false); const [, setPlaying] = Media.useMediaPlaying(); const duration = Media.useMediaDuration(); const [anchor, setAnchor] = useState(0); const [anchorTime, setAnchorTime] = useState(0); const [dragging, setDragging] = useState(false); const timelineIntervalCount = useMemo(() => { if (!duration) return 0; return Math.ceil(duration / interval); }, [duration, interval]); const { width: trackWidth, ref: trackRef } = useResizeObserver(); const { width: containerWidth, ref: containerRef } = useResizeObserver(); const { width: indicatorWidth, ref: indicatorRef } = useResizeObserver(); const { indicators, intervalWidth } = useMemo(() => { const indicators = []; const includeHours = Boolean(duration && duration > 3600); for (let i = 0; i < timelineIntervalCount; i++) { indicators.push(_jsx("div", { style: typeof (styling === null || styling === void 0 ? void 0 : styling.timelineHeaderTimestampIndicator) === 'string' ? {} : styling === null || styling === void 0 ? void 0 : styling.timelineHeaderTimestampIndicator, className: typeof (styling === null || styling === void 0 ? void 0 : styling.timelineHeaderTimestampIndicator) === 'string' ? styling.timelineHeaderTimestampIndicator : undefined, children: toTimestampStringShort(i * interval, includeHours) }, i)); } const finalIntervalTime = (timelineIntervalCount - 1) * interval; const finalIntervalWidth = Math.abs(((duration || 0) - finalIntervalTime) / interval * 8); if (duration) { indicators.push(_jsx("div", { style: typeof (styling === null || styling === void 0 ? void 0 : styling.timelineHeaderTimestampIndicator) === 'string' ? {} : styling === null || styling === void 0 ? void 0 : styling.timelineHeaderTimestampIndicator, className: typeof (styling === null || styling === void 0 ? void 0 : styling.timelineHeaderTimestampIndicator) === 'string' ? styling.timelineHeaderTimestampIndicator : undefined, ref: indicatorRef, children: toTimestampStringShort(duration, includeHours) }, timelineIntervalCount)); } // the second last indicator must be made trasparent if (indicators.length > 1) { indicators[indicators.length - 2] = cloneElement(indicators[indicators.length - 2], { style: Object.assign(Object.assign({}, indicators[indicators.length - 2].props.style), { color: 'transparent' }) }); } return { indicators, intervalWidth: finalIntervalWidth }; }, [duration, interval, timelineIntervalCount, styling]); return _jsxs(_Fragment, { children: [_jsx("div", { style: typeof (styling === null || styling === void 0 ? void 0 : styling.timelineHeaderTimestampInputContainer) === 'string' ? {} : styling === null || styling === void 0 ? void 0 : styling.timelineHeaderTimestampInputContainer, className: typeof (styling === null || styling === void 0 ? void 0 : styling.timelineHeaderTimestampInputContainer) === 'string' ? styling.timelineHeaderTimestampInputContainer : undefined, children: _jsx("input", { className: typeof (styling === null || styling === void 0 ? void 0 : styling.timelineHeaderTimestampInput) === 'string' ? styling.timelineHeaderTimestampInput : undefined, style: typeof (styling === null || styling === void 0 ? void 0 : styling.timelineHeaderTimestampInput) === 'string' ? {} : styling === null || styling === void 0 ? void 0 : styling.timelineHeaderTimestampInput, type: "text", "aria-label": "Timestamp", value: internalTime !== false ? internalTime : toTimestampString(currentTime), onChange: e => { setInternalTime(e.target.value.replace(/[^0-9:.]/g, "")); setPlaying(false); setCurrentTime(parseTimestampString(e.target.value)); }, onFocus: () => setPlaying(false), onBlur: () => setInternalTime(false) }) }), _jsxs(TimelineOverflowContainer, { componentRole: "timeline-header", ref: containerRef, onMouseDown: e => { if (e.button !== 0) return; setPlaying(false); setAnchor(e.clientX); setAnchorTime(currentTime); setDragging(true); }, onTouchStart: e => { const touch = e.touches[0]; if (!touch) return; setPlaying(false); setAnchor(touch.clientX); setAnchorTime(currentTime); setDragging(true); }, onMouseMove: e => { if (!indicatorWidth || !dragging) return; const delta = anchor - e.clientX; const timeDelta = (delta / (indicatorWidth + 16)) * interval; setCurrentTime(anchorTime + timeDelta); }, onTouchMove: e => { const touch = e.touches[0]; if (!touch || !indicatorWidth) return; const delta = anchor - touch.clientX; const timeDelta = (delta / (indicatorWidth + 16)) * interval; setCurrentTime(anchorTime + timeDelta); }, onMouseUp: () => setDragging(false), onMouseLeave: () => setDragging(false), onMouseEnter: () => setDragging(false), onTouchEnd: () => setDragging(false), style: { cursor: dragging ? 'grabbing' : 'grab' }, children: [_jsx("div", { style: Object.assign(Object.assign({}, (typeof (styling === null || styling === void 0 ? void 0 : styling.timelineHeaderPlayhead) === 'string' ? {} : styling === null || styling === void 0 ? void 0 : styling.timelineHeaderPlayhead)), { left: '50%', transform: `translateX(-50%)`, position: 'absolute', zIndex: 2 }), className: typeof (styling === null || styling === void 0 ? void 0 : styling.timelineHeaderPlayhead) === 'string' ? styling.timelineHeaderPlayhead : undefined }), _jsx("div", { style: Object.assign(Object.assign({ '--time-indicator-count': timelineIntervalCount + 1, '--time-indicator-final-width': duration ? `${intervalWidth}rem` : undefined, transform: `translateX(calc(${(containerWidth !== null && containerWidth !== void 0 ? containerWidth : 0) / 2}px - ${(currentTime / duration) * (trackWidth !== null && trackWidth !== void 0 ? trackWidth : 0)}px + ${(currentTime / duration) * 8}rem))` }, (typeof (styling === null || styling === void 0 ? void 0 : styling.timelineHeaderIndicatorReel) === 'string' ? {} : styling === null || styling === void 0 ? void 0 : styling.timelineHeaderIndicatorReel)), { display: 'grid', height: '100%', width: 'min-content', gridTemplateColumns: 'repeat(calc(var(--time-indicator-count, 2) - 2), 8rem) var(--time-indicator-final-width, 8rem) 8rem' }), className: typeof (styling === null || styling === void 0 ? void 0 : styling.timelineHeaderIndicatorReel) === 'string' ? styling.timelineHeaderIndicatorReel : undefined, ref: trackRef, children: indicators })] })] }); } //# sourceMappingURL=TimelineHeader.js.map