ことれいのもり

【TypeScript】クリックした位置にシャドウDOMを表示する機能を実装してみた

はじめに

前回の記事では、シャドウDOMを実装するコードをJavaScriptからTypeScriptに書き直しました。

このコードには、最大の問題があります。

それは、ダブルクリックをする度にシャドウDOMが生成されてしまうということです!


そこで今回は、シャドウDOMの生成は一度だけにして、何度も生成されないように変更します。

また、既存の処理は関数にまとめるなどリファクタリングも並行しておこないました。

機能追加+リファクタリングしたので参考にしてください。


前回の記事:シャドウDOM実装をJavaScriptからTypeScriptに書き直してみた

仕様を決める

今回作成する機能の仕様を決めていきます。

前回まではダブルクリックをしたらシャドウDOMを<body>の末尾に追加していましたが、今回はダブルクリックをした位置に表示させます。


  • ダブルクリックをした位置にシャドウDOMを生成する
    • 既にシャドウDOMが生成されているときは位置とテキストだけ更新する
    • シャドウDOM生成処理は関数でまとめる
  • 見た目は以前作成した吹き出しのCSSを使用
  • Chrome拡張で使うことを想定


使う吹き出しのCSS:【CSS自作UI】AIに頼らずCSSで吹き出し(tooltip)を作ってみた

制作環境

  • TypeScript
  • Vite

ダブルクリックした位置にシャドウDOMを生成する

機能追加+リファクタリングしたコードです。


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++;
});

シャドウDOM(吹き出し)生成処理

前回までで使っていたコードは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でクリックした座標を代入しています。

既にシャドウDOMが生成されているときの考え方

クリックした位置に表示したい場合、2通りの方法が思いつきました。


  1. 現在のシャドウDOMを削除して、新しいシャドウDOMを生成する
  2. 現在のシャドウDOMをそのまま使い、位置だけ更新する


そもそもDOMというものはオブジェクトであり、生成する度にメモリを消費します。

クリックする度に生成していると、そこそこコストを使うわけです。

そのため、毎回生成するよりも一度だけ生成して位置だけ更新する方が軽い処理になると判断しました。


そのコードが次の部分です。

既にDOMが生成されているときは、位置とテキストのみ更新して早期リターンで処理を止めます。


  // 既にDOMが生成されている
  if (bubbleHost && bubbleText) {
    bubbleText.textContent = `${clickCount}回目のクリック`;
    bubbleHost.style.left = `${bubbleX}px`;
    bubbleHost.style.top = `${bubbleY}px`;
    clickCount++;
    return;
  }


個人で作るレベルだとメモリのことを意識する必要は無いかもしれません。

しかし、最終的に大きいアプリを作るときにこの考えは大事になると思っています!

おわりに

この機能を作るにあたり、知らない関数がまだまだあると感じました。

そういう関数に限って日本語の記事が少ないので、この記事で少し分かる人が増えれば良いと思います。

今後も引き続き、TypeScriptの記事を増やしていきますので、役に立った方はシェアしていただけると喜びます!

参考サイト

CSSStyleSheet:[CSSStyleSheet - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/CSSStyleSheet)