jsmind icon indicating copy to clipboard operation
jsmind copied to clipboard

设置了 custom_node_render 后 jmexpander 位置计算错误被遮蔽

Open hqm19 opened this issue 1 year ago • 6 comments

版本 0.8.3,复现代码:

import React, { useEffect, useRef } from "react"
import ReactDOM from "react-dom"
import jsMind from "jsmind"
import "jsmind/style/jsmind.css"

const MindmapTest = () => {
  const jmContainer = useRef(null)
  const jm = useRef(null)

  const nodeRenderTest = (jm, element, node) => {
    if (node.isroot || node.id === "2") {
      return false
    }
    ReactDOM.render(<span>{node.topic}</span>, element)
    return true
  }

  useEffect(() => {
    if (!jmContainer.current) {
      return
    }
    jm.current = new jsMind({
      container: jmContainer.current,
      editable: true,
      theme: "orange", //"greensea", //
      shortcut: {
        enable: true, // 是否启用快捷键
      },
      view: {
        custom_node_render: nodeRenderTest, // 使用自定义渲染函数
      },
    })

    jm.current.show({
      meta: { name: "", author: "", version: "" },
      format: "node_array",
      data: [
        { id: "root", topic: "前端库", isroot: true },
        { id: "1", topic: "脑图(custom_node_render展开按钮被遮蔽了)", parentid: "root", expanded: false },
        { id: "11", topic: "JSMind", parentid: "1" },
        { id: "2", topic: "流程图", parentid: "root", expanded: false },
        { id: "21", topic: "PlantUML", parentid: "2" },
      ],
    })

    jm.current.resize()
  })

  return <div ref={jmContainer} style={{ width: "100%", height: "100%" }} />
}

export { MindmapTest }

产生的dom结构:

<div style="width: 100%; height: 100%;">
  <div class="jsmind-inner jmnode-overflow-hidden" tabindex="1">
    <canvas class="jsmind" width="1512" height="410"></canvas>
    <jmnodes class="theme-orange" style="width: 1512px; height: 410px;">
      <jmexpander nodeid="1" style="visibility: visible; left: 765px; top: 169px;">+</jmexpander>
      <jmnode nodeid="1" class="" style="visibility: visible; left: 745px; top: 166px;">
        <span>脑图(custom_node_render展开按钮被遮蔽了)</span>
      </jmnode>
      <jmexpander nodeid="2" style="visibility: visible; left: 813px; top: 218px;">-</jmexpander>
      <jmnode nodeid="2" style="visibility: visible; left: 745px; top: 206px;">
        流程图
      </jmnode>
      <jmexpander nodeid="11" style="visibility: hidden; display: none;">-</jmexpander>
      <jmnode nodeid="11" style="visibility: hidden; display: none;">
        <span>JSMind</span>
      </jmnode>
      <jmexpander nodeid="21" style="visibility: hidden; display: none;">-</jmexpander>
      <jmnode nodeid="21" style="visibility: visible; left: 856px; top: 215px;">
        <span>PlantUML</span>
      </jmnode>
      <jmnode class="root selected" nodeid="root" style="visibility: visible; left: 623px; top: 181.5px;">
        前端库
      </jmnode>
    </jmnodes>
  </div>
</div>

页面效果:

image.png

注意上面第一个子节点,看不到展开按钮。实际因 left top 设值错误,被节点本身遮住了。并且被 custom_node_render 渲染出的节点,上下的 margin 也消失了,挤在一起。

hqm19 avatar Mar 23 '24 01:03 hqm19

你在脑图…那个节点再加一个下级节点看看,看一下下一级节点的位置是不是正常的。 如果正常的话,这个节点可能是受页面上的其他样式的影响才这样的。 我稍后也试试看能不能复现这个现象。谢谢反馈!

hizzgdev avatar Mar 23 '24 03:03 hizzgdev

哇,您回复好快。

看我例子的数据里面,是加了子节点的。

用开发者工具修改 css 让展开图标露出来,点击后的子节点位置也不对:

image.png

hqm19 avatar Mar 23 '24 12:03 hqm19

我想这个问题可能的原因是 ReactDOM.render(<span>{node.topic}</span>, element) 这一句,按我对 React 粗浅的理解,它并不是实时把 html element 给 render 出来的。jsMind 会在 custom_node_render 后尝试读取这个 node 的尺寸以进行排版,但是在 React 的环境下,读取到的 size 可能是个 0,由此产生了这个布局问题。

要验证这个原因,你可以把ReactDOM.render 这句直接改成 html dom 的写法,像 document.createElement 这种实验一下。

抱歉 @hqm19 , 我对 React 并不是很在行,在我本地配置了一个 react 18 的项目,但加上你上面帖的代码后一直跑不起来,所以也没能复现这个情况。你能否把你项目的 package.json 帖出来?或者在github/gitee上搭建一个小的项目演示这个问题,我也好可以运行起来试一下?

hizzgdev avatar Mar 24 '24 17:03 hizzgdev

抱歉忘记说明 react 的版本了,我用的是 react 16 目前比较稳定。 react 18 + jsmind 还没调通。 我提交了一个工程把完整的复现代码放上去了: https://github.com/hqm19/jsmind-react

hqm19 avatar Mar 25 '24 04:03 hqm19

Hi @hqm19 我提交了一个 PR https://github.com/hqm19/jsmind-react/pull/1 你可以看看。

hizzgdev avatar Mar 25 '24 16:03 hizzgdev

react 整体都是异步渲染的,把 react 改成同步没什么便捷的办法。 问了 ChatGPT ,综合后,下面的方案可以试试:

  1. 修改 jsmind 内部实现,允许将 view.custom_node_render 设置为一个返回 Promise 的函数。 jsmind 判断返回值,如果是 Promise 对象,则将后续动作放在, Promise 的 than 中执行。例如
const obj = custom_node_render_func(jm, element, node)
if (!!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'){
  obj.then((result) => {
      // 原来对返回 true 的处理   (label x)
   }
}else if(obj){
    // 原来对返回 true 的处理
}else{
   // 原来对返回 false 的处理
}
  1. custom_node_render 设置为返回 一个 Promise 的函数:
view.custom_node_render = (jm, element, node) => {
  // 函数返回一个Promise
  return new Promise((resolve, reject) => {
      ReactDOM.render(<span>{node.topic}</span>, element, () => {
        //  ReactDOM.render在异步渲染 element 完成后,会执行这个回调函数,
         resolve(true)  // 解决Promise后, 执行(label x)的地方
      })
   })
}

hqm19 avatar Mar 26 '24 04:03 hqm19

原因总结:

  1. jsMind 使用同步模式获取 html 元素尺寸并计算位置
  2. 本用例在 custom_node_render 里使用 React 框架异步填充 html 元素
  3. jsMind 在获取 html 元素尺寸时,该元素尚未 render 出来
  4. 导致位置的计算结果不符合预期

解决办法: 方法1: 在 custom_node_render 的实现里,使用同步模式填充 html 元素 (推荐) 方法2: 修改 jsMind 以支持异步方式生成节点。这种方法需要对 jsMind 进行较大规模重构,而且无法保证目前文档中 api 的兼容性,因此考虑暂不支持,后续或考虑以新项目的方式支持异步模型。

感谢 @hqm19 带来的建设性的讨论,以及非常有价值的尝试 https://github.com/hizzgdev/jsmind/pull/594 。 相关的 PR 将会被关闭,此 issue 将会移至 discussion 区。

hizzgdev avatar May 31 '24 18:05 hizzgdev