今でも簡単にできるリキッドレイアウトを模索する

Hatena::agenda - 本物のリキッドレイアウトの具体化。

リキッドレイアウトは動的なスタイルシートによって実現する。CSS2は閲覧環境に応じてある程度柔軟な変化を提供するが、まだ充分ではない。今現在でもJavascriptの助けを借りれば、本物のリキッドレイアウトに近づけることはできる。

リキッドレイアウトを助ける主なstuffは、今のところ次の四つ。

  • 1. CSS2のfloatプロパティ本来の使い方
  • 2. CSS2のfloatプロパティでのページレイアウト
  • 3. CSS2のtableでのページレイアウト
  • 3. CSS3のマルチカラムの先行実装

1. CSS2のfloatプロパティ本来の使い方

floatプロパティは「サイドバー」を表現するために作られたのでは、ない。テキストが回りこむ浮動オブジェクトを描くのがfloatプロパティの本来の姿である。しかし例えばあるテーブルにfloatプロパティを指定したとき、たとえそれがウィンドウの幅の8割を占めていても、テキストは回りこんでしまう。すると一行が異常に短くなってしまうことがあり、読みづらいことこの上ない。充分に小さなテーブルや画像に対してのみfloatを指定したいが、どの程度が充分に小さいかを製作者が判断することはできない。

この問題を解決するために、動的にスタイルシート(クラス名)を変更する。ウィンドウ幅に対して半分以下の幅をもつtableなら浮動オブジェクト化し、そうでなければ浮動させないようにすればいい。イメージとしてはこのようなスクリプトだ。

function optimizeLayout(){
	var w = document.documentElement.clientWidth;
	for each(let e in document.selectNodes("descendant::table"))
		e.className = e.clientWidth/w < 0.5 ? "FloatingObject" : "";
}

window.addEventListener("resize", optimizeLayout, true);
window.addEventListener("load", optimizeLayout, true);

半分というのは単純化のための例に過ぎず、より良いリキッドレイアウトを追求するなら「ウィンドウ幅の残りが20em以上」のような計算が必要である。フォントサイズがユーザーの好みに応じて可変となり得るメディアにおいては、width(幅)の概念は一文字の大きさが基準となる。

2. CSS2のfloatプロパティでのページレイアウト

イメージとしてはこのような構造だ:

<body>
    <div class="container">
        <div class="contents" href="contents.xht" rel="contents"></div>
        <div class="main">本文</div>
        <div class="appendix" href="appendix.xht" rel="appendix"></div>
    </div>
</body>

もちろんdiv要素にhref属性などない。言いたいのは「埋め込むな。参照せよ。」ということだ。実際には目次や付録はlink要素でリンクしておき、必要となった時に埋め込む。

  1. CSSのem単位に相当するpx値を計算する
  2. メイン領域(div class="main"; max-width: 30emの指定あり)の幅が30em以上となり、左右の余白が15em以上となったところで、contentsとしてリンクされているXML文書をdiv要素(div rel="contents")内にロードし、左側に浮動させる。
  3. さらに余白が15em以上となった時、appendixとしてリンクされているXML文書をdiv要素(div rel="appendix")内にロードし、右側に浮動させる。

15emとか30emという値、右側、左側というのは一例に過ぎない。

3. CSS2のtableでのページレイアウト

2.とほぼ同じだが、floatではなくより適切なdisplay: tableを利用しようということ。

4. CSS3のマルチカラムの先行実装

本文部分のブロック要素の高さの合計値が表示領域の高さ未満であったなら、本文部分を包括するdivブロックに対して-moz-multi-column-widthを指定する。しかしそのような適切なdivブロックが常にあるとは限らない。

長文の場合、本文部分のブロック要素(主にp要素)のshrink to fitした場合の「面積」を順々に加算していき、表示領域の面積を超えない範囲でdiv要素で包括するというプロセスが必要となる。

逆に短い文章、小さなコンテンツはマルチカラム化できない。表示領域の高さに対し30%未満の場合、前述の2.と3.の方法で外部リソースをロードした方が良い。

Javascript1.7のカスタムイテレータとXPath

数年前Hatena::agendaで公開したselectNodesメソッドの実装に加えて、カスタムイテレータを定義してfor each文で使えるようにしてみた。

_XPathNSResolver

まず_XPathNSResolverを定義。非XMLなHTMLで使う限りこれは読み飛ばし可。経緯はHatena::agenda - 2003-12-04

function _XPathNSResolver(nsmap, nodeResolver){
  this._nsmap = nsmap;
  this._resolver = nodeResolver?
    nodeResolver.ownerDocument.createNSResolver(nodeResolver) : null;
}
_XPathNSResolver.prototype.lookupNamespaceURI = function(prefix){
  var v;
  if (v = this._nsmap[prefix])
    return v;
  if (v = this._resolver)
    return v.lookupNamespaceURI(prefix);
  return v;
};

