Hexo Light主題新增程式碼複製功能

注意

本文由 Claude Sonnet 4.5 語言模型產生。

適用於使用 highlight.js 的 hexo-theme-light 主題。

本教學使用 CSS ::before 顯示 Font Awesome 圖示,無需載入完整的 Font Awesome CSS。

已針對 Firefox、Brave、Safari 進行跨瀏覽器測試與優化。

前置準備

確認你的主題符合以下條件:

  • ✅ 使用 highlight.js 進行程式碼語法高亮。
  • ✅ 主題內建 Font Awesome 字體檔案(在 source/css/font/ 目錄)。

步驟 1:下載 clipboard.js

下載 clipboard.js 並放到主題的 js 資料夾:

  1. 前往 https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js
  2. 右鍵另存新檔,存為 clipboard.min.js
  3. 將檔案放到 themes/hexo-theme-light/source/js/ 目錄。

步驟 2:建立 clipboard.use.js

themes/hexo-theme-light/source/js/ 目錄下建立 clipboard.use.js 檔案,內容如下:

// 添加複製按鈕的函數
function addCopyButtons() {
  const codeBlocks = document.querySelectorAll('pre code');

  codeBlocks.forEach(function(code) {
    const pre = code.parentElement;

    // 確保父元素真的是 pre
    if (!pre || pre.tagName.toLowerCase() !== 'pre') return;

    // 如果已經有複製按鈕就跳過
    if (pre.querySelector('.copy-btn')) return;

    // 確保 pre 有相對定位
    const prePosition = window.getComputedStyle(pre).position;
    if (prePosition !== 'relative' && prePosition !== 'absolute' && prePosition !== 'fixed') {
      pre.style.position = 'relative';
    }

    // 建立複製按鈕(不需要放 HTML 內容,用 CSS ::before 顯示圖示)
    const btn = document.createElement('button');
    btn.className = 'copy-btn';
    btn.type = 'button';
    btn.title = '複製程式碼';

    // 插入到 pre 的最前面
    pre.insertBefore(btn, pre.firstChild);
  });
}

// 初始化 Clipboard
function initClipboard() {
  // 確保 ClipboardJS 有成功載入
  if (typeof ClipboardJS === 'undefined') return;

  const clipboard = new ClipboardJS('.copy-btn', {
    target: function(trigger) {
      return trigger.parentElement.querySelector('code');
    }
  });

  clipboard.on('success', function(e) {
    e.clearSelection();
    const btn = e.trigger;

    // 只需要加上 class,CSS 會自動切換圖示
    btn.classList.add('copy-success');

    setTimeout(function() {
      btn.classList.remove('copy-success');
    }, 2000);
  });

  clipboard.on('error', function(e) {
    const btn = e.trigger;

    // 只需要加上 class,CSS 會自動切換圖示
    btn.classList.add('copy-error');

    setTimeout(function() {
      btn.classList.remove('copy-error');
    }, 2000);
  });
}

// 當頁面載入完成後執行
document.addEventListener('DOMContentLoaded', function() {
  addCopyButtons();

  if (document.querySelectorAll('pre code').length === 0) {
    setTimeout(addCopyButtons, 200);
  }

  initClipboard();
});

window.addEventListener('load', function() {
  addCopyButtons();
});

編輯 themes/hexo-theme-light/layout/_partial/after_footer.ejs,調整 JS 載入順序:

<%- js('js/jquery-3.4.1.min.js') %>
<%- js('js/jquery.imagesloaded.min.js') %>
<%- js('js/gallery.js') %>
<%- js('js/highlight.min.js') %>
<script>hljs.highlightAll();</script>
<%- js('js/clipboard.min.js') %>
<%- js('js/clipboard.use.js') %>
<% if (theme.fancybox){ %>
<%- css('fancybox/jquery.fancybox.min.css') %>
<%- js('fancybox/jquery.fancybox.min.js') %>
<script>
(function($){
  $('.fancybox').fancybox();
})(jQuery);
</script>
<% } %>

重點:確保 highlight.min.jshljs.highlightAll()clipboard.min.jsclipboard.use.js 之前載入。

步驟 4:建立 CSS 樣式檔案

4.1 建立自定義樣式資料夾

themes/hexo-theme-light/source/css/ 目錄下建立 _custom 資料夾:

source/css/_custom/

4.2 建立 copy-code.styl

_custom 資料夾中建立 copy-code.styl 檔案,內容如下:

