0
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?

GASとD2.jsで開発したダイアグラムビューアにインラインエディタ機能を追加

Posted at

はじめに

前回は、GASとD2.jsとCodeMirrorで作成した簡易ダイアグラムエディタにファイルロードとセーブ機能を追加しました。

今回は、最初に作成したダイアグラムビューアに、簡易ダイアグラムエディタの機能を追加して、ダイアグラムの図をクリックした瞬間にエディタが現れ、編集して保存ボタンでDrive に上書きできる インラインエディタ機能 を追加します。

ゴール

ビューアに表示されているダイアグラムをクリックでインラインエディアに切り替わる
「閉じる」ボタンでビューアに戻る

d2_inline_editor.gif

インラインエディタを追加したダイアグラムビューア

ダイアグラムビューアの Code.gs に対して、 前回の記事で実現した Google Drive に作成したD2ファイルの読み書き機能を追加します。

Code.gs
/** ---------------------------------------------
 * D2 ファイルを Drive で取得(存在しなければ作成)
 * ------------------------------------------- */
function getOrCreateD2File() {
  const FILE_NAME = "sample.d2";
  const files = DriveApp.getFilesByName(FILE_NAME);
  if (files.hasNext()) return files.next();
  // 無ければ空ファイルを新規作成
  return DriveApp.createFile(FILE_NAME, "x -> y\ny -> z");
}

/** HTML 配信 */
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index");
}

/** Drive から D2 コードを読み込む */
function getD2Code() {
  return getOrCreateD2File().getBlob().getDataAsString();
}

/** Drive へ D2 コードを保存する */
function saveD2Code(content) {
  const file = getOrCreateD2File();
  file.setContent(content);
  return "saved";
}

ダイアグラムビューアの index.html に対して次の2つの機能を追加します。

  1. 前回の記事の簡易エディタ機能
  2. ビューアと簡易エディタの切り替え機能
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>D2.js エディタ (クリック編集 & Drive 保存)</title>
  <style>
    /* 既存 CSS ― 変更なし */
    body {
      font-family: sans-serif;
      padding: 1em;
    }
    .container {
      display: flex;
      gap: 1em;
      align-items: flex-start;
    }
    #editor { width: 50%; border: 1px solid #ccc; }
    #diagram { width: 50%; overflow: hidden; }
    #diagram svg {
      box-sizing: border-box;
      width: 100%;
      height: 80vh;
      display: block;
      border: 1px solid #ccc;
    }
    /* ▼ 保存中モーダルだけ追加(既存見栄えを壊さない) */
    #savingMask {
      position: fixed; inset: 0;
      display: none; justify-content: center; align-items: center;
      background: rgba(255,255,255,0.7); cursor: wait; z-index: 9999;
    }
    #savingMask div {
      padding: 1.5em 2.5em; border: 1px solid #666; border-radius: 8px;
      background: #fff; font-size: 1.2em; font-weight: bold;
      box-shadow: 0 0 10px rgba(0,0,0,0.2);
    }
  </style>

  <!-- CodeMirror importmap(変更なし) -->
  <script type="importmap">
  {
    "imports": {
      "@codemirror/state": "https://esm.sh/@codemirror/state@6.4.1"
    }
  }
  </script>