_XPathNSResolverの使い方。第一引数に接頭辞とURIのペアのオブジェクトリテラルを渡してインスタンスを作って、evaluateメソッドの第三引数に渡す:

/* Tipical usage of _XPathNSResolver
var de = document.documentElement;
var type = XPathResult.FIRST_ORDERED_NODE_TYPE;
var nsresolver = new _XPathNSResolver(
	{xht: "http://www.w3.org/1999/xhtml"}, de);
var result = document.evaluate(
	'descendant::xht:*', de, nsresolver, type, null);
 */

Nodeインターフェイスを拡張

次。Nodeインターフェイスを拡張。まず個人的に良く使う手でスコープを作る:

(function(__proto__/* Node.prototype */){

次にselectNodesメソッドを書く:

var resolver = (
	Document.prototype.xpathNSResolver = new _XPathNSResolver({}, null)
);
__proto__.selectNodes = function(expression){
	var doc = this.nodeType == Node.DOCUMENT_NODE ?
		this : this.ownerDocument;
	var result = doc.evaluate(
			expression, // XPath expression
			this, // Context node
			resolver, // Name space resolver
			XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, // Result type
			null // XPathResult if reuse
		);
	if (result.snapshotLength == 0) return null;
	return new _NodeList(result);
};

たぶん毎回_XPathNSResolverを作るのがアレだからdocumentオブジェクトのメンバにして使いまわそうとしたんだろう>自分。ここ改善の余地。

_NodeListコンストラク

で、ここからがついさっき書き加えた部分。_NodeListコンストラクタ。selectNodesメソッドの戻り値はNodeListなので、それに似たものを自作。

function _NodeList(result){ // 「死」んでいる点に注意する
	this.__index = 0;
	this.__result = result;
	this.length = result.snapshotLength;
}

_NodeList自身をカスタムイテレータ

いよいよ本題というか、_NodeListのメソッドと共にカスタムイテレータを定義。Javascript1.7は……本当にいいものだ。

(function(__proto__){
	__proto__.__iterator__ = function(){
		return this;
	};
	__proto__.item = function(index){
		return this.__result.snapshotItem(index);
	};
	__proto__.next = function(){
		var item = this.item(this.__index++);
		if (!item) {
			this.__index = 0;
			throw StopIteration;
		} else {
			return item;
		}
	};
})(_NodeList.prototype);

_NodeList自身をイテレータにした。こうすることでfor each文を自然なPython風に使用できるという寸法。おっとスコープを閉じなきゃ。:

})(Node.prototype);

使用例

で使い方なんだけど例えば次のようなタスクを考える:

  • 文書中のUL要素とOL要素全てについて、子のLI要素の数が4未満のものに"short"というクラス名を付けてやる

DOMでは割と小面倒な部類なんじゃないかな。北村さん(誰)に突っ込まれそうなクラス名だけど気にしない。

for each(let e in document.selectNodes("descendant::ul | descendant::ol"))
		e.selectNodes("child::li").length < 4 && (e.className = "short");

終了。XPathをもっとうまく使う別解もあり。

Googleをマルチカラム環境に最適化する

CSS3のマルチカラム(草案)の先行実装を利用すると、Googleの検索結果がモニタ上で一望できる。それに伴ってGoogleナビゲーションその他の位置も最適化してみた。

  1. 「次へ」という最も利用する頻度の高いナビゲーションは常に右上に表示しフォントサイズも巨大にしてクリックしやすくする。
  2. ついでにアクセスキーも設定してキーボードで操作しやすくしておく。
  3. 検索結果のステータス(「100件中 1 - 10件 何秒」とかいうあれ)は縦幅を取って邪魔なので、ブラウザのステータスバーに移動させる。
  4. ログイン情報が縦幅を奪う理由は何もないので、最下部へ移動していただく。

滅多に褒めてくれないうちの嫁さんも「いいね」って言ってた。greasemonkeyスクリプトとユーザースタイルシートで実現している。

先日のユーザースタイルシートに少し指定を加えた:

@-moz-document domain("www.google.com"), domain("www.google.co.jp") {
body {margin: 0; padding: 0 }
div#res{
  -moz-column-width: 25em;
  -moz-column-gap: 1em;
}
.purlOrgJintrickNext b{
  font-size: 30pt !important;
}
div#res table {
    width: 100%;
}
#mbEnd{ display: none; }
}

追記:長いURL対策

Google検索結果には緑色の文字でURLが表示されていて、ウェブページの信頼性を判断する基準として利用できるのだが、これが長すぎると折り返されず、隣のカラムにはみ出して重なってしまう。これを防ぐためoverflowプロパティを追加指定した。

