UNPKG

@rxflow/base

Version:

BaseFlow - 核心 Flow 组件库

198 lines (192 loc) 7.42 kB
/** * @author: yanxianliang * @date: 2025-08-09 21:05 * @desc: 滚动条插件 * * 适配 reactflow 无限延伸特性,在 wheel 事件中自动调整滚动区域尺寸,操作滚动条时锁定尺寸 * Copyright (c) 2025 by yanxianliang, All Rights Reserved. */ import { useNodes, useReactFlow, useViewport } from "@xyflow/react"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react"; import "./index.less"; import { useScrollerOptions } from "./hooks/useScrollerOptions"; import { useMemoizedFn } from "ahooks"; import { OverlayScrollbars, ClickScrollPlugin } from 'overlayscrollbars'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import 'overlayscrollbars/overlayscrollbars.css'; import cat from 'classcat'; import { jsx as _jsx } from "react/jsx-runtime"; OverlayScrollbars.plugin(ClickScrollPlugin); // 取向下的最小倍数 var getFloorNumber = function getFloorNumber(current, splitSize) { var ratio = Math.floor(current / splitSize); return ratio * splitSize; }; // 取向上的最小倍数 var getCeilNumber = function getCeilNumber(current, splitSize) { var ratio = Math.ceil(current / splitSize); return ratio * splitSize; }; var padding = 50; export var Scrollbar = function Scrollbar(props) { var width = props.width, height = props.height, pageWidth = props.pageWidth, pageHeight = props.pageHeight, pageBreak = props.pageBreak, pageVisible = props.pageVisible; var scrollerContainerRef = useRef(null); var scrollbarRef = useRef(null); var scrollOptionsRef = useRef(null); // 锁定 viewport,不自动增长画布 var lockAutoViewportRef = useRef(false); // 部分正常的 onScroll 也会被拦截 var refInit = useCallback(function (ref) { scrollerContainerRef.current = ref ? ref.getElement() : null; }, []); var onInitial = useCallback(function (instance) { scrollbarRef.current = instance; // 监听事件 bindUserActions(instance); updateScrollBar(); }, []); var onPointerDown = useCallback(function () { lockAutoViewportRef.current = true; }, []); var onPointerUp = useCallback(function () { lockAutoViewportRef.current = false; }, []); var bindUserActions = useCallback(function (instance) { var _instance$elements = instance.elements(), scrollbarHorizontal = _instance$elements.scrollbarHorizontal, scrollbarVertical = _instance$elements.scrollbarVertical; scrollbarHorizontal.scrollbar.addEventListener('pointerdown', onPointerDown); scrollbarVertical.scrollbar.addEventListener('pointerdown', onPointerDown); document.addEventListener('pointerup', onPointerUp); }, []); var unBindUserActions = useCallback(function (instance) { if (instance) { var _instance$elements2 = instance.elements(), scrollbarHorizontal = _instance$elements2.scrollbarHorizontal, scrollbarVertical = _instance$elements2.scrollbarVertical; scrollbarHorizontal.scrollbar.removeEventListener('pointerdown', onPointerDown); scrollbarVertical.scrollbar.removeEventListener('pointerdown', onPointerDown); document.removeEventListener('pointerup', onPointerUp); } }, []); useEffect(function () { return function () { unBindUserActions(scrollbarRef.current); }; }, []); var options = useScrollerOptions({ width: width, height: height, pageWidth: pageWidth, pageHeight: pageHeight, pageBreak: pageBreak, pageVisible: pageVisible, scrollerContainer: scrollerContainerRef }); var flowInstance = useReactFlow(); var nodes = useNodes(); var viewport = useViewport(); var bounds = useMemo(function () { return flowInstance.getNodesBounds(nodes); // 获取可能有问题,nodeLookup为什么获取不到???应该是创建了才对。 }, [nodes]); var scrollOptions = useMemo(function () { if (lockAutoViewportRef.current) { return scrollOptionsRef.current; // 不更新 } var pageHeight = options.pageHeight, pageWidth = options.pageWidth, clientHeight = options.clientHeight, clientWidth = options.clientWidth; // 页码宽和高 if (!pageHeight || !pageWidth) { return undefined; } // 根据 bounds 计算最小的 rect 范围 var boundLeft = bounds.x - pageWidth; var boundTop = bounds.y - pageHeight; var boundRight = bounds.x + bounds.width + pageWidth; var boundBottom = bounds.y + bounds.height + pageHeight; // 根据 viewport 调整对应的上下左右 rect 范围, viewport 是不是也自动叠加 0.5 宽高 var _viewport$x = viewport.x, x = _viewport$x === void 0 ? 0 : _viewport$x, _viewport$y = viewport.y, y = _viewport$y === void 0 ? 0 : _viewport$y, zoom = viewport.zoom; var viewportLeft = getFloorNumber(-x - padding, pageWidth / 2); var viewportTop = getFloorNumber(-y - padding, pageHeight / 2); var viewportRight = getCeilNumber(-x + zoom * clientWidth + padding, pageWidth / 2); var viewportBottom = getCeilNumber(-y + zoom * clientHeight + padding, pageHeight / 2); var left = Math.min(viewportLeft, boundLeft); var top = Math.min(viewportTop, boundTop); var bottom = Math.max(viewportBottom, boundBottom); var right = Math.max(viewportRight, boundRight); scrollOptionsRef.current = { left: left, top: top, scrollLeft: Math.ceil(-x - left), // 滚动条 必须要动起来,不然拖动滚动条的时候变化非常大。 scrollTop: Math.ceil(-y - top), width: Math.ceil(right - left), height: Math.ceil(bottom - top) }; return scrollOptionsRef.current; }, [options, viewport, bounds.x, bounds.y, bounds.width, bounds.height]); var updateScrollBar = useMemoizedFn(function () { if (scrollerContainerRef.current && scrollOptions && scrollbarRef.current) { var _elements = scrollbarRef.current.elements(), scrollOffsetElement = _elements.scrollOffsetElement; scrollOffsetElement.scrollTo({ // behavior: 'smooth', left: scrollOptions.scrollLeft, top: scrollOptions.scrollTop }); } }); useLayoutEffect(function () { updateScrollBar(); }, [scrollOptions]); // 从 viewport 触发的滚动事件不应该是 var onScroll = useMemoizedFn(function (scroller, ev) { if (scrollOptions) { var _ref = ev.target, scrollTop = _ref.scrollTop, scrollLeft = _ref.scrollLeft; var x = -(scrollLeft + scrollOptions.left); var y = -(scrollTop + scrollOptions.top); // 计算的 viewport 不对 var _viewport = flowInstance.getViewport(); if (Math.abs(_viewport.x - x) >= 1 || Math.abs(_viewport.y - y) >= 1) { flowInstance.setViewport({ x: x, y: y, zoom: _viewport.zoom }); } } }); return /*#__PURE__*/_jsx(OverlayScrollbarsComponent, { className: cat(["overlayscrollbars-react", 'rxflow-plugin-scrollbar']), ref: refInit, options: { scrollbars: { clickScroll: true, // autoHide: 'leave', theme: 'os-theme-dark' } }, events: { scroll: onScroll, initialized: onInitial }, defer: true, children: scrollOptions ? /*#__PURE__*/_jsx("div", { style: { width: scrollOptions.width, height: scrollOptions.height } }) : null }); };