EditContextAPI触ってみた

# はじめに

下記の記事でEditContext_APIというのを知り、遊んでみたかったので触ってみる。

https://zenn.dev/cybozu_frontend/articles/5667796d4168bc

EditContext_APIについて

※現時点では下記のようなブラウザのサポートになっているので注意してください。

https://developer.mozilla.org/en-US/docs/Web/API/EditContext_API

# 触ってみる

今回はmozillaのEditContextの内容に沿って触ってみる

下記HTMLのようにCanvas上に描画する。

```html:index.html
    <canvas id="editor-canvas" width="400" height="40" style="background: rgb(223, 255, 223); width: 400px; height: 40px;"></canvas>
    <script type="module" src="/main.js"></script>
```

EditContext APIを利用し、宣言したeditをcanvas.editContextに代入する。

editのtextupdate(テキスト更新イベント)で文字を描画させる。

```javascript:main.js
const canvas = document.getElementById("editor-canvas");
const ctx = canvas.getContext("2d");
const FONT_SIZE = 16;
ctx.font = `${FONT_SIZE}px monospace`;
const edit = new EditContext();
canvas.editContext = edit;

const TEXT_X = 10;
const TEXT_Y = 30;
function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "black";
  ctx.fillText(edit.text, TEXT_X, TEXT_Y);
}
// テキスト更新イベント
edit.addEventListener("textupdate", (e) => {
  render();
});

```

![EditContextAPI触ってみる_01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/306ff3fe-c628-d844-dbbe-861e1c31509b.jpeg)

![EditContextAPI触ってみる_02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/685bb8b0-712b-0dd3-fb08-b7c660f862a9.jpeg)

次に、compositionstartとcompositionendでIME入力中わかりやすく枠線をCSSで表示してみる。

(compositionstartとcompositionendは普通にinput要素にもあるイベントです。)

```javascript:main.js
// IMEの入力開始イベント
edit.addEventListener("compositionstart", (e) => {
  canvas.classList.add("is-composing");
});

// IMEの入力終了イベント
edit.addEventListener("compositionend", (e) => {
  canvas.classList.remove("is-composing");
});
```

```css:index.css
#editor-canvas {
    border: 5px solid black;
}
#editor-canvas.is-composing {
    border: 5px solid red;
}
```

![EditContextAPI触ってみる_03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/2f06003d-532d-1ac8-f73d-e2dd53f875d7.jpeg)

![EditContextAPI触ってみる_04.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/c1a8b2b7-16ec-34d0-8b67-5fb280088e24.gif)

現在のコードだと、上記のようにIME入力中、IMEダイアログの位置がおかしいので

editのcharacterboundsupdateイベントでIMEダイアログの位置が入力文字の下部らへんに

IMEダイアログの位置がくるように設定する。

```javascript:main.js
// 文字列の幅を計算するヘルパー関数
function measureTextWidth(text) {
  return ctx.measureText(text).width;
}
// 文字境界更新イベント
edit.addEventListener('characterboundsupdate', (e) => {
  // IMEダイアログの位置を設定する
  const charBounds = [];
  for (let offset = e.rangeStart; offset < e.rangeEnd; offset++) {
    charBounds.push(computeCharacterBound(offset));
  }
  edit.updateCharacterBounds(e.rangeStart, charBounds);
});
function computeCharacterBound(offset) {
  const widthBeforeChar = measureTextWidth(edit.text.substring(0, offset));
  const charWidth = measureTextWidth(edit.text[offset]);
  const charX = canvas.offsetLeft + widthBeforeChar;
  const charY = canvas.offsetTop;
  return DOMRect.fromRect({
    x: charX,
    y: charY + FONT_SIZE + 10,
    width: charWidth,
    height: FONT_SIZE,
  });
}
```

![EditContextAPI触ってみる_05.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/bd252566-7599-4b74-413a-a1169aabce97.gif)

メモなどでIME入力すると、IME入力文字の下にアンダーラインが表示されると思う。

同じようにeditのtextformatupdateを使用してIME入力中にアンダーラインを設定する。

