簡易テキストビューアーを作ってみました

前回のエントリ のテスト結果もそうなのですが、いろんな情報をテキストファイルに残しています。 手軽でいいですよね。 ただ長いテキストファイルを参照するのは面倒なので、Text Viewer という簡単な JS アプリを作成してみました。

http://rinco.jp/app/text-viewer.html?url=http://rinco.jp/study/body-view-size.txt

のように、?url= の後に対象のテキストファイルを指定して閲覧します。 項目を閉じられるのがポイントかな。

日本語を使用する場合には、テキストファイルを UTF-8 形式で保存するのがよいでしょう。 僕は昔から TeraPad を愛用させてもらってますが、最近は Windows 標準のメモ帳でも UTF-8 保存ができますね。

あ、Ajax の制限により同一ドメインにあるファイルしか表示できないとおもいます。 他サイトにあるテキストファイルを表示したい場合には、上記 text-viewer.html をそのサイトにコピーして実行してください。

簡易的な実装なので、いろいろ改造しても良いとおもいます。 利用も改変も特に制限はありません。

テキストファイルに適用される3つのルール

1) '=== ' で始まる行は大見出しです。 Text Viewer 上ではセクション帯として表示されます。

=== は3文字以上であれば何文字でもかまいません。 ただ最後にスペースを忘れないでください。 またその行の最後に === が3文字以上続いていた場合には削除されます。 以下は全て有効な指定です。

=== 大見出し
===== 大見出し
===== 大見出し =====

2) '*** ' で始まる行は小見出しです。 Text Viewer 上では展開可能な領域として表示されます。

*** は3文字以上であれば何文字でもかまいません。 ただ最後にスペースを忘れないでください。 またその行の最後に *** が3文字以上続いていた場合には削除されます。 以下は全て有効な指定です。

*** 小見出し
***** 小見出し
***** 小見出し *****

*** だけの行があると、そこで展開可能な領域は終了します。

3) 行の先頭が http:// もしくは https:// で始まっている場合、その行は URL リンク として表示されます。

コンテンツ表示領域のサイズを得る

コンテンツの表示領域のサイズ

コンテンツの表示領域のサイズをきちんと知りたいときってありますよね。 厳密には以下の三種類があるとおもわれます。 サイズ的には A) <= B) <= C) となるでしょう。

 A) スクロールバーの状態に影響されない最低限の領域
 B) その時点で実際に利用できる領域
 C) スクロールバーも含めたコンテンツ領域すべて

スクロールバーの有無で表示に影響がでる (例: 画面の中央に表示したはずが、スクロールバーが表示されると少しズレる) のを嫌う場合、スクロールバーの領域は使わないでおく A) の方法がよく利用されています。 とはいえ A) のサイズをきちんと得るのは難しいので、スクロールバーを常に出しっぱなしにするような CSS を利用したりします。

余談ですが IE6,IE7,IE8 では、右側に表示される縦スクロールバーは 常に表示 されるようです。

現在の表示領域をめいっぱい利用したい場合は、B) のサイズが必要です。 スクロールバーが表示されていない場合は、その部分も活用したいわけです。 例えばポップアップで画像を拡大表示する際、領域の端に隙間ができると間抜けに見えちゃうわけで。

さて、prototype.js には表示領域のサイズを得る document.viewport.getDimensions() などがあります。 が、v1.6.0.2 で試してみて驚きました。 非常にメジャーな IE6 SP2 の互換モードで 0 の値が返ってきてしまいました...。

実験してみる

迷ったらとりあえずいろいろ試してみる、結果みて考える、というのが僕のパターンです。 で、用意したテストページは以下の4つ。


それぞれのテストページは、5つのテストケースを含んでいます。 例えば Width を調べる場合は以下のような感じで、各テストケースでそれっぽい値をガーッと書き出しています。

 1) 200x200 の黄色い箱を表示 (BODYマージン無し、スクロールバー無し)
 2) 箱の横幅を 2000px に変更 (BODYマージン無し、横スクロールバー表示)
 3) BODYマージンを 8px に変更 (BODYマージン 8px、横スクロールバー表示)
 4) BODYマージンを 0 に、箱の縦幅を 2000px に変更 (BODYマージン無し、両スクロールバー表示)
 5) BODYマージンを 8px に変更 (BODYマージン 8px、両スクロールバー表示)

