ここ最近の開発では、CSS のカスタムプロパティ(CSS 変数)を活用する機会が増えていますが、calc()
関数や var()
関数を組み合わせると値を予測するのが難しくなることがあります。
具体的な例として、以下はブログ記事「CSS で学ぶ三角関数」のデモで使用したコードを改変したものです。
ここでは、コードの内容を理解する必要はありませんが、cos()
と sin()
の 2 つの関数を使用して円周にオブジェクトを配置しており、カスタムプロパティ --angle
の値に応じてオブジェクトの位置が変わるといったものです。
上記のコードを見てわかるように、calc()
や var()
といった関数、さらには cos()
、sin()
のような関数まで加わると、一見してどのようなスタイルが適用されるのかを把握するのは容易ではありません。
このわかりづらさを打開する方法はないものかと考え、手軽にスタイルの計算後の値を確認するための Bookmarklet「css-console」を作成しました。
Bookmarklet
見出し「Bookmarklet」今回作成した Bookmarklet は、以下のリンクをブラウザのブックマークバーにドラッグ & ドロップすることで登録できます。
Bookmarklet のコードは、以下の GitHub Gist にもアップしています。
https://gist.github.com/griponminds/159fb9e3aac44b4d48d57cb87878ec29
Bookmarklet とは
Bookmarklet(ブックマークレット)は、ウェブブラウザのブックマークから JS のコードを実行する仕組みのことをいい、javascript:
スキームを即時関数で実行することによって実現します。
javascript:(() => {
/* 実行するスクリプト */
})();
例えば、以下は閲覧している記事の title
を prompt
で表示する Bookmarklet です。
javascript:(() => {
prompt('title', document.title);
})();
まず、以下の手順で Bookmarklet を登録します。
- 適当なウェブページをブックマーク(このページでも可)
- ブックマークを編集して、URL を JS のコードに差し替える
- ブックマーク名をわかりやすい名前に変更(任意)
試しに、先ほどの Bookmarklet を登録して実行すると、ダイアログが表示されテキストフィールドにページの <title>
が表示されます。
デベロッパーツールのコンソールに、JS コード全体をペーストしても実行可能です。
なお、Bookmarklet は JS なので、以下の制約が挙げられます。
- Content Security Policy(CSP)の指定内容によっては、スクリプトの実行が拒否される
- ブラウザの拡張機能や、ドキュメントで読み込んでいる JS や CSS と競合する場合がある
<iframe>
や Shadow DOM の内側には、基本的にはアクセス不可- 異なるオリジンのリソースには、基本的にはアクセス不可
また、ブックマークとして登録するときに 1 行のコードになるため、JS を記述するときには以下の点に気をつける必要があります。
- 行末のセミコロン(
;
)を省略しない - コメントは複数行コメント(
/* */
)を使用する
使用方法
見出し「使用方法」Bookmarklet を実行すると、以下のウィジェットが表示されます。
項目 | 内容 |
---|---|
タイトル | Bookmarklet の名称 |
Close | ウィジェットを閉じるボタン |
ID | アイテム ID |
値 | 計算済みのスタイルの値 |
セレクタ | CSS セレクタの入力フィールド |
プロパティ | CSS プロパティの入力フィールド |
インスペクタ | インスペクタモードに切り替えるボタン |
削除 | アイテムを削除するボタン |
追加 | アイテムを追加するボタン |
再計算 | スタイルの値を再取得するボタン |
ウィジェットの移動
見出し「ウィジェットの移動」「タイトル」の領域をドラッグすることで、ウィジェットを移動できます。
ウィジェットの表示・非表示
見出し「ウィジェットの表示・非表示」「Close」ボタンを選択するとウィジェットが非表示になり、移動した位置と変更したサイズはリセットされます。ただし、入力した値はページを再読み込みしない限り維持されます。
また、Bookmarklet の JS を実行したときも同様です。実行するごとにウィジェットの表示・非表示が切り替わりますが、値は破棄されずに位置とサイズのみリセットされます。
この機能は、ウィジェットをビューポートの外側まで移動させてしまい、元の位置に戻せないときなどにも活用できます。
アイテムの追加・削除
見出し「アイテムの追加・削除」「追加」ボタンを選択すると直後にアイテムが挿入されます。異なる HTML 要素の値を並べて表示したり、同じ要素の異なるプロパティを並べて表示したいときなどに使用します。
「削除」ボタンを選択するとそのアイテムは削除されます。
スタイルの計算済みの値を表示
見出し「スタイルの計算済みの値を表示」この Bookmarklet のメインの機能ですが、「セレクタ」と「プロパティ」を入力すると「値」に計算済みの値が表示されます。
「セレクタ」に入力した文字列は document.querySelector()
に渡されるので、セレクタにマッチする要素が複数ある場合には、最初の要素が対象になります。例えば「p」と入力した場合には文書内で最初に出現する <p>
要素が対象になります。
もし、2 番目以降の要素を対象としたい場合や、画面内から自由に要素をピックアップしたい場合には「インスペクタ」ボタンを選択して、インスペクタモードを有効にします。
- インスペクタモードはマウスポインタに特化しているため、キーボードやタッチデバイスでの操作はサポートしておりません。
::before
、::after
擬似要素は、インスペクタモードでは選択できませんが、例えば「セレクタ」のフィールドに.foo::after
と直接入力することで対象とすることがきます。
インスペクタモードを有効にすると、以下のようにウィジェットが一時的に非表示になり、HTML 要素を選択するモードに切り替わります。
もし、インスペクタモードを途中で終了したい場合には、ESC キーを押下します。
HTML 要素にホバーするとハイライト表示されます。そのとき、要素名と id
属性、class
属性もあわせて表示されます。要素をクリックするとウィジェットが再表示され、選択した HTML 要素のセレクタが反映されます。
なお、この状態から「セレクタ」の値を直接変更すると、インスペクタモードで選択した要素とのリンクが解除され、document.querySelector()
で取得する通常の状態に戻ります。
スタイルの値を再取得
見出し「スタイルの値を再取得」「再計算」ボタンを選択すると、スタイルの値を再取得します。例えばサイズや位置を変更したり、ダークモードに切り替えたときに、変更後の値を取得するために使用します。
現時点では継続的に値を取得する機能は備えていません。
デベロッパーツールで計算済みの値を確認する
見出し「デベロッパーツールで計算済みの値を確認する」スタイルの計算済みの値は、本来であればブラウザ標準のデベロッパーツールを使用して確認します。
本記事での Bookmarklet と比較すると、ブラウザ標準の機能なので当然ながらパフォーマンスに優れており、サイズや位置を変更したときなどにリアルタイムに値を取得できます。また、<iframe>
や Shadow DOM の内側にもアクセス可能です。
一方で、計算済みの値にたどり着くまでの経路が長かったり、複数要素の値を同時に確認できなかったりと、ちょっとした使いづらさを感じます。
例えば、Google Chrome で特定のカスタムプロパティの計算済みの値を確認するには、以下のステップを踏む必要があります。
- デベロッパーツールを起動
- 「Elements(要素)」を選択
- 「Computed(計算済み)」を選択
- 「Show all(すべて表示)」を有効化
- 「Filter」で対象のカスタムプロパティを検索
Firefox や Safari のデベロッパーツールでは、より少ないステップでたどり着けますが、どの方法を採用したとしても一長一短があると感じています。
基本設定の変更
見出し「基本設定の変更」ウィジェットは Shadow DOM として実装しており、JS の先頭に記述しているクラスフィールドで基本設定を指定しています。
以下にその一部を抜粋しますが、#title
は「タイトル」で表示されるテキストで、#inputDebounce
はテキストフィールドに入力したときの処理を実行する遅延時間(ms)です。
#styles
では CSS のコードをテンプレートリテラルで格納しています。
theme
レイヤーで指定しているカスタムプロパティの値を変更して、ウィジェットの配色やフォント指定、基本となるサイズや余白を変更できます。
既知の問題
見出し「既知の問題」現時点では以下の問題や課題を認識しています。
継続的に値を取得できない
見出し「継続的に値を取得できない」現状は変更された値は継続的に取得されません。そのため、インタラクティブ性の高いコンポーネントの場合、その都度「calc」ボタンを選択する必要があり非効率です。
パフォーマンスを悪化させないように注意を払う必要はありますが、例えば「calc」ボタンを「sync」スイッチのようなものに変更して、スイッチがオンの状態では継続的に値を取得するのがよいかもしれないと考えています。
計算途中の値を確認しづらい
見出し「計算途中の値を確認しづらい」スタイルの値は getPropertyValue()
を使用して取得していますが、通常のプロパティを指定した場合には、計算済みの値しか確認することができません。
対して、カスタムプロパティ(--*
)を指定すると、var()
関数のみ展開されて、calc()
、sin()
、cos()
といった関数は計算されずにそのままになります。
具体的な例を見ていきます。以下は記事の冒頭で紹介した CSS コードの抜粋です。
ここで、JS の getPropertyValue()
を使用して、通常のプロパティである translate
と、カスタムプロパティである --translateX
、--translateY
の値を取得します。
このように、カスタムプロパティでは var()
のみが展開され、計算前の状態を確認することができますが、理解しやすいとはいえません。
インスペクタモードにおけるクリックイベントの制御
見出し「インスペクタモードにおけるクリックイベントの制御」インスペクタモードでは、リンク遷移を無効にする目的で、一時的にすべての <a>
要素に対して preventDefault()
を指定しています。
しかし、ドキュメントで読み込まれている JS のイベントは防止できないため、例えばページ遷移をするクリックイベントが指定されていると、要素を選択したときに実行されてしまいます。
stopPropagation()
メソッドおよび stopImmediatePropagation()
メソッドなどを試してみたのですが、現状の構成ではうまくいかず、回避する方法は見つかっていません。
状態の保持
見出し「状態の保持」ブラウザをリロードしたり、別ページへ遷移するとウィジェットは消えてしまいます。
また、フレームワークを使用して開発しているときに、ファイルを更新するとブラウザで開いているページに変更箇所が反映されますが(ホットリロード)、このときに <body>
要素以下の DOM が置き換わるとウィジェットごと消えてしまいます。
この問題に対応するには、sessionStorage
などを使用して、一時的に入力内容を保存する機能を実装する必要があります。
ただ、インスペクタモードで紐づけた要素は保存できないので、それらも踏まえて実装を見直すことになりそうです。
Bookmarklet が対応できないケース
見出し「Bookmarklet が対応できないケース」<iframe>
の内部(「CodePen」のコードなど)や、Shadow DOM の内側の HTML 要素は取得できないので対象外です。
加えて、前述した Content Security Policy(CSP)の指定内容や、ブラウザの拡張機能、ドキュメントで読み込まれている JS や CSS の内容によっては、正しく動作しない可能性があります。
おわりに
見出し「おわりに」今回作成した Bookmarklet が、実際の開発の場面で活用できるかどうかは、もう少し多くの場面で使っていかないとわかりません。また、現時点ではいくつか課題も残っているので、引き続きブラッシュアップを重ねていきたいと考えています。
本記事では、具体的な実装内容についてはあまり言及できませんでしたが、まとめられるトピックがあれば、別途ブログ記事のなかで取り上げていきます。