paged.jsで作るPagedMedia帳票

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/46e449de-2952-c08d-e8bb-b25c16c84195.png" alt="javascript" width="80">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/0d8bb0da-00a6-dee8-a550-e5c57a751f70.png" alt="css" width="80">

帳票、契約書などのビジネス文書をページ化メディアで印刷するまでのロードマップ。

## Paged Media

Paged MediaとはCSS3のページ化メディア= **版物**の用紙サイズ、ふち、改ページなどのスタイルを指定するためのど真ん中の仕様セット。

`ページ化メディア`とか`ページ組版`とか`Paged Media`とか、いかにも専門チックな用語が登場するが、ページ概念のあるスタイルセットという理解でOK。

しかしながら、いま現在もメジャーブラウザの対応状況はまちまちだそうで、ブラウザ互換性という意味で安定していないようだ。

## ライブプレビュー

それでも`PagedMedia`で印刷をスタイリング出来るのは結構だが、(印刷ダイアログではなく)スクリーンでも精度の高いライブプレビューを再現したいというもの。

残念ながら`PagedMedia`はスクリーンのそれとは異なるため、スクリーンのスタイルは別途作り込む必要がある。

単ページの帳票なら印刷ダイアログっぽく用紙サイズのページ要素のコンテナを作ってCSSオンリーでスタイリング出来るが、
可変ページ概念が出てくるとページ要素の増減、ページを横断する`段落`/`テーブル`要素の分割&リビルドとか、`画像`は改ページして、、気の遠くなる調整実装が待っている。

## paged.js

そこで、上記の気の遠くなるページ化メディア調整をしてくれる**paged.js**というオープンソースのポリフィルがある。

スクリーンでPagedMediaオリエントにDOMを再構築しれるので、スクリーンを印刷に合わせるのではなくて、印刷にスクリーンを再現させることができる。

使用するPaged Media仕様は`@page`ディレクティブの用紙サイズ`size`/マージン`margin`/ふち印字`@bottom-center`/改ページ制御`break-inside`あたり

### 実装

#### スクリプト

