代码块隐藏模块
代码块隐藏模块
笔记
一个代码块的代码太多,会占据大量的篇幅,如果能选择性隐藏,页面也许更加好看。
版权声明
:::warning
本着开源共享、共同学习的精神:
本文是在 博主《youngkbt》 文章:《本站 - 代码块隐藏模块》https://notes.youngkbt.cn/about/website/code-block-hidden/基础上增加了一些自己的实际操作记录和修改,内容依旧属于原作者《youngkbt》 所有。转载无需和我联系,但请注明文章来源。如果侵权之处,请联系博主进行删除,谢谢~(这里万分感谢原作者的优质文章😜,感谢开源,拥抱开源💖)
:::

本人测试环境
2024年12月26日测试
2024年12月23日从官方拉取的项目:
基于官方https://github.com/xugaoyi/vuepress-theme-vdoing搭建的仓库。


前言
目前适用版本是 Vdoing v1.x。
代码块可以隐藏,也可以展开,这和 ::: details 类似,下面是简单的代码块 Demo:
1public class Hello {
2 public static void main(String[] args) {
3 System.out.println("Hello,World");
4 }
5}

看到代码块右边的箭头了吗,点击即可隐藏代码块,再次点击则会展开代码块。
本内容实现并不难,只需三步:
- 添加箭头图标
- 编写代码块模块的 Vue 组件
- 全局注册 Vue 组件
实现内容:
代码块的隐藏和显示
美化代码块的 UI,趋向于 Mac
优化代码块语言的显示,因为默认主题的一些语言如 stylus 是不会显示出来。本内容的优化无论代码块语言是什么(如
abc),都会显示出来,如下1我的语言不是 Java、PHP、JS、SH,而是 abdedfg
前提 1
本内容重新实现的一键复制功能是基于 vuepress-plugin-one-click-copy 插件(箭头左边),该插件已经内置 vuepress-theme-vdoing 主题,所以无需担心,如果你曾经卸载了该插件,则需要安装回来;如果已经安装,则无需看这一步:
1yarn 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 里开启行号:
1export default defineConfig4CustomTheme({
2 theme: "vdoing", // 使用 npm 包主题
3 // ...
4 markdown: {
5 lineNumbers: true, // 显示代码块的行号
6 extractHeaders: ["h2", "h3", "h4"], // 支持 h2、h3、h4 标题
7 },
8 // ...
9});
1、添加箭头图标
图标库来自阿里云:https://www.iconfont.cn/。
如果你没有账号,或者觉得添加比较麻烦,就使用我的图标库地址,当你发现图标失效了,就请来这里获取新的地址,如果还没有更新,请在评论区留言。
当然,建议你使用自己的图标库,比较稳定。就像注册一个购物账户,然后添加到购物车即可。
在 docs/.vuepress/config.js(新版是 config.ts)的 head 模块里添加如下内容:
1['link', { rel: 'stylesheet', href: '//at.alicdn.com/t/font_3114978_qe0b39no76.css' }]
2、添加Vue组件
在 docs/.vuepress/components 目录下创建 Vue 组件:BlockToggle.vue。如果不存在 components 目录,则请创建。
添加如下内容:
1<template></template>
2
3<script>
4export default {
5 mounted() {
6 setTimeout(() => {
7 this.addExpand(40);
8 }, 1000);
9 },
10 watch: {
11 $route(to, from) {
12 if (to.path != from.path || this.$route.hash == "") {
13 setTimeout(() => {
14 this.addExpand(40);
15 }, 1000);
16 }
17 },
18 },
19 methods: {
20 // 隐藏代码块后,保留 40 的代码块高度
21 addExpand(hiddenHeight = 40) {
22 let modes = document.getElementsByClassName("line-numbers-mode");
23 // 遍历出每一个代码块
24 Array.from(modes).forEach((item) => {
25 // 首先获取 expand 元素
26 let expand = item.getElementsByClassName("expand")[0];
27 // expand 元素不存在,则进入 if 创建
28 if (!expand) {
29 // 获取代码块原来的高度,进行备份
30 let modeHeight = item.offsetHeight;
31 // display:none 的代码块需要额外处理,图文卡片列表本质是代码块,所以排除掉
32 if (
33 modeHeight == 0 &&
34 item.parentNode.className != "cardImgListContainer"
35 ) {
36 modeHeight = this.getHiddenElementHight(item);
37 }
38 // modeHeight 比主题多 12,所以减掉,并显示赋值,触发动画过渡效果
39 modeHeight -= 12;
40 item.style.height = modeHeight + "px";
41 // 获取代码块的各个元素
42 let pre = item.getElementsByTagName("pre")[0];
43 let wrapper = item.getElementsByClassName("line-numbers-wrapper")[0];
44 // 创建箭头元素
45 const div = document.createElement("div");
46 div.className = "expand icon-xiangxiajiantou iconfont";
47 // 箭头点击事件
48 div.onclick = () => {
49 // 代码块已经被隐藏,则进入 if 循环,如果没有被隐藏,则进入 else 循环
50 if (parseInt(item.style.height) == hiddenHeight) {
51 div.className = "expand icon-xiangxiajiantou iconfont";
52 item.style.height = modeHeight + "px";
53 setTimeout(() => {
54 pre.style.display = "block";
55 wrapper.style.display = "block";
56 }, 80);
57 } else {
58 div.className = "expand icon-xiangxiajiantou iconfont closed";
59 item.style.height = hiddenHeight + "px";
60 setTimeout(() => {
61 pre.style.display = "none";
62 wrapper.style.display = "none";
63 }, 300);
64 }
65 };
66 item.append(div);
67 item.append(this.addCircle());
68 }
69 // 解决某些代码块的语言不显示在页面上
70 this.getLanguage(item);
71 // 移动一键复制图标到正确的位置
72 let flag = false;
73 let interval = setInterval(() => {
74 flag = this.moveCopyBlock(item);
75 if (flag) {
76 clearInterval(interval);
77 }
78 }, 1000);
79 });
80 },
81 getHiddenElementHight(hiddenElement) {
82 let modeHeight;
83 if (
84 hiddenElement.parentNode.style.display == "none" ||
85 hiddenElement.parentNode.className !=
86 "theme-code-block theme-code-block__active"
87 ) {
88 hiddenElement.parentNode.style.display = "block";
89 modeHeight = hiddenElement.offsetHeight;
90 hiddenElement.parentNode.style.display = "none";
91 // 清除 vuepress 自带的 deetails 多选代码块
92 if (
93 hiddenElement.parentNode.className == "theme-code-block" ||
94 hiddenElement.parentNode.className == "cardListContainer"
95 ) {
96 hiddenElement.parentNode.style.display = "";
97 }
98 }
99 return modeHeight;
100 },
101 // 添加三个圆圈
102 addCircle() {
103 let div = document.createElement("div");
104 div.className = "circle";
105 return div;
106 },
107 // 移动一键复制图标
108 moveCopyBlock(element) {
109 let copyElement = element.getElementsByClassName("code-copy")[0];
110 if (copyElement && copyElement.parentNode != element) {
111 copyElement.parentNode.parentNode.insertBefore(
112 copyElement,
113 copyElement.parentNode
114 );
115 return true;
116 } else {
117 return false;
118 }
119 },
120 // 解决某些代码块的语言不显示在页面上
121 getLanguage(element) {
122 // 动态获取 before 的 content 属性
123 let content = getComputedStyle(element, ":before").getPropertyValue(
124 "content"
125 );
126 // "" 的长度是 2,不是 0,"x" 的长度是 3
127 if (content.length == 2 || content == "" || content == "none") {
128 let language = element.className.substring(
129 "language".length + 1,
130 element.className.indexOf(" ")
131 );
132 element.setAttribute("data-language", language);
133 }
134 },
135 },
136};
137</script>
138
139<style>
140/* 代码块元素 */
141.line-numbers-mode {
142 overflow: hidden;
143 transition: height 0.3s;
144 margin-top: 0.85rem;
145}
146.line-numbers-mode::before {
147 content: attr(data-language);
148}
149/* 箭头元素 */
150.expand {
151 width: 16px;
152 height: 16px;
153 cursor: pointer;
154 position: absolute;
155 z-index: 3;
156 top: 0.8em;
157 right: 0.5em;
158 color: rgba(238, 255, 255, 0.8);
159 font-weight: 900;
160 transition: transform 0.3s;
161}
162
163/* 代码块内容 */
164div[class*="language-"].line-numbers-mode pre {
165 margin: 30px 0 0.85rem 0;
166}
167/* 代码块的行数 */
168div[class*="language-"].line-numbers-mode .line-numbers-wrapper,
169.highlight-lines {
170 margin-top: 30px;
171}
172/* 箭头关闭后旋转 -90 度 */
173.closed {
174 transform: rotate(90deg) translateY(-3px);
175 transition: all 0.3s;
176}
177li .closed {
178 transform: rotate(90deg) translate(5px, -8px);
179}
180/* 代码块的语言 */
181div[class*="language-"]::before {
182 position: absolute;
183 z-index: 3;
184 top: 0.3em;
185 left: 4.7rem;
186 font-size: 1.15em;
187 color: rgba(238, 255, 255, 0.8);
188 text-transform: uppercase;
189 font-weight: bold;
190 width: fit-content;
191}
192/* li 下的代码块的语言和 li 下的箭头 */
193li div[class*="language-"]::before,
194li .expand {
195 margin-top: -4px;
196}
197/* 代码块行数的线条 */
198div[class*="language-"].line-numbers-mode::after {
199 margin-top: 35px;
200}
201/* 代码块的三个圆圈颜色 */
202.circle {
203 position: absolute;
204 top: 0.8em;
205 left: 0.9rem;
206 width: 12px;
207 height: 12px;
208 border-radius: 50%;
209 background: #fc625d;
210 -webkit-box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b;
211 box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b;
212}
213/* 代码块一键复制图标 */
214.code-copy {
215 position: absolute;
216 top: 0.8rem;
217 right: 2rem;
218 fill: rgba(238, 255, 255, 0.8);
219 opacity: 1;
220}
221.code-copy svg {
222 margin: 0;
223}
224
225/* 如果你浅色模式的代码块背景色是浅灰色,则取消下面的注释使代码生效,如果是黑色,则注释下面的三段代码(我注释了,因为是黑色背景) */
226/* .theme-mode-light .expand {
227 color: #666;
228}
229.theme-mode-light div[class*="language-"]::before {
230 color: #666;
231}
232.theme-mode-light .code-copy {
233 fill: #666;
234} */
235</style>
第 7 行和第 14 行的参数 40 是隐藏代码块后,保留的代码块高度,40 是默认值。
注意
- 如果浅色模式的代码块背景色是浅灰色,则取消 226 - 234 的注释使代码生效(模板已经取消注释)
- 如果是黑色,则注释 226 - 234 的代码(我自己的注释了,因为我的代码块是黑色背景)
- 如果不喜欢代码块的语言变成大写,则注释 188 行的
text-transform: uppercase;
如果你想要你的代码块和我一样是 黑色,则打开 docs/.vuepress/styles/palette.styl 文件,替换掉原来的浅色模式:
1.theme-mode-light
2 --bodyBg: #f4f4f4
3 --mainBg: rgba(255,255,255,1)
4 --sidebarBg: rgba(255,255,255,.8)
5 --blurBg: rgba(255,255,255,.9)
6 --customBlockBg: rgba(255,255,255,.9)
7 --textColor: #00323c
8 --textLightenColor: #0085AD
9 --borderColor: rgba(0,0,0,.15)
10 // 代码块浅色主题
11 //--codeBg: #f6f8fa
12 //--codeColor: #24292e
13 //codeThemeLight()
14 // 行高亮颜色,和代码块浅色主题一起使用,一起注释
15 //div[class*="language-"]
16 // .highlight-lines
17 // .highlighted
18 // background-color rgba(200,200,200,.4)
19 // &.line-numbers-mode
20 // .highlight-lines .highlighted
21 // &:before
22 // background-color rgba(200,200,200,.4)
23 // 代码块深色主题
24 --codeBg: #282C34
25 --codeColor: #D4D4D4
26 codeThemeDark()
27 // 行高亮颜色,和代码块深色主题一起使用,一起注释
28 div[class*="language-"]
29 .highlight-lines
30 .highlighted
31 background-color rgba(0,0,0,.66)
32 &.line-numbers-mode
33 .highlight-lines .highlighted
34 &:before
35 background-color rgba(0,0,0,.66)
36 div[class*="language-"].line-numbers-mode::after // 代码块的行数和内容分割线颜色
37 border-right 1px solid rgba(0, 0, 0, 0.66)
如果你喜欢加粗的 绿色、`` 包裹的 英文高亮 abcd、 包裹的 文字高亮、深色模式的颜色(点击右下角的衣服图标,切换深色模式)等等,那么可以参考我的自定义样式模块,左侧的关于本站目录下就能找到。
自己本次代码:(设置了黑色背景)
1<template></template>
2
3<script>
4export default {
5 mounted() {
6 setTimeout(() => {
7 this.addExpand(40);
8 }, 1000);
9 },
10 watch: {
11 $route(to, from) {
12 if (to.path != from.path || this.$route.hash == "") {
13 setTimeout(() => {
14 this.addExpand(40);
15 }, 1000);
16 }
17 },
18 },
19 methods: {
20 // 隐藏代码块后,保留 40 的代码块高度
21 addExpand(hiddenHeight = 40) {
22 let modes = document.getElementsByClassName("line-numbers-mode");
23 // 遍历出每一个代码块
24 Array.from(modes).forEach((item) => {
25 // 首先获取 expand 元素
26 let expand = item.getElementsByClassName("expand")[0];
27 // expand 元素不存在,则进入 if 创建
28 if (!expand) {
29 // 获取代码块原来的高度,进行备份
30 let modeHeight = item.offsetHeight;
31 // display:none 的代码块需要额外处理,图文卡片列表本质是代码块,所以排除掉
32 if (
33 modeHeight == 0 &&
34 item.parentNode.className != "cardImgListContainer"
35 ) {
36 modeHeight = this.getHiddenElementHight(item);
37 }
38 // modeHeight 比主题多 12,所以减掉,并显示赋值,触发动画过渡效果
39 modeHeight -= 12;
40 item.style.height = modeHeight + "px";
41 // 获取代码块的各个元素
42 let pre = item.getElementsByTagName("pre")[0];
43 let wrapper = item.getElementsByClassName("line-numbers-wrapper")[0];
44 // 创建箭头元素
45 const div = document.createElement("div");
46 div.className = "expand icon-xiangxiajiantou iconfont";
47 // 箭头点击事件
48 div.onclick = () => {
49 // 代码块已经被隐藏,则进入 if 循环,如果没有被隐藏,则进入 else 循环
50 if (parseInt(item.style.height) == hiddenHeight) {
51 div.className = "expand icon-xiangxiajiantou iconfont";
52 item.style.height = modeHeight + "px";
53 setTimeout(() => {
54 pre.style.display = "block";
55 wrapper.style.display = "block";
56 }, 80);
57 } else {
58 div.className = "expand icon-xiangxiajiantou iconfont closed";
59 item.style.height = hiddenHeight + "px";
60 setTimeout(() => {
61 pre.style.display = "none";
62 wrapper.style.display = "none";
63 }, 300);
64 }
65 };
66 item.append(div);
67 item.append(this.addCircle());
68 }
69 // 解决某些代码块的语言不显示在页面上
70 this.getLanguage(item);
71 // 移动一键复制图标到正确的位置
72 let flag = false;
73 let interval = setInterval(() => {
74 flag = this.moveCopyBlock(item);
75 if (flag) {
76 clearInterval(interval);
77 }
78 }, 1000);
79 });
80 },
81 getHiddenElementHight(hiddenElement) {
82 let modeHeight;
83 if (
84 hiddenElement.parentNode.style.display == "none" ||
85 hiddenElement.parentNode.className !=
86 "theme-code-block theme-code-block__active"
87 ) {
88 hiddenElement.parentNode.style.display = "block";
89 modeHeight = hiddenElement.offsetHeight;
90 hiddenElement.parentNode.style.display = "none";
91 // 清除 vuepress 自带的 deetails 多选代码块
92 if (
93 hiddenElement.parentNode.className == "theme-code-block" ||
94 hiddenElement.parentNode.className == "cardListContainer"
95 ) {
96 hiddenElement.parentNode.style.display = "";
97 }
98 }
99 return modeHeight;
100 },
101 // 添加三个圆圈
102 addCircle() {
103 let div = document.createElement("div");
104 div.className = "circle";
105 return div;
106 },
107 // 移动一键复制图标
108 moveCopyBlock(element) {
109 let copyElement = element.getElementsByClassName("code-copy")[0];
110 if (copyElement && copyElement.parentNode != element) {
111 copyElement.parentNode.parentNode.insertBefore(
112 copyElement,
113 copyElement.parentNode
114 );
115 return true;
116 } else {
117 return false;
118 }
119 },
120 // 解决某些代码块的语言不显示在页面上
121 getLanguage(element) {
122 // 动态获取 before 的 content 属性
123 let content = getComputedStyle(element, ":before").getPropertyValue(
124 "content"
125 );
126 // "" 的长度是 2,不是 0,"x" 的长度是 3
127 if (content.length == 2 || content == "" || content == "none") {
128 let language = element.className.substring(
129 "language".length + 1,
130 element.className.indexOf(" ")
131 );
132 element.setAttribute("data-language", language);
133 }
134 },
135 },
136};
137</script>
138
139<style>
140/* 代码块元素 */
141.line-numbers-mode {
142 overflow: hidden;
143 transition: height 0.3s;
144 margin-top: 0.85rem;
145}
146.line-numbers-mode::before {
147 content: attr(data-language);
148}
149/* 箭头元素 */
150.expand {
151 width: 16px;
152 height: 16px;
153 cursor: pointer;
154 position: absolute;
155 z-index: 3;
156 top: 0.8em;
157 right: 0.5em;
158 color: rgba(238, 255, 255, 0.8);
159 font-weight: 900;
160 transition: transform 0.3s;
161}
162
163/* 代码块内容 */
164div[class*="language-"].line-numbers-mode pre {
165 margin: 30px 0 0.85rem 0;
166}
167/* 代码块的行数 */
168div[class*="language-"].line-numbers-mode .line-numbers-wrapper,
169.highlight-lines {
170 margin-top: 30px;
171}
172/* 箭头关闭后旋转 -90 度 */
173.closed {
174 transform: rotate(90deg) translateY(-3px);
175 transition: all 0.3s;
176}
177li .closed {
178 transform: rotate(90deg) translate(5px, -8px);
179}
180/* 代码块的语言 */
181div[class*="language-"]::before {
182 position: absolute;
183 z-index: 3;
184 top: 0.3em;
185 left: 4.7rem;
186 font-size: 1.15em;
187 color: rgba(238, 255, 255, 0.8);
188 text-transform: uppercase;
189 font-weight: bold;
190 width: fit-content;
191}
192/* li 下的代码块的语言和 li 下的箭头 */
193li div[class*="language-"]::before,
194li .expand {
195 margin-top: -4px;
196}
197/* 代码块行数的线条 */
198div[class*="language-"].line-numbers-mode::after {
199 margin-top: 35px;
200}
201/* 代码块的三个圆圈颜色 */
202.circle {
203 position: absolute;
204 top: 0.8em;
205 left: 0.9rem;
206 width: 12px;
207 height: 12px;
208 border-radius: 50%;
209 background: #fc625d;
210 -webkit-box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b;
211 box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b;
212}
213/* 代码块一键复制图标 */
214.code-copy {
215 position: absolute;
216 top: 0.8rem;
217 right: 2rem;
218 fill: rgba(238, 255, 255, 0.8);
219 opacity: 1;
220}
221.code-copy svg {
222 margin: 0;
223}
224
225/* 如果你浅色模式的代码块背景色是浅灰色,则取消下面的注释使代码生效,如果是黑色,则注释下面的三段代码(我注释了,因为是黑色背景) */
226.theme-mode-light .expand {
227 color: #666;
228}
229.theme-mode-light div[class*="language-"]::before {
230 color: #666;
231}
232.theme-mode-light .code-copy {
233 fill: #666;
234}
235</style>
3、注册Vue组件
在 docs/.vuepress/config.js(新版是 config.ts)的 plugins 中添加插件配置。
添加如下内容:
js
1module.exports = {
2 plugins: [
3 {
4 name: 'custom-plugins',
5 globalUIComponents: ["BlockToggle"] // 2.x 版本 globalUIComponents 改名为 clientAppRootComponentFiles
6 }
7 ],
8}
ts
1import { UserPlugins } from 'vuepress/config'
2plugins: <UserPlugins>[
3 [
4 {
5 name: 'custom-plugins',
6 globalUIComponents: ["BlockToggle"] // 2.x 版本 globalUIComponents 改名为 clientAppRootComponentFiles
7 }
8 ]
9]
效果
ok 了,nice😜

注意
vuepress-plugin-one-click-copy插件在移动端(手机端)失效,因为其自带的隐藏效果原因,这并不是本模块引起,而是本身插件的设计问题,所以如果觉得移动端也想要支持一键复制,请更换其他插件,并自行修改源码进行适配- 低分辨率的电脑,会导致代码的行数与代码不对应(代码行数溢出),这并非本模块原因,而是 VuePress 代码块本身的原因,可能新版本会修复
结束语
如果你正在热编译 markdown 的代码块,它不会立马生效,你只需要刷新下就能看到效果,而打包后,效果是会生效,无需担心。
如果你还有疑惑,可以去我的 GitHub 仓库或者 Gitee 仓库查看源码。
如果你有更好的方式,评论区留言告诉我,或者加入 Vdoing 主题的 QQ 群:694387113。谢谢!

