はじめに
下記の記事でEditContext_APIというのを知り、遊んでみたかったので触ってみる。
EditContext_APIについて
※現時点では下記のようなブラウザのサポートになっているので注意してください。
触ってみる
今回はmozillaのEditContextの内容に沿って触ってみる
下記HTMLのようにCanvas上に描画する。
<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(テキスト更新イベント)で文字を描画させる。
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();
});
次に、compositionstartとcompositionendでIME入力中わかりやすく枠線をCSSで表示してみる。
(compositionstartとcompositionendは普通にinput要素にもあるイベントです。)
// IMEの入力開始イベント
edit.addEventListener("compositionstart", (e) => {
canvas.classList.add("is-composing");
});
// IMEの入力終了イベント
edit.addEventListener("compositionend", (e) => {
canvas.classList.remove("is-composing");
});
#editor-canvas {
border: 5px solid black;
}
#editor-canvas.is-composing {
border: 5px solid red;
}
現在のコードだと、上記のようにIME入力中、IMEダイアログの位置がおかしいので
editのcharacterboundsupdateイベントでIMEダイアログの位置が入力文字の下部らへんに
IMEダイアログの位置がくるように設定する。
// 文字列の幅を計算するヘルパー関数
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,
});
}
メモなどでIME入力すると、IME入力文字の下にアンダーラインが表示されると思う。
同じようにeditのtextformatupdateを使用してIME入力中にアンダーラインを設定する。
// テキストフォーマット更新イベント
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();
}
});
文字入力中にカーソルがなくわかりにくいのでカーソルを表示する
// カーソル位置を表示する
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();
keydownイベントを使用してペースト機能を追加、
矢印キーで文字入力のカーソルを移動させる
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);
}
};
});
今回の成果物_demoURL/ソース
デモURL
ソース
さいごに
EditContext_API触ってみた。
Canvasに描画する場合だと、改行や入力中のCursor、UndoRedoなどは自前で実装する必要がありそうなので面倒だなと思うが、
Contenteditableを使って日本語のようなIME入力を行う場合、変換中の文字列の扱いがブラウザによって異なるなど、挙動が不安定になる場合があるので
IMEのイベントサポートがしっかりされていて、日本語IMEの利用者としては今後も注目していきたい。