/* 緑色のURL部分 */
span.a{
  display: block;
  overflow-x: auto;
  overflow-y: hidden;
}

-moz-column-*系プロパティが真価を発揮するとき

しまったなんで気づかなかったんだろう。いやこの糞ぶろぐでリキッドマルチカラムをやるというのが無謀というかそりゃデメリットがあり過ぎるだろ何を今更っていうツッコミはやる前から重々承知していた。-moz-colomn系のマルチカラムは文章量によって適切な指定が異なるから、文章量の不定なHatena::agendaに適用させても縦スクロールを要求してしまう記事も出てくるんだ。そうではなくて、-moz-column系プロパティを、どうして今までGoogle検索結果ユーザースタイルシートとして使わなかったかってことだ。つまりコンテンツの物理的な量が概ね分かっていて、かつその量を閲覧者が調節できて、かつ一覧性が重要なものに対して、-moz-column-widthプロパティは適切なスタイル指定となる。

@-moz-document domain("www.google.com") {
div#res{
-moz-column-width: 25em;
-moz-column-gap: 1em;
}
div#res table {
width: 100%;
}
}

人によってはgoolge.co.jpかも。よく分からなかったらStylishとか使えば幸せになれるかも。ウィンドウを最大化してGoogleUXGA以上推奨かな。WXGAのノートでも相当使い勝手がいいけど。フィットしなければ表示件数を調整すべし。

今まで様々なブラウザを同時に使ってきたし、特定のブラウザを薦めるということはしなかった。だが今は敢えて言おう。Firefoxこそ真のウェブブラウザであると。こういうことが出来てこそウェブブラウザだ。まあ明日になったら気が変わっていると思うけど。わらい。いやーなんで昔検証したとき気づかなかったんだろう馬鹿だな自分。

追記。縦書きならリキッドなマルチカラムの弱点が消えるという話。Re:-moz-column-*系プロパティが真価を発揮するとき 及び Re:リキッド・マルチカラム考察 (インターネット帳面)。そもそも一寸した縦スクロールが本当に問題なのかという示唆もあり。

Hatena::agenda - Googleをマルチカラム環境に最適化するに続く...

本物のリキッドレイアウト - 補足

Hatena::agenda - 本物のリキッドレイアウトの続き。

リキッドなマルチカラムの実例

Hatena::agendaは実験場ではないけれども、CSS3 Columns - MDCで紹介されているmozilla独自拡張プロパティを使用してリキッドマルチカラムにしてみた。デメリットもかなりあってまだ改善の余地があり余っているものの、場合によっては低解像度から高解像度まで、シングルウィンドウから並行閲覧まで手広くカバーできる。

このプロパティを実装していないブラウザ向けに、一応スクリーンショットを用意してみた。

念を押しておくが、-moz-で始まる独自拡張プロパティは、正しく使えば問題ない。これはCSS validatorを通らない。しかしCSS実装は、不明なプロパティは無視しなければならないことになっているし、事実上-moz-で始まるプロパティを他のベンダが重複して定義することなどない。一部の似非W3C信者に騙されないように。

高解像度も視野に入れるのが本物のウェブデザイン

Liner Note - リキッドカラムデザインの実験 :実践1カラムテクニックス(3)より引用:

今まで「どの解像度環境を最低基準にしてデザインを進めるか」ばかりが注目されてきて、「高解像度環境」ではそれに見合ったメリットが得られないどころか、逆に見づらくなるデザインも散見されてきたと思います。

笑えるのは、それを率先してやっているのが専門家だという事実。情報デザインの何とかとか、ウェブユーザビリティの何とかを自称する連中だ。おかげでPersonnelに影響されました!とか言っていた人も釣られて固定幅に改悪してしまった。

私は固定幅デザインを絶対にウェブデザインとは認めない。だって、様々な媒体を選択可能なウェブの特性とは何の関係もないもの。そういった無数の固定幅デザインを各媒体向けにサーブする技術の一部としてありかもしれないけれども、それ単体をウェブデザインと思い込んでいるのは大間違いだ。特に日本ではウェブユーザビリティの専門家を名乗る連中がこれをやらかし始めている。リキッドレイアウト、及びウェブデザインというものの本質を最初から理解していなかった証拠だ。彼らが一時的にリキッドレイアウトを採用したのは、単にそのころ800×600px程度の低解像度モニタの利用者が、1024×768pxと同程度に存在した為、両者の都合に合わせてみたからに過ぎない。つまり彼らは、ウェブデザインをやっていたのではなくて、800×600pxと1024×768pxの二つにマッチさせようとしていただけなのだ。だから今になって800×600pxの解像度がマイナーになって一つの解像度に分布が集中するやいなや、笑止、態度を豹変させて待ってましたとばかり安易な固定幅デザインに移行したというわけだ。

