Skip to content

代码块隐藏模块

代码块隐藏模块

笔记

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

版权声明

警告

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

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

image-20241226162619920

本人测试环境

2024年12月26日测试

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

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

image-20241223130417258

image-20241226142619421

前言

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

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

java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello,World");
    }
}

PixPin_2024-12-26_16-03-01

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

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

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

实现内容:

  • 代码块的隐藏和显示

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

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

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

    image-20241226160425252

前提 1

本内容重新实现的一键复制功能是基于 vuepress-plugin-one-click-copy 插件(箭头左边),该插件已经内置 vuepress-theme-vdoing 主题,所以无需担心,如果你曾经卸载了该插件,则需要安装回来;如果已经安装,则无需看这一步:

sh
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 里开启行号:

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 模块里添加如下内容:

js
['link', { rel: 'stylesheet', href: '//at.alicdn.com/t/font_3114978_qe0b39no76.css' }]

2、添加Vue组件

在 docs/.vuepress/components 目录下创建 Vue 组件:BlockToggle.vue。如果不存在 components 目录,则请创建。

添加如下内容:

vue
<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 文件,替换掉原来的浅色模式:

stylus
.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 包裹的 文字高亮、深色模式的颜色(点击右下角的衣服图标,切换深色模式)等等,那么可以参考我的自定义样式模块,左侧的关于本站目录下就能找到。


自己本次代码:(设置了黑色背景)

vue
<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

js
module.exports = {
    plugins: [
        {
            name: 'custom-plugins',
            globalUIComponents: ["BlockToggle"] // 2.x 版本 globalUIComponents 改名为 clientAppRootComponentFiles
        }
    ],
}

ts

bash
import { UserPlugins } from 'vuepress/config'
plugins: <UserPlugins>[
    [
    	{
        	name: 'custom-plugins',
        	globalUIComponents: ["BlockToggle"] // 2.x 版本 globalUIComponents 改名为 clientAppRootComponentFiles
    	}
    ]
]

效果

ok 了,nice😜

image-20241226161845406

注意

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

结束语

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

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

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

最近更新