tinymce6实现目录功能(包含生成目录大纲和点击跳转)

本文介绍了如何在Vue3项目中使用tinymce6实现目录功能,包括生成目录结构、添加点击事件跳转以及注意事项。作者还分享了如何解决引入插件和配置的问题。
摘要由CSDN通过智能技术生成

tinymce6实现目录功能(包含生成目录大纲和点击跳转)

前言

终于最近又回到文档编辑器开发这边了,可以继续给大家更新tinymce的更多经验了。之前是在vue2开发的系统中使用tinymce5编辑器,实现了很多功能没有分享,最近做框架迁移,在vue3搭建的框架中实现之前的功能,同时我把tinymce的版本也升级到了6.x,后面会多多分享。

需求

编辑器的引入和插件的引入在之前的文章有介绍过,如果还没有引入成功的可以去翻翻。
还有一些根据文章依旧没有引入成功的小伙伴,试试以下步骤:
1.首先看一下版本,tinymce5和tinymce6的插件引入方式不一定完全相同,一般建议是将plugin.js和plugin.min.js都加上;
2.然后确定插件引入存放的位置,我是在public文件夹下,保证路径正确,插件引入时的baseURL真的很重要,一开始我也因为baseURL的问题没引入成功;
3.最后看一下报错,如果是“<”之类的字符问题,那就是插件已经读取到了自定义的文件,但是文件中有错误;如果最后的最后还是没有生效,并且没有报错,那就是没有读到文件,那可能是你引入tinymce的地方读取插件的配置不对,可以看一下我前面“如何引入tinymce”的文章,这个插件引入有几种方法有很多细微的差别。

tinymce6将很多原本的插件都写入核心编辑器功能了,比如字体、列表、排版等,不是你的菜单栏有内容就代表引入插件成功的,这个要注意。

目录的实现

这个实现过程我就不多赘述了,就是根据一级二级三级目录生成一个目录信息,即h1、h2、h3等做不同处理;拼装时加入herf,以便增加点击事件跳转;最后将拼装完成的东西再加不可编辑属性,使其在文档中不能被随意编辑,只能更新。代码如下

// plugin.js
; (function () {
  tinymce.PluginManager.add('toc', function (editor) {
    const getTocClass = () => {
      return editor.getParam('toc_class', 'mce-toc');
    };

    const getTocHeader = () => {
      const tagName = editor.getParam('toc_header', 'h2');
      return /^h[1-6]$/i.test(tagName) ? tagName : 'h2';
    };

    const getTocDepth = () => {
      const depth = parseInt(editor.getParam('toc_depth', '3'), 10);
      return depth >= 1 && depth <= 9 ? depth : 3;
    };

    const create = (prefix) => {
      let counter = 0;
      return () => {
        const guid = new Date().getTime().toString(32);
        return prefix + guid + (counter++).toString(32);
      };
    };

    const scrollToElement = (id) => {
      const element = editor.getBody().querySelector(`#${id}`);
      if (element) {
        element.scrollIntoView({ behavior: 'smooth' });
      }
    };

    // 点击事件处理程序,处理目录点击跳转到相应内容
    const handleClick = (e) => {
      e.preventDefault();
      const target = e.target;
      if (target.tagName.toLowerCase() === 'a' && target.hasAttribute('href')) {
        const href = target.getAttribute('href').replace('#', '');
        scrollToElement(href);
      }
    };

    const setupClickEvent = () => {
      const tocClass = getTocClass();
      editor.on('click', (e) => {
        const $tocElm = editor.getBody().querySelector(`.${tocClass}`);
        if ($tocElm && e.target.closest(`.${tocClass}`)) {
          handleClick(e);
        }
      });
    };

    setupClickEvent();

    const tocId = create('mcetoc_');

    const readHeaders = () => {
      const tocClass = getTocClass();
      const headerTag = getTocHeader();
      const selector = `h1, h2, h3, h4, h5, h6`;
      const headers = Array.from(editor.getBody().querySelectorAll(selector));
      if (headers.length && /^h[1-9]$/i.test(headerTag)) {
        return headers.filter((el) => !el.classList.contains(tocClass)).map((h) => {
          const id = h.id ? h.id : tocId();
          return {
            id,
            level: parseInt(h.tagName.replace(/^H/i, ''), 10),
            title: h.textContent,
            element: h,
          };
        });
      }
      return [];
    };

    const generateTocContentHtml = () => {
      const headers = readHeaders();
      const minLevel = headers.reduce((min, h) => Math.min(min, h.level), 9);

      let html = '';
      if (!headers.length) {
        return html;
      }

      html += `<${getTocHeader()} contenteditable="true">${editor.translate('目录')}</${getTocHeader()}>`;
      let prevLevel = minLevel - 1;

      headers.forEach((h) => {
        h.element.id = h.id;
        const nextLevel = headers[headers.indexOf(h) + 1]?.level || null;

        if (prevLevel === h.level) {
          html += '<li>';
        } else {
          for (let i = prevLevel; i < h.level; i++) {
            html += '<ul><li>';
          }
        }

        html += `<a href="#${h.id}">${h.title}</a>`;

        if (nextLevel === h.level || !nextLevel) {
          html += '</li>';
          if (!nextLevel) {
            html += '</ul>';
          }
        } else {
          for (let i = h.level; i > nextLevel; i--) {
            if (i === nextLevel + 1) {
              html += '</li></ul><li>';
            } else {
              html += '</li></ul>';
            }
          }
        }

        prevLevel = h.level;
      });

      return html;
    };

    const insertToc = () => {
      const tocClass = getTocClass();
      const $tocElm = editor.getBody().querySelector(`.${tocClass}`);
      const tocHtml = generateTocContentHtml();

      if (!$tocElm || !$tocElm.textContent) {
        editor.insertContent(`<div class="${tocClass}" contenteditable="false">${tocHtml}</div>`);
      } else {
        updateToc(tocHtml);
      }
    };

    const updateToc = (tocHtml) => {
      const tocClass = getTocClass();
      const $tocElm = editor.getBody().querySelector(`.${tocClass}`);
      if ($tocElm) {
        editor.undoManager.transact(() => {
          $tocElm.innerHTML = tocHtml;
        });
      }
    };

    editor.addCommand('mceInsertToc', () => {
      insertToc();
    });

    editor.addCommand('mceUpdateToc', () => {
      const tocHtml = generateTocContentHtml();
      updateToc(tocHtml);
    });

    editor.ui.registry.addButton('toc', {
      icon: 'toc',
      tooltip: '目录',
      onAction: () => {
        insertToc();
      },
    });

    editor.ui.registry.addButton('tocupdate', {
      icon: 'reload',
      tooltip: 'Update',
      onAction: () => {
        const tocHtml = generateTocContentHtml();
        updateToc(tocHtml);
      },
    });

    editor.ui.registry.addMenuItem('toc', {
      icon: 'toc',
      text: '目录',
      onAction: () => {
        insertToc();
      },
    });

    editor.ui.registry.addContextToolbar('toc', {
      items: 'tocupdate',
      predicate: (node) => node.classList.contains(getTocClass()),
      scope: 'node',
      position: 'node',
    });
  });
})()

tinymce的官方付费版是包含很多功能的,比如目录、评注等,但是价格还挺贵的,好像一个月六百多美元,相信大家都想自己开发自定义插件来省这笔钱,那就关注我,接下来我会更新评注功能的开发过程

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值