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

# はじめに

[前回作成したウェブブラウザで動作する **簡易ダイアグラムエディタ**](https://qiita.com/rairaii/items/8058d2abc22f3c82f1b4) は「入力→即描画」まで対応しました。

今回はさらに一歩進めて、

1. Google Drive 上の **D2 ファイル** を自動読み込み  
2. 編集結果を **保存ボタン** で Google Drive の **D2 ファイル** に上書き  
3. 保存前に確認ダイアログを表示  

という 3 つの機能を追加します。

# ゴール
保存ボタンと確認ダイアログを追加しています。  

![d2editor_with_save.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/1331e550-351d-4da2-9f10-243b99d43575.gif)



---

## 手順 1 – GAS 側 (`Code.gs`)

```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";
}
```

:::note info
**ポイント**   
- 保存処理は setContent() によるシンプルな上書きのみで完結
:::

---

## 手順 2 – HTML/JS 側 (`index.html`)

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>D2.js エディタ</title>
  <style>
    /* 全体のフォントと余白設定 */
    body {
      font-family: sans-serif;
      padding: 1em;
    }

    /* エディタと描画結果を左右に並べる */
    .container {
      display: flex;
      gap: 1em;
      align-items: flex-start;
    }

    /* 左側：CodeMirror エディタ */
    #editor {
      width: 50%;
      border: 1px solid #ccc;
    }

    /* 右側：SVG描画エリア */
    #diagram {
      width: 50%;
      overflow: hidden;
    }

    /* SVGにボーダーを付けて見やすく、サイズをウィンドウに対して自動調整 */
    #diagram svg {
      box-sizing: border-box;
      width: 100%;
      height: 80vh;
      display: block;
      border: 1px solid #ccc;
    }
    
    /* 保存中モーダル --------------------------------------------------- */
    #savingMask {
      position:fixed; inset:0;             /* 画面全体 */
      background:rgba(255,255,255,0.7);    /* 半透明白 */
      display:none; justify-content:center; align-items:center;
      z-index:9999;                        /* 最前面 */
      cursor:wait;                         /* カーソルも待機状態 */
    }
    #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 の状態管理モジュールをインポート -->
  <script type="importmap">
  {
    "imports": {
      "@codemirror/state": "https://esm.sh/@codemirror/state@6.4.1"
    }
  }
  </script>
</head>
<body>
  <h2>D2.js エディタ (Drive 読み書き版)</h2>

  <!-- 保存ボタン -->
  <div class="toolbar">
    <button id="saveBtn">保存</button>
  </div>

  <!-- エディタ & 図表示 -->
  <div class="container">
    <div id="editor"></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";

    const diagramDiv = document.getElementById("diagram");
    const saveBtn    = document.getElementById("saveBtn");
    const mask       = document.getElementById("savingMask");
    let editor;

    /* ---------- 初期コード取得 ---------- */
    google.script.run.withSuccessHandler(initEditor).getD2Code();

    function initEditor(initialCode) {
      const updateListener = EditorView.updateListener.of(u => {
        if (u.docChanged) renderDiagram(u.state.doc.toString());
      });
      editor = new EditorView({
        state:EditorState.create({ doc:initialCode, extensions:[basicSetup, updateListener] }),
        parent:document.getElementById("editor"),
      });
      renderDiagram(initialCode);
    }

    /* ---------- 描画 ---------- */
    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>`;
      }
    }

    /* ---------- 保存処理 ---------- */
    saveBtn.addEventListener("click", () => {
      if (!editor) return;
      if (!confirm("現在の図を sample.d2 に保存しますか？")) return;

      const content = editor.state.doc.toString();
      toggleMask(true);               // モーダル表示 & ボタン無効化

      google.script.run
        .withSuccessHandler(() => {
          toggleMask(false);
          alert("保存しました");
        })
        .withFailureHandler(err => {
          toggleMask(false);
          alert("保存失敗: " + err.message);
        })
        .saveD2Code(content);
    });

    /* ---------- モーダル制御 ---------- */
    function toggleMask(show) {
      mask.style.display = show ? "flex" : "none";
      saveBtn.disabled   = show;      // 二重クリック防止
    }
  </script>
</body>
</html>
```

:::note info
**ポイント**  
- `google.script.run.withSuccessHandler(initEditor)` でエディタの内容を初期化  
- 保存時は `confirm()` で確認して、 OK なら Drive へ上書きし、 成功で `alert()`  
:::

## 動作確認

1. GAS でデプロイしたウェブアプリの URL を開く  
2. Drive 上の `sample.d2` 内容がエディタに読み込まれる  
3. 変更後 **保存** → 「OK」 → 「保存しました」アラート  
4. Drive でファイルを開き内容が更新されていることを確認 (ここではブラウザで再読み込みして確認)

![d2editor_with_save2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/cc17d2d8-e98a-4a6d-8f38-ebbc9c62f04c.gif)



# まとめ

これで **読み込み → 編集 → 確認ダイアログ付き保存** の 3 点セットが完成しました。 

次回は、この機能を**ダイアグラムビューア**に反映させて、表示したダイアグラムをクリックしてインライン編集する機能を追加します

https://qiita.com/rairaii/items/44a187c8a1c7ca3978c7