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

# はじめに

前回は、[GASとD2.jsとCodeMirrorで作成した簡易ダイアグラムエディタにファイルロードとセーブ機能を追加](https://qiita.com/rairaii/items/147f89804179319f8f21)しました。

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


# ゴール

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

![d2_inline_editor.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/5fab791b-2a82-401d-ae1f-0d3c7cfb08f7.gif)


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

ダイアグラムビューアの **Code.gs** に対して、 [前回の記事](https://qiita.com/rairaii/items/147f89804179319f8f21)で実現した Google Drive に作成したD2ファイルの読み書き機能を追加します。




```javascript: 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** に対して次の２つの機能を追加します。

1. [前回の記事](https://qiita.com/rairaii/items/147f89804179319f8f21)の簡易エディタ機能
1. ビューアと簡易エディタの切り替え機能

```html: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>
```


## トグル表示のコード

図をクリックするだけで編集モードを開閉し、`display` と `width` 切替で実現する
  
  ```js
  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 を使った図の表示アプリは、最初はシンプルな「ビューア」として始まりました。しかし、より柔軟で実用的な使い方を目指す中で、編集・保存機能を備えた「インラインエディタ」へと進化しました。


# これまでの記事

https://qiita.com/rairaii/items/ea38a9ad6e7c4d31dd8a

https://qiita.com/rairaii/items/8058d2abc22f3c82f1b4

https://qiita.com/rairaii/items/147f89804179319f8f21