vue3 markdown与锚点的完美交互

markdown与锚点的交互使用是现在博客网站的标配。例如CSDN、简书、掘金,都有类似的功能。最近在开发博客网站时也遇到了同样的需求。本文详细总结了markdown与锚点交互相关的问题解决方法,如果你的博客网站也有类似的需求,希望这篇文章可以帮助到你。

一、功能需求与效果

1. 实现功能

  • 根据markdown的h标题,自动生成文章大纲目录
  • 点击锚点时,页面滑动到对应的位置,且锚点高亮
  • 当页面滑动时,滑动到指定位置时,对应的锚点高亮显示

2. 效果示例

这种效果在csdn、简书、掘金上都有类型的功能。包括本人的博客网站也有,右侧为markdown锚点。

在这里插入图片描述

二、获取锚点列表

1. 思路分析

在这里插入图片描述

查看网页源代码分析可知,markdown所有标题均为h1-h6的标签。通过js的标签选择器,选中这些标签,组成一个列表。列表中除了要包含标题的名称外,还有标题距离顶部的距离(当前锚点高亮使用)、标题的层级(生成目录时,不同层级左侧缩进距离不同)、标题的id(点击标题滚动时,跳转到指定id的位置)

2. 示例代码

<template>
  <div>
    <div
        v-for="anchor in titleList"
        :style="{ padding: `10px 0 10px ${anchor.indent * 20}px` }"
    >
      <a style="cursor: pointer">{{ anchor.title }}</a>
    </div>
    <v-md-preview :text="text" ref="editor" />
  </div>
</template>

<script setup>
import {ref, onMounted, nextTick} from 'vue';
import {getArticleDetail} from "@/api/blog";
// markdown-文章内容
const article = ref()
// markdown-对象
const editor = ref(null)
// markdown-文章标题列表
const titleList = ref([])
// markdown-获取内容
async function articleData(DetailID) {
  // axios获取内容
  article.value =  await getArticleDetail(DetailID)
}
// markdown-生成标题
async function getTitle() {
  await nextTick()
  // 使用js选择器,获取对应的h标签,组合成列表
  const anchors = editor.value.querySelectorAll(
      '.v-md-editor-preview h1,h2,h3,h4,h5,h6'
  )
  // 删除标题头尾的空格
  const titles = Array.from(anchors).filter((title) => !!title.innerText.trim());
  // 当文章h标签为空时,直接返回
  if (!titles.length) {
    titleList.value = [];
    return;
  }
  // 从h标签属性中,提取相关信息
  const hTags = Array.from(new Set(titles.map((title) => title.tagName))).sort();
  titleList.value = titles.map((el) => ({
    title: el.innerText, // 标题内容
    lineIndex: el.getAttribute('data-v-md-line'), // 标签line id
    indent: hTags.indexOf(el.tagName), // 标签层级
    height: el.offsetTop, // 标签距离顶部距离
  }));
}
onMounted(async () => {
  // 获取第一篇文章内容
  await articleData(1)
  // 生成文章标题列表
  await getTitle()
})
</script>
<style lang="scss">
………
</style>
  • 其中h标签的详细属性信息可以通过控制台打印anchors查看

3. 效果演示

  • 通过vue devtools可以查看到提取的标签列表如下所示

在这里插入图片描述

  • 页面生成的大纲内容如下所示

在这里插入图片描述

在这里插入图片描述

三、点击锚点滚动

1. 思路分析

点击锚点滚动实现起来较为简单,基本思路是定义一个变量存放当前高亮的标题列表的id,通过三元表达式添加当前标签高亮的class。页面目录元素添加点击事件,传入标签对象,js查找对应的lineIndex,实现页面滚动跳转

2. 示例代码

<template>
  <div>
    <div
        v-for="anchor in titleList"
        :style="{ padding: `10px 0 10px ${anchor.indent * 20}px` }"
        @click="rollTo(anchor,index)" :class="index===heightTitle?'title-active':''"
    >
      <a style="cursor: pointer">{{ anchor.title }}</a>
    </div>
    <v-md-preview :text="text" ref="editor" />
  </div>
</template>

