UNPKG

@react-av/editor

Version:

Editor Timeline Components built on React AV.

432 lines 24.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TimelineTrack = TimelineTrack; const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = require("react"); const Media = __importStar(require("@react-av/core")); const use_resize_observer_1 = __importDefault(require("use-resize-observer")); const TimelineOverflowContainer_1 = require("./TimelineOverflowContainer"); const TimelineElements_1 = require("./TimelineElements"); const Editor_1 = require("./Editor"); const TimelineEditor_1 = require("./TimelineEditor"); function TimelineTrack({ labelComponent, draft, snap, onDraftCreate, entries, onEntrySelect, selectedSymbol, onEntryMove, onEntryEdit, onEntryDelete, onEntryResize, selectedRef: externalSelectedRef }) { const { styling } = (0, Editor_1.useEditorContext)(); const { timelineInterval: interval } = (0, TimelineEditor_1.useTimelineEditorContext)(); const [currentTime, setCurrentTime] = Media.useMediaCurrentTimeFine(); const duration = Media.useMediaDuration(); const [, setPlaying] = Media.useMediaPlaying(); const [anchor, setAnchor] = (0, react_1.useState)(0); const [currentAnchor, setCurrentAnchor] = (0, react_1.useState)(0); const [dragging, setDragging] = (0, react_1.useState)(false); const [moving, setMoving] = (0, react_1.useState)(false); const [resizing, setResizing] = (0, react_1.useState)(undefined); const internalSelectedRef = (0, react_1.useRef)(null); const [interactionCursor, setInteractionCursor] = (0, react_1.useState)("default"); const selectedRef = externalSelectedRef !== null && externalSelectedRef !== void 0 ? externalSelectedRef : internalSelectedRef; const containerRef = (0, react_1.useRef)(null); const { width: trackWidth, ref: trackRef } = (0, use_resize_observer_1.default)(); const { width: containerWidth } = (0, use_resize_observer_1.default)({ ref: containerRef }); const selectedEntry = (0, react_1.useMemo)(() => { if (!selectedSymbol || !entries) return; return entries.find(entry => entry.symbol === selectedSymbol); }, [selectedSymbol, entries]); const anchorTime = (0, react_1.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 = (0, react_1.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] = (0, react_1.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(); } (0, react_1.useEffect)(() => { var _a; (_a = selectedRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, [selectedRef, selectedSymbol]); const [depthMap, maxDepth, sentries] = (0, react_1.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 (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [labelComponent, (0, jsx_runtime_1.jsxs)(TimelineOverflowContainer_1.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: [(0, jsx_runtime_1.jsx)(TimelineElements_1.PlayheadLine, {}), (0, jsx_runtime_1.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" ? TimelineElements_1.DraftElement : _draft === "selection" ? TimelineElements_1.DragElement : TimelineElements_1.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 (0, jsx_runtime_1.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