定義
ユーザーが文字をドラッグで選択すると、その上に**浮かぶツールバー(フローティングツールバー — 太字・斜体のようなボタンが出る小さなポップアップ)**を出したいとき、そのツールバーをいつ表示するか決める方法に関するパターンだ。
よく使う方式はマウスボタンを押すとき(mousedown)ツールバーを隠し、離すとき(mouseup)また出すというものだが、この記事はその代わりに**document の selectionchange イベント**(ブラウザで選択範囲が変わるたびに発生するイベント)で更新しようという話だ。選択範囲が変わるたびにツールバーに必要な情報の束(snapshot — 選択が空か、画面上の位置 rect、太字かどうかといった状態を一度にまとめたもの)を再計算するので、ドラッグしている最中にもツールバーを出せる。
なぜ必要か
マウスを押すとき隠し離すとき見せる方式は、「選択が完全に終わってからやっとツールバーが出る」体験になる。ドラッグ中にリアルタイムでハイライトを見せたり、すぐにフォーマットボタンを出したいなら、マウスの状態ではなく選択そのものの変化に合わせる必要がある。
さらにマウスイベントだけに依存するとよく壊れる: HTML5 ドラッグ&ドロップ(要素を引っ張って置くブラウザの標準機能)はドロップが終わった後 mouseup を送らない場合があり、「押したのに離す信号が来ない」状態でツールバー表示ロジックが止まる(stuck)ことがある。selectionchange はこの落とし穴を避ける。
動作原理
- ユーザーが pointer でテキストをドラッグする。
- ブラウザが Selection range を更新し
selectionchangeを発火する。 - ハンドラーが
window.getSelection()・anchor/focus node を読んでツールバー snapshot(空か、rect、marks)を計算する。 - 空でなければフローティング UI をレンダーする。
- ハイブリッドエディタでは anchor/focus が「現在の guest container」外なら単一ブロックツールバーは隠し、上位の range-level ツールバーが所有する — 二重 UI 防止。
selectionchangeはスクロールを発生させない — anchor 座標の drift は scroll リスナーで別途処理する。
実務適用
isSelectingText+ mousedown/mouseup +requestAnimationFrameの応急処置を除去する。- 単一ブロック guest は editor トランザクションで bold/italic を維持; クロス境界だけ document-level handler + patch。
- テスト:
document.execCommand('selectAll')または programmatic selection の後、ツールバーが mouseup なしで現れるか確認する。 - Storybook・ブラウザでドラッグ選択・クロス境界選択・二重ツールバーなしを目視検証する。
トレードオフ
selectionchangeは入力中に頻繁に発火しうる — snapshot ビルドは軽く、rect equality で bail する。- shadow DOM・複数の contenteditable があると container 境界の判別が必要だ。
- すべてを patch 経路に統合すると既存の align・PM semantics の回帰リスク — 単一ブロックは guest を維持する方が安全な場合が多い。
使ってはいけない場合
- 選択と無関係な固定ツールバー(常に visible)。
- ブロック並べ替え DnD ハンドルだけの UI — テキスト selection がなくフォーマットツールバーが無意味。
- 「常に表示」が要求されるが実際には空の selection —
!emptyの self-gate を維持する。
よくある間違い
- ブロック DnD とテキスト selection UI を同じ mouse フラグでまとめる。
- クロス境界で単一・range ツールバーを同時表示(container ガード欠落)。
- selectionchange だけで scroll anchor まで解決しようとする。
- jsdom で layout rect がない → 視覚検証をブラウザに委ねない。
関連概念
- fixed-position-floating-ui-scroll — スクロール時の anchor 再測定
- focused-guest-editor — guest 1 ブロックの lease
- contenteditable-keyboard-history-delegation — キーボード・履歴の委任