说在前面
不知道大家发布公众号文章用的是哪个排版工具呢?我这边使用的是
墨滴
,用起来体验感还是不错的,就是有一点让我觉得比较不习惯,正常写文章的时候应该有个目录可以查看会比较方便,比如掘金写文章的时候就可以查看文章目录,而墨滴是没有这个功能的,于是我便想着自己简单写一个插件来解决一下,给它加一个目录功能。
墨滴文章编写页面如下图,没有一个可以查看目录结构的功能。
掘金文章编写页面如下图,我们可以点击目录按钮来查看目录
插件效果展示
墨滴文章编写页面最右边有一栏操作按钮,我们可以在这里加上一个查看目录的按钮,点击查看按钮后显示当前文章的目录结构信息。
插件功能实现
配置文件
- manifest.json
编写浏览器插件的第一步,首先我们要先编写配置文件
manifest.json
。
{
"manifest_version": 3,
"name": "墨滴插件",
"version": "1.0.0",
"description": "墨滴插件",
"icons": {
"16": "目录.png",
"48": "目录.png",
"128": "目录.png"
},
"action": {
"default_title": "墨滴插件",
"default_icon": "目录.png",
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["https://editor.mdnice.com/*"],
"js": ["bg.js"],
"run_at": "document_end",
"css": []
}
],
"web_accessible_resources": [
{
"resources": [],
"matches": ["https://editor.mdnice.com/*"]
}
],
"permissions": []
}
各配置项说明如下:
-
基本信息:
manifest_version
为 3,表明遵循 Manifest V3 规范。name
是插件的名称“墨滴插件”。version
是插件的版本号“1.0.0”。description
对插件进行了简要描述。
-
图标设置:
- 通过
icons
对象指定了不同尺寸的图标为“目录.png”,分别对应 16、48、128 像素大小。
- 通过
-
插件动作设置:
action
部分定义了插件的动作。default_title
是插件动作的默认标题“墨滴插件”。default_icon
也是“目录.png”。default_popup
指定了当用户点击插件图标时显示的弹出页面为“popup.html”。
-
内容脚本设置:
content_scripts
数组中定义了要注入到网页中的脚本。matches
指定了匹配的网址模式,这里只匹配“editor.mdnice.com/*”。js
列出了要注入的脚本文件为“bg.js”。run_at
设置为“document_end”,表示在文档加载结束时注入脚本。
-
可访问资源设置:
web_accessible_resources
数组允许插件声明哪些资源可以被网页通过特定的方式访问。- 当前
resources
为空数组,可能表示暂时没有特定的资源需要被网页访问。 matches
同样匹配“editor.mdnice.com/*”,限制了资源可被访…
-
权限设置:
permissions
数组目前为空,表示插件目前没有请求特定的权限。
注入脚本编写
(1)插入目录查看按钮
首先我们需要在页面右边的操作栏最下方加上一个目录查看按钮
const addBtn = () => {
const btnBox = document.querySelector("#nice-sidebar");
const svg = `<svg t="1730880340340" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4296" width="16" height="16"><path d="M106.666667 192a21.333333 21.333333 0 1 0 0 42.666667h85.333333a21.333333 21.333333 0 0 0 0-42.666667H106.666667z m0 298.666667a21.333333 21.333333 0 0 0 0 42.666666h85.333333a21.333333 21.333333 0 0 0 0-42.666666H106.666667z m0 298.666666a21.333333 21.333333 0 0 0 0 42.666667h85.333333a21.333333 21.333333 0 0 0 0-42.666667H106.666667zM320 192a21.333333 21.333333 0 0 0 0 42.666667h597.333333a21.333333 21.333333 0 0 0 0-42.666667H320z m0 298.666667a21.333333 21.333333 0 0 0 0 42.666666h597.333333a21.333333 21.333333 0 0 0 0-42.666666H320z m0 298.666666a21.333333 21.333333 0 0 0 0 42.666667h597.333333a21.333333 21.333333 0 0 0 0-42.666667H320z" fill="#1196db" p-id="4297"></path></svg>`;
const menuBtn = document.createElement("a");
menuBtn.id = "nice-sidebar-menu-type";
menuBtn.classList.add("nice-btn-previewtype");
menuBtn.innerHTML = svg;
menuBtn.title = "查看目录";
menuBtn.addEventListener("click", () => {
const mdniceMenu = document.querySelector("#mdniceMenu");
if (!mdniceMenu) return;
const display = mdniceMenu.style.display;
mdniceMenu.style.display = display === "none" ? "block" : "none";
});
btnBox.appendChild(menuBtn);
};
打开控制台,我们可以查看到右边操作栏元素具体selector,使用document.querySelector
方法查找页面中具有id
为nice-sidebar
的元素,该元素即为右边的操作栏实例;按钮图标的 SVG 可以自己绘制或者直接到iconfont上找。监听按钮点击事件,在点击按钮的时候对目录面板进行显示或隐藏。
(2)获取文章目录结构
文章标题基本都是h
标签包裹的,所以我们可以直接找出页面上的文章容器中的所有h
标签,最后将获取到的所有h
标签按其offsetTop
的高度进行排序即可,具体代码如下:
const getMenuItem = () => {
const tmp = document.querySelector("#nice-rich-text-box");
const titleTag = ["h1", "h2", "h3", "h4", "h5", "h6"];
const menuList = [];
titleTag.forEach((item) => {
const list = tmp.querySelectorAll(item);
menuList.push(...list);
});
return menuList.sort((a, b) => {
return a.offsetTop - b.offsetTop;
});
};
(3)插入目录面板
我们直接在页面上插入一个div
作为目录面板容器,使用fixed
定位将其固定到目录查看按钮左边即可。
const addMenuPanel = () => {
const btnBox = document.querySelector("#nice-sidebar");
const menuBtn = document.querySelector("#nice-sidebar-menu-type");
const menuBox = document.createElement("div");
menuBox.id = "mdniceMenu";
menuBox.style.position = "fixed";
menuBox.style.right = btnBox.offsetWidth + "px";
menuBox.style.top = menuBtn.offsetTop + btnBox.offsetTop + "px";
menuBox.style.background = "#c4dde9";
menuBox.style.opacity = "0.7";
menuBox.style.padding = "1em 0.5em";
menuBox.style.lineHeight = "1.5em";
menuBox.style.borderRadius = "1em";
menuBox.style.height = "20em";
menuBox.style.overflow = "scroll";
menuBox.style.color = "#0335f3";
menuBox.style.display = "none";
menuBox.style.width = "15em";
menuBox.style.wordBreak = "break-all";
document.body.appendChild(menuBox);
updateMenu();
};
(4)将目录结构插入到目录面板
- 判断目录面板是否存在
const mdniceMenu = document.querySelector("#mdniceMenu");
if (!mdniceMenu) return;
- 判断目录结构是否有更新
const menuList = getMenuItem();
const lastMenuList = originMenuList.join("");
originMenuList = [];
menuList.forEach((item) => {
originMenuList.push(item.nodeName + item.innerText);
});
if (lastMenuList === originMenuList.join("")) return;
- 获取最小标题
我们需要根据最小标题来计算标题的缩进。
const minNumber = Math.min(
...menuList.map((item) => {
return item.nodeName.slice(1);
})
);
- 插入目录信息
使用mousemove
+mouseleave
可以实现一个hover
效果。
mdniceMenu.innerHTML = "";
menuList.forEach((item) => {
const div = document.createElement("div");
div.style.paddingLeft = item.nodeName.slice(1) - minNumber + "em";
div.innerText = item.innerText;
div.style.cursor = "pointer";
div.addEventListener("click", () => {
const tmp = document.querySelector("#nice-rich-text-box");
tmp.scroll(0, item.offsetTop);
});
div.addEventListener("mousemove", () => {
div.style.textDecoration = "underline";
div.style.backgroundColor = "#f0f0f0";
});
div.addEventListener("mouseleave", () => {
div.style.textDecoration = "none";
div.style.backgroundColor = "#c4dde9";
});
mdniceMenu.appendChild(div);
});
- 完整代码
const updateMenu = () => {
const mdniceMenu = document.querySelector("#mdniceMenu");
if (!mdniceMenu) return;
const menuList = getMenuItem();
const lastMenuList = originMenuList.join("");
originMenuList = [];
menuList.forEach((item) => {
originMenuList.push(item.nodeName + item.innerText);
});
if (lastMenuList === originMenuList.join("")) return;
mdniceMenu.innerHTML = "";
const minNumber = Math.min(
...menuList.map((item) => {
return item.nodeName.slice(1);
})
);
menuList.forEach((item) => {
const div = document.createElement("div");
div.style.paddingLeft = item.nodeName.slice(1) - minNumber + "em";
div.innerText = item.innerText;
div.style.cursor = "pointer";
div.addEventListener("click", () => {
const tmp = document.querySelector("#nice-rich-text-box");
tmp.scroll(0, item.offsetTop);
});
div.addEventListener("mousemove", () => {
div.style.textDecoration = "underline";
div.style.backgroundColor = "#f0f0f0";
});
div.addEventListener("mouseleave", () => {
div.style.textDecoration = "none";
div.style.backgroundColor = "#c4dde9";
});
mdniceMenu.appendChild(div);
});
};
源码
插件源码已开源到gitee,有兴趣的也可以到这里看看:gitee.com/zheng_yongt…
🌟觉得有帮助的可以点个star~
🖊有什么问题或错误可以指出,欢迎pr~
📬有什么想要实现的功能或想法可以联系我~
使用
将插件下载到本地,在扩展程序(chrome://extensions/)中加载已解压的扩展程序即可。
公众号
关注公众号『前端也能这么有趣
』,获取更多有趣内容。
说在后面
🎉这里是JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打打羽毛球🏸 ,平时也喜欢写些东西,既为自己记录📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解🙇,写错的地方望指出,定会认真改进😊,在此谢谢大家的支持,我们下文再见🙌。