</head>
<body>
  <h2>D2.js 簡易エディタ (クリックで編集)</h2>

  <!-- 保存ボタン(初期は非表示) -->
  <div id="toolbar" style="margin-bottom:0.5em; display:none;">
    <button id="saveBtn">保存</button>
    <button id="closeBtn">閉じる</button>
  </div>

  <!-- エディタ & 図表示 -->
  <div class="container">
    <div id="editor" style="display:none;"></div>
    <div id="diagram">Loading...</div>
  </div>

  <!-- 保存中モーダル -->
  <div id="savingMask"><div>保存しています…</div></div>

  <script type="module">
    /* --------- モジュール読み込み --------- */
    import { EditorView, basicSetup } from "https://esm.sh/@codemirror/basic-setup@0.20.0?external=@codemirror/state";
    import { EditorState } from "https://esm.sh/@codemirror/state@6.4.1";
    import { D2 } from "https://esm.sh/@terrastruct/d2@0.1.23";

    /* --------- DOM 取得 --------- */
    const diagramDiv = document.getElementById("diagram");
    const editorDiv  = document.getElementById("editor");
    const toolbar    = document.getElementById("toolbar");
    const saveBtn    = document.getElementById("saveBtn");
    const closeBtn   = document.getElementById("closeBtn");
    const mask       = document.getElementById("savingMask");
    let editor, currentCode;

    /* --------- Drive から読み込み → エディタ初期化 --------- */
    google.script.run.withSuccessHandler(initEditor).getD2Code();

    function initEditor(codeFromDrive) {
      currentCode = codeFromDrive;              // 後で閉じ戻し用
      const updateListener = EditorView.updateListener.of(u => {
        if (u.docChanged) {
          currentCode = u.state.doc.toString();
          renderDiagram(currentCode);
        }
      });
      editor = new EditorView({
        state: EditorState.create({ doc: codeFromDrive, extensions:[basicSetup, updateListener] }),
        parent: editorDiv,
      });
      renderDiagram(codeFromDrive);
    }

    /* --------- D2.js 描画 --------- */
    const d2 = new D2();
    async function renderDiagram(code) {
      try {
        const { diagram, renderOptions } = await d2.compile(code);
        const svg = await d2.render(diagram, renderOptions);
        diagramDiv.innerHTML = svg;
      } catch (err) {
        diagramDiv.innerHTML = `<pre style="color:red;">${err.message}</pre>`;
      }
    }

    /* --------- SVG クリックで編集トグル --------- */
    diagramDiv.addEventListener("click", () => {
      const isHidden = editorDiv.style.display === "none";
      editorDiv.style.display = isHidden ? "block" : "none";
      toolbar.style.display   = isHidden ? "block" : "none";
      /* 配置調整:エディタが隠れている間はダイアグラムを全幅に */
      diagramDiv.style.width  = isHidden ? "50%" : "100%";
      if (isHidden) editor.focus();
    });

    /* --------- 保存処理 --------- */
    saveBtn.addEventListener("click", () => {
      if (!confirm("現在の図を sample.d2 に保存しますか?")) return;
      toggleMask(true);
      google.script.run
        .withSuccessHandler(() => {
          toggleMask(false);
          alert("保存しました");
        })
        .withFailureHandler(err => {
          toggleMask(false);
          alert("保存失敗: " + err.message);
        })
        .saveD2Code(currentCode);
    });

    /* --------- 閉じるボタン --------- */
    closeBtn.addEventListener("click", () => {
      editorDiv.style.display = "none";
      toolbar.style.display   = "none";
      diagramDiv.style.width  = "100%";
    });

    /* --------- モーダル制御 --------- */
    function toggleMask(show) {
      mask.style.display = show ? "flex" : "none";
      saveBtn.disabled   = show;
      closeBtn.disabled  = show;
    }
  </script>
</body>
</html>

トグル表示のコード

図をクリックするだけで編集モードを開閉し、displaywidth 切替で実現する

diagramDiv.addEventListener("click", () => {
  const hidden = editorDiv.style.display === "none";
  editorDiv.style.display = hidden ? "block" : "none";
  toolbar.style.display   = hidden ? "block" : "none";
  diagramDiv.style.width  = hidden ? "50%" : "100%";
});

まとめ

これで 見る → クリック → 編集 → 保存 → 閉じる というフローを実現しました

D2.js を使った図の表示アプリは、最初はシンプルな「ビューア」として始まりました。しかし、より柔軟で実用的な使い方を目指す中で、編集・保存機能を備えた「インラインエディタ」へと進化しました。

これまでの記事

0
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
0
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?