Skip to content
Published on

ウェブエディタに定規が無い本当の理由 — 測る定規が無いのではなく、測る紙が無いのです

シェア
Authors

Wordから貼り付けた文書が崩れる地点

社内の文書ツールをウェブへ移すプロジェクトでほぼ必ず出てくる要求があります。Wordのようにルーラーをつけてほしい、というものです。段落ごとに左余白を三角形で引いて合わせ、最初の行だけ字下げし、表の列境界を目盛りの上でつかむ、あの道具です。

開発の側からはこの要求が些細に見えます。定規の形の目盛りをひとつ描いてドラッグハンドルを載せればよさそうだからです。ところが着手すると初日で詰まります。目盛りに3cmと書くには画面の何ピクセルが3cmなのかを知らなければならないのに、その答えがありません。

誰も作らなかったわけではありません

これは作る人がいないから空いている場所ではありません。最近公開されたeditor-rulerのREADMEはこの状況をこう整理します。古典的なウェブのWYSIWYGエディタ、つまりFroalaやTinyMCEやCKEditor 5やQuillのようなものはルーラー無しで出てきて、ルーラーがあるのはSyncfusionやDevExpressやONLYOFFICEのように文書モデルを丸ごと抱えている重いコンポーネントだけだ、というものです。

この分類がヒントです。ルーラーがある側と無い側を分けるのは開発人員でも製品成熟度でもなく、文書モデルを持っているかどうかです。

ウェブにはページがありません

Wordでルーラーが成立する理由は、紙が先に決まっているからです。A4を選べば幅が210mmで確定し、左右の余白がそれぞれ25mmなら本文幅は160mmです。ルーラーはこの160mmを描く定規であり、三角形を3cmの位置へ引けばそれは紙の左端から3cmという意味です。画面サイズを変えてもこの数字は変わりません。紙は画面と無関係に存在するからです。

ウェブにはその紙がありません。編集領域の幅はブラウザのウィンドウサイズとサイドバーの状態と拡大率によって変わり続けます。そしてブラウザの物理単位は実際の物理長ではありません。CSSで1インチは96ピクセルと定義された値であり、その96ピクセルが画面上で実際に何ミリメートルなのかはモニタごとに違います。

だからウェブエディタにルーラーを描くというのは、無い紙をひとつ仮定する作業です。問題は定規を描く技術ではなく、何を基準に測るかを決めることです。

コンテンツ幅を紙とみなすという選択

editor-rulerが選んだ方式はコアAPIにそのまま表れています。使う側が現在の状態を知らせる関数を渡すようになっています。

import { createRuler } from '@devslab/editor-ruler';

const ruler = createRuler(mountElement, {
  unit: 'cm',
  getMetrics: () => ({ contentWidth, leftMargin, rightMargin, firstLineIndent }),
  onChange(change, phase) {
    // 現在の段落にピクセル値を適用する。離した瞬間に phase が commit になる
  },
});
ruler.refresh(); // 選択や内容が変わるたびに呼び出す

ここで読むべきなのは第一引数ではなくgetMetricsです。ルーラーは紙幅を自分では知らず、毎回ホストに尋ねます。つまり紙の役割をするのは編集領域の現在のコンテンツ幅であり、その幅が変わればルーラーの数字も再計算されます。

この選択には代償があります。画面を狭めるとルーラーの3cmの位置が実際には別の場所を指すようになります。だからこの定規はWordの定規と名前が同じだけで別のものを測っています。Wordの定規は印刷される紙を測り、この定規は今画面に取られている編集領域を測ります。チームに導入するとき利用者に必ず伝えなければならない違いです。

コアとアダプタに分けた構造

このプロジェクトは目盛りとドラッグ処理と単位換算だけを担当する依存関係の無いコアを置き、エディタごとの差は薄いアダプタに分離しました。Froala、Tiptap、CKEditor 5、Summernoteのアダプタがそれぞれ別パッケージとして出ています。

この構造が必要な理由は、エディタごとに字下げを保存する場所が違うからです。Tiptapアダプタは字下げをノード属性として保存しインラインCSSでレンダリングします。CKEditor 5アダプタはモデル属性として保存しダウンキャストの段階でインラインCSSに変えます。FroalaとSummernoteのアダプタはDOMを直接触ります。ルーラーがする計算は同じなのに、その結果をどこへ書くかが四つとも違います。

