この記事は GMOアドマーケティング Advent Calendar 2023 22日目の記事です。
お疲れ様です。GMOアドマーケティングの天河です。
テキストエリアなどの入力系のタグ内のテキストを選択した際に、その部分の座標情報を取得する方法を共有したいと思います(なかなか情報がなく苦戦した)。
ちなみに現時点で一発で取得できるメソッドは存在しません。ライブラリなどを利用する必要があります。
ちなみにこれができると、Twitter(現 X)や Instagram などのSNSでよく見るハッシュタグの予測変換機能が作れます。
HTML内で選択したDOM要素の情報は
1 |
const selectedText = document.getSelection(); |
によって、Range
オブジェクトとして取得することが可能です。ここで取得した Range
オブジェクト情報からselectedText.getRangeAt(0).getBoundingClientRect()
で、選択した箇所の座標情報を取得することができます。
しかし、input
タグや textarea
タグの中のテキストを選択しても同じように Range
オブジェクト情報は取得できません。空のオブジェクトが返却されてしまいます。input
タグや textarea
タグではこの Range
情報を取得するメソッドはありません。
※ しかし input
, textarea
タグ内のテキストに選択範囲を設定するメソッドはなぜかある
HTMLInputElement: setSelectionRange() method
この入力系DOM内の座標情報を取得するためにはどのような手順を踏めばよいでしょうか。
実装のコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
/** * テキスト入力内の指定された選択箇所におけるspanの絶対位置のx、y座標を返却 * @param {object} input - 座標を取得する入力要素 * @param {number} selectionPoint - 入力の選択箇所 */ const getCursorXY = (input, selectionPoint) => { const { offsetLeft: inputX, offsetTop: inputY, } = input // 入力のクローンとなるダミー要素を作成 const div = document.createElement('div') // 入力の計算されたスタイルを取得し、ダミー要素にコピー const copyStyle = getComputedStyle(input) for (const prop of copyStyle) { div.style[prop] = copyStyle[prop] } div.style.position = "fixed"; div.style.top = "0px"; div.style.left = "0px"; div.style.opacity = 0; // <input/>の場合空白を置き換える const swap = '.' const inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value // テキストエリアの選択箇所までのdivの内容を設定する const textContent = inputValue.substring(0, selectionPoint) // ダミー要素divのテキストコンテンツを設定 div.textContent = textContent if (input.tagName === 'TEXTAREA') div.style.height = 'auto' if (input.tagName === 'INPUT') div.style.width = 'auto' // span要素を作成してキャレット位置を取得する const span = document.createElement('span') span.textContent = inputValue.substring(selectionPoint) || '.' // ダミー要素にspanマーカーを追加 div.appendChild(span) // ダミー要素をbodyに追加 document.body.appendChild(div) const { offsetLeft: spanX, offsetTop: spanY } = span document.body.removeChild(div) return { x: inputX + spanX, y: inputY + spanY, } } |
実装の手順
大まかな手順は以下の通りです。
- 選択したテキストの
input
,textarea
タグと似たスタイルを持つ、仮のdiv
タグを生成する - カーソル位置までの文字を取得して、仮の
div
タグのtextContent
に挿入する - 仮の
span
タグを生成して、カーソル位置以降の文字をtextContent
に挿入する。 - 仮の
span
タグを、仮のdiv
タグにappend
する - 仮の
div
タグを DOM に挿入する - 仮の
span
タグの座標情報を取得する - 仮の
div
タグを削除する
実装の詳細
- 選択したテキストの
input
,textarea
タグと似たスタイルを持つ、仮のdiv
タグを生成する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const { offsetLeft: inputX, offsetTop: inputY, } = input // or textarea // 入力のクローンとなるダミー要素を作成する const div = document.createElement('div') // 入力の計算されたスタイルを取得し、ダミー要素にコピーする const copyStyle = getComputedStyle(input) for (const prop of copyStyle) { div.style[prop] = copyStyle[prop] } div.style.position = "fixed"; div.style.top = "0px"; div.style.left = "0px"; div.style.opacity = 0; |
input
タグと textarea
タグ自体の座標情報は取れるので、offsetLeft
と offsetTop
の情報(つまり左上の座標)の情報を取得します。
そしてダミーの div
タグを用意して、入力系タグのスタイルを全部コピーします。ページ全体におけるカーソル座標を取得するので、入力/テキストエリアの位置も考慮する必要があります。
最後に fixed
を適応し、div
タグを左上の位置に固定しておきます。
- キャレット位置までの文字を取得して、仮の
div
タグのtextContent
に挿入する
1 2 3 4 5 6 7 8 9 10 11 12 |
// ダミー要素作成 // 単一行の<input/>の場合は空白を置き換えないとずれる const swap = '*' const inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value // テキストエリアの選択位置までの内容を取得し、ダミーdivに設定 const textContent = inputValue.substring(0, selectionPoint) div.textContent = textContent if (input.tagName === 'TEXTAREA') div.style.height = 'auto'<br />if (input.tagName === 'INPUT') div.style.width = 'auto' // inputタグは単一行 |
input
タグ内の場合、空白文字も一緒にそのままダミータグに突っ込むと正しく座標が取得できないので何かの文字で置き換える必要があります。
- 仮の
span
タグを生成して、キャレット位置以降の文字をtextContent
に挿入する。 - 仮の
span
タグを、仮のdiv
タグにappend
する
1 2 3 4 5 |
// span要素を作成し、選択箇所以降の文字を挿入する const span = document.createElement('span') span.textContent = inputValue.substring(selectionPoint) || '.' div.appendChild(span) |
- 仮の
div
タグを DOM に挿入する - 仮の
span
タグの座標情報を取得する - 仮の
div
タグを削除する
1 2 3 4 5 6 7 8 9 10 11 12 |
// キャレット位置を取得 // これは、入力に対する左上のキャレット位置になる const { offsetLeft: spanX, offsetTop: spanY } = span // 最後に、そのダミー要素を削除 document.body.removeChild(div) // キャレット座標(カーソルの位置座標)を返却 return { x: inputX + spanX, y: inputY + spanY, } |
body にダミー要素を挿入することで、ページ内におけるDOMの座標を取得することができます。
こうして取得した座標を使って様々なUIを実現することが可能です。
例えば、選択箇所の近くにマーカーを表示させることができます。座標を計算して、付近にUIを表示するといった流れです。
See the Pen SelectionMarker by 10K (@10JII_K) on CodePen.
そのほかにも冒頭で述べたハッシュタグ機能も作ることができます。
See the Pen HashTag by 10K (@10JII_K) on CodePen.
おわりに
結構めんどくさいので、ライブラリで実現するのがいいかもしれませんね。本末転倒ですが。
お役に立てられたら幸いです。お役に立ったら、はてブお願いします(おねだり)
明日は @thomi40 さんによる「Notion APIでYouTubeやVimeoの動画を埋め込む方法」です。
引き続き、GMOアドマーケティング Advent Calendar 2023 をお楽しみください!
■エンジニア採用ページはこちら!
https://recruit.gmo-ap.jp/
■GMOアドパートナーズ 公式noteはこちら!
https://note.gmo-ap.jp/
