使用 wangeditor 解析富文本并生成目录与代码块复制功能

在 Web 开发中,经常需要使用富文本编辑器来编辑和展示内容。wangeditor 是一个强大的富文本编辑器,提供了丰富的功能和灵活的配置,但是官方并没有提供目录导航和代码块的复制功能,所以我自己搞了一个

<template>
  <div class="editor" flex w-full>
    <!-- 文章内容 -->
    <div flex-grow overflow-hidden w-full>
      <slot/>
      <div>
        <Editor
          ref="editorContent" v-model="defaultHtml"
          :defaultConfig="editorConfig"
          :mode="mode"
          m-t-60px
          overflow-hidden
          w-auto
          @onChange="handleChange"
          @onCreated="handleCreated"
        />
      </div>
    </div>

    <div v-if="directory" class="flex-container" flex-none h-500px ml-20px p-t-160px relative w-300px>
      <el-affix :offset="10">
        <div border-b border-b-solid border-gray200 class="table-of-title" flex font-bold items-center p-10px w-300px>
          <el-icon>
            <Expand/>
          </el-icon>
          <span ml-5px>目录</span>
        </div>
        <!-- 目录 -->
        <div v-if="tableOfContents.length > 0" b-rd-5px class="table-of-contents " max-h-450px overflow-y-scroll p-10px relative
             w-300px>
          <!-- 目录内容 -->
          <ul list-none p-l-0>
            <li v-for="(item, index) in tableOfContents" :key="item.id" :style="{ paddingLeft: item.level * 20 + 'px' }" border-rd mb-5px
                py-3px>
              <a :class="{ active: activeIndex === index }" :href="`#${item.id}`" block decoration-none
                 @click="handleItemClick(index)">{{ item.text }}</a>
            </li>
          </ul>
        </div>
      </el-affix>

    </div>
  </div>
</template>

<script lang="ts" setup>
import {Editor} from "@wangeditor/editor-for-vue";
import {copy} from "@/utils";

// API 引用
import {upload} from "@/utils/request";

const props = defineProps({
  modelValue: {
    type: [String],
    default: "",
  },
  directory: {
    type: [Boolean],
    default: true,
  }
});

const emit = defineEmits(["update:modelValue"]);

const defaultHtml = useVModel(props, "modelValue", emit);

const editorRef = shallowRef(); // 编辑器实例,必须用 shallowRef
const mode = ref("default"); // 编辑器模式
// 在编辑器创建后生成目录
const tableOfContents = ref([]);
// 编辑器配置
const editorConfig = ref({
  placeholder: "请输入内容...",
  MENU_CONF: {
    uploadImage: {
      // 自定义图片上传
      async customUpload(file: any, insertFn: any) {
        const formData = new FormData();
        formData.set("file", file);
        upload(formData).then(({data: res}) => {
          insertFn(res.url);
        });
      },
    },
  },
  readOnly: true,
});

const handleCreated = (editor: any) => {
  editorRef.value = editor; // 记录 editor 实例,重要!
};

function handleChange(editor: any) {
  emit("update:modelValue", editor.getHtml());
}

// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
  const editor = editorRef.value;
  if (editor == null) return;
  editor.destroy();
});
watch(() => props.modelValue, (newVal) => {
  defaultHtml.value = newVal;
});
// 添加复制按钮的逻辑
const copyCode = () => {
  const codeBlocks = document.querySelectorAll(".editor pre > code");
  codeBlocks.forEach((codeBlock) => {
    // 创建复制按钮
    const copyButton = document.createElement("button");
    copyButton.innerText = "复制";
    copyButton.classList.add("copy-button");

    // 为复制按钮添加点击事件处理程序
    copyButton.addEventListener("click", () => {
      const codeText = codeBlock.querySelector("span").textContent;
      copy(codeText, "已复制", false);
      // 修改按钮文本为 "已复制"
      copyButton.innerText = "已复制";
      // 延迟一段时间后恢复按钮文本为 "复制"
      setTimeout(() => {
        copyButton.innerText = "复制";
      }, 3000); // 毫秒为单位,您可以调整时间长度
    });

    // 将复制按钮添加到代码块的父级元素中
    codeBlock.appendChild(copyButton);
  });
};

