UNPKG

@react-av/editor

Version:

Editor Timeline Components built on React AV.

403 lines 22.6 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useEffect, useMemo, useRef, useState } from "react"; import * as Media from '@react-av/core'; import useResizeObserver from "use-resize-observer"; import { TimelineOverflowContainer } from "./TimelineOverflowContainer"; import { DraftElement, DragElement, PlayheadLine, TimelineElement } from "./TimelineElements"; import { useEditorContext } from "./Editor"; import { useTimelineEditorContext } from "./TimelineEditor"; export function TimelineTrack({ labelComponent, draft, snap, onDraftCreate, entries, onEntrySelect, selectedSymbol, onEntryMove, onEntryEdit, onEntryDelete, onEntryResize, selectedRef: externalSelectedRef }) { const { styling } = useEditorContext(); const { timelineInterval: interval } = useTimelineEditorContext(); const [currentTime, setCurrentTime] = Media.useMediaCurrentTimeFine(); const duration = Media.useMediaDuration(); const [, setPlaying] = Media.useMediaPlaying(); const [anchor, setAnchor] = useState(0); const [currentAnchor, setCurrentAnchor] = useState(0); const [dragging, setDragging] = useState(false); const [moving, setMoving] = useState(false); const [resizing, setResizing] = useState(undefined); const internalSelectedRef = useRef(null); const [interactionCursor, setInteractionCursor] = useState("default"); const selectedRef = externalSelectedRef !== null && externalSelectedRef !== void 0 ? externalSelectedRef : internalSelectedRef; const containerRef = useRef(null); const { width: trackWidth, ref: trackRef } = useResizeObserver(); const { width: containerWidth } = useResizeObserver({ ref: containerRef }); const selectedEntry = useMemo(() => { if (!selectedSymbol || !entries) return; return entries.find(entry => entry.symbol === selectedSymbol); }, [selectedSymbol, entries]); const anchorTime = useMemo(() => { var _a; if (!duration) return 0; const rect = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect(); if (!rect) return 0; const currentTimestampOffset = (rect.width / 2); const relativeAnchorX = anchor - rect.left; const delta = relativeAnchorX - currentTimestampOffset; const timeDelta = delta / (8 * 16) * interval; return Math.min(duration, Math.max(0, currentTime + timeDelta)); }, [duration, anchor, interval, currentTime]); const currentAnchorTime = useMemo(() => { var _a; if (!duration) return 0; const rect = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect(); if (!rect) return 0; const currentTimestampOffset = (rect.width / 2); const relativeAnchorX = currentAnchor - rect.left; const delta = relativeAnchorX - currentTimestampOffset; const timeDelta = delta / (8 * 16) * interval; return Math.min(duration, Math.max(0, currentTime + timeDelta)); }, [duration, currentAnchor, interval, currentTime]); const [snapDeltaStart, snapDeltaEnd] = useMemo(() => { var _a, _b; if (!snap) return [0, 0]; if (!entries) return [0, 0]; const snappingThreshold = interval / 20; let start = 0; let end = 0; if (dragging) { start = Math.min(anchorTime, currentAnchorTime); end = Math.max(anchorTime, currentAnchorTime); } else if (moving && selectedEntry) { const selected = entries === null || entries === void 0 ? void 0 : entries.find(entry => entry.symbol === selectedEntry.symbol); if (!selected) return [0, 0]; start = selected.start + currentAnchorTime - anchorTime; end = selected.end + currentAnchorTime - anchorTime; } else if (resizing && selectedEntry) { const selected = entries === null || entries === void 0 ? void 0 : entries.find(entry => entry.symbol === selectedEntry.symbol); if (!selected) return [0, 0]; start = resizing === "start" ? currentAnchorTime : selected.start; end = resizing === "end" ? currentAnchorTime : selected.end; } // find entry within snapping threshold const snappingStartStart = entries.find(entry => Math.abs(entry.start - start) < snappingThreshold); const snappingStartEnd = entries.find(entry => Math.abs(entry.end - start) < snappingThreshold); const snappingEndStart = entries.find(entry => Math.abs(entry.start - end) < snappingThreshold); const snappingEndEnd = entries.find(entry => Math.abs(entry.end - end) < snappingThreshold); const startSnap = (_a = snappingStartStart === null || snappingStartStart === void 0 ? void 0 : snappingStartStart.start) !== null && _a !== void 0 ? _a : snappingStartEnd === null || snappingStartEnd === void 0 ? void 0 : snappingStartEnd.end; const endSnap = (_b = snappingEndStart === null || snappingEndStart === void 0 ? void 0 : snappingEndStart.start) !== null && _b !== void 0 ? _b : snappingEndEnd === null || snappingEndEnd === void 0 ? void 0 : snappingEndEnd.end; const snapDeltaStart = startSnap ? startSnap - start : 0; const snapDeltaEnd = endSnap ? endSnap - end : 0; return [snapDeltaStart, snapDeltaEnd]; }, [snap, entries, interval, dragging, moving, selectedEntry, resizing, anchorTime, currentAnchorTime]); function focusSelected() { var _a; (_a = selectedRef.current) === null || _a === void 0 ? void 0 : _a.focus(); } useEffect(() => { var _a; (_a = selectedRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, [selectedRef, selectedSymbol]); const [depthMap, maxDepth, sentries] = useMemo(() => { var _a; const overlaps = new Map(); const allEntries = [...(entries !== null && entries !== void 0 ? entries : [])]; if (dragging) allEntries.push({ start: Math.min(anchorTime, currentAnchorTime) + snapDeltaStart, end: Math.max(anchorTime, currentAnchorTime) + snapDeltaEnd, _draft: "selection" }); if (draft) allEntries.push({ start: draft.start, end: draft.end, _draft: "draft" }); if (moving) { const snapDelta = (snapDeltaStart || snapDeltaEnd) ? Math.min(snapDeltaStart || Infinity, snapDeltaEnd || Infinity) : 0; const delta = ((currentAnchorTime + snapDelta) - anchorTime); const entry = allEntries.findIndex(e => e.symbol === (selectedEntry === null || selectedEntry === void 0 ? void 0 : selectedEntry.symbol)); if (entry !== -1) { // get the entry const e = Object.assign({}, allEntries[entry]); // now remove the entry allEntries.splice(entry, 1); e.start += delta; e.end += delta; allEntries.push(e); } } const sizeThreshold = interval / 20; if (resizing) { const entry = allEntries.findIndex(e => e.symbol === (selectedEntry === null || selectedEntry === void 0 ? void 0 : selectedEntry.symbol)); if (entry !== -1) { // get the entry const e = Object.assign({}, allEntries[entry]); // now remove the entry allEntries.splice(entry, 1); if (resizing === "start") { e.start = Math.min(currentAnchorTime + snapDeltaStart, e.end - sizeThreshold); } if (resizing === "end") { e.end = Math.max(currentAnchorTime + snapDeltaEnd, e.start + sizeThreshold); } allEntries.push(e); } } const sentries = allEntries.sort((a, b) => a.start - b.start).filter(entry => Math.abs(entry.start - entry.end) >= sizeThreshold); for (let i = 0; i < sentries.length; i++) { const entryX = sentries[i]; for (let j = 0; j < i; j++) { const entryY = sentries[j]; if ((entryX.end - entryY.start) * (entryY.end - entryX.start) > 0.001) overlaps.set(i, [...((_a = overlaps.get(i)) !== null && _a !== void 0 ? _a : []), j]); } } const depths = []; for (let i = 0; i < sentries.length; i++) { if (!overlaps.has(i)) depths.push(0); else { let depth = 0; const indices = overlaps.get(i); const takenDepths = indices.map(i => depths[i]); while (takenDepths.includes(depth)) depth++; depths.push(depth); } } return [depths, Math.max(...depths), sentries]; }, [entries, dragging, anchorTime, currentAnchorTime, snapDeltaStart, snapDeltaEnd, draft, moving, interval, resizing, selectedEntry === null || selectedEntry === void 0 ? void 0 : selectedEntry.symbol]); function handleEntryDelete() { var _a, _b; onEntryDelete === null || onEntryDelete === void 0 ? void 0 : onEntryDelete(); // select next sibling onEntrySelect === null || onEntrySelect === void 0 ? void 0 : onEntrySelect(undefined); (_b = (_a = selectedRef.current) === null || _a === void 0 ? void 0 : _a.nextElementSibling) === null || _b === void 0 ? void 0 : _b.focus(); } function determineInteractionType(x) { var _a; const selectedEntryBounds = (_a = selectedRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect(); const innerDelta = selectedEntryBounds ? Math.min(selectedEntryBounds.width / 4, 10) : 0; if (selectedEntryBounds && x > selectedEntryBounds.left - 10 && x < selectedEntryBounds.left + innerDelta) return "resize-start"; if (selectedEntryBounds && x > selectedEntryBounds.right - innerDelta && x < selectedEntryBounds.right + 10) return "resize-end"; if (selectedEntryBounds && x > selectedEntryBounds.left && x < selectedEntryBounds.right) return "move"; return "drag"; } function handleInteractionDown(x) { const interactionType = determineInteractionType(x); setPlaying(false); setAnchor(x); setCurrentAnchor(x); switch (interactionType) { case "move": setMoving(true); break; case "resize-start": setResizing("start"); break; case "resize-end": setResizing("end"); break; case "drag": setDragging(true); break; } } function handleInteractionCursor(x) { const interactionType = determineInteractionType(x); if (resizing) return "ew-resize"; if (moving) return "move"; setInteractionCursor(interactionType === "move" ? "move" : interactionType === "resize-start" ? "ew-resize" : interactionType === "resize-end" ? "ew-resize" : "default"); } function handleDraftCreate() { if (!dragging) return; const start = Math.min(anchorTime, currentAnchorTime) + snapDeltaStart; const end = Math.max(anchorTime, currentAnchorTime) + snapDeltaEnd; onDraftCreate === null || onDraftCreate === void 0 ? void 0 : onDraftCreate({ start, end }); setDragging(false); } function handleInteractionEnd() { if (dragging) { focusSelected(); handleDraftCreate(); } else if (moving) { focusSelected(); const snapDelta = (snapDeltaStart || snapDeltaEnd) ? Math.min(snapDeltaStart || Infinity, snapDeltaEnd || Infinity) : 0; const delta = Math.round(((currentAnchorTime + snapDelta) - anchorTime) * 1000) / 1000; onEntryMove === null || onEntryMove === void 0 ? void 0 : onEntryMove(delta); setMoving(false); } else if (resizing && selectedEntry) { focusSelected(); const sizeThreshold = interval / 20; if (resizing === "start") { const cleanedStart = Math.round(Math.min(currentAnchorTime + snapDeltaStart, selectedEntry.end - sizeThreshold) * 1000) / 1000; onEntryResize === null || onEntryResize === void 0 ? void 0 : onEntryResize(cleanedStart, selectedEntry.end); } else { const cleanedEnd = Math.round(Math.max(currentAnchorTime + snapDeltaEnd, selectedEntry.start + sizeThreshold) * 1000) / 1000; onEntryResize === null || onEntryResize === void 0 ? void 0 : onEntryResize(selectedEntry.start, cleanedEnd); } setResizing(undefined); } } function handleKeyboardMove(delta) { const entry = selectedEntry; if (!entry) return; onEntryMove === null || onEntryMove === void 0 ? void 0 : onEntryMove(delta); focusSelected(); setCurrentTime(entry.start + delta); } function getSnappingEntry(entry, end) { if (!entries) return; const sorted = entries.sort((a, b) => (end ? a.end - b.end : a.start - b.start)); const thisEntry = sorted.findIndex(e => e.symbol === entry.symbol); if (thisEntry === -1) return; const closest = end ? thisEntry + 1 : thisEntry - 1; if (closest < 0 || closest >= sorted.length) return; const closestEntry = sorted[closest]; return closestEntry; } function handleKeyboardSnap(end) { const entry = selectedEntry; if (!entry) return; // find the closest entry in the direction const closestEntry = getSnappingEntry(entry, end); if (!closestEntry) return; const delta = end ? closestEntry.start - entry.end : closestEntry.end - entry.start; onEntryMove === null || onEntryMove === void 0 ? void 0 : onEntryMove(delta); } function handleKeyboardResize(e, end, shrink = false) { const entry = selectedEntry; if (!entry) return; e.preventDefault(); e.stopPropagation(); const delta = Math.round(Math.max(0.001, interval / 50) * 1000) / 1000; const sizeThreshold = interval / 20; if (end) { onEntryResize === null || onEntryResize === void 0 ? void 0 : onEntryResize(entry.start, shrink ? Math.max(entry.start + sizeThreshold, entry.end - delta) : entry.end + delta); } else { onEntryResize === null || onEntryResize === void 0 ? void 0 : onEntryResize(shrink ? Math.min(entry.start + delta, entry.end - sizeThreshold) : entry.start - delta, entry.end); } } function handleKeyboardResizeSnap(e, end) { const entry = selectedEntry; if (!entry) return; e.preventDefault(); e.stopPropagation(); const closestEntry = getSnappingEntry(entry, end); if (!closestEntry) return; if (end) { onEntryResize === null || onEntryResize === void 0 ? void 0 : onEntryResize(entry.start, closestEntry.start); } else { onEntryResize === null || onEntryResize === void 0 ? void 0 : onEntryResize(closestEntry.end, entry.end); } } return _jsxs(_Fragment, { children: [labelComponent, _jsxs(TimelineOverflowContainer, { ref: containerRef, onMouseDown: e => { if (e.button !== 0) return; handleInteractionDown(e.clientX); }, onTouchStart: e => { const touch = e.touches[0]; if (!touch) return; handleInteractionDown(touch.clientX); }, onMouseMove: e => { handleInteractionCursor(e.clientX); if (!(dragging || moving || resizing)) return; focusSelected(); setCurrentAnchor(e.clientX); }, onTouchMove: e => { const touch = e.touches[0]; if (!touch) return; if (!(dragging || moving || resizing)) return; focusSelected(); setCurrentAnchor(touch.clientX); }, onMouseUp: handleInteractionEnd, onMouseLeave: handleInteractionEnd, onMouseEnter: handleInteractionEnd, onTouchEnd: handleInteractionEnd, style: { '--lines': Math.max(0, maxDepth), cursor: interactionCursor }, children: [_jsx(PlayheadLine, {}), _jsx("div", { className: typeof (styling === null || styling === void 0 ? void 0 : styling.timelineTrackTape) === 'string' ? styling.timelineTrackTape : undefined, style: Object.assign(Object.assign({}, (typeof (styling === null || styling === void 0 ? void 0 : styling.timelineTrackTape) === 'string' ? {} : styling === null || styling === void 0 ? void 0 : styling.timelineTrackTape)), { width: `${duration / interval * 8}rem`, transform: `translateX(calc(${(containerWidth !== null && containerWidth !== void 0 ? containerWidth : 0) / 2}px - ${(currentTime / duration) * (trackWidth !== null && trackWidth !== void 0 ? trackWidth : 0)}px))`, position: 'relative', height: 'calc(100% - 1rem)', marginTop: '0.5rem', marginBottom: '0.5rem' }), ref: trackRef, children: sentries ? sentries.map((entry, i) => { const { start, end, content, _draft } = entry; const ChosenTimelineElement = _draft === "draft" ? DraftElement : _draft === "selection" ? DragElement : TimelineElement; const selected = !!(selectedEntry === null || selectedEntry === void 0 ? void 0 : selectedEntry.symbol) && ((selectedEntry === null || selectedEntry === void 0 ? void 0 : selectedEntry.symbol) === entry.symbol); const disabled = !selected && interactionCursor === "ew-resize"; return _jsx(ChosenTimelineElement, { style: { width: `${(end - start) / duration * 100}%`, left: `${(start / duration) * 100}%`, top: `calc(${depthMap[i] * 40}px - ${depthMap[i] * 0.5}rem)`, cursor: interactionCursor === "default" ? undefined : interactionCursor, }, onFocus: !_draft ? e => { e.preventDefault(); e.stopPropagation(); if (containerRef.current) containerRef.current.scrollLeft = 0; setPlaying(false); if (disabled) { return focusSelected(); } !(moving || resizing) && !(selectedRef.current === document.activeElement) && setCurrentTime(entry.start); onEntrySelect === null || onEntrySelect === void 0 ? void 0 : onEntrySelect(entry); } : undefined, onKeyDown: !_draft ? e => { const delta = Math.max(0.001, interval / 50); const meta = e.ctrlKey || e.metaKey; if (meta && e.shiftKey && e.key === 'ArrowLeft') return handleKeyboardResizeSnap(e, false); if (meta && e.shiftKey && e.key === 'ArrowRight') return handleKeyboardResizeSnap(e, true); if (e.altKey && e.key === 'ArrowLeft') return handleKeyboardResize(e, true, true); if (e.altKey && e.key === 'ArrowRight') return handleKeyboardResize(e, false, true); if (meta && e.key === 'ArrowLeft') return handleKeyboardResize(e, false); if (meta && e.key === 'ArrowRight') return handleKeyboardResize(e, true); if (e.shiftKey && e.key === 'ArrowLeft') return handleKeyboardSnap(false); if (e.shiftKey && e.key === 'ArrowRight') return handleKeyboardSnap(true); if (e.key === 'ArrowLeft') return handleKeyboardMove(-delta); if (e.key === 'ArrowRight') return handleKeyboardMove(delta); if (e.key === 'Backspace' || e.key === 'Delete') return handleEntryDelete === null || handleEntryDelete === void 0 ? void 0 : handleEntryDelete(); if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); return onEntryEdit === null || onEntryEdit === void 0 ? void 0 : onEntryEdit(entry); } } : undefined, tabIndex: !_draft && !disabled ? 0 : -1, selected: selected, "data-timeline-selected": selected ? "true" : "false", "data-timeline-sentry-index": i, ref: selected ? selectedRef : undefined, children: content }, _draft !== null && _draft !== void 0 ? _draft : i); }) : undefined })] })] }); } //# sourceMappingURL=TimelineTrack.js.map