star back image
people4
電飾 電飾
moon
astronaut

【WordPress】Highlighting Code Blockの改修(折りたたみ機能の追加)

BLOG AIWEBログWordPressプラグイン
読了約:19分

ソースコードをカラフルに見せるWordPressプラグインがあります。
現在愛用中でもあるそのプラグインの名はHighlighting Code Blockと申します。

このブログのリニューアル時からお世話になっていて、その当時から利用者も大勢いる人気のプラグインです。例に漏れず私も、自然とインストールをしていました。

見やすくなりますし、単純にカッコいいのです。

しかしソースコードがロングになるにつれ、課題が出てきました。
長いコードはスクロールがとても大変だということです。

仕方ないですよね。

前から思っていました。スクロールがしんどいです。

私も思っていました。指が痛いンです。

≡;゚д゚)━ン!!!

折りたたみ機能がないか探したのですがありません。
新しいプラグインを探した方が早いかと思ったのですが、愛着もあります。
これはもう作るしかありません。でも。。

プラグインの改造なんて私にできるのでしょうか。さっぱりわかりません。
でも今はAIがあります。

動作確認環境
WordPressのメジャー更新も複数ある中、もう2年ほどバージョン更新がないのですが、php8.3のWordPress 6.9でも安定動作しております。

【AI】一緒にプラグインの改修をしよう

プラグインの管理画面、[HCB]設定には、cssやjsのカスタムファイルを差し込める設定があったので、それで仕込んでいこうと思いました。

しかしcssとjsをダウンロードしたら圧縮ファイルで非常に読み取りにくい。

見るのが辛いですとAIに伝えました。
すると。。〆(゚ε゚AI). サササ

functions.phpに、別ファイルで読み込ませる設定を書いたらいいです。

圧縮JSを回避/解凍して改修は可能ではあります(デミニファイ/整形ツールで読みやすくはできる)が、HCB更新で上書きされる・差分管理が地獄になりがちなのでおすすめしません。

というのです。なるほどぉ。

themes/functions.php

<?php

〜 ↓ 他の設定の下に追加 ↓ 〜

//Highlighting Code Blockのフォールド機能を有効にする
add_action('wp_enqueue_scripts', function () {
  wp_enqueue_style(
    'hcb-fold',
    get_stylesheet_directory_uri() . '/hcb-fold.css',
    array(),
    filemtime(get_stylesheet_directory() . '/hcb-fold.css')
  );

  wp_enqueue_script(
    'hcb-fold',
    get_stylesheet_directory_uri() . '/hcb-fold.js',
    array(),
    filemtime(get_stylesheet_directory() . '/hcb-fold.js'),
    true
  );
});

?>

functions.phpとは気づきませんでした。
でもまぁ、そうですよね。

とにかくテーマ直下に、自作のファイルを置いて読み込ませるという技です。

【共有】改修の追加ソースコード

以下のように、open / close ボタンを付けたいと相談しました。

ボタンをcode出力エリアの上下に[ open ] [close] のボタンを配置して、

[ open ] [close]
------------------
html code
------------------
[ open ] [close]

min-heightを適用させるなどして、トグルで開閉させたいです。

すると、コード部分が pre なのか/親にどんなclassが付いてるかだけ出力確認してくれというので、ブラウザの検証ツールで見ると。。以下のような構成であることがわかりました。

<div class="hcb_wrap">
<pre class="prism ...">...</pre>
<button class="hcb-clipboard"...></button>
</div>

セレクタは .hcb_wrap > pre.prism が最適(HCBの出力だけ確実に拾える)

要件は以下です。

  • プラグイン本体は改造しない。(JS/CSS を追加して拡張する)
  • 1記事内に複数コードがあっても対応する。
  • 「長いコードだけ」折りたたまれる。(短い時はボタンを出さない)
  • 「下の close にだけ」閉じた時にコード上端へスムーズスクロールする。

すると。。〆(゚ε゚AI). サササ。ファイルを2つ提案してくれました。

themes/hcb-fold.css

.hcb_wrap {
  position: relative;
}

.hcb-folded {
  max-height: 280px;
  overflow: auto;
}

.hcb-unfolded {
  max-height: none;
}

.hcb-fold-controls {
  display: flex;
  gap: 8px;
  margin: 6px 0;
}

.hcb-fold-btn {
  font-size: 12px;
  cursor: pointer;
}

/* ボタンとコードが密着しすぎる時の保険 */
.hcb_wrap > pre.prism {
  margin-top: 0;
}

/* 無効ボタンの見た目 */
.hcb-fold-btn:disabled{
  opacity: .4 !important;
  cursor: not-allowed !important;
  background: #f5f5f5 !important;
  color: #999 !important;
}

/* 有効時に少し強調(任意) */
.hcb-fold-btn:not(:disabled):hover {
  opacity: 0.85;
}

/* 上部ボタンだけ */
.hcb-fold-controls[data-pos="top"] {
  position: absolute;
  top: -50px;
  right: 0;
}

/* 下部ボタンだけ */
.hcb-fold-controls[data-pos="bottom"] {
  justify-content: flex-end;
}

themes/hcb-fold.js

