2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MD設計書に記述したMermaid図に拡大縮小ボタンをつける

Posted at

はじめに

  • AI対応のため、設計書などは、マークダウンで書く場合が多くなってきましたが、その時によく使われるのが、mermaidでの図の記述です。
  • mermaidはいろいろな図を簡単に書けていいのですが、画像になったときのサイズがうまく指定できず、設計書に載せると、文字がすごく小さくなってしまう場合がある。
  • また縦横比率が合わずに、伸びたり縮んだり、見づらくなってしまいがちです。
  • そんなときのために、今回は設計書Webサイトを構築した時に、HTMLで出力した場合の、拡大縮小ボタンと、全表示ボタンを作りました。
  • ホイールでの拡大縮小の対応しています。マウスのドラッグでグリグリ動かせます。

作成した環境

  • Github PagesでサイトをMDから構築
  • 標準のjekyllで生成
  • テーマはjust-the-docs
  • MDの中に、mermaidで図を作成した。

コード

_includes\footer_custom.html

<script src="https://unpkg.com/mermaid@11/dist/mermaid.min.js"></script>
<script src="https://unpkg.com/@panzoom/panzoom@4.6.1/dist/panzoom.min.js"></script>

<script>
  mermaid.initialize({
    startOnLoad: false,
    securityLevel: 'loose',
    theme: 'default'
  });
</script>