// 添加锚点
const addAnchorLinks = () => {
  const headings = document.querySelectorAll(".editor h1, .editor h2, .editor h3");
  headings.forEach((heading, index) => {
    const anchorLink = document.createElement("a");
    anchorLink.setAttribute("href", `#section-${index + 1}`);
    // anchorLink.textContent = heading.textContent; // 设置锚点文本为标题文本
    anchorLink.style.pointerEvents = "none"; // 设置 pointer-events 为 none,使链接不可点击

    // 设置标题的id属性
    heading.setAttribute("id", `section-${index + 1}`);

    // 将锚点链接插入到标题内
    heading.innerHTML = anchorLink.outerHTML + heading.innerHTML;
  });
};

// 更新目录项点击事件处理函数
const handleItemClick = (index: number) => {
  activeIndex.value = index;

  // 获取目标目录项的锚点链接 href 属性值
  const targetItem = document.querySelector(`.table-of-contents a[href="#section-${index + 1}"]`) as HTMLElement;

  // 滚动目录以确保当前点击的目录项可见
  if (targetItem) {
    const container = document.querySelector(".table-of-contents") as HTMLElement;
    const containerRect = container.getBoundingClientRect();
    const scrollTop = targetItem.offsetTop - containerRect.height / 2;
    container.scrollTop = scrollTop;
  }
};

// 生成目录
const generateTableOfContents = () => {
  const headings = document.querySelectorAll(".editor h1, .editor h2, .editor h3");
  const toc = [];
  headings.forEach((heading, index) => {
    const id = `section-${index + 1}`;
    const level = heading.tagName === "H1" ? 1 : heading.tagName === "H2" ? 2 : 3; // 根据标题等级设置目录项的缩进
    heading.setAttribute("id", id); // 设置标题的id属性
    toc.push({id: id, text: heading.textContent, level: level, index: index}); // 将标题文本、id和等级添加到目录项中
  });
  return toc;
};

const handleScroll = () => {
  requestAnimationFrame(() => {
    const sections = document.querySelectorAll(".editor h1, .editor h2, .editor h3");
    const scrollY = window.scrollY || window.pageYOffset;
    let currentIndex = 0;
    for (let i = 0; i < sections.length; i++) {
      const sectionTop = (sections[i] as HTMLElement).offsetTop;
      if (scrollY >= sectionTop) {
        currentIndex = i;
      }
    }

    // 检查当前视图中是否有标题元素,如果有,将其索引赋给 currentIndex
    const visibleSections = Array.from(sections).filter((section) => {
      const sectionTop = (section as HTMLElement).offsetTop;
      const sectionBottom = sectionTop + (section as HTMLElement).offsetHeight;
      return scrollY >= sectionTop && scrollY <= sectionBottom;
    });
    if (visibleSections.length > 0) {
      currentIndex = Array.from(sections).indexOf(visibleSections[visibleSections.length - 1]);
    }

    activeIndex.value = currentIndex;

    // 滚动目录以确保当前高亮的目录项可见
    const activeItem = document.querySelector(".table-of-contents .active") as HTMLElement;
    if (activeItem) {
      const container = document.querySelector(".table-of-contents");
      const containerRect = container.getBoundingClientRect();
      const activeRect = activeItem.getBoundingClientRect();
      const scrollTop = activeItem.offsetTop - containerRect.height / 2 + activeRect.height / 2;
      container.scrollTop = scrollTop;
    }
  });
};

// 在编辑器创建后添加复制按钮
onMounted(() => {
  tableOfContents.value = generateTableOfContents();
  addAnchorLinks();
  copyCode();
  window.addEventListener("scroll", handleScroll);
});

// 在组件销毁时移除滚动事件监听器
onBeforeUnmount(() => {
  window.removeEventListener("scroll", handleScroll);
});

// 当前高亮的目录项索引
const activeIndex = ref(0);
</script>

<style src="@wangeditor/editor/dist/css/style.css"></style>
<style lang="scss" scoped>
@import "@/assets/styles/variables.module";

html {
  scroll-behavior: smooth;
}

.editor {
  overflow: hidden;
  min-height: 250px;
  z-index: 999;
}

