代码块隐藏模块
代码块隐藏模块
笔记
一个代码块的代码太多,会占据大量的篇幅,如果能选择性隐藏,页面也许更加好看。
版权声明
警告
本着开源共享、共同学习的精神:
本文是在 博主《youngkbt》 文章:《本站 - 代码块隐藏模块》https://notes.youngkbt.cn/about/website/code-block-hidden/基础上增加了一些自己的实际操作记录和修改,内容依旧属于原作者[《youngkbt》](https://notes.youngkbt.cn/) 所有。转载无需和我联系,但请注明文章来源。如果侵权之处,请联系博主进行删除,谢谢~(这里万分感谢原作者的优质文章😜,感谢开源,拥抱开源💖)
本人测试环境
2024年12月26日测试
2024年12月23日从官方拉取的项目:
基于官方https://github.com/xugaoyi/vuepress-theme-vdoing
搭建的仓库。
前言
目前适用版本是 Vdoing v1.x。
代码块可以隐藏,也可以展开,这和 ::: details
类似,下面是简单的代码块 Demo:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello,World");
}
}
看到代码块右边的箭头了吗,点击即可隐藏代码块,再次点击则会展开代码块。
本内容实现并不难,只需三步:
- 添加箭头图标
- 编写代码块模块的 Vue 组件
- 全局注册 Vue 组件
实现内容:
代码块的隐藏和显示
美化代码块的 UI,趋向于 Mac
优化代码块语言的显示,因为默认主题的一些语言如 stylus 是不会显示出来。本内容的优化无论代码块语言是什么(如
abc
),都会显示出来,如下text我的语言不是 Java、PHP、JS、SH,而是 abdedfg
前提 1
本内容重新实现的一键复制功能是基于 vuepress-plugin-one-click-copy
插件(箭头左边),该插件已经内置 vuepress-theme-vdoing
主题,所以无需担心,如果你曾经卸载了该插件,则需要安装回来;如果已经安装,则无需看这一步:
yarn add vuepress-plugin-one-click-copy -D
当然,如果你懂得看下面的源码,则将适配 vuepress-plugin-one-click-copy
插件的代码进行修改,只需要提供其他插件的 class 名进行判断(Vue 组件的 108 - 119 行代码),并自行在 F12 调试,移动到满意的位置。
如果不知道自己是否曾卸载或存在该插件,则前往根目录下的 package.json
文件查看 devDependencies
是否有 vuepress-plugin-one-click-copy
插件。
前提 2
本功能需要代码块需要开启 行号 功能,该功能已经内置 VuePress,所以只需要开启该配置即可。
在 docs/.vuepress/config.ts
里开启行号:
export default defineConfig4CustomTheme({
theme: "vdoing", // 使用 npm 包主题
// ...
markdown: {
lineNumbers: true, // 显示代码块的行号
extractHeaders: ["h2", "h3", "h4"], // 支持 h2、h3、h4 标题
},
// ...
});
1、添加箭头图标
图标库来自阿里云:https://www.iconfont.cn/
。
如果你没有账号,或者觉得添加比较麻烦,就使用我的图标库地址,当你发现图标失效了,就请来这里获取新的地址,如果还没有更新,请在评论区留言。
当然,建议你使用自己的图标库,比较稳定。就像注册一个购物账户,然后添加到购物车即可。
在 docs/.vuepress/config.js(新版是 config.ts)的 head 模块里添加如下内容:
['link', { rel: 'stylesheet', href: '//at.alicdn.com/t/font_3114978_qe0b39no76.css' }]
2、添加Vue组件
在 docs/.vuepress/components 目录下创建 Vue 组件:BlockToggle.vue
。如果不存在 components 目录,则请创建。
添加如下内容:
<template></template>
<script>
export default {
mounted() {
setTimeout(() => {
this.addExpand(40);
}, 1000);
},
watch: {
$route(to, from) {
if (to.path != from.path || this.$route.hash == "") {
setTimeout(() => {
this.addExpand(40);
}, 1000);
}
},
},
methods: {
// 隐藏代码块后,保留 40 的代码块高度
addExpand(hiddenHeight = 40) {
let modes = document.getElementsByClassName("line-numbers-mode");
// 遍历出每一个代码块
Array.from(modes).forEach((item) => {
// 首先获取 expand 元素
let expand = item.getElementsByClassName("expand")[0];
// expand 元素不存在,则进入 if 创建
if (!expand) {
// 获取代码块原来的高度,进行备份
let modeHeight = item.offsetHeight;
// display:none 的代码块需要额外处理,图文卡片列表本质是代码块,所以排除掉
if (
modeHeight == 0 &&
item.parentNode.className != "cardImgListContainer"
) {
modeHeight = this.getHiddenElementHight(item);
}
// modeHeight 比主题多 12,所以减掉,并显示赋值,触发动画过渡效果
modeHeight -= 12;
item.style.height = modeHeight + "px";
// 获取代码块的各个元素
let pre = item.getElementsByTagName("pre")[0];
let wrapper = item.getElementsByClassName("line-numbers-wrapper")[0];
// 创建箭头元素
const div = document.createElement("div");
div.className = "expand icon-xiangxiajiantou iconfont";
// 箭头点击事件
div.onclick = () => {
// 代码块已经被隐藏,则进入 if 循环,如果没有被隐藏,则进入 else 循环
if (parseInt(item.style.height) == hiddenHeight) {
div.className = "expand icon-xiangxiajiantou iconfont";
item.style.height = modeHeight + "px";
setTimeout(() => {
pre.style.display = "block";
wrapper.style.display = "block";
}, 80);
} else {
div.className = "expand icon-xiangxiajiantou iconfont closed";
item.style.height = hiddenHeight + "px";
setTimeout(() => {
pre.style.display = "none";
wrapper.style.display = "none";
}, 300);
}
};
item.append(div);
item.append(this.addCircle());
}
// 解决某些代码块的语言不显示在页面上
this.getLanguage(item);
// 移动一键复制图标到正确的位置
let flag = false;
let interval = setInterval(() => {
flag = this.moveCopyBlock(item);
if (flag) {
clearInterval(interval);
}
}, 1000);
});
},
getHiddenElementHight(hiddenElement) {
let modeHeight;
if (
hiddenElement.parentNode.style.display == "none" ||
hiddenElement.parentNode.className !=
"theme-code-block theme-code-block__active"
) {
hiddenElement.parentNode.style.display = "block";
modeHeight = hiddenElement.offsetHeight;
hiddenElement.parentNode.style.display = "none";
// 清除 vuepress 自带的 deetails 多选代码块
if (
hiddenElement.parentNode.className == "theme-code-block" ||
hiddenElement.parentNode.className == "cardListContainer"
) {
hiddenElement.parentNode.style.display = "";
}
}
return modeHeight;
},
// 添加三个圆圈
addCircle() {
let div = document.createElement("div");
div.className = "circle";
return div;
},
// 移动一键复制图标
moveCopyBlock(element) {
let copyElement = element.getElementsByClassName("code-copy")[0];
if (copyElement && copyElement.parentNode != element) {
copyElement.parentNode.parentNode.insertBefore(
copyElement,
copyElement.parentNode
);
return true;
} else {
return false;
}
},
// 解决某些代码块的语言不显示在页面上
getLanguage(element) {
// 动态获取 before 的 content 属性
let content = getComputedStyle(element, ":before").getPropertyValue(
"content"
);
// "" 的长度是 2,不是 0,"x" 的长度是 3
if (content.length == 2 || content == "" || content == "none") {
let language = element.className.substring(
"language".length + 1,
element.className.indexOf(" ")
);
element.setAttribute("data-language", language);
}
},
},
};
</script>
<style>
/* 代码块元素 */
.line-numbers-mode {
overflow: hidden;
transition: height 0.3s;
margin-top: 0.85rem;
}
.line-numbers-mode::before {
content: attr(data-language);
}
/* 箭头元素 */
.expand {
width: 16px;
height: 16px;
cursor: pointer;
position: absolute;
z-index: 3;
top: 0.8em;
right: 0.5em;
color: rgba(238, 255, 255, 0.8);
font-weight: 900;
transition: transform 0.3s;
}
/* 代码块内容 */
div[class*="language-"].line-numbers-mode pre {
margin: 30px 0 0.85rem 0;
}
/* 代码块的行数 */
div[class*="language-"].line-numbers-mode .line-numbers-wrapper,
.highlight-lines {
margin-top: 30px;
}
/* 箭头关闭后旋转 -90 度 */
.closed {
transform: rotate(90deg) translateY(-3px);
transition: all 0.3s;
}
li .closed {
transform: rotate(90deg) translate(5px, -8px);
}
/* 代码块的语言 */
div[class*="language-"]::before {
position: absolute;
z-index: 3;
top: 0.3em;
left: 4.7rem;
font-size: 1.15em;
color: rgba(238, 255, 255, 0.8);
text-transform: uppercase;
font-weight: bold;
width: fit-content;
}
/* li 下的代码块的语言和 li 下的箭头 */
li div[class*="language-"]::before,
li .expand {
margin-top: -4px;
}
/* 代码块行数的线条 */
div[class*="language-"].line-numbers-mode::after {
margin-top: 35px;
}
/* 代码块的三个圆圈颜色 */
.circle {
position: absolute;
top: 0.8em;
left: 0.9rem;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fc625d;
-webkit-box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b;
box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b;
}
/* 代码块一键复制图标 */
.code-copy {
position: absolute;
top: 0.8rem;
right: 2rem;
fill: rgba(238, 255, 255, 0.8);
opacity: 1;
}
.code-copy svg {
margin: 0;
}
/* 如果你浅色模式的代码块背景色是浅灰色,则取消下面的注释使代码生效,如果是黑色,则注释下面的三段代码(我注释了,因为是黑色背景) */
/* .theme-mode-light .expand {
color: #666;
}
.theme-mode-light div[class*="language-"]::before {
color: #666;
}
.theme-mode-light .code-copy {
fill: #666;
} */
</style>
第 7 行和第 14 行的参数 40 是隐藏代码块后,保留的代码块高度,40 是默认值。
注意
- 如果浅色模式的代码块背景色是浅灰色,则取消 226 - 234 的注释使代码生效(模板已经取消注释)
- 如果是黑色,则注释 226 - 234 的代码(我自己的注释了,因为我的代码块是黑色背景)
- 如果不喜欢代码块的语言变成大写,则注释 188 行的
text-transform: uppercase;
如果你想要你的代码块和我一样是 黑色,则打开 docs/.vuepress/styles/palette.styl
文件,替换掉原来的浅色模式:
.theme-mode-light
--bodyBg: #f4f4f4
--mainBg: rgba(255,255,255,1)
--sidebarBg: rgba(255,255,255,.8)
--blurBg: rgba(255,255,255,.9)
--customBlockBg: rgba(255,255,255,.9)
--textColor: #00323c
--textLightenColor: #0085AD
--borderColor: rgba(0,0,0,.15)
// 代码块浅色主题
//--codeBg: #f6f8fa
//--codeColor: #24292e
//codeThemeLight()
// 行高亮颜色,和代码块浅色主题一起使用,一起注释
//div[class*="language-"]
// .highlight-lines
// .highlighted
// background-color rgba(200,200,200,.4)
// &.line-numbers-mode
// .highlight-lines .highlighted
// &:before
// background-color rgba(200,200,200,.4)
// 代码块深色主题
--codeBg: #282C34
--codeColor: #D4D4D4
codeThemeDark()
// 行高亮颜色,和代码块深色主题一起使用,一起注释
div[class*="language-"]
.highlight-lines
.highlighted
background-color rgba(0,0,0,.66)
&.line-numbers-mode
.highlight-lines .highlighted
&:before
background-color rgba(0,0,0,.66)
div[class*="language-"].line-numbers-mode::after // 代码块的行数和内容分割线颜色
border-right 1px solid rgba(0, 0, 0, 0.66)
如果你喜欢加粗的 绿色、`` 包裹的 英文高亮 abcd
、 包裹的 文字高亮、深色模式的颜色(点击右下角的衣服图标,切换深色模式)等等,那么可以参考我的自定义样式模块,左侧的关于本站目录下就能找到。
自己本次代码:(设置了黑色背景)
<template></template>
<script>
export default {
mounted() {
setTimeout(() => {
this.addExpand(40);
}, 1000);
},
watch: {
$route(to, from) {
if (to.path != from.path || this.$route.hash == "") {
setTimeout(() => {
this.addExpand(40);
}, 1000);
}
},
},
methods: {
// 隐藏代码块后,保留 40 的代码块高度
addExpand(hiddenHeight = 40) {
let modes = document.getElementsByClassName("line-numbers-mode");
// 遍历出每一个代码块
Array.from(modes).forEach((item) => {
// 首先获取 expand 元素
let expand = item.getElementsByClassName("expand")[0];
// expand 元素不存在,则进入 if 创建
if (!expand) {
// 获取代码块原来的高度,进行备份
let modeHeight = item.offsetHeight;
// display:none 的代码块需要额外处理,图文卡片列表本质是代码块,所以排除掉
if (
modeHeight == 0 &&
item.parentNode.className != "cardImgListContainer"
) {
modeHeight = this.getHiddenElementHight(item);
}
// modeHeight 比主题多 12,所以减掉,并显示赋值,触发动画过渡效果
modeHeight -= 12;
item.style.height = modeHeight + "px";
// 获取代码块的各个元素
let pre = item.getElementsByTagName("pre")[0];
let wrapper = item.getElementsByClassName("line-numbers-wrapper")[0];
// 创建箭头元素
const div = document.createElement("div");
div.className = "expand icon-xiangxiajiantou iconfont";
// 箭头点击事件
div.onclick = () => {
// 代码块已经被隐藏,则进入 if 循环,如果没有被隐藏,则进入 else 循环
if (parseInt(item.style.height) == hiddenHeight) {
div.className = "expand icon-xiangxiajiantou iconfont";
item.style.height = modeHeight + "px";
setTimeout(() => {
pre.style.display = "block";
wrapper.style.display = "block";
}, 80);
} else {
div.className = "expand icon-xiangxiajiantou iconfont closed";
item.style.height = hiddenHeight + "px";
setTimeout(() => {
pre.style.display = "none";
wrapper.style.display = "none";
}, 300);
}
};
item.append(div);
item.append(this.addCircle());
}
// 解决某些代码块的语言不显示在页面上
this.getLanguage(item);
// 移动一键复制图标到正确的位置
let flag = false;
let interval = setInterval(() => {
flag = this.moveCopyBlock(item);
if (flag) {
clearInterval(interval);
}
}, 1000);
});
},
getHiddenElementHight(hiddenElement) {
let modeHeight;
if (
hiddenElement.parentNode.style.display == "none" ||
hiddenElement.parentNode.className !=
"theme-code-block theme-code-block__active"
) {
hiddenElement.parentNode.style.display = "block";
modeHeight = hiddenElement.offsetHeight;
hiddenElement.parentNode.style.display = "none";
// 清除 vuepress 自带的 deetails 多选代码块
if (
hiddenElement.parentNode.className == "theme-code-block" ||
hiddenElement.parentNode.className == "cardListContainer"
) {
hiddenElement.parentNode.style.display = "";
}
}
return modeHeight;
},
// 添加三个圆圈
addCircle() {
let div = document.createElement("div");
div.className = "circle";
return div;
},
// 移动一键复制图标
moveCopyBlock(element) {
let copyElement = element.getElementsByClassName("code-copy")[0];
if (copyElement && copyElement.parentNode != element) {
copyElement.parentNode.parentNode.insertBefore(
copyElement,
copyElement.parentNode
);
return true;
} else {
return false;
}
},
// 解决某些代码块的语言不显示在页面上
getLanguage(element) {
// 动态获取 before 的 content 属性
let content = getComputedStyle(element, ":before").getPropertyValue(
"content"
);
// "" 的长度是 2,不是 0,"x" 的长度是 3
if (content.length == 2 || content == "" || content == "none") {
let language = element.className.substring(
"language".length + 1,
element.className.indexOf(" ")
);
element.setAttribute("data-language", language);
}
},
},
};
</script>
<style>
/* 代码块元素 */
.line-numbers-mode {
overflow: hidden;
transition: height 0.3s;
margin-top: 0.85rem;
}
.line-numbers-mode::before {
content: attr(data-language);
}
/* 箭头元素 */
.expand {
width: 16px;
height: 16px;
cursor: pointer;
position: absolute;
z-index: 3;
top: 0.8em;
right: 0.5em;
color: rgba(238, 255, 255, 0.8);
font-weight: 900;
transition: transform 0.3s;
}
/* 代码块内容 */
div[class*="language-"].line-numbers-mode pre {
margin: 30px 0 0.85rem 0;
}
/* 代码块的行数 */
div[class*="language-"].line-numbers-mode .line-numbers-wrapper,
.highlight-lines {
margin-top: 30px;
}
/* 箭头关闭后旋转 -90 度 */
.closed {
transform: rotate(90deg) translateY(-3px);
transition: all 0.3s;
}
li .closed {
transform: rotate(90deg) translate(5px, -8px);
}
/* 代码块的语言 */
div[class*="language-"]::before {
position: absolute;
z-index: 3;
top: 0.3em;
left: 4.7rem;
font-size: 1.15em;
color: rgba(238, 255, 255, 0.8);
text-transform: uppercase;
font-weight: bold;
width: fit-content;
}
/* li 下的代码块的语言和 li 下的箭头 */
li div[class*="language-"]::before,
li .expand {
margin-top: -4px;
}
/* 代码块行数的线条 */
div[class*="language-"].line-numbers-mode::after {
margin-top: 35px;
}
/* 代码块的三个圆圈颜色 */
.circle {
position: absolute;
top: 0.8em;
left: 0.9rem;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fc625d;
-webkit-box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b;
box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b;
}
/* 代码块一键复制图标 */
.code-copy {
position: absolute;
top: 0.8rem;
right: 2rem;
fill: rgba(238, 255, 255, 0.8);
opacity: 1;
}
.code-copy svg {
margin: 0;
}
/* 如果你浅色模式的代码块背景色是浅灰色,则取消下面的注释使代码生效,如果是黑色,则注释下面的三段代码(我注释了,因为是黑色背景) */
.theme-mode-light .expand {
color: #666;
}
.theme-mode-light div[class*="language-"]::before {
color: #666;
}
.theme-mode-light .code-copy {
fill: #666;
}
</style>
3、注册Vue组件
在 docs/.vuepress/config.js(新版是 config.ts)的 plugins 中添加插件配置。
添加如下内容:
js
module.exports = {
plugins: [
{
name: 'custom-plugins',
globalUIComponents: ["BlockToggle"] // 2.x 版本 globalUIComponents 改名为 clientAppRootComponentFiles
}
],
}
ts
import { UserPlugins } from 'vuepress/config'
plugins: <UserPlugins>[
[
{
name: 'custom-plugins',
globalUIComponents: ["BlockToggle"] // 2.x 版本 globalUIComponents 改名为 clientAppRootComponentFiles
}
]
]
效果
ok 了,nice😜
注意
vuepress-plugin-one-click-copy
插件在移动端(手机端)失效,因为其自带的隐藏效果原因,这并不是本模块引起,而是本身插件的设计问题,所以如果觉得移动端也想要支持一键复制,请更换其他插件,并自行修改源码进行适配- 低分辨率的电脑,会导致代码的行数与代码不对应(代码行数溢出),这并非本模块原因,而是 VuePress 代码块本身的原因,可能新版本会修复
结束语
如果你正在热编译 markdown 的代码块,它不会立马生效,你只需要刷新下就能看到效果,而打包后,效果是会生效,无需担心。
如果你还有疑惑,可以去我的 GitHub 仓库或者 Gitee 仓库查看源码。
如果你有更好的方式,评论区留言告诉我,或者加入 Vdoing 主题的 QQ 群:694387113。谢谢!