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