ここで前の論旨がまた出てきます。ルーラーをつけるというのは、段落の余白をどこに保存するかを決める作業です。ノード属性なのかインラインスタイルなのかによって、貼り付けと書き出しと共同編集の動作が全部変わります。UI部品をひとつ載せる決定ではありません。

印刷が割り込むと紙がまた生まれます

ここで決定が分かれる二つめの地点が出てきます。この文書が画面でしか読まれないなら、紙は最後まで存在しません。ところがほとんどの社内文書ツールは結局印刷されるかPDFとして出ていきます。その瞬間に紙がまた生まれます。

印刷経路があるなら、ルーラーが測るべきなのは編集領域の幅ではなく印刷される紙の本文幅です。ウェブでその紙を宣言する場所はCSSの@page規則であり、ここで用紙サイズと余白が決まります。つまりルーラーの基準を決める作業は、印刷スタイルシートの値と同じ値を見させる作業になります。

この二つを混ぜるともっとも悪い結果が出ます。利用者が画面基準で3cmに合わせて印刷すると、紙の上では3cmではありません。そうなるとルーラーがあるせいでかえって信頼を失います。だから選択肢は事実上二つです。編集領域を固定幅の偽の紙にして画面と印刷を一致させるか、ルーラーが測っているものは印刷結果ではないという点をUIではっきり言うか。どちらもしないことが、今多くのツールがルーラーを入れない理由でもあります。

作ってみると引っかかる細かいこと

このプロジェクトのREADMEには、実際に作ってみた人だけが書ける項目がいくつかあります。二つが特に実務的です。

第一に、取り消しの単位です。ドラッグ一回は数十回の値変更を生み出します。そのままにすると取り消しを二十回押さないと元の位置に戻りません。このプロジェクトはドラッグ一回を取り消し一段階にまとめると明記します。前のAPIでphaseがコミットかどうか区別される理由がこれです。

第二に、縦ルーラーをオンオフするときのレイアウトのずれです。縦の定規が現れるとその分だけ本文が押されます。このプロジェクトは場所をあらかじめ空けておくオプションを置き、その動作をCSSのscrollbar-gutter: stableになぞらえて説明します。このたとえは問題の性格を正確に突いています。あとで現れうるUIは、現れる前に場所を取っておかなければならないということです。

このプロジェクトの現状をありのままに書くと

誇張できない部分をはっきりさせておきます。このリポジトリは作られてから数日しか経っておらず、スター数は一桁で、個人あるいはごく小さなチームが作ったものです。ライセンスはApache-2.0、言語はTypeScriptで、npmパッケージが上がっています。READMEには1.0.0から安定しているという文言がありますが、これは第三者の評価ではなく作った本人の宣言です。

だから私はこの記事でこのライブラリを使えとは勧めません。プロダクション導入の可否は各自で検証すべき問題です。このプロジェクトが今の時点で見る価値があるのは別のところです。問題を正確に記述した文書がついているからです。著者はページモデルが無いときルーラーが何を意味しうるのか、作りながら引っかかったのが何だったのかを別に文章として整理しておきました。自分で作る計画があってもなくても、その問題定義は読む値打ちがあります。

まとめと出典

ウェブエディタにルーラーが無いのは機能が抜けているのではなく、前提が無いのです。紙を決めなければ測れず、紙を決めた瞬間それはUIではなく文書モデルの決定になります。社内ツールでこの機能を要求されたなら、実装を見積もる前に何を紙とみなすかから合意してください。

  • devslab-kr/editor-rulerリポジトリ — コアAPI、アダプタごとの保存方式、取り消しの処理、縦の定規の場所予約オプション。本文のこのプロジェクトに関する記述はすべてREADMEで確認しました。
  • Every web rich-text editor is missing a ruler — リポジトリが背景記事としてリンクしている著者の文章
  • リポジトリのメタデータで確認した事項: ライセンスApache-2.0、言語TypeScript、リポジトリ作成時点とスター数。成熟度についての記述はこれらの数値に基づくものであり、コード品質を評価したものではありません。
  • CSSで1インチが96ピクセルと定義される点は、CSS Values and Units仕様の絶対長単位の定義によるものです。