<script setup>
import {ref, onMounted, nextTick} from 'vue';
import {getArticleDetail} from "@/api/blog";
// markdown-文章内容
const article = ref()
// markdown-对象
const editor = ref(null)
// markdown-文章标题列表
const titleList = ref([])
// markdown-获取内容
async function articleData(DetailID) {
  ……
}
// markdown-生成标题
async function getTitle() {
  ……
}
// markdown-当前高亮的标题index
const heightTitle = ref(0)
// markdown-标题跳转
const rollTo = (anchor, index) => {
  // 获取要跳转的标签的lineIndex
  const {lineIndex} = anchor;
  // 查找lineIndex对应的元素对象
  const heading = editor.value.querySelector(
      `.v-md-editor-preview [data-v-md-line="${lineIndex}"]`
  );
  // 页面跳转
  if (heading) {
    heading.scrollIntoView({behavior: "smooth", block: "start"})
  }
  // 修改当前高亮的标题
  heightTitle.value = index
}
onMounted(async () => {
  // 获取第一篇文章内容
  await articleData(1)
  // 生成文章标题列表
  await getTitle()
})
</script>
<style lang="scss">
.title-active {
  background-color: $color-background-input;
  color: $color-text-primary;
  border-left: 2px solid $color-primary;
  }
</style>

3. 效果演示

  • 点击目录标题,跳转至对应位置

在这里插入图片描述

  • 控制台打印lineIndex对应的元素对象

在这里插入图片描述

四、当前锚点高亮

1. 思路分析

获取markdown所有标签对应的高度。然后监听页面的滚动事件。当每次页面滚动时,查找页面滚动值和标签高度最接近的那个标签对应的index。通过三元表达式添加当前标签高亮的class。

2. 示例代码

<template>
  <div>
    <div
        v-for="anchor in titleList"
        :style="{ padding: `10px 0 10px ${anchor.indent * 20}px` }"
        @click="rollTo(anchor,index)" :class="index===heightTitle?'title-active':''"
    >
      <a style="cursor: pointer">{{ anchor.title }}</a>
    </div>
    <v-md-preview :text="text" ref="editor" />
  </div>
</template>

<script setup>
import {ref, onMounted, nextTick, onBeforeUnmount} from 'vue';
import {getArticleDetail} from "@/api/blog";
// markdown-文章内容
const article = ref()
// markdown-对象
const editor = ref(null)
// markdown-文章标题列表
const titleList = ref([])
// markdown-获取内容
async function articleData(DetailID) {
 ……
}
// markdown-生成标题
async function getTitle() {
  ……
}
// markdown-当前高亮的标题index
const heightTitle = ref(0)
// markdown-标题跳转
const rollTo = (anchor, index) => {
  ……
}
// markdown-页面滚动。
const scroll = () => {
  // 监听屏幕滚动时防抖(在规定的时间内触发的事件,只执行最后一次,降低性能开销)
  let timeOut = null; // 初始化空定时器
  return () => {
    clearTimeout(timeOut)   // 频繁操作,一直清空先前的定时器
    timeOut = setTimeout(() => {  // 只执行最后一次事件
      let scrollTop = window.pageYOffset
      // console.log(window.pageYOffset)
      const absList = [] // 各个h标签与当前距离绝对值
      titleList.value.forEach((item) => {
        absList.push(Math.abs(item.height - scrollTop))
      })
      // 屏幕滚动距离与标题高度最近的index高亮
      heightTitle.value = absList.indexOf(Math.min.apply(null, absList))
    }, 500)
  }
}
onMounted(async () => {
  ……
  // 监听页面滚动事件
  window.addEventListener('scroll', scroll())
})
onBeforeUnmount(() => {
  // 离开该页面时移除这个监听的事件
  window.removeEventListener('scroll', scroll())
})
</script>
<style lang="scss">
.title-active {
  background-color: $color-background-input;
  color: $color-text-primary;
  border-left: 2px solid $color-primary;
  }
</style>

3. 效果演示

  • 控制台打印scrollTop和heightTitle查看

在这里插入图片描述

4. 完整代码

