markdown的代码块复制插件

# 图例

针对 vuepress 的架构,开发的这个 vue 插件,主要用来复制代码块内的代码

An image

# 实现思路

  • 写一个 vue 组件 CodeCopy ,这个组件构造了一个 vue 实例,内容是一个 svg 图片,就是 copy 的那张图片,图片注册了点击复制的事件

  • 在页面渲染完成后,通过 className 查到到页面上所有的代码块的 dom,再将代码块内的 text 全部提取出来,然后实例化一个 CodeCopy 组件并将代码块的 text 文本传入,将这个实例化后的组件 dom 插入到代码块 dom 的前面,从而这样的一个点击负责的 icon 就实现了

# 实现代码

点击 展开/收起 jsx 代码
/**
 * CodeCopy 组件
 */
<template>
  <div class="code-copy">
    <svg
      @click="copyToClipboard"
      xmlns="http://www.w3.org/2000/svg"
      width="24"
      height="24"
      viewBox="0 0 24 24"
      :class="iconClass"
      :style="alignStyle"
    >
      <path fill="none" d="M0 0h24v24H0z" />
      <path
        :fill="options.color"
        d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z"
      />
    </svg>
    <span :class="success ? 'success' : ''" :style="alignStyle">
      {{ options.successText }}
    </span>
  </div>
</template>

<script>
export default {
  props: {
    parent: Object,
    code: String,
    options: {
      align: String,
      color: String,
      backgroundTransition: Boolean,
      backgroundColor: String,
      successText: String,
      staticIcon: Boolean,
    },
  },
  data() {
    return {
      success: false,
      originalBackground: null,
      originalTransition: null,
    };
  },
  computed: {
    alignStyle() {
      let style = {};
      style[this.options.align] = '7.5px';
      return style;
    },
    iconClass() {
      return this.options.staticIcon ? '' : 'hover';
    },
  },
  mounted() {
    this.originalTransition = this.parent.style.transition;
    this.originalBackground = this.parent.style.background;
  },
  beforeDestroy() {
    this.parent.style.transition = this.originalTransition;
    this.parent.style.background = this.originalBackground;
  },
  methods: {
    hexToRgb(hex) {
      let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
      return result
        ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16),
          }
        : null;
    },
    copyToClipboard(el) {
      if (!navigator.clipboard) {
        let inputEle = document.getElementById('clipboard');

        if (!inputEle) {
          inputEle = document.createElement('input');
          inputEle.id = 'clipboard';
          document.body.appendChild(inputEle);
        }
        inputEle.setAttribute('value', content);
        inputEle.style.display = 'block';
        if (inputEle && inputEle.select) {
          inputEle.select();
          try {
            document.execCommand('copy');
            this.setSuccessTransitions();
          } catch (err) {
            console.error(err);
          }
        }
        inputEle.style.display = 'none';
      } else {
        navigator.clipboard
          .writeText(this.code)
          .then(() => {
            this.setSuccessTransitions();
          })
          .catch((err) => {});
      }
    },
    setSuccessTransitions() {
      clearTimeout(this.successTimeout);

      if (this.options.backgroundTransition) {
        this.parent.style.transition = 'background 350ms';

        let color = this.hexToRgb(this.options.backgroundColor);
        this.parent.style.background = `rgba(${color.r}, ${color.g}, ${color.b}, 0.1)`;
      }

      this.success = true;
      this.successTimeout = setTimeout(() => {
        if (this.options.backgroundTransition) {
          this.parent.style.background = this.originalBackground;
          this.parent.style.transition = this.originalTransition;
        }
        this.success = false;
      }, 500);
    },
  },
};
</script>

<style scoped>
svg {
  position: absolute;
  right: 7.5px;
  opacity: 0.75;
  cursor: pointer;
}

svg.hover {
  opacity: 0;
}

svg:hover {
  opacity: 1 !important;
}

span {
  position: absolute;
  font-size: 0.85rem;
  line-height: 0.425rem;
  right: 50px;
  opacity: 0;
  transition: opacity 500ms;
}

.success {
  opacity: 1 !important;
}
</style>


/**
 * clientRootMixin
 */
import Vue from 'vue';
import CodeCopy from './CodeCopy.vue';
import './style.css';

export default {
  updated() {
    this.update();
  },
  methods: {
    update() {
      setTimeout(() => {
        document.querySelectorAll(selector).forEach((el) => {
          if (el.classList.contains('code-copy-added')) return;
          let ComponentClass = Vue.extend(CodeCopy);
          let instance = new ComponentClass();

          let options = {
            align: align,
            color: color,
            backgroundTransition: backgroundTransition,
            backgroundColor: backgroundColor,
            successText: successText,
            staticIcon: staticIcon,
          };
          instance.options = { ...options };

          // 获取 el 下的第一个 code 标签
          const codeElement = el.querySelector('code');
          // 如果存在 code 标签,获取其文本内容,否则的话直接获取el下的文本内容
          const textContent = codeElement
            ? codeElement.textContent
            : el.textContent;
          instance.code = textContent;
          instance.parent = el;
          instance.$mount();
          el.classList.add('code-copy-added');
          // el.appendChild(instance.$el);
          el.insertBefore(instance.$el, codeElement);
        });
      }, 100);
    },
  },
};