JavaScriptを使用してWebページ上でマウスが指している単語を特定する

出版日: 2024年5月31日

JavaScriptを使用してWebページ上でマウスが指している単語を特定する

背景

私はRikaiKunというChrome拡張機能を使っています。これは、日本語の単語にカーソルを合わせるだけでその意味を表示してくれるものです。

Rikai Kun

単語を選択する必要はなく、単語にカーソルを合わせるだけでその単語の意味を表示してくれます。

このブログ投稿では、同じことを実装してみましょう。

方法: 人間レベル

まず、現在ホバーされている要素を見つけて、そのテキストを取得するコードを書きましょう:

ホバーされた単語: -

All states and governments that ever ruled over men have been either republics or monarchies. Monarchies may be hereditary, if the ruler’s family has governed for generations, or new. New monarchies can either be entirely new, as when Francesco Sforza captured Milan, or they could be territories a ruler has added to his existing hereditary state by conquest, as when the King of Spain took Naples. An additional territory won by conquest will be accustomed either to living under a monarch or to the freedom of self government and may be conquered by the new ruler’s own army or that of a third party, by luck or deservedly.

これを達成するためのコードは以下の通りです:

<script>
	import { onMount } from 'svelte';

	let hoveredWord;

	let text = `All states and governments that ever ruled over men have been either republics or monarchies. Monarchies may be hereditary, if the ruler’s family has governed for generations, or new. New monarchies can either be entirely new, as when Francesco Sforza captured Milan, or they could be territories a ruler has added to his existing hereditary state by conquest, as when the King of Spain took Naples. An additional territory won by conquest will be accustomed either to living under a monarch or to the freedom of self government and may be conquered by the new ruler’s own army or that of a third party, by luck or deservedly.`;
	
	onMount(async () => {
		document.addEventListener('mouseover', function (e) {
			document.querySelectorAll('.textContainer span').forEach((node) => {
				node.style.color = '';
			});

			let target = e.target.closest('.textContainer span');

			if (target) {
				target.style.color = 'red';

				hoveredWord =
					target.innerText.replace(',', '').replace('.', '') ||
					target.textContent.replace(',', '').replace('.', '');
			}
		});
	});
</script>