次のCDNスクリプトを読み込むと自動的にpaged.jsのポリフィルが走る。
[Getting Started with Paged.js —](https://pagedjs.org/documentation/2-getting-started-with-paged.js/)

```html:html
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"></script>
```

2024.3.18現在 CDNで公開中のステーブルバージョン**v0.4.3**

```javascript:javascript
/**
 * @license Paged.js v0.4.3 | MIT | https://gitlab.coko.foundation/pagedjs/pagedjs
 */
```


#### 文書を組む

##### paged.jsは`@media print`クエリを展開する

paged.jsはPagedMediaを完全擬態しようとするので`@media print`クエリを展開する。

逆に`@media screen`クエリはデフォルトで無視するため、スクリーンにのみ適用したいスタイルは`link`タグや`style`タグに`media=“screen”`指定することで適用してくれる。

```html:html
<!-- linkはmedia=“screen”を入れる -->
<link rel="stylesheet" type="text/css" href="screen.css" media="screen">

<!-- styleタグにmedia="screen" -->
<style media="screen">  
  p {  
    color: red;  
  }  
</style>
```

じつは上記仕様に至るに破壊的な変更があり、v0.4.0以前はスクリーン向けには本来の`@media screen`クエリを展開していたが、スクリーンも完全なPagedMediaオリエントへと変遷したようだ。


以前v0.2を利用していて、今回現最新版のv0.4.3を試したところ、プレビュー専用のスタイルが言うことを聞かないので頭を悩ませた。
> バージョン 0.4.1 以降、Pages.js は `@media screen`のメディア クエリ内にある CSS の解析を停止します。
[Paged Break, the long overdue update —](https://pagedjs.org/posts/paged-break-the-long-overdue-update/)

##### キャンバス

以下のスタイルを当てると用紙サイズのキャンバスができるので、あとは画面ノリで組むだけ。

![](image%203.png)<!-- {"width":173} -->![image 3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/fe22bd14-d203-3faa-69d7-d6172c365d72.png)

```html:html
<!-- スクリーンにのみあてるスタイル -->
<style>
	@page{
		/* A4・横向きでサイズ指定 */
    	size: A4 landscape;
		/* ふちを20mmとる */
    	margin: 20mm;
		/* 各ページのフッター中央に { 現在ページ } / { 総ページ } を印字 */
    	@bottom-center{
        	content: counter(page) '/' counter(pages);
    	}
	}
</style>

<!-- スクリーンにのみあてるスタイル media="screen" -->
<style media="screen">
body{
	/* 背景色を指定してプレビューぽくする */
    background: #666;
}

/* ポリフィルがあてるシートとそのコンテナブラッシュアップ */

/* コンテナクラス は縦並びでセンタリングする*/
.pagedjs_pages {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    width: calc(var(--pagedjs-width));
    margin: 0 auto;
}
/* ポリフィルがあてるページシートクラス */
.pagedjs_page {
    margin: 5mm;
    border: 1px solid #333;
    background: #fff;
}
</style>
```

**スクリーンに帳票を組んだ結果**

![image 4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/59592017-35ca-ec58-796f-ea443fc20b26.png)

**印刷プレビュー**
スクリーンをそのまま再現できている

![image 6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/7a876774-ba75-5d3b-7e95-d50e21393008.png)

**シートから溢れた要素を適宜改ページしてくれる**

![image 5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/39fe9730-0e12-b1d6-d55a-20030c6cb9e3.png)

### 表の改ページ精度問題

シートから溢れた文字列とかブロック要素は問題なく改ページしてくれるが、`Table`、`Grid`は下図のようにちょん切れたり(widthもありえない数字)、不可視領域に吹っ飛んだり動作不安定でまともに使えず、公式フォーラムでもこの手のイシューがちらほらあるが猛者たちが調整で頑張っている。

[Paged.JS and CSS Flex / Grid](https://thinkdrastic.net/journal/2022/02/07/paged-js-and-css-flex-grid/)
[Paged.js TABLE breaks · Issue \#149 · pagedjs/pagedjs](https://github.com/pagedjs/pagedjs/issues/149)
[Chrome: Missing row content on page break within table \(v0.4.3 vs v0.5.0-beta.0\) \(\#431\) · イシュー · pagedjs / pagedjs · GitLab](https://gitlab.coko.foundation/pagedjs/pagedjs/-/issues/431)

![image 7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/502b7fec-cbd3-4f64-15b3-f60f2e8060e8.png)

表に関しては下記のようなFlexテーブルで代替する。それでもちょん切れる場合は**行要素**に改ページ禁止`break-inside: avoid;`を当てるとうまくいく。

```html:html
<style>
    .flex-column{ display: flex; flex-flow: column;}
    .flex-row{ display: flex; flex-flow: row;}
    .flex-cell {
        flex-grow: 1; padding: 2pt 6pt 2pt 6pt;
        border: 1px solid #000;
    }

    /* 隣接するセル間のボーダーを重複させない */
    .flex-row .flex-cell:not(:first-child) {
        border-left: none;
    }
    .flex-row:not(:first-child) .flex-cell {
        border-top: none;
    }
    .flex-row.head .flex-cell {
        text-align: center;
    }
    .flex-cell:nth-of-type(1){flex-basis: 30%;}
    .flex-cell:nth-of-type(2){flex-basis: 20%;}
    .flex-cell:nth-of-type(3){flex-basis: 10%;}
    .flex-cell:nth-of-type(4){flex-basis: 10%;}
    .flex-cell:nth-of-type(5){flex-basis: 30%;}

    /* 項目と表途中の改ページ禁止 */
    .no-break{
        break-inside: avoid;
    }

</style>

<div class="flex-column">
    <div class="flex-row head">
        <div class="flex-cell">品名</div>
        <div class="flex-cell">規格</div>
        <div class="flex-cell">単価</div>
        <div class="flex-cell">金額</div>
        <div class="flex-cell">摘要</div>
    </div>

    <div class="flex-row">
        <div class="flex-cell">1</div><div class="flex-cell">2</div><div class="flex-cell">3</div><div class="flex-cell">4</div><div class="flex-cell">5</div>
    </div>
    <div class="flex-row">
        <div class="flex-cell">2</div><div class="flex-cell">2</div><div class="flex-cell">3</div><div class="flex-cell">4</div><div class="flex-cell">5</div>
    </div>
    <div class="flex-row">
        <div class="flex-cell">3</div><div class="flex-cell">2</div><div class="flex-cell">3</div><div class="flex-cell">4</div><div class="flex-cell">5</div>
    </div>
    <div class="flex-row">
        <div class="flex-cell">4</div><div class="flex-cell">2</div><div class="flex-cell">3</div><div class="flex-cell">4</div><div class="flex-cell">5</div>
    </div>
    <div class="flex-row">
        <div class="flex-cell">5</div><div class="flex-cell">2</div><div class="flex-cell">3</div><div class="flex-cell">4</div><div class="flex-cell">5</div>
    </div>
    <div class="flex-row no-break">
        <div class="flex-cell" style="flex-basis: 100%; height: 36mm;">
            【備考欄】
        </div>
    </div>
</div>

```