// 程式碼區塊複製按鈕
// 特性:平時隱藏、懸停顯示、Flexbox 置中、Safari 微調修正
// 已測試:Firefox、Brave、Safari
article
  pre
    position: relative

    // 觸發顯示機制:滑鼠移到程式碼區塊時按鈕才出現
    &:hover .copy-btn
      opacity: 1

    .copy-btn
      position: absolute
      top: 5px
      right: 5px

      // 核心佈局:使用 Flexbox 處理水平垂直置中
      display: flex
      justify-content: center
      align-items: center
      padding: 0

      // 外觀設定
      width: 24px
      height: 24px
      background: rgba(255, 255, 255, 1)
      border: 1px solid #ddd
      border-radius: 3px
      cursor: pointer
      font-size: 12px
      color: #666
      z-index: 10
      line-height: normal
      outline: none
      font-family: FontAwesome

      // 動畫效果
      opacity: 0
      transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease, transform 0.1s ease

      // 圖示設定與 Safari 微調
      &:before
        content: '\f0ea'  // fa-clipboard 圖示
        display: inline-block
        transform: translateY(1px)  // 向下微調 1px 修正 Safari 偏高問題

      // 懸停狀態
      &:hover
        background: rgba(255, 255, 255, 1)
        color: #333
        border-color: #999

      // 點擊狀態 (注意:必須同時保留 scale 和 translateY)
      &:active
        transform: scale(0.95) translateY(1px)

      // 複製成功 (強制顯示,黑色打勾)
      &.copy-success
        color: #333
        opacity: 1

        &:before
          content: '\f00c'  // fa-check 圖示
          // 自動繼承 translateY(1px),不需重寫

      // 複製失敗 (強制顯示,紅色叉叉)
      &.copy-error
        color: #f44336
        opacity: 1

        &:before
          content: '\f00d'  // fa-times 圖示

4.3 引入自定義樣式

編輯 themes/hexo-theme-light/source/css/style.styl,在檔案最後加入:

// 自定義樣式
@import '_custom/copy-code'

步驟 5:重新生成部落格

hexo clean
hexo g
hexo s

測試功能

  1. 開啟瀏覽器訪問你的部落格。
  2. 找到任何包含程式碼區塊的文章。
  3. 將滑鼠移到程式碼區塊上,複製按鈕應該會淡入顯示。
  4. 點擊按鈕,應該會顯示黑色打勾圖示並成功複製程式碼。
  5. 貼到文字編輯器驗證內容是否正確。
  6. 滑鼠移開程式碼區塊,按鈕應該會淡出消失。

效果如下:
複製程式碼

已複製程式碼

測試複製失敗情境

開啟瀏覽器開發者工具,在 Console 輸入:

(function() {
  // 選取第一個 pre 和第一個 copy-btn
  const firstPre = document.querySelector('pre');
  const firstBtn = document.querySelector('.copy-btn');

  if (!firstPre || !firstBtn) {
    console.warn('頁面上找不到程式碼區塊或複製按鈕喔!');
    return;
  }

  // 滑鼠移到程式碼區塊(觸發 hover)
  firstPre.dispatchEvent(new MouseEvent('mouseenter'));

  // 點擊按鈕
  firstBtn.click();

  // 手動觸發失敗狀態
  setTimeout(function() {
    firstBtn.classList.remove('copy-success');
    firstBtn.classList.add('copy-error');
  }, 100);

  // 2秒後恢復
  setTimeout(function() {
    firstBtn.classList.remove('copy-error');
    firstPre.dispatchEvent(new MouseEvent('mouseleave'));
  }, 2000);
})();

模擬複製失敗流程(顯示紅色叉叉)。

自訂選項

更換圖示

修改 copy-code.styl 中的 Unicode:

.copy-btn
  &:before
    content: '\f0c5'  // 改用 fa-copy 圖示

調整按鈕大小

.copy-btn
  width: 28px      // 調整寬度
  height: 28px     // 調整高度
  font-size: 14px  // 調整圖示大小

調整按鈕位置

.copy-btn
  top: 8px         // 距離頂部
  right: 8px       // 距離右邊

調整淡入淡出速度

.copy-btn
  transition: opacity 0.3s ease, ...  // 0.2s 改成 0.3s(更慢)

調整提示文字

clipboard.use.js 中修改按鈕建立時的 title 屬性:

const btn = document.createElement('button');
btn.className = 'copy-btn';
btn.type = 'button';
btn.title = '點擊複製';

調整成功/失敗顯示時間

clipboard.use.js 中修改 setTimeout 的延遲時間:

// 成功時
setTimeout(function() {
  btn.classList.remove('copy-success');
}, 3000); // 這裡改 3000

// 失敗時
setTimeout(function() {
  btn.classList.remove('copy-error');
}, 3000); // 這裡改 3000

永久顯示按鈕(不要懸停效果)

移除 opacity: 0 和懸停觸發:

.copy-btn
  // opacity: 0  ← 註解或刪除這行

// 也要註解或刪除這段
// &:hover .copy-btn
//   opacity: 1

調整按鈕顏色(深色模式)

.copy-btn
  background: rgba(0, 0, 0, 0.7)   // 深色背景
  color: #fff                       // 白色圖示
  border-color: rgba(255, 255, 255, 0.3)

檔案結構總覽

完成後的檔案結構:

themes/hexo-theme-light/
├── source/
│   ├── css/
│   │   ├── _custom/
│   │   │   └── copy-code.styl     ← 新建
│   │   ├── font/
│   │   │   ├── fontawesome-webfont.eot  ← 主題內建
│   │   │   ├── fontawesome-webfont.svg  ← 主題內建
│   │   │   ├── fontawesome-webfont.ttf  ← 主題內建
│   │   │   └── fontawesome-webfont.woff ← 主題內建
│   │   └── style.styl              ← 修改(加入 import)
│   └── js/
│       ├── clipboard.min.js        ← 新建(下載)
│       └── clipboard.use.js        ← 新建
└── layout/
    └── _partial/
        └── after_footer.ejs        ← 修改(調整載入順序)

參考資源