はじめに
前回の記事では、シャドウDOMを実装するコードをJavaScriptからTypeScriptに書き直しました。
このコードには、最大の問題があります。
それは、ダブルクリックをする度にシャドウDOMが生成されてしまうということです!
そこで今回は、シャドウDOMの生成は一度だけにして、何度も生成されないように変更します。
また、既存の処理は関数にまとめるなどリファクタリングも並行しておこないました。
機能追加+リファクタリングしたので参考にしてください。
ことれいのもり
2026-04-02
前回の記事では、シャドウDOMを実装するコードをJavaScriptからTypeScriptに書き直しました。
このコードには、最大の問題があります。
それは、ダブルクリックをする度にシャドウDOMが生成されてしまうということです!
そこで今回は、シャドウDOMの生成は一度だけにして、何度も生成されないように変更します。
また、既存の処理は関数にまとめるなどリファクタリングも並行しておこないました。
機能追加+リファクタリングしたので参考にしてください。
今回作成する機能の仕様を決めていきます。
前回まではダブルクリックをしたらシャドウDOMを<body>の末尾に追加していましたが、今回はダブルクリックをした位置に表示させます。
使う吹き出しのCSS:【CSS自作UI】AIに頼らずCSSで吹き出し(tooltip)を作ってみた
機能追加+リファクタリングしたコードです。
import css from './index.css?inline'
let clickCount = 0; // クリックした回数
let bubbleHost: HTMLDivElement | null = null; // 吹き出しを覆うDOM
let bubbleText: HTMLSpanElement | null = null; // 吹き出し内のテキスト
/**
* 吹き出しを生成する
*/
function createBubble(x: number, y: number): { host: HTMLDivElement; tooltipText: HTMLSpanElement } {
// CSS
const sheet = new CSSStyleSheet();
sheet.replaceSync(css);
// Shadow DOMで吹き出しを作成
const host = document.createElement('div');
host.id = 'shadow';
host.style.position = 'absolute';
host.style.left = `${x}px`;
host.style.top = `${y}px`;
host.style.zIndex = '9999';
// 吹き出しの中の要素を追加
// ルート
const shadow = host.attachShadow({ mode: "open" });
shadow.adoptedStyleSheets = [sheet];
// 内枠
const tooltip = document.createElement("div");
tooltip.className = "tooltip";
// テキスト部分
const tooltipText = document.createElement("span");
tooltipText.className = "tooltip-text";
tooltipText.textContent = "";
// 要素を追加
tooltip.appendChild(tooltipText);
shadow.appendChild(tooltip);
document.body.appendChild(host);
return { host, tooltipText };
}
document.addEventListener('dblclick', function () {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return;
}
const selectedText = selection.toString();
const selectionRect = selection.getRangeAt(0).getBoundingClientRect();
const bubbleX = selectionRect.left + selectionRect.width / 2 + scrollX;
const bubbleY = selectionRect.bottom + scrollY;
// 既にDOMが生成されている
if (bubbleHost && bubbleText) {
bubbleText.textContent = `${clickCount}回目のクリック`;
bubbleHost.style.left = `${bubbleX}px`;
bubbleHost.style.top = `${bubbleY}px`;
clickCount++;
return;
}
if (!selectedText) {
return;
}
// DOMを新規作成
const bubbleElements = createBubble(bubbleX, bubbleY);
bubbleHost = bubbleElements.host;
bubbleText = bubbleElements.tooltipText;
bubbleText.textContent = `${clickCount}回目のクリック`;
clickCount++;
});前回までで使っていたコードはcreateBubble()関数にまとめました。
function createBubble(x: number, y: number): { host: HTMLDivElement; tooltipText: HTMLSpanElement } {
// CSS
const sheet = new CSSStyleSheet();
sheet.replaceSync(css);
// Shadow DOMで吹き出しを作成
const host = document.createElement('div');
host.id = 'shadow';
host.style.position = 'absolute';
host.style.left = `${x}px`;
host.style.top = `${y}px`;
host.style.zIndex = '9999';
...
}注目ポイント1は、CSSの適用です。
今回はViteを使っているので、1行目のimportで別ファイルのCSSを読み込みます。
その後、CSSStyleSheet()クラスを用いることで現在のコードにCSSを適用させています。
注目ポイント2は、シャドウDOMで吹き出しを作成する部分です。
クリックした場所に表示するには、直接CSSのプロパティを変更するしかありません。
そのため、host.style.left, host.style.topでクリックした座標を代入しています。
クリックした位置に表示したい場合、2通りの方法が思いつきました。
そもそもDOMというものはオブジェクトであり、生成する度にメモリを消費します。
クリックする度に生成していると、そこそこコストを使うわけです。
そのため、毎回生成するよりも一度だけ生成して位置だけ更新する方が軽い処理になると判断しました。
そのコードが次の部分です。
既にDOMが生成されているときは、位置とテキストのみ更新して早期リターンで処理を止めます。
// 既にDOMが生成されている
if (bubbleHost && bubbleText) {
bubbleText.textContent = `${clickCount}回目のクリック`;
bubbleHost.style.left = `${bubbleX}px`;
bubbleHost.style.top = `${bubbleY}px`;
clickCount++;
return;
}個人で作るレベルだとメモリのことを意識する必要は無いかもしれません。
しかし、最終的に大きいアプリを作るときにこの考えは大事になると思っています!
この機能を作るにあたり、知らない関数がまだまだあると感じました。
そういう関数に限って日本語の記事が少ないので、この記事で少し分かる人が増えれば良いと思います。
今後も引き続き、TypeScriptの記事を増やしていきますので、役に立った方はシェアしていただけると喜びます!