<style>
  code.language-mermaid {
    display: block;
    overflow: hidden;
    position: relative;
    border-radius: 4px;
    border: 1px solid #aaa;
  }

  .language-mermaid svg {
    display: block;
    border: 1px solid #ddd;
  }

  .panzoom-controls {
    display: flex;
    gap: 8px;
    padding: 8px;
    background: #f5f5f5;
    border-bottom: 1px solid #ddd;
    position: sticky;
    top: 0;
    z-index: 10;
  }

  .panzoom-controls button {
    padding: 6px 12px;
    font-size: 14px;
    cursor: pointer;
    border: 1px solid #ccc;
    border-radius: 4px;
    background: white;
    transition: all 0.2s;
  }

  .panzoom-controls button:hover {
    background: #e9e9e9;
    border-color: #999;
  }

  .mermaid-modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.9);
    z-index: 9999;
    display: flex;
    align-items: center;
    justify-content: center;
    animation: fadeIn 0.2s ease-in;
  }

  @keyframes fadeIn {
    from {
      opacity: 0;
    }

    to {
      opacity: 1;
    }
  }

  .mermaid-modal-content {
    position: relative;
    width: 95vw;
    height: 95vh;
    background: white;
    border-radius: 8px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .mermaid-modal-close {
    position: absolute;
    top: 16px;
    right: 16px;
    width: 40px;
    height: 40px;
    background: rgba(255, 255, 255, 0.9);
    border: none;
    border-radius: 50%;
    font-size: 24px;
    cursor: pointer;
    z-index: 10000;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
    transition: all 0.2s;
  }

  .mermaid-modal-close:hover {
    background: white;
    transform: scale(1.1);
  }

  .mermaid-modal-content svg {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    border: 1px solid #ddd;
  }
</style>

<script>
  const initZoomControls = () => {
    document.querySelectorAll('.language-mermaid svg').forEach(svg => {
      if (svg.dataset.zoomInitialized) return;
      svg.dataset.zoomInitialized = true;

      const div = document.createElement('div', { className: 'panzoom-controls' });
      const container = svg.parentElement;

      const button1 = document.createElement('button');
      button1.innerHTML = '🔍+';
      button1.setAttribute('aria-label', 'Zoom in');

      const button2 = document.createElement('button');
      button2.innerHTML = '🔍−';
      button2.setAttribute('aria-label', 'Zoom out');

      const button3 = document.createElement('button');
      button3.innerHTML = '';
      button3.setAttribute('aria-label', 'Reset zoom');

      const button4 = document.createElement('button');
      button4.innerHTML = '';
      button4.setAttribute('aria-label', 'Full screen');

      div.prepend(button1, button3, button2, button4);

      const wrapper = container.parentElement;
      wrapper.prepend(div);

      const panzoom = Panzoom(svg, {
        step: 0.5,
        handleStartEvent: (e) => e.preventDefault()
      });
      button1.onclick = () => panzoom.zoomIn();
      button2.onclick = () => panzoom.zoomOut();
      button3.onclick = () => panzoom.reset();
      svg.parentElement.addEventListener('wheel', panzoom.zoomWithWheel);

      button4.onclick = () => {
        const modal = document.createElement('div');
        modal.className = 'mermaid-modal-overlay';

        const modalContent = document.createElement('div');
        modalContent.className = 'mermaid-modal-content';

        const closeBtn = document.createElement('button');
        closeBtn.className = 'mermaid-modal-close';
        closeBtn.innerHTML = '';
        closeBtn.setAttribute('aria-label', 'Close preview');

        const svgClone = svg.cloneNode(true);
        modalContent.appendChild(svgClone);

        modal.appendChild(closeBtn);
        modal.appendChild(modalContent);
        document.body.appendChild(modal);

        const modalPanzoom = Panzoom(svgClone, {
          step: 0.5,
        });
        svgClone.parentElement.addEventListener('wheel', modalPanzoom.zoomWithWheel);

        const closeModal = () => {
          modal.remove();
          document.body.style.overflow = '';
        };

        closeBtn.onclick = closeModal;
        modal.onclick = (e) => {
          if (e.target === modal) closeModal();
        };

        const escHandler = (e) => {
          if (e.key === 'Escape') {
            closeModal();
            document.removeEventListener('keydown', escHandler);
          }
        };
        document.addEventListener('keydown', escHandler);

        document.body.style.overflow = 'hidden';
      };
    });
  };

  if (typeof mermaid !== 'undefined') {
    document.addEventListener('DOMContentLoaded', () => {
      console.log('Initializing mermaid diagrams with zoom controls...');
      mermaid.run({
        querySelector: '.language-mermaid'
      }).then(() => {
        console.log('Mermaid diagrams rendered.');
        initZoomControls();
      }).catch((err) => {
        console.error('Mermaid error:', err);
      });
    });
  }
</script>

ポイント

  • 作成した環境により、HTMLのカスタムレイアウトでフッターを利用してスクリプトを実行しています。
    • 他の場所でも動けば問題ないです。
    • mermaidの図の生成と、生成した図に対するズームの設定を行っています。
  • HTMLファイルとして記述しています。スタイルとスクリプト一緒に含めています。分けても問題ありません。
  • display: block; overflow: hidden;が必ず当たるようにしてください。
    • はじめは、CSSがテーマで書き換えられていて、うまく動作しなかった。
  • panzoom/panzoomのライブラリを使用して、ズーム等の処理をしています。
    • ZOOM処理のためボタンのつなぎ込みと、ホイール対応をコードに記述しています。
  • language-mermaidのクラス名で、mermaidが処理しているので、.language-mermaidで全体的に処理しています。環境によっては、クラス名等が環境により違うので変えてください。
  • 全表示ボタンもズームを使用しつつ、実装してあります。
  • just-the-docsでのmermaidの設定を消し、このコードで処理するように修正しています。

HTMLの生成例

image.png

  • pre - code - svgの階層になっています。うまく動かない場合は、生成する環境で確認してください。

作成されたボタン

image.png

最後に

  • 設計書の図がだいぶ見やすくなりました。大きな図を書いても問題なしです。
  • 他の環境でも、同じ様に処理できれば、うまく表示されるはずです。
  • デザインはシンプルに実装したので、変更してください。ボタンの位置など変えてもいいかも。
  • 静的なサイトで設計書サイト作成しましたが、コメントとか記述できるようになると、うれしいですね。
2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?