```javascript:main.js
// テキストフォーマット更新イベント
edit.addEventListener("textformatupdate", (e) => {
  render();
  // IME入力選択中のアンダーラインを引く
  const formats = e.getTextFormats();
  for (const format of formats) {
    const { rangeStart, rangeEnd, underlineStyle, underlineThickness } = format;
    const underlineXStart = measureTextWidth(edit.text.substring(0, rangeStart));
    const underlineXEnd = measureTextWidth(edit.text.substring(0, rangeEnd));
    const underlineY = TEXT_Y + 3;
    ctx.beginPath();
    ctx.moveTo(TEXT_X + underlineXStart, underlineY);
    ctx.lineTo(TEXT_X + underlineXEnd, underlineY);
    ctx.stroke();
  }
});
```

![EditContextAPI触ってみる_06.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/0a9175ed-eb8c-ee53-caa2-e68acb2a288c.gif)

文字入力中にカーソルがなくわかりにくいのでカーソルを表示する

```javascript:main.js
// カーソル位置を表示する
function drawTextCursor() {
  let cursorVisible = true;
  setInterval(() => {
    const cursorX = measureTextWidth(edit.text.substring(0, edit.selectionEnd)) + TEXT_X;
    ctx.clearRect(cursorX, TEXT_Y - FONT_SIZE, 2, FONT_SIZE);
    if (cursorVisible && edit.selectionStart === edit.selectionEnd) { // 選択範囲がない場合のみカーソルを表示
      ctx.fillStyle = "black";
      ctx.fillRect(cursorX, TEXT_Y - FONT_SIZE, 2, FONT_SIZE);
    }
    cursorVisible = !cursorVisible;
  }, 500);
}
drawTextCursor();
```
![EditContextAPI触ってみる_07.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/33858d4e-25e3-5224-e230-315203a4908d.gif)

keydownイベントを使用してペースト機能を追加、

矢印キーで文字入力のカーソルを移動させる

```javascript:main.js
function breakTextCursor(prev_selection) {
  ctx.clearRect(prev_selection * FONT_SIZE + TEXT_X, FONT_SIZE, 2, FONT_SIZE);
}

canvas.addEventListener("keydown", async (e) => {
  // Ctrl+VまたはCommand+Vでペースト処理
  if (e.key == "v" && (e.ctrlKey || e.metaKey)) {
    const pastedText = await navigator.clipboard.readText();
    // EditContextのテキストを更新
    edit.updateText(
      edit.selectionStart,
      edit.selectionEnd,
      pastedText,
    );
    // 選択範囲を更新
    edit.updateSelection(
      edit.selectionStart + pastedText.length,
      edit.selectionStart + pastedText.length,
    );
    render();
  }
  if (e.key == "ArrowLeft" || e.key == "ArrowRight") {
    // 範囲選択の再描画をトリガー
    drawRangeCursor();
  }
  if (e.key == "ArrowLeft") {
    const prev_selection = edit.selectionStart;
    const newPosition = Math.max(prev_selection - 1, 0);
    if (e.shiftKey) {
      // 選択範囲指定は今回は実装しない
    } else {
      // Shiftキーが押されていない場合はカーソル位置を更新
      edit.updateSelection(newPosition, newPosition);
      breakTextCursor(prev_selection);
    }
  } else if (e.key == "ArrowRight") {
    const prev_selection = edit.selectionEnd;
    const newPosition = Math.min(prev_selection + 1, edit.text.length);
    if (e.shiftKey) {
      // 選択範囲指定は今回は実装しない
    } else {
      // Shiftキーが押されていない場合はカーソル位置を更新
      edit.updateSelection(newPosition, newPosition);
      breakTextCursor(prev_selection);
    }
  };
});

```

![EditContextAPI触ってみる_08.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/0ae19d52-c6eb-8e4a-d610-c00e2ff0af52.gif)

# 今回の成果物_demoURL/ソース

デモURL

https://shisojuice.github.io/EditContextAPI/

ソース

https://github.com/shisojuice/EditContextAPI


# さいごに

EditContext_API触ってみた。

Canvasに描画する場合だと、改行や入力中のCursor、UndoRedoなどは自前で実装する必要がありそうなので面倒だなと思うが、

Contenteditableを使って日本語のようなIME入力を行う場合、変換中の文字列の扱いがブラウザによって異なるなど、挙動が不安定になる場合があるので

IMEのイベントサポートがしっかりされていて、日本語IMEの利用者としては今後も注目していきたい。