<p class="text-center font-bold">Hovered word: {hoveredWord ? hoveredWord : '-'}</p>
<div class="textContainer px-4 text-sm">
	{#each text.split(' ') as word}
		<span class="italic">{`${word} `}</span>
	{/each}
</div>

上記のコードを見てわかるように、私たちは現在簡単なモードで作業しています。これは、テキスト内の各単語がspan要素で囲まれているため、どの要素がハイライトされ、その要素が持つ単語を取得するのが非常に簡単だからです。

それでも、mouseoverイベントを通じてマウスの位置を識別する方法を理解するのは良い練習です。

さて、少し難しくして楽しくしてみましょう。

方法: 怪物レベル

次に、同じテキストを大量に、一語一語に分かれていない状態で持つことにします。これで、マウスがこの大量のテキストの中でどの単語の上にあるのかを見つける必要があります。

開発した関数がどのように機能するか見てみましょう:

All states and governments that ever ruled over men have been either republics or monarchies. Monarchies may be hereditary, if the ruler’s family has governed for generations, or new. New monarchies can either be entirely new, as when Francesco Sforza captured Milan, or they could be territories a ruler has added to his existing hereditary state by conquest, as when the King of Spain took Naples. An additional territory won by conquest will be accustomed either to living under a monarch or to the freedom of self government and may be conquered by the new ruler’s own army or that of a third party, by luck or deservedly.

テキストにカーソルを合わせると、テキスト全体がハイライトされるのがわかりますが、これは私たちが望むものではありません。私たちは、マウスがホバーしている単語だけをハイライトしたいのです。では、どのようにしてホバーされている正確な単語を見つけることができるのでしょうか?私はこれを行う方法が3つあると仮定しました。

うまくいかない: 1. プログラム的にダブルクリックイベントを発火させる

PCでテキストをダブルクリックすると、マウスがホバーしている単語だけが自動的にハイライトされることをご存知でしょう。これを自動的に発火させることができれば、その単語が選択されると考えました。

単語が選択された後、以下のコードを使って選択された単語を取得できます:

// When mouse is hovered on a word, trigger double click to select that word
document.addEventListener('mouseover', function (e) {
	var event = new MouseEvent('dblclick', {
		view: window,
		bubbles: true,
		cancelable: true
	});

	// Programatically fire it for the element we are interested in
	document.getElementById('textContainer').dispatchEvent(event);
});

// Get the selection when souble click fires
document.addEventListener('dblclick', function (e) {
	let target = e.target.closest('#textContainer');

	// Check if it is firing for the textContainer
	if (target) {
		let selectedWord =
			(document.selection && document.selection.createRange().text) ||
			(window.getSelection && window.getSelection().toString());

		// Do something (E.g. Show meaning of that word)
	}
});

悪いニュース! 残念ながら、ダブルクリックイベントを発火させても単語は自動的に選択されません。

うまくいかない: 2. ホバーされている単語を計算する試み

要素がホバーされたとき、その幅と高さ、テキスト内容と行の高さを取得し、これと要素に対するマウスの位置を組み合わせて、マウスの位置を推測することができます:

document.addEventListener('mouseover', function (e) {
	let target = e.target.closest('.textContainer span');

	// Get width and height of container as this will be different depending on viewport
	if (target) {
		let targetRect = target.getBoundingClientRect();
		let height = targetRect.height;
		let width = targetRect.width;
		// ...
	}
});

このアプローチは非常に複雑で、良いアルゴリズムを書いたとしても、エッジケースに関しては誤りが生じる可能性が高いため、コードを完成させませんでした。このオプションはスキップしましょう。

機能する: 3. テキストをスパンに分割する

このアプローチは、この記事の最初で使用したものです。ターゲット要素を取得し、そのテキスト内容を個別のspan要素に分割します。

これにより、各単語を個別のスパン要素として再生成し、そのイベントを制御することができます。

最初に話し合った内容よりも少し難しくするために、ターゲット要素にdiv, p, anchor, spanなどのサブ要素が含まれていると仮定しましょう。これらのサブ要素を含む親要素をテキストだけを操作して個別のスパン要素に変換し、クラス、ID、タグを元のまま保持する方法を見てみましょう。

All states and governments that ever ruled over men have been either republics or monarchies. Monarchies may be hereditary, if the ruler’s family has governed for generations, or new.

New monarchies can either be entirely new, as when Francesco Sforza captured Milan, or they could be territories a ruler has added to his existing hereditary state by conquest, as when the King of Spain took Naples.

An additional territory
won by conquest will be accustomed either to living under a monarch or to the freedom of self government and may be conquered by the new ruler’s own army or that of a third party, by luck or deservedly.

結果

以下は目的を達成するためのコードです:

<script>
// Get the container we want to modify
let container = document.getElementById('textContainer');

// Check if there is any text that is directly under the container
let firstBit = container.innerHTML[0] !== '<' ? container.innerHTML.split('<')[0] : '';
let lastBit =
	container.innerHTML.slice(-1) !== '>' ? container.innerHTML.split('>').slice(-1) : '';

// Add those text to a span element
if (firstBit) {
	container.innerHTML = `<span>${firstBit}</span>` + container.innerHTML.replace(firstBit, '');
}
if (lastBit) {
	container.innerHTML = container.innerHTML.replace(lastBit, '') + `<span>${lastBit}</span>`;
}

// Get children of the container
let children = getAllDescendants(container);

// Get texts of children that do not have any further children
let texts = children.filter((c) => c.childNodes.length === 0).map((c) => c.textContent);

// For each text block, split it into words and convert each word to a span element
texts.forEach((t) => {
	container.innerHTML = container.innerHTML
		.replace(
			`>${t}<`,
			`>${t
				.split(' ')
				.map((t) => `<span class="hover">${t}</span> `)
				.join('')}<`
		)
		.replace(
			`>"${t}"<`,
			`>${t
				.split(' ')
				.map((t) => `<span class="hover">${t}</span> `)
				.join('')}<`
		);
});

</script>


// This is the HTML container we will be manipulating
// With manipulation, we mean we are going to divide its text into words
// And enclose each word with span element
<div class="px-4 text-sm" id="textContainer">
	All states and governments that ever ruled over men have been either republics or monarchies. Monarchies
	may be hereditary, if the
	<a class="font-bold text-sky-500" href="javascript:void(0)">ruler’s family</a> has governed for
	generations, or new.
	<p>
		New monarchies can either be entirely new, as when Francesco Sforza captured Milan, or they
		could be territories a ruler has added to his existing hereditary state by conquest, as when
		the King of Spain took Naples.
	</p>
	<div><div><span class="font-bold">An additional territory</span></div></div>
	won by conquest will be accustomed either to living
	<span class="italic">under a monarch</span>
	or to the freedom of self government and may be conquered by the new ruler’s own army or that of
	a third party, <span class="text-lg">by luck</span> or deservedly.
</div>
</div>

// Add hover class for highlighting each word
<style>
:global(.hover:hover) {
	background: yellow;
} </style>

適用すると、下のテキストにカーソルを合わせると、各単語がホバーされたときにハイライトされるかどうかを確認できます。うまくいきますね?

All states and governments that ever ruled over men have been either republics or monarchies. Monarchies may be hereditary, if the ruler’s family has governed for generations, or new.

New monarchies can either be entirely new, as when Francesco Sforza captured Milan, or they could be territories a ruler has added to his existing hereditary state by conquest, as when the King of Spain took Naples.

An additional territory
won by conquest will be accustomed either to living under a monarch or to the freedom of self government and may be conquered by the new ruler’s own army or that of a third party, by luck or deservedly.

結論

このブログ投稿では、ユーザーが各単語にホバーしたときにその単語をハイライトする方法を開発しました。このコードは、単語の意味を表示したり、その単語に関する情報を表示するモーダルを開くなど、他の目的にも使用できます。

エッジケースでコードが機能するかどうかはテストしていませんが、問題が発生した場合はお知らせください。

それでは、コーディングを楽しんでください!

このブログは英語からChatGPTによって翻訳されました。不明な点がある場合は、お問い合わせページからご連絡ください。

コメントを残す

コメント

その他のブログ

SvelteとJavaScriptを使用してシンプルで動的なツールチップを作成する

2024/06/19

SvelteとJavaScriptを使用してシンプルで動的なツールチップを作成する

JavaScriptSvelteTooltip動的シンプルツールチップフロントエンド
JavaScriptを用いて東京都のインタラクティブな地図を作成する

2024/06/17

JavaScriptを用いて東京都のインタラクティブな地図を作成する

SvelteSVGJavaScriptTailwindインタラクティブな地図東京市区町村23区地図
Matplotlibで日本語文字化けを解決できる簡単な方法

2024/06/14

Matplotlibで日本語文字化けを解決できる簡単な方法

MatplotlibグラフチャートPython日本語文字化け問題バグ
書評 | トーキング・トゥ・ストレンジャーズ 「よく知らない人」について私たちが知っておくべきこと by マルコム・グラッドウェル

2024/06/13

書評 | トーキング・トゥ・ストレンジャーズ 「よく知らない人」について私たちが知っておくべきこと by マルコム・グラッドウェル

書評トーキング・トゥ・ストレンジャーズ「よく知らない人」について私たちが知っておくべきことマルコム・グラッドウェル
日本語で最もよく使われる3000字の漢字

2024/06/07

日本語で最もよく使われる3000字の漢字

3000よく使う準漢字使用回数漢字日本語漢字リスト漢字普及率日本語能力試験独学勉強単語
VSCodeでRegexを使用してReplaceする方法

2024/06/07

VSCodeでRegexを使用してReplaceする方法

VSCodeRegex検索置き換える条件付き置換FindReplaceConditional Replace
SvelteではReadable Storeを使用するな

2024/06/06

SvelteではReadable Storeを使用するな

SvelteReadableWritableステート管理ストアStore速度メモリファイルサイズ
GzipとPakoでデータを圧縮してWebサイトのローディング速度を上げる方法

2024/06/05

GzipとPakoでデータを圧縮してWebサイトのローディング速度を上げる方法

Gzip圧縮PakoWebサイトローディング速度SvelteKit
SvelteとSVGを用いてインタラクティブな地図を作成する

2024/05/29

SvelteとSVGを用いてインタラクティブな地図を作成する

SvelteSVGインタラクティブな地図フロントエンド
書評 | Originals 誰もが「人と違うこと」ができる時代 by アダム・グラント & シェリル・サンドバーグ

2024/05/28

書評 | Originals 誰もが「人と違うこと」ができる時代 by アダム・グラント & シェリル・サンドバーグ

書評Originals誰もが「人と違うこと」ができる時代アダム・グラント & シェリル・サンドバーグ
Javascriptを使用して数独を解く方法

2024/05/27

Javascriptを使用して数独を解く方法

数独を解くアルゴリズムJavaScriptコーディング
ウェブサイトへのトラフィックを1か月で10倍に増やした方法

2024/05/26

ウェブサイトへのトラフィックを1か月で10倍に増やした方法

ウェブサイトへのトラフィック増加クリックインプレッションGoogle Search Console
人生はサイクリングに似ている

2024/05/24

人生はサイクリングに似ている

サイクリング人生哲学成功
JavaScriptでバックトラッキング・アルゴリズムを用いて完全な数独グリッドを生成する

2024/05/19

JavaScriptでバックトラッキング・アルゴリズムを用いて完全な数独グリッドを生成する

数独バックトラッキング・アルゴリズム完全なグリッドJavaScript
Tailwindが素晴らしい理由とWeb開発をいかに楽にするか

2024/05/16

Tailwindが素晴らしい理由とWeb開発をいかに楽にするか

Tailwind素晴らしいフロントエンドWeb開発
PythonとGitフックを使用してサイトマップを自動的に生成する

2024/05/15

PythonとGitフックを使用してサイトマップを自動的に生成する

GitフックPythonサイトマップSvelteKit
書評 | Range (レンジ) 知識の「幅」が最強の武器になる by デイビッド・エプスタイン

2024/05/14

書評 | Range (レンジ) 知識の「幅」が最強の武器になる by デイビッド・エプスタイン

書評Range (レンジ)David Epstein (デイビッド・エプスタイン)知識の「幅」が最強の武器になる
SvelteとSvelteKitはなんですか?

2024/05/13

SvelteとSvelteKitはなんですか?

SvelteSvelteKitFront-endVite
SvelteKitで国際化(多言語化)

2024/05/12

SvelteKitで国際化(多言語化)

国際化多言語SvelteKitI18N
SvelteでCachingを用いてDeploy時間を短縮する方法

2024/05/11

SvelteでCachingを用いてDeploy時間を短縮する方法

SvelteEnhanced ImageCachingDeploy Time
SvelteとIntersection Oberverによるレイジーローディング

2024/05/10

SvelteとIntersection Oberverによるレイジーローディング

レイジーローディングウェブサイト速度の最適化SvelteIntersection Observer
遺伝的アルゴリズムで最適な株式ポートフォリオを作る方法

2024/05/10

遺伝的アルゴリズムで最適な株式ポートフォリオを作る方法

株式書状ポートフォリ最適化遺伝的アルゴリズムPython
Pythonを用いてShapeFileをSVGに変換できる方法

2024/05/09

Pythonを用いてShapeFileをSVGに変換できる方法

ShapeFileSVGPythonGeoJSON
Svelteの反応性:変数、バインディング、およびキー関数

2024/05/08

Svelteの反応性:変数、バインディング、およびキー関数

Svelte反応性バインディングキー関数
書評 | 孫子の兵法

2024/05/07

書評 | 孫子の兵法

書評The Art Of War (兵法)Sun Tzu (孫子)Thomas Cleary
スペシャリストは終了。ゼネラリスト万歳!

2024/05/06

スペシャリストは終了。ゼネラリスト万歳!

専門家ジェネラリストパラダイムシフトソフトウエア・エンジニアリング
トルコ人の有権者の投票行動をPythonでの分析

2024/05/03

トルコ人の有権者の投票行動をPythonでの分析

トルコ投票者年齢分析国家投票有権者行動分析
Seleniumを用いてトルコ投票データベースを作る方法

2024/05/01

Seleniumを用いてトルコ投票データベースを作る方法

PythonSeleniumWeb Scrapingトルコ国家投票
SvelteとTailwindを使用してInfinite Scrollできる方法

2024/04/30

SvelteとTailwindを使用してInfinite Scrollできる方法

SvelteTailwindInfinite ScrollFront-end
1年間以内で日本語を駆使できるようになるための方法

2024/04/29

1年間以内で日本語を駆使できるようになるための方法

日本語短時間言語学習日本語能力試験ビジネス日本語
SvelteとTailwindを用いたWebサイトテンプレート

2024/04/25

SvelteとTailwindを用いたWebサイトテンプレート

Web開発フロントエンドSvelteTailwind
怠惰なエンジニアとひどいデザイン

2024/01/29

怠惰なエンジニアとひどいデザイン

怠け者エンジニア質の悪い製品StarbucksSBI証券
偉大さについて

2024/01/28

偉大さについて

雄大さ人生の意味満足できる人生目的
MacBook で PDF を PNG に変換する

2024/01/28

MacBook で PDF を PNG に変換する

PDFPNGMacBookAutomator
2023年振り返り:24冊の読んだ本のまとめ

2023/12/31

2023年振り返り:24冊の読んだ本のまとめ

読書 2023振り返り
Python PILを使用して写真コラージュを作成する方法

2023/12/30

Python PILを使用して写真コラージュを作成する方法

PythonPIL画像処理コラージュ
ウェブサイトの訪問者のデバイスとブラウザを検出する方法

2024/01/09

ウェブサイトの訪問者のデバイスとブラウザを検出する方法

Javascript端末検知ブラウザ検知Website分析
ChatGPT回答の解析

2024/01/19

ChatGPT回答の解析

ChatGPT大規模言語モデル機械学習生成AI