0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Web EditorAdvent Calendar 2024

Day 9

EditContextAPI触ってみた

Posted at

はじめに

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

EditContext_APIについて

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

触ってみる

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

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

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(テキスト更新イベント)で文字を描画させる。

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

EditContextAPI触ってみる_02.jpg

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

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

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

// IMEの入力終了イベント
edit.addEventListener("compositionend", (e) => {
  canvas.classList.remove("is-composing");
});
index.css
#editor-canvas {
    border: 5px solid black;
}
#editor-canvas.is-composing {
    border: 5px solid red;
}

EditContextAPI触ってみる_03.jpg

EditContextAPI触ってみる_04.gif

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

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

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

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

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

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

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

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

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

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

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

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

今回の成果物_demoURL/ソース

デモURL

ソース

さいごに

EditContext_API触ってみた。

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

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

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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?