hugo-teek is loading...

代码块隐藏模块

最后更新于:

代码块隐藏模块

笔记

一个代码块的代码太多,会占据大量的篇幅,如果能选择性隐藏,页面也许更加好看。

版权声明

:::warning

本着开源共享、共同学习的精神:

本文是在 博主《youngkbt》 文章:《本站 - 代码块隐藏模块》https://notes.youngkbt.cn/about/website/code-block-hidden/基础上增加了一些自己的实际操作记录和修改,内容依旧属于原作者《youngkbt》 所有。转载无需和我联系,但请注明文章来源。如果侵权之处,请联系博主进行删除,谢谢~(这里万分感谢原作者的优质文章😜,感谢开源,拥抱开源💖)

:::

image-20241226162619920

本人测试环境

2024年12月26日测试

2024年12月23日从官方拉取的项目:

基于官方https://github.com/xugaoyi/vuepress-theme-vdoing搭建的仓库。

image-20241223130417258

image-20241226142619421

前言

目前适用版本是 Vdoing v1.x。

代码块可以隐藏,也可以展开,这和 ::: details 类似,下面是简单的代码块 Demo:

1public class Hello {
2    public static void main(String[] args) {
3        System.out.println("Hello,World");
4    }
5}

PixPin_2024-12-26_16-03-01

看到代码块右边的箭头了吗,点击即可隐藏代码块,再次点击则会展开代码块。

本内容实现并不难,只需三步:

  • 添加箭头图标
  • 编写代码块模块的 Vue 组件
  • 全局注册 Vue 组件

实现内容:

  • 代码块的隐藏和显示

  • 美化代码块的 UI,趋向于 Mac

  • 优化代码块语言的显示,因为默认主题的一些语言如 stylus 是不会显示出来。本内容的优化无论代码块语言是什么(如 abc),都会显示出来,如下

    1我的语言不是 Java、PHP、JS、SH,而是 abdedfg
    

    image-20241226160425252

前提 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😜

image-20241226161845406

注意

  • vuepress-plugin-one-click-copy 插件在移动端(手机端)失效,因为其自带的隐藏效果原因,这并不是本模块引起,而是本身插件的设计问题,所以如果觉得移动端也想要支持一键复制,请更换其他插件,并自行修改源码进行适配
  • 低分辨率的电脑,会导致代码的行数与代码不对应(代码行数溢出),这并非本模块原因,而是 VuePress 代码块本身的原因,可能新版本会修复

结束语

如果你正在热编译 markdown 的代码块,它不会立马生效,你只需要刷新下就能看到效果,而打包后,效果是会生效,无需担心。

如果你还有疑惑,可以去我的 GitHub 仓库或者 Gitee 仓库查看源码。

如果你有更好的方式,评论区留言告诉我,或者加入 Vdoing 主题的 QQ 群:694387113。谢谢!

推荐使用微信支付
微信支付二维码
推荐使用支付宝
支付宝二维码
最新文章

文档导航