@rxflow/base
Version:
BaseFlow - 核心 Flow 组件库
198 lines (192 loc) • 7.42 kB
JavaScript
/**
* @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
});
};