テストページですが、結果が * になっている行は5つのテストケース全てで同じ値を返したことを示しています。 またURLハッシュを指定すると、テストケースを中断することができます。 #1 でテストケース1のみの実行。 それぞれのテストケースの実際の状況 (例:本当にスクロールバーが表示されているか) を知りたい場合に使用してみてください。

仮説をたててみる

とりあえず B) の実際に利用できる領域を求めてみます。

テストページでいろいろ試しているうちに、標準準拠モードでは document.documentElement.clientWidth が、互換モードでは document.body.clientWidth が利用できそうにみえました。 以下のような感じです。 評価式の最後は必要なさそうですが、ま、念のため。

function getViewWidth() {
  return document.compatMode && document.compatMode=="CSS1Compat" && document.documentElement.clientWidth ? document.documentElement.clientWidth : document.body.clientWidth;
}

また「単にサイズが縦横100%の要素を表示してみて、そのサイズを測るか、値をコピーすれば良いのでは?」という別のアプローチで getViewWidth2() も追加してみました。 100% の意味って?とかいろいろ未調査なので、まぁ、オマケだとおもってください。 コードもエイヤ書きしたものですし...。

function getViewWidth2() {
  var e = $('_getViewWidth2_work');
  if (!e) {
    e = $(document.createElement("div"));
    e.setAttribute('id','_getViewWidth2_work');
    e.setAttribute('display','none');
    document.body.appendChild(e);
 }
  e.setStyle({position:'absolute', left:'0px', top:'0px', width:'100%', height:'100%'});
  return e.getWidth();
}

更に、LightBox v2.04 に getPageSize() という関数があったので、これもテスト対象に加えてみました。

テスト結果

今回、試してみたのは以下の環境です。 最後の2つはオマケ。


テスト結果はテキストファイルにまとめてみました。 以下のビューワー経由で見てみてください。

http://rinco.jp/app/text-viewer.html?url=http://rinco.jp/study/body-view-size.txt

期待の getViewWidth(), getViewHeight() ですが、Opera 以外はわりと良い感じでした。 以下、懸念点を2つ。

まず、Firefox 2.0.0.14 における getViewWidth() の値が少しおかしなことになっています。 以下のように Case2,3 で 475 のはずが、458 に。 getViewHeight() のほうは問題ありません。

 1: getViewWidth(): 475 (number)
 2: getViewWidth(): 458 (number)
 3: getViewWidth(): 458 (number)
 4: getViewWidth(): 458 (number)
 5: getViewWidth(): 458 (number)

テストページを #2 や #3 を付けて表示するとわかりますが、何故か Firefox 2.0.0.14 では「横スクロールバーが表示されると、つられて縦スクロールバーも表示されてしまう」という動作になりました。 不思議ですが、実際にスクロールバーが表示されており、getViewWidth() の返した値は正しい、ということになります。

ちなみにこれ、Firefox 3.0.3 ではおこりませんでした。 バグでしょうか?

また、IE8 Beta2 の標準準拠モードで getViewHeight() がおかしく、スクロールバーの幅が反映されません。 β版なのでとりあえず様子見、としておきます。

Opera について

さて、やっかいな Opera について考えてみましょう。 Opera 9.6.1 (xhtml) で以下のような出力があります。

 1: getViewHeight(): 458 (number)
 2: getViewHeight(): 474 (number)
 3: getViewHeight(): 474 (number)
 4: getViewHeight(): 458 (number)
 5: getViewHeight(): 458 (number)

Case1 の値がおかしいですね。 ここで考えられるのは「表示するコンテンツの幅が表示幅より短い場合、Opera 9.6.1 はより安全な値、スクロールバー領域を確保した後の幅を返す」という仮定です。