(function () {
  const WRAP_SELECTOR = '.hcb_wrap';
  const PRE_SELECTOR = '.hcb_wrap > pre.prism';
  const COLLAPSE_HEIGHT = 280;
  const EXTRA_MARGIN = 60;

  /* ===== 状態制御(先に定義) ===== */
  function setState(pre, state, fromPos) {
    const wrap = pre.closest(WRAP_SELECTOR);
    if (!wrap) return;

    const isOpen = (state === 'open');

    if (isOpen) {
      pre.classList.remove('hcb-folded');
      pre.classList.add('hcb-unfolded');
    } else {
      pre.classList.add('hcb-folded');
      pre.classList.remove('hcb-unfolded');
      pre.scrollTop = 0;

      // 閉じたらコードブロックの上へ戻す(ページ側のスクロール)
      if (fromPos === 'bottom') {
        const y = wrap.getBoundingClientRect().top + window.scrollY - 100;
        window.scrollTo({ top: Math.max(y, 0), behavior: 'smooth' });
      }
    }

    // ボタン同期
    wrap.querySelectorAll('.hcb-fold-btn[data-role="open"]').forEach((btn) => {
      btn.disabled = isOpen;
    });
    wrap.querySelectorAll('.hcb-fold-btn[data-role="close"]').forEach((btn) => {
      btn.disabled = !isOpen;
    });

    // 状態(機械可読)
    pre.dataset.foldState = isOpen ? 'open' : 'closed';

    // スクリーンリーダー向け状態
    pre.setAttribute('aria-expanded', isOpen ? 'true' : 'false');

    // 表示/非表示ルール:openは上だけ、closeは上下(開いてる時だけ)
    wrap.querySelectorAll('.hcb-fold-controls').forEach((controls) => {
      const pos = controls.dataset.pos; // top / bottom
      const openBtn = controls.querySelector('.hcb-fold-btn[data-role="open"]');
      const closeBtn = controls.querySelector('.hcb-fold-btn[data-role="close"]');

      if (!openBtn || !closeBtn) return;

      if (state === 'open') {
        // 開いてる時:closeを上下に、openは隠す
        openBtn.style.display = 'none';
        closeBtn.style.display = '';
      } else {
        // 閉じてる時:openは上だけ、closeは隠す
        closeBtn.style.display = 'none';
        openBtn.style.display = (pos === 'top') ? '' : 'none';
      }
    });
  }

  /* ===== ボタン生成 ===== */
  function makeControls(pre, pos) {
    const controls = document.createElement('div');
    controls.className = 'hcb-fold-controls';
    controls.dataset.pos = pos; // 追加
  
    const openBtn = document.createElement('button');
    openBtn.type = 'button';
    openBtn.className = 'hcb-fold-btn';
    openBtn.textContent = '[ open ]';
    openBtn.dataset.role = 'open';
  
    const closeBtn = document.createElement('button');
    closeBtn.type = 'button';
    closeBtn.className = 'hcb-fold-btn';
    closeBtn.textContent = '[ close ]';
    closeBtn.dataset.role = 'close';
  
    openBtn.addEventListener('click', () => setState(pre, 'open'));
    closeBtn.addEventListener('click', () => setState(pre, 'close', pos));
  
    controls.appendChild(openBtn);
    controls.appendChild(closeBtn);
    return controls;
  }
  

  /* ===== 初期化 ===== */
  function init() {
    const pres = document.querySelectorAll(PRE_SELECTOR);
    if (!pres.length) return;

    pres.forEach((pre) => {
      const wrap = pre.closest(WRAP_SELECTOR);
      if (!wrap) return;

      if (pre.scrollHeight <= COLLAPSE_HEIGHT + EXTRA_MARGIN) return;
      if (wrap.dataset.hcbFoldInit === '1') return;
      wrap.dataset.hcbFoldInit = '1';

      pre.classList.add('hcb-folded');
      pre.dataset.foldState = 'closed';

      const top = makeControls(pre, 'top');
      const bottom = makeControls(pre, 'bottom');

      wrap.insertBefore(top, wrap.firstChild);

      const clip = wrap.querySelector('.hcb-clipboard');
      if (clip && clip.parentNode === wrap) {
        clip.insertAdjacentElement('afterend', bottom);
      } else {
        wrap.appendChild(bottom);
      }

      setState(pre, 'close');
    });
  }

  window.addEventListener('load', init);
})();

↑ボタンが付きましたね。素晴らしいです!

ボタンを最小限にしたいポイント・落とし所

・閉じている時(初期)
上:[ open ] だけ表示
下:何も表示しない(=無駄を出さない)

・開いている時
上:[ close ] 表示(上に戻ってきた人用)
下:[ close ] 表示(読み終わった人用)

つまり 「open は上だけ」「close は上下」 です。

詳しくはAIに放り込んでください。
HCBプラグインで長期運用している方の役に立てばいいなと思っています。

以上になります。

【AI】イラストを描いてもらった

今回の記事のキャッチ画像で使わせてもらいます「Google ImageFX」で作成した画像です。誰でもgoogleアカウントでログインして使えます。

この記事にピッタリなイラストのための考えたリクエストは、「魔空間。人型の巨大合体ロボットが集まって1体に合体していく瞬間。パーツは乗り物デザイン。腕と胴が繋がる、胴と下半身が繋がる。両方の接合部から電磁アークが迸る。背景には男女のヒーローの真剣な顔が薄く合成されている。ゴールデンレイトで、実写的に。」です。

プラグインを合体と捉えました。

星間旅路のメロディ

「宇宙の静けさに包まれながら、漂流する過去の音楽を捜し求め、銀河の奥底でその旋律に耳を傾ける。」

「この電波はどこの星からきたのだろうか。」

こぶしの効いた波長、どこかで聞いたことがあるような。

人生がズンドコなんですね。