collaborative-ui
Version:
React component library for building real-time collaborative editing applications.
224 lines (223 loc) • 8.97 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Bar = void 0;
const tslib_1 = require("tslib");
const React = tslib_1.__importStar(require("react"));
const nano_theme_1 = require("nano-theme");
const useBehaviorSubject_1 = require("nice-ui/lib/hooks/useBehaviorSubject");
const constants_1 = require("../constants");
const Tick_1 = require("./Tick");
const context_1 = require("../../context");
const useModelTick_1 = require("../../../hooks/useModelTick");
const useMeasure_1 = tslib_1.__importDefault(require("react-use/lib/useMeasure"));
const useScratch_1 = tslib_1.__importDefault(require("react-use/lib/useScratch"));
const json_crdt_1 = require("json-joy/lib/json-crdt");
const startingTickWidth = 42;
const timelinePadding = 4;
const scrollHeight = 12;
const css = {
block: (0, nano_theme_1.drule)({
pd: '24px 8px 8px',
mr: '-8px 0 0',
us: 'none',
bdrad: '4px',
'&:focus': {
out: 0,
},
}),
slots: (0, nano_theme_1.drule)({
h: constants_1.TIMELINE_HEIGHT + 'px',
d: 'flex',
bdrad: '3px',
pad: '1px 0 1px 1px',
}),
scrollBed: (0, nano_theme_1.drule)({
pos: 'relative',
bxz: 'border-box',
h: scrollHeight + 'px',
w: '100%',
mr: '1px 0 0',
bdrad: '3px',
}),
scrollHandle: (0, nano_theme_1.drule)({
d: 'block',
pos: 'absolute',
bxz: 'border-box',
h: scrollHeight + 'px',
w: '111px',
t: '0px',
l: '0pxd',
bdrad: '4px',
cur: 'ew-resize',
}),
};
const Bar = ({ log }) => {
const state = (0, context_1.useLogState)();
const scroll = (0, useBehaviorSubject_1.useBehaviorSubject)(state.timelineScroll$);
const [, setForceUpdate] = React.useState(0);
const isMouseDown = React.useRef(false);
const isScrubbing = React.useRef(false);
React.useEffect(() => {
const body = document.body;
const listener = () => {
if (isMouseDown.current) {
isMouseDown.current = false;
isScrubbing.current = false;
setForceUpdate((x) => x + 1);
}
};
body.addEventListener('mouseup', listener);
return () => {
body.removeEventListener('mouseup', listener);
};
}, []);
const scrollRef = React.useRef(scroll);
scrollRef.current = scroll;
const scratchStartScroll = React.useRef(scroll);
const [ref, { width }] = (0, useMeasure_1.default)();
(0, useModelTick_1.useModelTick)(log.end);
const pinned = (0, useBehaviorSubject_1.useBehaviorSubject)(state.pinned$);
const theme = (0, nano_theme_1.useTheme)();
const wheelTimeout = React.useRef();
// biome-ignore lint: manual dependency list
const moveScrollByPx = React.useCallback((dx) => {
if (!width)
return 0;
if (typeof dx !== 'number')
return 0;
const totalPatches = log.patches.size();
const TICK_WIDTH = Math.max(3, 100 - totalPatches);
const slotWidth = TICK_WIDTH + constants_1.TICK_MARGIN;
const scrollBedWidth = width;
const slotListViewportWidth = width - timelinePadding;
const slotBedWidth = totalPatches * slotWidth;
const scrollHandleRatio = slotListViewportWidth / slotBedWidth;
const scrollHandleWidth = scrollHandleRatio * scrollBedWidth;
const scrollRunway = scrollBedWidth - scrollHandleWidth;
const dScroll = dx / scrollRunway;
const currentScroll = scratchStartScroll.current;
let newScroll = scrollRef.current + dScroll;
if (newScroll < 0)
newScroll = 0;
if (newScroll > 1)
newScroll = 1;
if (newScroll === currentScroll)
return 0;
state.setTimelineScroll(newScroll);
return newScroll - currentScroll;
}, [log, width]);
const [scratchSlotsRef] = (0, useScratch_1.default)({
onScratch: ({ dx }) => {
if (typeof dx === 'number' && Math.abs(dx) > 8 && isMouseDown.current) {
isScrubbing.current = true;
setForceUpdate((x) => x + 1);
}
},
});
const [scratchRef, { isScratching }] = (0, useScratch_1.default)({
onScratchStart: () => {
scratchStartScroll.current = scroll;
},
onScratch: ({ dx }) => {
moveScrollByPx(dx);
},
});
const startTime = React.useMemo(() => {
return log.start().clock.time - 1;
}, [log]);
// Block the body from scrolling (or any other element)
React.useEffect(() => {
const cancelWheel = (e) => wheelTimeout.current && e.preventDefault();
const body = document.body;
body.addEventListener('wheel', cancelWheel, { passive: false });
return () => body.removeEventListener('wheel', cancelWheel);
}, []);
const isScrolling = !!wheelTimeout.current || isScratching;
const totalPatches = log.patches.size() + 1;
const tickWidth = totalPatches > 5000 ? 2 : Math.max(3, startingTickWidth - totalPatches);
const slotWidth = tickWidth + constants_1.TICK_MARGIN;
const scrollBedWidth = width;
const slotListViewportWidth = width - timelinePadding;
const slotsPerViewport = width ? Math.floor(slotListViewportWidth / slotWidth) : 0;
const slotBedWidth = log.patches.size() * slotWidth;
const scrollHandleRatio = slotListViewportWidth / slotBedWidth;
const scrollHandleWidth = scrollHandleRatio * scrollBedWidth;
const scrollRunway = scrollBedWidth - scrollHandleWidth;
const slotsFitInViewport = totalPatches <= slotsPerViewport;
const slotIndexOffset = slotsFitInViewport ? 0 : Math.floor(scroll * (totalPatches - slotsPerViewport));
const items = [];
const rulerInterval = totalPatches > 1000 || log.end.clock.time > 9999 ? 25 : 10;
if (slotIndexOffset <= 0) {
items.push(React.createElement(Tick_1.Tick, { key: 'start', id: new json_crdt_1.Timestamp(0, startTime), selected: pinned === 'start', marker: '.' + startTime, tickWidth: tickWidth, noHover: isScrolling, scrubbing: isScrubbing.current }));
}
let i = 1;
// biome-ignore lint: .forEach is the way to iterate here
log.patches.forEach(({ v: patch }) => {
const id = patch.getId();
if (!id)
return;
if (i >= slotIndexOffset && i < slotIndexOffset + slotsPerViewport) {
const tenth = i % rulerInterval === 0;
items.push(React.createElement(Tick_1.Tick, { key: id.sid + '.' + id.time, id: id, patch: patch, selected: pinned === patch, marker: tenth ? '.' + id.time : undefined, tickWidth: tickWidth, noHover: isScrolling, scrubbing: isScrubbing.current }));
}
i++;
});
const scrollBed = (React.createElement("div", { className: css.scrollBed({
display: slotsFitInViewport ? 'none' : 'block',
bg: theme.g(0.98),
'&:hover': {
bg: theme.g(0.97),
},
}) }, scrollHandleRatio < 1 && (React.createElement("div", { ref: scratchRef, className: css.scrollHandle({
bg: theme.g(0.92),
'&:hover': {
bg: theme.g(0.88),
},
'&:active': {
bg: theme.g(0.82),
},
}), style: {
left: scrollRunway * scroll,
width: scrollHandleWidth,
} }))));
return (React.createElement("div", { ref: ref,
// biome-ignore lint: allow tabIndex
tabIndex: 0, className: css.block(), style: {
overflow: isScrubbing.current ? undefined : 'hidden',
}, onWheel: (e) => {
const dx = e.deltaY || e.deltaX;
const didMove = !!moveScrollByPx(dx);
if (didMove) {
clearTimeout(wheelTimeout.current);
wheelTimeout.current = setTimeout(() => {
setForceUpdate((x) => x + 1);
wheelTimeout.current = null;
}, 300);
}
}, onMouseDown: () => {
isMouseDown.current = true;
setForceUpdate((x) => x + 1);
}, onKeyDown: (e) => {
switch (e.code) {
case 'ArrowUp':
case 'ArrowRight': {
state.next();
break;
}
case 'ArrowDown':
case 'ArrowLeft': {
state.prev();
break;
}
}
} },
React.createElement("div", { ref: scratchSlotsRef, className: css.slots({
bd: `1px solid ${theme.g(0.9)}`,
bg: theme.g(0.99),
'&:hover': {
bd: `1px solid ${theme.g(0.7)}`,
},
}) }, width ? items : null),
scrollBed));
};
exports.Bar = Bar;