ある幅まで来たら文字列を折り返す処理をスクラッチしたい時があります。これは次のデモの様に実現できます。
この処理は雑なものならば簡単にできるのですが作りこむと案外難しいです。ここでは次の 3 点のカバーが特徴です。
- 折り返し時に絵文字が壊れない
- 文字数ではなく文字幅で折り返している
- フォントを変えても JavaScript 側のコードを変える必要がない
絵文字はバイト数が特殊なため何かしらライブラリを用いるかブラウザに処理を任せた方が安全です。ここでは
orling/grapheme-splitter: A JavaScript library that breaks strings into their individual user-perceived characters.
という絵文字を考慮して文字列を一文字ずつの配列に分解するライブラリを用いています。
import "./styles.css";
import GraphemeSplitter from "grapheme-splitter";
/**
* テキストの幅を得る関数
* @see https://lab.syncer.jp/Web/JavaScript/Canvas/
* @param {String} text 幅を知りたいテキスト
* @param {HTMLElement} element
*/
function mesureText(text, element) {
const canvasEl = document.createElement("canvas");
const context = canvasEl.getContext("2d");
// パスをリセット
context.beginPath();
// フォントを取得
// @see https://developer.mozilla.org/ja/docs/Web/API/Window/getComputedStyle
context.font = window
.getComputedStyle(element, null)
.getPropertyValue("font");
// というテキストを描く場合の幅を取得 (a.widthに幅を表す数値が含まれる)
return context.measureText(text).width;
}
let thresholdEl; // 折り返し幅を保存する要素
let textEl; // 折り返し対象文字列を保持する要素
(() => {
thresholdEl = document.getElementById("th");
textEl = document.getElementById("text");
// 初期値代入
thresholdEl.value = 120;
textEl.value = "あい:家族の絵文字:うえお1:家族の絵文字:かきabcdeこけこ";
// 折り返し計算 & 描画イベント登録
thresholdEl.addEventListener("input", main);
textEl.addEventListener("input", main);
// 初期描画
main();
})();
/**
* 入力された文字列を任意の幅で折り返して描画する処理
*/
function main() {
/** 文字列書き込み先要素 */
const tgtEl = document.getElementById("textarea");
/** 折り返し幅を取得 */
const threshold = thresholdEl.value;
/** 折り返し対象テキスト */
const text = textEl.value;
// 折り返し幅を描画
document.querySelector("#line").style.left = `${threshold}px`;
// 文字列を一文字ずつの配列に分解
// @see https://github.com/orling/grapheme-splitter
const chars = new GraphemeSplitter().splitGraphemes(text);
// 折り返し後の各行を格納する配列
const lines = [];
// 各行になりうる文字列の一時変数
let currentLine = "";
// 折り返し次第 lines に格納する一文字ずつ見ていくループを実行
chars.forEach((char) => {
if (mesureText(currentLine + char, tgtEl) > threshold) {
// もし一時変数の行と現在参照中の一文字の間で折り返しが発生するならば
// 現在までの currentLine を lines に保存して
lines.push(currentLine);
// currentLine を 現在参照中の一文字だけに初期化
currentLine = char;
} else {
// もし折り返しが発生しないならば、一時変数の行に現在参照中の文字を追加
currentLine += char;
}
});
// 余っている一時変数の行を lines に追加
lines.push(currentLine);
// 今まで格納した行を折り返しタグでつなげて描画
tgtEl.innerHTML = lines.join("<br/>");
}