UNPKG

collaborative-ui

Version:

React component library for building real-time collaborative editing applications.

224 lines (223 loc) 8.97 kB
"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;