これが正しいとすれば、その時点で明示的に利用している以外の部分なので、勝手されても文句は言いづらいです。 が、ちょっと勘弁してほしい感じ。 とりあえず今回は見なかったことに...。

また Opera 8.5.4 (xhtml) で妙な動作をみつけました。

  o['document.body.clientHeight'] = document.body.clientHeight;
  o['document.documentElement.clientHeight'] = document.documentElement.clientHeight;

というコードだと document.body.clientHeight の値が狂うのですが、

  o['document.documentElement.clientHeight'] = document.documentElement.clientHeight;
  o['document.body.clientHeight'] = document.body.clientHeight;

と順番を変えるだけで正常になります。 内部の処理の都合でしょうか? 困ったものです。 とりあえず評価順を替えて対応しましょう。

また Opera 8.5.4 (xhtml) では Height がまったく表示サイズではない値を返します。 これはあきらかに変なので、とりあえず特別対応してごまかします。

とりあえずの関数

以上をふまえて、とりあえず書いてみた「実際に利用できる表示領域」を求める関数です。

function getViewWidth3() {
  return document.documentElement.clientWidth && document.compatMode && document.compatMode=="CSS1Compat" ? document.documentElement.clientWidth : document.body.clientWidth;
}
function getViewHeight3() {
  return document.documentElement.clientHeight && document.compatMode && document.compatMode=="CSS1Compat" ?  (navigator.userAgent.indexOf('Opera') > -1 && parseFloat(window.opera.version()) < 9.5 ? document.body.clientHeight : document.documentElement.clientHeight) : document.body.clientHeight;
}

さきほどのテストページに組み込んでみました。 なんか時間かけちゃったので、とりあえずコレを使ってみます。 問題あったら直していきましょう。

prototype 1.6.0.3 の CHANGELOG を翻訳してみる

