印刷用スタイルを @media print から代替スタイルシートへ変更

公開日

このサイトにはヘッダーやサイドバーを非表示にするなどの微調整を行う印刷用スタイルを設定しています。

これまでは共通で読み込むCSSファイルの中に @media print で記述していたのですが、ファイルを分けた(print.css に分離)うえで代替スタイルシートへ変更しました。

代替スタイルシートはHTML4 時代から存在する概念(www.w3.org)ですが、切替メニューを有するブラウザが少ない[1]のと、CSSファイルの数が少し増えてしまう(一つのファイルにまとめることができない)デメリットがあります。

一方、 @media print の中に書いてしまうやり方だと、印刷時には強制的に適用されてしまう問題があります。万人にとって意味のあるスタイル指定ならともかく、ヘッダーやサイドバーを非表示にするといったようなものは、ディスプレイ表示と同じ状態を印刷したいという選択肢をユーザーから奪うことになってしまうのであまり良くありません。

そこで、 JavaScript を使って以下のようにしてみました。

  • 代替スタイルシートの切替機構があるブラウザ(実質 Firefox のみ)では印刷用スタイルを代替スタイルシートとして適用し、ユーザーが任意に切り替えられるようにする(画面表示と同じ状態を印刷することもできるし、逆に印刷用スタイルを画面表示時に適用することもできる)
  • 代替スタイルシートの切替機構がないブラウザ(Chrome 83, Safari 13.1, Edge 83 など)では印刷用スタイルを強制適用

スタイルシートの指定(HTML)

<link> 要素を使って代替スタイルシートの指定を行います。その際、印刷用スタイルファイル(print.css)には JavaScript で特定するための ID を振ります。

<link rel="stylesheet" href="common.css"> <!-- 固定スタイルシート -->
<link rel="stylesheet" href="screen.css" title="画面表示"> <!-- 優先スタイルシート -->
<link rel="alternate stylesheet" href="print.css" title="印刷" id="stylesheet-print"> <!-- 代替スタイルシート -->

print.css には media="print" はあえて指定しません。当サイトの印刷用スタイルは紙やインクを節約する目的で必要性の低いパーツを非表示にする微調整のみであり、細かく最適化をしているわけではありませんから、印刷時以外に適用されると困るような性質ではありません。前述のように「印刷用スタイルを適用させて画面で閲覧したい」というユーザーがいるかもしれませんから、選択肢を狭めるようなことはしていません。

切替機構がないブラウザへの印刷用スタイルの強制適用(JavaScript)

JavaScript では以下の2つの処理を行います。

  1. ブラウザが代替スタイルシートの切替機構を有しているかどうか調べる
  2. 代替スタイルシートの切替機構がない場合のみ、印刷用スタイルファイル(print.css)を強制適用する

ブラウザが代替スタイルシートの切替機構を有しているかどうか調べる

ブラウザが代替スタイルシートの切替機構を有しているかを一発で判断するプロパティがあればいいのですが、どうもドンピシャなものはなさそうです。

そこで各ブラウザの挙動を調べてみたところ、 document.styleSheets で取得した代替スタイルシートの disabled の値が Firefox 76, IE 11, Opera 12 は true で、 Chrome 83, Edge 83 は false になることに着目しました。

const result = [...document.styleSheets].some((styleSheet) => {
  return styleSheet.title !== null && styleSheet.ownerNode.relList.contains('alternate') /* title 属性が存在し、 rel 属性値に 'alternate' が含まれていれば代替スタイルシートである */
    && styleSheet.disabled;
});

Safari 13.1 は他ブラウザと挙動が異なり、代替スタイルシートが document.styleSheets に含まれません。上記コードでは代替スタイルシートが存在しない場合は常に false を返すため、 Safari も false になります。

この StyleSheet.disabled の定義は仕様にはこう書かれています。

false if the style sheet is applied to the document. true if it is not. Modifying this attribute may cause a new resolution of style for the document. A stylesheet only applies if both an appropriate medium definition is present and the disabled attribute is false. So, if the media doesn't apply to the current user agent, the disabled attribute is ignored.