<template>
  <div>
    <div
        v-for="anchor in titleList"
        :style="{ padding: `10px 0 10px ${anchor.indent * 20}px` }"
        @click="rollTo(anchor,index)" :class="index===heightTitle?'title-active':''"
    >
      <a style="cursor: pointer">{{ anchor.title }}</a>
    </div>
    <v-md-preview :text="text" ref="editor" />
  </div>
</template>

<script setup>
import {ref, onMounted, nextTick, onBeforeUnmount} from 'vue';
import {getArticleDetail} from "@/api/blog";
// markdown-文章内容
const article = ref()
// markdown-对象
const editor = ref(null)
// markdown-文章标题列表
const titleList = ref([])
// markdown-获取内容
async function articleData(DetailID) {
  // axios获取内容
  article.value =  await getArticleDetail(DetailID)
}
// markdown-生成标题
async function getTitle() {
  await nextTick()
  // 使用js选择器,获取对应的h标签,组合成列表
  const anchors = editor.value.querySelectorAll(
      '.v-md-editor-preview h1,h2,h3,h4,h5,h6'
  )
  // 删除标题头尾的空格
  const titles = Array.from(anchors).filter((title) => !!title.innerText.trim());
  // 当文章h标签为空时,直接返回
  if (!titles.length) {
    titleList.value = [];
    return;
  }
  // 从h标签属性中,提取相关信息
  const hTags = Array.from(new Set(titles.map((title) => title.tagName))).sort();
  titleList.value = titles.map((el) => ({
    title: el.innerText, // 标题内容
    lineIndex: el.getAttribute('data-v-md-line'), // 标签line id
    indent: hTags.indexOf(el.tagName), // 标签层级
    height: el.offsetTop, // 标签距离顶部距离
  }));
}
// markdown-当前高亮的标题index
const heightTitle = ref(0)
// markdown-标题跳转
const rollTo = (anchor, index) => {
  // 获取要跳转的标签的lineIndex
  const {lineIndex} = anchor;
  // 查找lineIndex对应的元素对象
  const heading = editor.value.querySelector(
      `.v-md-editor-preview [data-v-md-line="${lineIndex}"]`
  );
  // 页面跳转
  if (heading) {
    heading.scrollIntoView({behavior: "smooth", block: "start"})
  }
  // 修改当前高亮的标题
  heightTitle.value = index
}
// markdown-页面滚动。
const scroll = () => {
  // 监听屏幕滚动时防抖(在规定的时间内触发的事件,只执行最后一次,降低性能开销)
  let timeOut = null; // 初始化空定时器
  return () => {
    clearTimeout(timeOut)   // 频繁操作,一直清空先前的定时器
    timeOut = setTimeout(() => {  // 只执行最后一次事件
      let scrollTop = window.pageYOffset
      // console.log(window.pageYOffset)
      const absList = [] // 各个h标签与当前距离绝对值
      titleList.value.forEach((item) => {
        absList.push(Math.abs(item.height - scrollTop))
      })
      // 屏幕滚动距离与标题高度最近的index高亮
      heightTitle.value = absList.indexOf(Math.min.apply(null, absList))
    }, 500)
  }
}
onMounted(async () => {
  // 获取第一篇文章内容
  await articleData(1)
  // 生成文章标题列表
  await getTitle()
  // 监听页面滚动事件
  window.addEventListener('scroll', scroll())
})
onBeforeUnmount(() => {
  // 离开该页面时移除这个监听的事件
  window.removeEventListener('scroll', scroll())
})
</script>
<style lang="scss">
.title-active {
  background-color: $color-background-input;
  color: $color-text-primary;
  border-left: 2px solid $color-primary;
  }
</style>

参考链接
https://www.jianshu.com/p/e07a8e4efb7d
http://ckang1229.gitee.io/vue-markdown-editor/zh/senior/anchor.html
https://blog.csdn.net/weixin_41192489/article/details/111284951

查看更多

微信公众号

微信公众号同步更新,欢迎关注微信公众号第一时间获取最近文章。在这里插入图片描述

博客网站

崔亮的博客-专注devops自动化运维,传播优秀it运维技术文章。更多原创运维开发相关文章,欢迎访问https://www.cuiliangblog.cn

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值