JavaScript is required

自定义工具栏与跨层提示框

这个示例展示如何关闭 relation-graph 内置工具栏,并在 `RGSlotOnView` 中放置自定义悬浮工具栏。它还演示了跨层悬浮提示系统:覆盖工具栏按钮、view 插槽锚点、canvas 插槽锚点、节点插槽内容以及图渲染的节点/连线,依靠递归 tooltip 发现与图命中检测实现。

构建带跨层提示框的自定义工具栏

这个示例构建了什么

这个示例构建了一个只读的 relation-graph 场景:它用一个悬浮的自定义工具栏替代内置工具栏,并让同一套悬停系统贯穿多个图层表面。用户会看到一个小型示例图、一组浅色的纵向工具栏、位于 view 层的四个提示锚点、位于 canvas 层的另外两个锚点、自定义节点内容,以及当指针在这些元素之间移动时出现的深色提示卡片。

重点并不只是“带英文提示的工具栏图标”。同一个包装层级的悬停流程还覆盖了自定义节点插槽中的一个徽标,并且当指针不再位于自定义 HTML 上时,会回退到图中渲染的节点和连线。这让该演示成为一个紧凑的参考案例,适合那些需要在同一场景中混合 overlay DOM 与原生图对象的产品。

数据如何组织

图数据在 initializeGraph() 中以内联方式组装为一个静态 RGJsonData 对象,其中包含 rootId: 'a'、四个节点和七条连线。节点记录本身已经带有视觉覆盖配置,包括 colorfontColornodeShapewidthheight。连线记录也编码了同一对节点之间的重复边、逐线的 lineShape 覆盖、自定义 fromJunctionPoint,以及一条设置了 showEndArrow: false 的连线。

在调用 setJsonData() 之前没有预处理流水线。组件直接在代码中构造最终数据集,并按原样加载,然后重新居中并让视口适配内容。在真实应用里,这种结构同样可以表示服务与依赖、人员与汇报关系,或者设备与网络路由,而逐节点和逐连线的覆盖配置则可以来自类型、状态或路由元数据。

relation-graph 的使用方式

index.tsxRGProvider 包裹整个页面,MyGraph.tsxMyToolbar.tsx 都通过 RGHooks 访问 relation-graph 的状态和 API。该示例没有设置显式布局选项,因此针对这个小数据集,图会使用 relation-graph 的默认布局行为。选项配置反而主要聚焦在展示和工具栏替换上:lineTextMaxLengthmultiLineDistancedefaultLineTextOnPathdefaultLineShape 以及 showToolBar: false

挂载后,图实例通过 setJsonData(...) 加载内联 JSON,然后调用 moveToCenter()zoomToFit()。从这里开始,图实例成为整个交互模型的核心。MyGraph.tsx 使用 getViewBoundingClientRect() 把悬停的 HTML 元素转换为相对于图视图的提示位置,同时使用 isLine(...)isNode(...)getViewXyByEvent(...),以便在图中渲染对象上也能保持悬停反馈。MyToolbar.tsx 则使用同一个实例调用 fullscreen()zoom(...)setZoom(100)moveToCenter()zoomToFit()

三个插槽层承载了自定义 UI。RGSlotOnView 放置悬浮工具栏和四个方向性提示锚点。RGSlotOnCanvas 直接在 canvas 上方添加更多支持提示的 HTML。RGSlotOnNode 替换节点内容,并在节点 e 内插入一个支持提示的徽标。这里没有编辑流程:该图是一个带实用控制与悬停检查能力的查看器。本地 SCSS 也主要聚焦于 .c-tips 中的深色悬停提示样式,以及 .my-toolbar-button 中的白色工具栏按钮样式。

关键交互

  • 当指针移动到任何带有 data-tooltip 的元素上时,会根据 data-tooltipposition 打开一个深色提示卡片。
  • 这套悬停查找机制同时适用于工具栏按钮、view 插槽中的区块、canvas 插槽中的区块,以及渲染在自定义节点插槽中的徽标。
  • 如果找不到支持提示的 HTML 祖先元素,同一个 onMouseMove 处理器会回退到 isLine(...)isNode(...),因此图中渲染的连线和节点仍然会显示一个小型悬停提示。
  • 点击工具栏按钮会通过图实例或本地点击回调触发全屏、缩放步进、适配视图和个人资料相关操作。
  • 下载按钮作为界面装饰存在,但它的处理函数只是一个占位注释,并不会导出图片。

关键代码片段

这个递归辅助函数说明,提示发现机制依赖的是 DOM 祖先链、分配的插槽以及 Shadow DOM 的宿主边界,而不是某一个特定图层。