@media screen and (max-width: 900px) {
  .flex-container {
    display: none !important; /* 添加 !important 以确保覆盖其他样式 */
  }
}

:deep() {
  .w-e-text-container {
    overflow: hidden;
    width: 100%;
    color: $base-text-color;
    border-bottom-right-radius: 8px;
    border-bottom-left-radius: 8px;
    background-color: $base-bg-box;

    .w-e-scroll {
      width: 100%;
      overflow: hidden !important;
    }

    [data-slate-editor] code {
      position: relative;
      background-color: $base-code-color;

      span {
        background-color: $base-code-color;
      }
    }

    pre > code {
      background-color: $base-code-color;
      // 防止花眼
      text-shadow: none;
    }

    iframe {
      width: 80%;
      height: 640px;
      display: block;
      border-radius: 8px;
      margin: 0 auto; /* 让图片水平居中 */
    }

    p {
      text-align: left; /* 保持文字左对齐 */
      line-height: 1.5rem;
      font-size: 0.875rem;
      font-family: "PingFang SC", sans-serif;
    }

    img {
      display: block;
      margin: 0 auto; /* 让图片水平居中 */
      max-width: 80%;
      //max-width: 80vw !important; /* 设置图片最大宽度为父元素宽度 */
      height: auto; /* 保持宽高比 */
      transform: scale(0.8); /* 设置缩放比例,这里是缩小为原来的80% */
    }

    [data-slate-editor] blockquote {
      background-color: $base-code-color;
      color: $base-text-color;
      border-radius: 2px;
    }

    [data-slate-editor] pre .copy-button {
      position: absolute;
      top: 6px;
      right: 6px;
      color: $base-text-color;
      border: none;
      padding: 0 5px;
      border-radius: 4px;
      cursor: pointer;
    }
  }

  .table-of-title {
    background-color: $base-bg-box;
  }

  .table-of-contents {
    color: $base-text-color;
    background-color: $base-bg-box;

    li:hover {
      color: #95c92c;
    }

    .active {
      font-weight: bold;
      color: #95c92c;
    }
  }
}
</style>
export const copy = (text: string, message: string, showSuccess: boolean = true) => {
  navigator.clipboard.writeText(text)
    .then(function () {
      if (showSuccess) {
        ElMessage({message: message, type: 'success', duration: 1500});
      }
    })
    .catch(function (err) {
      console.error('Unable to copy text to clipboard', err);
    });
}

实现功能如图:
目录功能:在这里插入图片描述
代码块复制功能:
在这里插入图片描述

具体实现效果可在平台https://web.yujky.cn/登录体验

租户:体验租户
用户名:cxks
密码: cxks123

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
wangeditor 富文本编辑器支持代码块功能。通过使用 wangeditor代码块插件,你可以在编辑器中插入代码块,并进行代码高亮显示。下面是一个示例代码,展示如何在 wangeditor使用代码块功能: 1. 首先,确保已经引入了 wangeditor 的相关文件和依赖。 2. 在 Vue 组件中,添加一个 textarea 元素作为编辑器的容器: ```html <textarea id="editor" v-model="content"></textarea> ``` 3. 在 Vue 组件的 `mounted` 钩子函数中,初始化 wangeditor 编辑器并配置代码块插件: ```javascript import Editor from 'wangeditor' export default { mounted() { const editor = new Editor('#editor') editor.config.plugins = [ // 其他插件... CodeSyntaxHighlighting() // 代码块插件 ] editor.create() // 监听编辑器内容变化 editor.txt.on('change', () => { this.content = editor.txt.html() }) }, data() { return { content: '' } } } ``` 4. 在样式文件中,引入代码块的 CSS 文件: ```css @import "~wangeditor/dist/css/wangEditor-codeHighlight.css"; ``` 这样就可以在 wangeditor 编辑器中使用代码块功能了。你可以输入代码,并选择对应的编程语言,然后点击插入代码块按钮即可实现代码高亮显示。 请注意,上述示例中的代码只是一个简单的示例,具体的使用方法和配置可能会因为你的项目和需求而有所不同。你可以根据 wangeditor 的官方文档和示例代码进行更详细的配置和使用

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值