@react-av/editor
Version:
Editor Timeline Components built on React AV.
432 lines • 24.2 kB
JavaScript
;
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