ぼーっとしてたら、先月末に Prototype 1.6.0.3 が出ていたようです。 CHANGELOG を見ると、57個の変更点が記載されています。 自分へのメモとしてざっと翻訳してみましたので、置いておきますね。

  • jstest.rb に Chrome browser サポートを追加
  • OperaJavaScript exception を仮実装
  • Safari で $A ファンクションの NodeList 検出を改善
  • IE で間違って出力されるのを回避するため、Opera を検出する方法を変更
    • 1.6.0.2: Opera: !!window.opera,
    • 1.6.0.3: navigator.userAgent.indexOf('Opera') > -1,
  • Form.Element.Serializers.select の変数名を変更
    • 1.6.0.2: select: function(element, index) {
    • 1.6.0.3: select: function(element, value) {
  • Opera の検出の際は常に、バージョン文字列を数字に強制変換する
    • 1.6.0.3: } else if (B.Opera && parseFloat(window.opera.version()) < 9.5){
    • 【メモ】 10以上のバージョンが出てきそうだから?
  • Object.isElement で false っぽい値をきちんと false として返す
    • 1.6.0.2: return object && object.nodeType == 1;
    • 1.6.0.3: return !!(object && object.nodeType == 1);
  • INT シグナルでテストタスクを終了するよう修正
  • IEユニットテストがフリーズする問題を修正
  • Hash が prototype チェーンのキー値を返さないように強化 (例:constructor,valueOf, toString)
    • 1.6.0.2: get: function(key) {
    • 1.6.0.2: return this._object[key];
    • 1.6.0.3: get: function(key) {
    • 1.6.0.3: // simulating poorly supported hasOwnProperty
    • 1.6.0.3: if (this._object[key] !== Object.prototype[key])
    • 1.6.0.3: return this._object[key];
  • Class#addMethods の中で toString/valueOf がクロジャー経由で同じメソッドを共有していた問題を修正。 Object.extend がこれらを列挙する際に失敗していたので、一般的なプロパティ設定を使うようにした
    • 1.6.0.2: valueOf: function() { return method },
    • 1.6.0.2: toString: function() { return method.toString() }
    • 1.6.0.3: value.valueOf = method.valueOf.bind(method);
    • 1.6.0.3: value.toString = method.toString.bind(method);
  • Form.Element.disable がフォーカスを勝手に外してしまうのを止めた
    • 1.6.0.2: disable: function(element) {
    • 1.6.0.2: element = $(element);
    • 1.6.0.2: element.blur(); // 1.6.0.3 でこの行を削除
  • Element.hide と Element.show で ID が指定された場合でも、Element を返すようにした
    • 1.6.0.2: $(element).style.display = 'none';
    • 1.6.0.2: return element;
    • 1.6.0.3: element = $(element);
    • 1.6.0.3: element.style.display = 'none';
    • 1.6.0.3: return element;
  • height の値が auto の場合に、Element#getStyle('height') が null を返す問題を修正
  • Element#descendantOf へのユニットテストを追加
  • Form#serializeElements はファイル入力をシリアライズすべきではない
    • 1.6.0.2: if (value != null && (element.type != 'submit' || (!submitted &&
    • 1.6.0.3: if (value != null && element.type != 'file' && (element.type !='submit' || (!submitted &&
    • 【メモ】 セキュリティ上の配慮?
  • DOM がロードされる前に Event.pointer を呼んだ場合に発生する問題を修正
    • 1.6.0.3: var docElement = document.documentElement,
    • 1.6.0.3: body = document.body || { scrollLeft: 0, scrollTop: 0 };
  • input element の Element#down がエラーを発生しなくなった
    • 1.6.0.2: element.select(expression)[index || 0];
    • 1.6.0.3: Element.select(element, expression)[index || 0];
  • Object.isHash に対するユニットテストを増やした
  • Function#argumentNames で複数行に分割された引数に対応するようになった
    • 1.6.0.2: var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");
    • 1.6.0.3: var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1]
    • 1.6.0.3: .replace(/\s+/g, '').split(',');
    • 【メモ】 Unicode 空白文字 (\s) を消すようになったんですね
  • 一貫性の維持のため、Number.prototype.times に "context" の追加パラメーターを追加した
    • 1.6.0.2: times: function(iterator) {
    • 1.6.0.2: $R(0, this, true).each(iterator);
    • 1.6.0.3: times: function(iterator, context) {
    • 1.6.0.3: $R(0, this, true).each(iterator, context);
  • Caja に準拠するため、foo.__proto__ 表記を全て foo['__proto__'] に置き換えた
    • 【メモ】 Caja は IFRAME を安全に実現するためのライブラリのようで「オブジェクト外部からプライベート名にアクセスする」ことが禁止されている模様
  • Enum に依存しないことにより Function#argumentNames を高速化した
    • 【メモ】 3つ↑のコードで、invoke() を使わなくなったことでしょうか
  • Event#element で存在しない tagName プロパティにアクセスする問題を修正(例: element が document の場合)
    • 1.6.0.2: if (!Prototype.Browser.Opera || element.tagName == 'BODY') {
    • 1.6.0.3: if (!Prototype.Browser.Opera || (element.tagName &&(element.tagName.toUpperCase() == 'BODY'))) {
  • 失敗していた Element#identify テストを修正
  • ユニットテストの内部構成を見直した。 ユニットテストJavaScript テストファイルと補助的な HTML, JS, CSS ファイルからダイナミックに生成されるようになった
  • Safari で element が自身の子孫として報告される問題を修正
    • 【メモ】 element.descendantOf(element) が true になっていたってことですかね?
    • 【メモ】 WindowsSafari 3.1.2 では再現できませんでした
  • Element#descendantOf の IE 用の実装が非常に簡潔になった
  • 存在しない属性(アトリビュート)をセレクターで検索したとき、例外が発生しないようにした
  • Firefox で Event.element が間違ったノードを返す問題を修正。 また IE でEvent.element が例外を発生する可能性のある部分を修正
  • 戻るボタンでページを戻した際、Safari 3 が document オブジェクトからカスタム・プロパティを削除してしまう問題を修正
  • Selector クラスに W3C Selectors API サポートを統合し、可能 (ブラウザーAPI をサポートし、かつ与えられたセレクターを認識する) であれば使用するようにした。 CSS 規定に従うため、:enabled, :disabled, :empty の意味に少し変更があったことも意味している。
  • Element#getDimensions で element を2回拡張していた無駄を排除
    • 1.6.0.2: var display = $(element).getStyle('display');
    • 1.6.0.3: var display = element.getStyle('display');
  • オブジェクトをシリアライズする際に Hash#toQueryString を除外する
  • IE 標準標準モードでの Event#pointer の問題を修正
    • 1.6.0.2: x: event.pageX || (event.clientX +
    • 1.6.0.2: (document.documentElement.scrollLeft ||document.body.scrollLeft)),
    • 1.6.0.3: x: event.pageX || (event.clientX +
    • 1.6.0.3: (docElement.scrollLeft || body.scrollLeft) -
    • 1.6.0.3: (docElement.clientLeft || 0)),
  • window ロード時に Test.Unit.Logger をインスタンス化するようにした
  • ユニットテストをクリーンアップ
  • String#escapeHTML の内部構成を見直して、`with` 構文の使用をやめた
    • 【メモ】 Caja は 「withとevalは使用不可」 らしいので、それへの対応?
  • ユニットテストから `with` 構文を利用した記述を削除
  • deprecation helper を完全に書き直し、UpdateHelper に名前を変え、他のライブラリから使いやすくした
  • IE で Element#writeAttribute が frameborder 属性に対応した
  • selector.js をちょっとクリーンアップした
  • IE で String#unescapeHTML がタグを除外するよう変更
    • 1.6.0.2: return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
    • 1.6.0.3: return this.stripTags().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
  • form observers のユニットテストを止めた
  • Enumerables の実行速度を改善した
  • deprecation extension: Hash.toJSON() を削除対象としてマーク
  • deprecation extension: 引数無しの Class.create() を削除対象としてマーク
  • deprecation extension: Event.unloadCache を削除対象としてマーク (これまでは、deprecation)
  • deprecation extension: console.error に関する deprecation 以外は全てログする
    • 【原文】 log everything *but* deprecations with console.error.
  • Firebug の console.warn と console.error を使用するよう deprecationextension を拡張
  • タグ名の判断を XHTML 準拠に
  • Element.prototype をサポートしたブラウザーではそれを使用するように
  • IE で Element#cumulativeOffset, Element#getOffsetParent,Element#positionedOffset, Element#viewportOffset and Element#clonePositionが親が指定されていない要素でエラーにならないよう変更
  • Enumerable#eachSlice で 1未満の引数が指定された場合に、無限ループに入らないよう変更
  • セレクターでネームスペースが指定された属性の存在を正しく検出できるように
  • Element#absolutize と Element#relativize が常に element を返すように変更
  • deprecation extension を追加


順番とか、そのまんまです。 理解の助けとして、コードの変更部分を少し転写しています。 ユニットテストあたりは使用していないので、ちょっと不安アリ。

IE 以外で body の右マージンが無視される訳

body 要素のサイズについて調べてたら、妙なことに気がつきました。 ブラウザによって 右マージン(margin-right) が表示されたりされなかったり。テスト用に作成した body width test (HTML) などで確認すると、IE6 IE7 以外では右マージンが表示されていないのがわかります。

nextindex.net によると、CSS過剰制約 (over-constrained) という仕様のようで。 表示しきれないような狭い領域にボックス要素を配置した場合、親の配置指定が左詰めならば右のマージン、右詰ならば左のマージン指定が無視されるそうな。

IECSSにあまり準拠していない、ってよく聞くけど、これもその1つかな。

ちなみに IE7 は互換モードでは右マージンを表示するけれど、標準準拠モード(DOM宣言がちゃんとあるxhtml)だと右マージンを表示しないみたい。 「互換性をなるべく維持しつつ、可能な限り標準には準拠していきたい」という MS の方針には基本的には賛成ではありますが、モード切り替えでこんなとこまで変わるのは プチ迷惑 だとも感じてみたり。