Document Object Model (DOM) Level 2 Style Specification - 1.2. Style Sheet Interfaces - disabled of type boolean(www.w3.org)

あくまでドキュメントにスタイルシートが適用されているかどうかが分かるものであり、代替スタイルシートの切替機構があるかどうかではありません。現在、切り替え機能のない各ブラウザはたまたま代替スタイルシートを false にしていますが、切り替えメニューがUIとして存在しないだけで代替スタイルシートであることの認識自体はする(デフォルトで適用はされない)ため、 true を返しても仕様上おかしくはない気がします。

なので、この判定プログラムはかなり危ういもので、今後のブラウザのアップデートによっては正しい挙動をしなくなる可能性がないとは言えません……。もっといい方法をご存知の方はご教示ください。

代替スタイルシートの切替機構がない場合のみ、印刷用スタイルファイル(print.css)を強制適用する

前述のプログラムで「切り替え機能がない」と判定された場合のみ、以下の処理を実行します。

const printStyleSheetSetName = document.getElementById('stylesheet-print')?.title; // 印刷用スタイルシートの名前
const preferredStyleSheetList = []; // 優先スタイルシート

for (const styleSheet of document.styleSheets) {
  const styleSheetSetName = styleSheet.title;
  if (styleSheetSetName !== null) {
    if (styleSheetSetName === printStyleSheetSetName) {
      /* 対象の印刷用スタイルシートと同じ名前のスタイルシートは新たに <link> 要素を生成する */
      const printStyleSheetElement = document.createElement('link');
      printStyleSheetElement.href = styleSheet.href;
      printStyleSheetElement.rel = 'stylesheet';
      printStyleSheetElement.media = 'print';
      document.head.appendChild(printStyleSheetElement);
    } else if (!styleSheet.ownerNode.relList.contains('alternate')) {
      /* title 属性が存在し、 rel 属性値に 'alternate' が含まれない場合は優先スタイルシート */
      preferredStyleSheetList.push(styleSheet);
    }
  }
}

window.addEventListener('beforeprint', () => {
  /* 優先スタイルシートを一時的に無効にする */
  for (const preferredStyleSheet of preferredStyleSheetList) {
    preferredStyleSheet.disabled = true;
  }
});
window.addEventListener('afterprint', () => {
  /* 優先スタイルシートを有効に戻す */
  for (const preferredStyleSheet of preferredStyleSheetList) {
    preferredStyleSheet.disabled = false;
  }
});

print.css の名前( <link> 要素の title 属性値)と同じ名前を持つスタイルシートをすべて取得し、新たに <link> 要素を作って挿入します。こちらには media="print" を設定しているので、印刷時にしか適用されないというわけです。

また、それだけだと印刷時に優先スタイルシートが適用されたままになってしまうので、beforeprint(html.spec.whatwg.org)イベントを検知して一時的に disabled 状態にします('afterprint' で解除します)。

最終的にはこんなイメージになるというわけです。

<link rel="stylesheet" href="common.css"> <!-- 【最初からHTMLにある要素】常に適用される -->
<link rel="stylesheet" href="screen.css" title="画面表示"> <!-- 【最初からHTMLにある要素】デフォルトでは適用されるが、印刷時は無効になる -->
<link rel="alternate stylesheet" href="print.css" title="印刷" id="stylesheet-print"> <!-- 【最初からHTMLにある要素】常に適用されない -->
<link rel="stylesheet" href="print.css" media="print"> <!-- 【JavaScript で挿入した要素】印刷時のみ適用される -->
  • [1]現在アップデートが行われているブラウザでは Firefox 76 は「表示」-「スタイルシート」から切り替え可能で、ほかに Opera 12 以前や IE 8 ~ 11 も切り替えメニューを備えています。 Chrome 83, Safari 13.1, Edge 83 等は切り替え機構がありません。