Vanessa219 / vditor

♏ 一款浏览器端的 Markdown 编辑器,支持所见即所得(富文本)、即时渲染(类似 Typora)和分屏预览模式。An In-browser Markdown editor, support WYSIWYG (Rich Text), Instant Rendering (Typora-like) and Split View modes.
https://b3log.org/vditor
MIT License
8.45k stars 870 forks source link

在渲染时,大纲点击不跳转 #1516

Closed dlpuzcl closed 10 months ago

dlpuzcl commented 10 months ago

编辑模式

描述问题

参考了render.js 里的代码 在vue3中 使用下面代码正常渲染成功。大纲也显示正确,但是始终点击大纲没反应,求助大佬看看, render的demo点击是好用的

<template>

  <div id="previewWrap">
    <div id="preview" ref="viewerRef" class="preview">
    </div>
  </div>
  <div id="outline"></div>

</template>

<script lang="js" setup>
  import { onBeforeUnmount, onDeactivated, ref, unref, watch } from 'vue';
  import Vditor from 'vditor';
  import { onMountedOrActivated } from '@vben/hooks';
  import { useRootSetting } from '/@/hooks/setting/useRootSetting';
  import { getTheme } from './getTheme';
  import {PageWrapper} from "@/components/Page";
  import {useGlobSetting} from "@/hooks/setting";
  const globSetting = useGlobSetting();

  const props = defineProps({
    value: { type: String },
    class: { type: String },
  });
  const viewerRef = ref(null);
  const outlineRef = ref(null);

  const vditorPreviewRef = ref(null);
  const { getDarkMode } = useRootSetting();

  function init() {
    const outlineElement = document.getElementById('outline')
    const previewElement = document.getElementById('preview')
    vditorPreviewRef.value = Vditor.preview(previewElement, props.value, {
      markdown: {
        toc: true,
      },
      speech: {
        enable: true,
      },
      anchor: 1,
      after: () => {
        if (window.innerWidth <= 768) {
          return
        }

        Vditor.outlineRender(previewElement, outlineElement)
        if (outlineElement.innerText.trim() !== '') {
          outlineElement.style.display = 'block'
        }
      },
      lazyLoadImage: `http://localhost:3000/static/vditor/dist/images/img-loading.svg`, renderers: {
        renderHeading: (node, entering) => {
          const id = Lute.GetHeadingID(node)
          if (entering) {
            return [`<h${node.__internal_object__.HeadingLevel} id="${id}" class="vditor__heading"><span class="prefix"></span><span>`, Lute.WalkContinue]
          } else {
            return [`</span><a id="vditorAnchor-${id}" class="vditor-anchor" href="#${id}"><svg viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a></h${node.__internal_object__.HeadingLevel}>`, Lute.WalkContinue]
          }
        },
      },

    });
  }
  watch(
    () => getDarkMode.value,
    (val) => {
      Vditor.setContentTheme(getTheme(val, 'content'));
      Vditor.setCodeTheme(getTheme(val, 'code'));
      init();
    },
  );

  watch(
    () => props.value,
    (v, oldValue) => {
      v !== oldValue && init();
    },
  );

  function destroy() {
    const vditorInstance = unref(vditorPreviewRef);
    if (!vditorInstance) return;
    try {
      vditorInstance?.destroy?.();
    } catch (error) {
      //
    }
    vditorPreviewRef.value = null;
  }

  onMountedOrActivated(init);

  onBeforeUnmount(destroy);
  onDeactivated(destroy);
</script>

渲染图片

image

截屏或录像

版本信息

其他信息

Vanessa219 commented 10 months ago

参照本地运行的 http://127.0.0.1:9000/render.html 界面,是可以点击滚动的。

grandsong commented 9 months ago

我也遇到了同样的问题。 我是参照 demo 的代码,调用了一个选项参数最简单的实例。 其它功能都很正常,只有大纲点击无效。

grandsong commented 9 months ago

我查看了一下源代码。

在文件 'src\ts\markdown\outlineRender.ts' 中,找到小标题元素 idElement 后,要滚动到它。

这个工作,按理说,用一行代码即可实现:

idElement.scrollIntoView()

不知为何(也许是为了兼容古老的浏览器?),源代码中的实现非常复杂。

可能就是在其中发生了错误。

相关部分如下。

if (target.classList.contains("vditor-outline__action")) {
  ...
} else if (target.getAttribute("data-target-id")) {
    event.preventDefault();
    event.stopPropagation();
    const idElement = document.getElementById(target.getAttribute("data-target-id"));
    if (!idElement) {
        return;
    }
    if (vditor) {
        if (vditor.options.height === "auto") {
            let windowScrollY = idElement.offsetTop + vditor.element.offsetTop;
            if (!vditor.options.toolbarConfig.pin) {
                windowScrollY += vditor.toolbar.element.offsetHeight;
            }
            window.scrollTo(window.scrollX, windowScrollY);
        } else {
            if (vditor.element.offsetTop < window.scrollY) {
                window.scrollTo(window.scrollX, vditor.element.offsetTop);
            }
            if (vditor.preview.element.contains(contentElement)) {
                contentElement.parentElement.scrollTop = idElement.offsetTop;
            } else {
                contentElement.scrollTop = idElement.offsetTop;
            }
        }
    } else {
        window.scrollTo(window.scrollX, idElement.offsetTop);
    }
    break;
}
grandsong commented 9 months ago

我打了一个外部的补丁,成功实现了预期的功能。

我采用了 vue ,在编辑器的元素上,监听事件 @pointerdown="clickEditor"

clickEditor(ev: PointerEvent) {
  const el = ev.target as HTMLElement
  const el_outline_item = findThisOrAncestor(el, '[data-target-id]')
  // warn('el_outline_item', el_outline_item)
  if (el_outline_item) {
    const outline_item_target_id = el_outline_item.getAttribute('data-target-id')!
    const el_target = document.getElementById(outline_item_target_id)!
    el_target.scrollIntoView()
  }
},
/**
 * 从`el`开始向上(祖先元素)遍历,返回匹配`selector`的第一个元素
 */
export function findThisOrAncestor(el: HTMLElement | null, selector: string) {
  while (el) {
    if (el.matches(selector)) {
      return el
    }
    else {
      el = el.parentElement
    }
  }
  return null
}