const getTooltip = (el: HTMLElement | null): HTMLElement | null => {
    if (!el) return null;
    if (el.dataset.tooltip) return el;
    if (el.classList.contains('my-graph')) return null;

    let nextParent: HTMLElement | null = null;
    if (el.assignedSlot) {
        nextParent = el.assignedSlot as HTMLElement;
    } else {
        nextParent = el.parentElement;
    }

这个选项块表明,relation-graph 的内置工具栏被有意禁用,而重复连线的间距和标签渲染则在图级别统一控制。

const graphOptions: RGOptions = {
    lineTextMaxLength: 6,
    multiLineDistance: 20,
    defaultLineTextOnPath: true,
    defaultLineShape: RGLineShape.StandardCurve,
    showToolBar: false
};

这个数据片段表明,在数据加载进图之前,演示数据本身就已经携带了节点样式、重复链接以及逐线的路径覆盖配置。

const myJsonData: RGJsonData = {
    rootId: 'a',
    nodes: [
        { id: 'a', text: 'A' },
        { id: 'b', text: 'B', color: '#43a2f1', fontColor: 'yellow' },
        { id: 'c', text: 'C', nodeShape: RGNodeShape.rect, width: 120, height: 80 },
        { id: 'e', text: 'E', nodeShape: RGNodeShape.circle, width: 150, height: 150 }
    ],
    lines: [
        { id: 'l1', from: 'a', to: 'b', text: 'text a -> b', color: '#43a2f1' },
        { id: 'l10', from: 'a', to: 'e', text: 'text a -> e', lineShape: RGLineShape.Curve7 }
    ]
};

这个 mouse-move 分支证明,HTML 提示锚点会相对于图视图进行测量,并转换成可复用的提示状态。

const tooltipElement = getTooltip(eventTaregtElement);
if (tooltipElement) {
    const tooltipText = tooltipElement.dataset.tooltip || '';
    const tooltipPostion = tooltipElement.dataset.tooltipposition || 'left';
    const tooltipElementRect = tooltipElement.getBoundingClientRect();
    const viewRect = graphInstance.getViewBoundingClientRect();
    setTooltipInfo({
        overObjectType: 'element',
        overObject: {
            text: tooltipText,
            width: tooltipElementRect.width,
            height: tooltipElement.height,

这个回退分支展示了当指针不位于自定义 HTML 上时,同一套悬停流程如何仍然支持图中渲染的连线和节点。

const line = graphInstance.isLine(eventTaregtElement);
if (line) {
    const basePositionXy = graphInstance.options.fullscreen
        ? { x: 0, y: 0 }
        : graphInstance.getViewXyByEvent($event.nativeEvent);
    setTooltipInfo({
        overObjectType: 'line',
        overObject: line,
        position: {
            x: basePositionXy.x + 10,
            y: basePositionXy.y + 10
        }
    });
}

这个插槽组合是 relation-graph 的核心模式:一个查看器场景在同一个图实例中同时组合了自定义 view 覆盖层、canvas 覆盖层和节点内容。

<RGSlotOnView>
    <MyToolbar onUserAvatarClick={onUserAvatarClick} />
    <div data-tooltipposition="top" data-tooltip="Top Tooltip Content">Top</div>
</RGSlotOnView>
<RGSlotOnCanvas>
    <div data-tooltipposition="top" data-tooltip="Canvas Slot Content 1">
        Canvas Slot Content 1
    </div>
</RGSlotOnCanvas>
<RGSlotOnNode>

这个工具栏片段证明,视口控制是通过普通的自定义按钮实现的,这些按钮会直接调用 relation-graph 实例 API。

const zoomToFit = async () => {
    graphInstance.setZoom(100);
    graphInstance.moveToCenter();
    graphInstance.zoomToFit();
};
const addZoom = (buff: number) => {
    graphInstance.zoom(buff);
};

这个示例的独特之处

对比数据表明,这个示例的突出点在于:它用同一套提示流程贯穿了 RGSlotOnViewRGSlotOnCanvasRGSlotOnNode、自定义工具栏按钮,以及图中渲染的节点或连线。这比只针对节点的提示方案覆盖范围更广,也比只定制工具栏的示例更强调交互。

toolbar-buttons 相比,这里的重点不是复用 RGToolBarRGMiniToolBar 容器。工具栏被刻意做得很朴素,而大部分实现精力都投入在悬停发现、方向定位,以及从 DOM 锚点回退到图命中检测上。与 node-line-tips-contentmenu 相比,这个示例的命令能力更轻,但覆盖的表面更广:它没有加入右键菜单,却展示了 HTML 覆盖层和图对象如何共享一条悬停管线。与 node-tips 相比,它并不局限于节点悬停卡片,因为它还覆盖了工具栏按钮、canvas 插槽内容以及连线悬停提示。

这种组合使它成为一个很强的起点,适合那些需要让悬停帮助在混合图层之间保持一致、同时又不引入编辑逻辑或为每个表面分别实现提示系统的团队。

这种模式还适用于哪里

这种模式非常适合图形化管理控制台、调查工具、架构地图以及监控仪表盘,这类产品通常会把图原生对象与自定义覆盖控件混合使用。它尤其适用于需求是“所有地方都使用同一种提示语言”,而不是只做一个专门的节点检查器时。

同样的结构还可以扩展出更丰富的提示内容、悬停延时、可固定的详情卡片、挂在占位下载按钮后的导出操作,或面向视口的边界钳制逻辑。它也可以在保持图本身处于查看模式的同时,为图周围增加带品牌风格的实用界面元素。