Hatena::agenda - -moz-column-*系プロパティが真価を発揮するときに続く...

本物のリキッドレイアウト

どうやら世の中の「リキッドレイアウト否定派」はリキッドレイアウトの真の姿をまるで知らないらしい。想像力が足りないのだろう。リキッドレイアウトというのは、ウィンドウ幅を小さくしても横スクロールバーがでないとか、本来そんなちんけなものではない。

リキッドレイアウトを否定するというのは、ウェブデザインを放棄することと同義だ。固定レイアウトというのは、例えば1024*768px向けデザインとでも言うべき代物で、ウェブデザインと呼ぶに相応しくない。

本物のリキッドレイアウトは隙間に流れ込む水のような振る舞いをする。Hatena::agenda - ブログのサイドバーは要らないではサイドバーを否定したが、実は一つ例外がある。本物のリキッドレイアウトを採用しているなら、サイドバーも有用となり得るのだ。

もちろんサイドバーとは何かを知っていなければならない。簡単にいえば、サイドバーのようなインターフェイスは、ユーザーのタスクの完了を補助するために使用できる。言い換えると、ユーザーが、今見ているメインの領域ではタスクを完了できない場合に、ユーザーをタスク完了のための適切なコンテンツにナビゲートしたり、ヒントを与えたりする為のある程度柔軟性のあるスペースとなり得るのが、サイドバーである。要するに意味不明なぶろぐぱーつや全然関係ない広告でなければそれでいい。

まず本文の部分はCSSでいうところのmax-widthプロパティが指定されたようにふるまう。閲覧者のウィンドウ幅が充分広くなると、当然左右、あるいは左右どちらかにスペースができる。そのスペースがある一定以上の幅になったとき、そのスペースに、それまで本文の下部にあった(あるいは異なるURIをもつ外部にあった)補足情報などがサイドバーとして登場する。つまり、ウィンドウをたくさん並べて並行閲覧していても、あるいは低解像度のモニタで閲覧しても、本文がキチンと表示されて利用しやすいリソースとなり、一方、高解像度のモニタでウィンドウを大きくしてみる閲覧者には、その投資に見合ったそれ相応の利益がある。

リキッドレイアウトのもう一つの有用な姿を。現在のウェブブラウザのページのメタファは、未来の利用者、あるいは現在のハイエンドな高解像度モニタ利用者にとっても本当に酷いものだ。あんな最低な縦スクロールで長い文章を読む気になるのは、他に選択肢がないからだろう。しかし本物のリキッドレイアウトは違う。あまった隙間に、今までスクロールして見ていた部分が流れ込むようになるのだ。本の見開き一ページのメタファを持ったスタイルシートをかぶせることも容易になる。もちろん閲覧環境によって1カラムだったりするし、場合によっては3カラムにも変化する。

実装について考えると、HTML5などという馬鹿げた進化の痛手を被る必要は全くない。本物のリンクタイプを色々追加して標準化してしまえばよいだけの話だ。例えば、margin:autoの隙間へリンクタイプ「contents」の外部リソースを自動で流し込むとか。ああ、「contents」は元々あったっけ。じゃあ今すぐにでもできるだろ。ついでにarticle要素とやらも、相当するclass名を標準化してしまえ。それで後方互換もバッチリだ。まあ私に言わせればarticle要素というのはbody要素のことだけどな。わらい。iGoogleかなんかでは「ぱーつ」をポンポン放り込んでカスタマイズできるようだけれども、全てのウェブページで出来たほうが圧倒的に便利。右上には「author」というタイプでリンクされた外部リソース、左側には同様に「contents」とか。全てのウェブページで同一の構成を保持できればもう迷うこともないだろう。

つづき

リキッドレイアウトの弱点を克服

リキッドレイアウト採用にあたって問題点を再検討してみた。ブラウザで解決できる問題だとは思うので、リキッドレイアウトに起因するデメリットではないが、少なくとも現在ある問題としてこれは明らかにまずい。

  • ブラウザのサイドバーを利用するなどしてメイン領域の幅を変更すると、文字の表示位置が大きくずれ、読んでいた場所が分からなくなる。

今のところ回避方法としては、内容が長大な場合には複数のページに分割するなどして縦方向のスクロールを多く要求しないようにすることくらいしかないが、よく考えてみればそれはウェブデザイン的に結構基本的なことだったりする。

ここで陥ってはならないのが、逆も真(謎)と思い込んでしまって「ある程度縦長にならざるを得ない場合には幅固定の選択肢もあり」と判断してしまうミス。複数のページに分割とは、何も物理的に分割しなければならないわけではない。こういう止むを得ない状況を打破するためのCSS + Javascriptであり、スライドショーのような方法で解決できる。