2023-9-6

ブログのコードブロックをいい感じにする(後編)

自作ブログ

フロントエンド

React

Typescript

前編の続き。 自作rehypeプラグインを作成して、ハイライトされたコードブロックを好きなタグで覆うことができるようになった。 あとはこのコードブロックに動きを付けていく。

可能であればマークダウンを直接Reactに変換して動きをつける、みたいなことが出来れば楽だったが挫折した。 そこで今回は生成したHTMLをラップするReactコンポーネントを作成して、addEventListenerなどを駆使して動きを付けていく。作成するコンポーネントは以下のような感じのもの。

typescript
Copied!
export const Article: React.FC<ArticleProps> = ({ dangerHtml }) => {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    // ここにaddEventListenerなどで動きを付けていく。
  }, []);
  useCodeBlock(ref);
  return <div ref={ref} dangerouslySetInnerHTML={{ __html: dangerHtml }} />;
};

dangerHtmlの中のHTMLエレメントは、ref.currrent.querySelectorなどで取得できる。 またArticleというコンポーネントにdangerHtmlや、生js的なコードを閉じ込めることができる。 useEffectでイベントリスナーを登録していくが、この部分をカスタムフックに閉じ込めることで全体の見通しも良くなる。 懸念事項としてdangerHTMLの中身を書き換えた後に再描画が走ると、書き換えた内容が上書きされる可能性がある。 今回はこれが問題になるような使い方はしないが、必要に応じてuseRefなどで変更内容を保持するようにしたほうがいいかもしれない。

以下実際に作成したコンポーネント。 本当はコピーボタンを押したときにコードブロックをコピーするコールバックも作成していたが、あまりに長くなるので割愛した。 あまり特筆するべきことはなくてquerySelectorで必要な要素を取ってきて、addEventListnerで必要なコールバックを登録するだけ。 addEventListenerを使うので、useEffectの返り値を指定してアンマウントされるときにリスナーを削除する。

typescript
Copied!
"use client";
import { useRef, RefObject, useEffect } from "react";

interface ArticleProps {
  dangerHtml: string;
とってき

export const Article: React.FC<ArticleProps> = ({ dangerHtml }) => {
  const ref = useRef<HTMLDivElement>(null);
  useCodeBlock(ref);
  return <div ref={ref} dangerouslySetInnerHTML={{ __html: dangerHtml }} />;
};

/**
 * CodeBlockに動きを与えるカスタムフック
 *
 * @param ref
 */
const useCodeBlock = (ref: RefObject<HTMLDivElement>) => {
  useEffect(() => {
    if (!ref.current) {
      return;
    }

    // コードブロックにhoverしたときのコールバック
    const codeNodes = ref.current.querySelectorAll(".codeBlock");
    const handleMouseEnter = (e: Event) => {
      const target = e.target as HTMLDivElement;
      const codeBlockRoot = target.closest(".codeBlock");
      if (!codeBlockRoot) {
        return;
      }
      const copyNode = codeBlockRoot.querySelector(".codeBlockHeader-copy");
      if (copyNode) {
        (copyNode as HTMLElement).dataset.hover = "true";
      }
    };
    const handleMouseLeave = (e: Event) => {
      const target = e.target as HTMLDivElement;
      const codeBlockRoot = target.closest(".codeBlock");
      if (!codeBlockRoot) {
        return;
      }
      const copyNode = codeBlockRoot.querySelector(".codeBlockHeader-copy");
      if (copyNode) {
        (copyNode as HTMLElement).dataset.hover = "false";
      }
    };
    codeNodes.forEach((node) => {
      node.addEventListener("mouseenter", handleMouseEnter);
      node.addEventListener("mouseleave", handleMouseLeave);
    });

    // アンマウントされる際にリスナーを削除する。
    return () => {
      codeNodes.forEach((node) => {
        node.removeEventListener("mouseover", handleMouseEnter);
        node.removeEventListener("mouseout", handleMouseLeave);
      });
    };
  }, [ref]);
};

こんな感じで頑張るとMarkdownから動きのあるコードブロックを作成できる。

改修後

おわりに

rehypeのプラグインを使いこなせるようになればOGPを使ってリンクをいい感じに表示したり、とできることが広がりそうだ。 といいつつ結構実装に時間を使って疲れたので、再び手を動かすのはしばらく先になりそう。