■ 画面に表示されるアイテム数を自動調整する はてなブックマーク - YaneuLabs このエントリーを含むはてなブックマーク

近年、ディスプレイ解像度は急速に高くなり、いまや1920×1200は当たり前になりつつある。こうなると難しいのはサイトレイアウトであって、今後、商用サイトを固定幅で設計するのはタブーになっていくのだろう。大きな画面でも正常に表示されなければならない。このようにユーザーの画面サイズに合わせてレイアウトを動的に変更するのはそれほど容易なことではない。

例えば、Amazonのトップページの場合を考えてみよう。



横幅が小さなときは、アイテム数は3つ。



横幅が大きいときは、アイテム数が自動的に5つにまで増える。

■ アイテム数自動調整スクリプト

では、このようなことをCSSのテクニックだけで実現できるのかと言えば、ノーだろう。 どうしてもJavaScriptに頼らざるを得ない。

さっそく書いてみた。→ アイテム数自動調整スクリプト テスト用ページ

上のテスト用ページのhtml,JavaScriptは自由に使っていただいて構わない。アイテムの間隔などはユーザー側で自由に設定できるようになっている。

// 以下の値は、サイトによって調整すべし。

screenMinWidth : 910, // 画面の横幅の最小値(設計値)
minRowItems : 4, // 商材の1列の最小表示数
maxRowItems : 8, // 商材の1列の最大表示数
itemWidth : 100, // 商材の幅(商材のdivの幅)
minItemMarginRight : 40, // 隣の商材との最小幅。


また、最大の列数は以下のところで設定する。

var onResize = function ()
{
AutoItemLayout.onResize('pickup_item',2); // css class名が'pickup_item'のところは最大2列
AutoItemLayout.onResize('newest_item',5); // css class名が'newest_item'のところは最大5列
};


私は、最初、こんなものは簡単だろうと思ったが、いくつかの点で難しかった。技術的につまずきやすいところ(私がつまずいたところ)を以下に書いておく。

■ JavaScriptによる画面レイアウトの調整のポイント

ここでは、JavaScriptで画面レイアウトを調整するときのポイントをいくつか書いておく。

以下では、IE7,Safari,Chrome,FireFoxをターゲット環境とする。IE8が出たというのに、いまさら2世代前のIE6なんかに対応させることもないと思う。

・ 画面の幅の取得

動的にレイアウトを調整するには、まずユーザーのブラウザのサイズを取得する必要がある。
横幅はdocument.body.clientWidthで取得できる。

参考) ブラウザの表示領域のサイズを取得する方法
http://d.hatena.ne.jp/onozaty/20060802/p1

古いSafariではwindow.innerWidthだったが、最新のSafariならdocument.body.clientWidthで取得できる。

IEもDOCTYPEを宣言していた場合はdocument.bodyではなく、document.documentElement要素で定義されるのだが、互換性の確保のため、document.body.clientWidthでも取得できるようになっている。

結局、document.body.clientWidthだけで良いと思う。
画面サイズが取得できたので、これを利用して各アイテムの幅などを調整していく。

・ getElementByIdは避けたほうが無難。

何かと問題があるのでgetElementByIdは、避けたほうが無難。

参考)
getElementByIdのバグ検証
http://d.hatena.ne.jp/CATAN/20080410/1207834314


代わりに、次のようなgetElementsByClassを用意して、css class名を指定するだとか。

// css class名から、そのelement一覧を返すhelper
function getElementsByClass(className)
{
  var classElements = new Array();
  var allElements = document.getElementsByTagName('*');
  for (var i = 0 ; i < allElements.length; i++)
  {
    if (allElements[i].className == className)
      classElements.push( allElements[i] );
  }
  return classElements;
}


・ 表示/非表示の切り替え

display:noneを設定すると非表示になる。
getElementsByClassで取ってきて、画面幅を考慮しながらdivブロックをdisplay:noneにしたりしてdivブロックの表示/非表示を切り替える。

・ floatプロパティ

まず、floatプロパティをJavaScriptから設定しようと思うとIEとそれ以外とでプロパティ名が異なる。これは次のように書く。

function cssFloat(element , style)
{
  // IEは'styleFloat',Firefox,Safariは'cssFloat',Operaはどちらも大丈夫

  var styleFloat = (element.style.styleFloat === undefined ? 'cssFloat' : 'styleFloat')
  element.style[styleFloat] = style;
}

注意すべきことは、UserAgentを見て判別はしてはいけないということだ。
UserAgentは容易に偽装されうるので、UserAgentを見て処理を分岐させる方法は他に手段がない場合以外はやるべきではない。

・ floatにnoneを設定するな

divブロックを(改行せず)横一列に配置したい場合、普通、float:leftを指定する。

本当は、display:inlineを指定してインライン要素にしたいのだが、インライン要素は幅や高さを持てないため、幅や高さが無視されてしまう。これは望むものではない。display:inline-blockを指定して、置換インライン要素にしてしまえば良いのだが、これは非対応のブラウザがあるので使えない。結局、float:leftを指定するしかない。

そこで、いま、このfloat:leftを動的に解除したいのだが、その場合、float:noneと指定してもChromeでは解除されない。(IEだと解除される) 結局、float:noneを指定したときの動作はブラウザ依存であって、使うべきではない。floatを解除したいなら、その次のブロックで、clear:bothを指定するしかない。

・ elementNode.insertAfter

divブロックでfloatを指定している場合、そのfloatを解除しようと思うと次のブロックでclear:bothをしなければならないことは上で説明したが、その次のブロックはclear:bothを設定するだけのものだから、これは動的に生成してしまいたい。

動的に生成するとき、よく使うのはappendChildだろう。今回の場合、divブロックの直後にinsertしたいので、elementNode.insertAfterのようなメソッドがあれば良いのだが、insertBeforeはあってもinsertAfterは無い。(何故無いのかその理由は知らないが…)

参考)
DOM Samples /Core Node/insertBefore()
http://allabout.co.jp/internet/javascript/closeup/CU20040627A/


DOM Samples /Core Node/removeChild()
http://allabout.co.jp/internet/javascript/closeup/CU20040728A/


outerHTMLを書き換えようにも、IEではこれはreadonly。また、document.body.innerHTMLをごりごり書き換えようにも、設定したあと、document.bodyの他の要素の更新がなされないため、上記のgetElementsByClassなどで新たに追加した要素を取得できない。

結局、動的に追加するなら、insertBefore/appendChildなどを使う必要がある。
また、insertAfterを実現するには次のようなコードを書く必要がある。

// HTML elementの直後に別のelementを挿入する。
function insertAfter(targetElement,insertElement)
{
  var parent = targetElement.parentNode;

  // 次の要素はnextSiblingで辿れるのでinsertBeforeすれば良いのだが、
  // 最後の要素だとnextSiblingがnullになるのでその場合はappendChildする。
    if (targetElement.nextSibling)
      parent.insertBefore(insertElement,targetElement.nextSibling);
    else
      parent.appendChild(insertElement);
}

■ 関連記事

プログラマが1ヶ月でWebデザイナーに転身する方法
http://d.hatena.ne.jp/yaneurao/20090318


■ 更新履歴

2009.03.25 公開