Home > Latest topics

Latest topics 近況報告

たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。

萌えるふぉくす子さんだば子本制作プロジェクトの動向はもえじら組ブログで。

宣伝1。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能! シス管系女子って何!? - 「シス管系女子」特設サイト

宣伝2。Firefox Hacks Rebooted発売中。本書の1/3を使って、再起動不要なアドオンの作り方のテクニックや非同期処理の効率のいい書き方などを解説しています。既刊のFirefox 3 Hacks拡張機能開発チュートリアルと併せてどうぞ。

Firefox Hacks Rebooted ―Mozillaテクノロジ徹底活用テクニック
浅井 智也 池田 譲治 小山田 昌史 五味渕 大賀 下田 洋志 寺田 真 松澤 太郎
オライリージャパン

Page 5/239: « 1 2 3 4 5 6 7 8 9 »

parseTemplate() の引数で渡すオブジェクトのプロパティをテンプレート内のコード片で普通の変数として参照したい - May 20, 2009

前書き

PHPとかerbのようなテンプレートをJavaScriptで。という話に書いたやつの続き。

大切な事なので3回言います。
<% for (var i = 0; i < 3; i++) { %>
  今日は<%= today %>です。
<% } %>
オーケー?

こういう文字列をテンプレートとして解釈したい場面は多分よくあると思う。この例だと、todayという変数をどっか外部から与えて値を埋め込むことになる。で、この変数をどうやって渡したらええねん、と。

解1:グローバル変数を使う

グローバル変数をがんがんに使って構わないのであれば、先にグローバル変数としてtodayを定義しておけばいい。eval()で実行されるコードの中からも普通に参照できる。

しかしアドオンのコードのように、個々のグローバル関数やグローバル変数の寿命が長い事が予想されるスクリプトでは、この手は使えない。

解2:テンプレート内に書くコード片にthis.を付ける

parseTemplate()の仕様としてはあくまで「書かれたコード片は第2引数で渡されたオブジェクトをthisとして実行されます」という風にしておいて、テンプレート風に書かれている方の文字列内のJavaScriptコード片では必ずthis.todayという風に書くようにする、という方法もある。

しかしいちいちthis.を付けるのは面倒だし、そもそも僕がこのような記法を知ったきっかけであるERBでは、self.なんて書く必要がなかった。同じような物は同じように使えた方がいい。なのでこの方法も使いたくない。

解3:varで宣言し直す

先のエントリに当初書いていたコードでは、parseTemplate()の第2引数で渡したオブジェクト(ハッシュ)のプロパティを走査してそれをvarで変数として宣言し直す、ということをしてた。外の名前空間をなるべく汚さないための苦肉の策だ。

  if (aContext && typeof aContext == 'object') {
    for (var prop in aContext) {
      if (!aContext.hasOwnProperty(prop)) continue;
      __parseTemplate__codes.unshift('var '+prop+' = aContext.'+prop+';');
    }
  }

こんな感じ。

でも、これだとエラーになりそうな場合がけっこう考えられる。例えばハッシュのキーが01234という文字列だと、実行されるJavaScriptのコードはvar 01234 = aContext.01234;となってしまい、eval()にかけた時点でSyntaxError: missing variable nameと怒られてしまう。

なので、こういうエラーの元になる名前のプロパティを除外して、安全な物だけをvarで宣言する、ということをやりたかった。言い換えると、そのプロパティの名前を構成している文字列がJavaScriptの識別子として妥当かどうかを判別したかった。

ということでやっと、このエントリの本題に入る。

続きを表示する ...

PHPとかerbのようなテンプレートをJavaScriptで。 - May 19, 2009

UxU 0.5.11に入れ損ねた。次版で標準のヘルパーメソッドに入れるけど、割といいかげんな実装で、たいした規模じゃないから、テストケースの中に直接書いて使ってもいいと思う。

function parseTemplate(aCode, aContext) {
  var __parseTemplate__codes = [];
  aCode.split('%>').forEach(function(aPart) {
    var strPart, codePart;
    [strPart, codePart] = aPart.split('<%');
    __parseTemplate__codes.push('__parseTemplate__results.push('+
                                strPart.toSource()+
                                ');');
    if (!codePart) return;
    if (codePart.charAt(0) == '=') {
      __parseTemplate__codes.push('__parseTemplate__results.push(('+
                                  codePart.substring(1)+
                                  ') || "");');
    }
    else {
      __parseTemplate__codes.push(codePart);
    }
  });
  var __parseTemplate__results = [];
  with(aContext|| {}) {
    eval('(function() { '+__parseTemplate__codes.join('\n')+' }).call(aContext|| {})');
  }
  return __parseTemplate__results.join('');
}

var source = <![CDATA[
      大切な事なので3回言います。
      <% for (var i = 0; i < 3; i++) { %>
        今日は<%= today %>です。
      <% } %>
      オーケー?
    ]]>.toString();
var params = {
      today : (new Date()).toString()
    };
var result = parseTemplate(source, params);

レガシーだけどクロスブラウザな書き方だったら、こうか。

function parseTemplate(aCode, aContext) {
  var __parseTemplate__codes = [];
  aCode = aCode.split('%>');
  var strPart, codePart;
  for (var i in aCode) {
    aCode[i] = aCode[i].split('<%');
    strPart = aCode[i][0];
    codePart = aCode[i].length == 1 ? null : aCode[i][1] ;
    __parseTemplate__codes.push('__parseTemplate__results.push(unescape("'+
                                escape(strPart)+
                                '"));');
    if (!codePart) continue;
    if (codePart.charAt(0) == '=') {
      __parseTemplate__codes.push('__parseTemplate__results.push(('+
                                  codePart.substring(1)+
                                  ') || "");');
    }
    else {
      __parseTemplate__codes.push(codePart);
    }
  }
  var __parseTemplate__results = [];
  with(aContext|| {}) {
    eval('(function() { '+__parseTemplate__codes.join('\n')+' }).call(aContext|| {})');
  }
  return __parseTemplate__results.join('');
}

var source = '大切な事なので3回言います。\n'+
             '<% for (var i = 0; i < 3; i++) { %>\n'+
             '  今日は<%= today %>です。\n'+
             '<% } %>\n'+
             'オーケー?';
var params = {
      today : (new Date()).toString()
    };
var result = parseTemplate(source, params);

他のアドオンと連携しやすくするためのライブラリを作ってみた - May 13, 2009

マルチプルタブハンドラについてRockridge氏ほかから「メニューをカスタマイズできるようにしてくれ」という要望が挙がっていたのだけれども、Menu Editorという素晴らしいアドオンがあるのに自前で同じような機能を再実装するのは徒労感しか無いなあと思ったので、開き直って、マルチプルタブハンドラの設定ダイアログを以下のようにしてみた。

  • Menu Editorがインストールされていなければ「Menu Editorをダウンロードする」リンクを表示する。
  • Menu Editorがインストールされていれば「Menu Editorの設定を開く」ボタンを表示する。

Menu EditorのAPIをよく分かってないので(ていうかそもそも公開APIなのかどうかも知らない)、タブ選択時のメニューをカスタマイズできるようにするためにちょっと強引な方法を使ってる。

で、同じようなこと(他のアドオンの有無を調べた上で設定を開く)を何度も書きたくなかったので、設定ダイアログに加えた変更の要点を他のアドオンと連携しやすくするためのライブラリとして分離してみた。

if (window['piro.sakura.ne.jp'].extensions.isInstalled('my.extension.id@example.com') &&
    window['piro.sakura.ne.jp'].extensions.isEnabled('my.extension.id@example.com'))
    window['piro.sakura.ne.jp'].extensions.goToOptions('my.extension.id@example.com');

アドオンがインストールされているかどうか・有効か無効かを調べるだけならFUELで事足りるので、あまり使い出がないといえば使い出がない。まあThunderbird 2あたりでだったらニーズがあるかもだけど。

ちなみにFUELで書く場合、アドオンがインストールされているかどうかはApplication.extensions.has('my.extension.id@example.com')、有効か無効かはApplication.extensions.get('my.extension.id@example.com').enabledで分かる。設定ダイアログのChrome URLを調べるAPIはなくて、それでこのライブラリを作ることにした次第です。

swapBrowsersAndCloseOtherの変更への追従と、メソッドの動的な書き換えのメリット・デメリット - Apr 30, 2009

以前書いたtabbrowser要素のswapBrowsersAndCloseOther()メソッドの上書きの話で書いてたサンプルが使えなくなっていたので直した。というか自作アドオンの機能を使おうとして正しく動かなかったのでコードを改めて見てみて、メソッドの書き換えの対象になってる箇所に変更があった事に気付いた(4月16日に変更が行われていたようだ)。

以下は、Shiretoko 3.5b5preに合わせて修正した後のバージョンのサンプルコード。

if ('swapBrowsersAndCloseOther' in aTabBrowser) {
  eval('aTabBrowser.swapBrowsersAndCloseOther = '+
    aTabBrowser.swapBrowsersAndCloseOther.toSource().replace(
      '{',
      '$& MyAddonService.destroyTab(aOurTab);'
    ).replace(
      'if (aOurTab == this.selectedTab) {this.updateCurrentBrowser(',
      'MyAddonService.initTab(aOurTab); $&'
    )
  );
}

if (aOurTab == this.selectedTab)を目印にして「タブを入れ換えた後の再初期化処理」を入れてたんだけど、同じif文がこれより前の位置に増えたせいで、コードの挿入位置がずれてしまい、本来期待していた順番とは異なる順番で処理が行われてしまっていた。if文の中まで目印に含めるようにして一応回避したけど、これはこれで、if文の中を書き換えるアドオンがあると破綻してしまう可能性があるわけで……何かいい方法はないものだろうか。

今回の場合に限らず、「メソッドの入れ替えではなく動的な書き換えを行う」やり方には、こういう変更に逐一追従しなくてはならないという弱点がある。メソッドのインターフェース(引数の数や返り値)はそうそう変わらなくても、中身は結構頻繁に変わる。なので、気をつけておかないといけない。

もう1つの弱点として、書き換え対象のメソッドの中で参照されている変数がそのメソッドが定義された変数スコープからしか参照できないものであった場合に、それへの対処も必要となる。その変数の値がグローバルな名前空間からアクセス可能なものであれば、メソッドの書き換え時にその変数を同時に宣言し直してやればいいんだけど……

(function() {
  var attrName = 'foo';
  window.ExampleAddonService = {
    method : function(aNode) {
      aNode.setAttribute(attrName, true);
    }
  };
})();

// メソッドの書き換えには成功するが、attrNameという変数が
// 見つからなくなってしまうので、実行時にエラーになる
eval(
  'window.ExampleAddonService.method = '+
  window.ExampleAddonService.method.toSource().replace(
    '{',
    '{ AnotherAddonService.preProcess(aNode);'
  )
);

この例のような場合になると、もうお手上げ。

それでも動的なメソッドの書き換えの方を僕が積極的に使う最大の理由は、同じやり方でメソッドを書き換える他のアドオンとの互換性を維持するためだ。別名で元のメソッドを保持しておくやり方の場合、他のアドオンが元のメソッドの名前で関数オブジェクトを取得しても、その関数の内容は元のメソッドとは全然違うから、書き換えられなくて動かなくなる、という事態が起こり得る。それを避けるためには、上の例のようなお手上げな事例を除き、可能な限り元のメソッドを動的に書き換えた方が良いということになる。

アニメーション効果を有効にしたツリー型タブの新版とデモ動画を公開したよ - Apr 09, 2009

Tree Style Tab 0.7.2009040901公開した。アニメーション効果の実装の他に、細かいバグ修正も色々。

あと、実際どんな感じかというのをわかりやすく示せるかなと思って、デモ動画も作ってみた。CamStudioもNiVEも使うの久しぶり(っていうかVistaにしてからは初)だから、やり方思い出すのに苦労したよ……

ヌルヌル動いてるのは倍速再生してるわけではなく、これで等倍速です(一応)。タブの影はbox-shadowで描画してるので、Firefox 3.0.xだと影無しになります。

しかしYouTubeはアップロードが簡単になってるわ画質が上がってるわで、いつの間にかすんげーパワーアップしてますね。Stage6とかあった頃とは隔世の感があります。

以下余談。

実装の最後の段階になって困った点として、画面外にタブが開かれた時にそこまで自動スクロールしてくれないという問題が起こってた。スクロールしても、新しく開かれたタブの一個前のタブの位置にスクロールしちゃったりして。上の動画で言うと、0:57あたりで画面下の方でサブツリーを展開した時に、子タブが画面内におさまるように自動でスクロールしてるけど、これがちゃんと動かなくなってた。

なんでこんな問題が起こってたかというと、「そのタブが画面外にあるのかどうかを判定する」「そのタブの位置までスクロールする」といった処理が全部「タブの座標」を基準にしてたせいで、アニメーション開始時点やアニメーション中の中途半端な座標を元に処理を行ってしまい、もうシッチャカメッチャカになってた、という……

上記の処理を行う時に、アニメーション中の座標とアニメーション終了後の座標とのズレをきちんと考慮して計算してやればいいってだけの話なんだけど、普通に考えるとこれがめんどくさい。それぞれのタブがちょっとずつズレて表示されてるわけだから、座標を調べたいタブだけじゃなくてそれより前(上)にあるタブ全部について、そのタブはアニメーション中か?とか、そのタブは非表示か?とかを判別しながらオフセット値を調べないといけないわけで……いちいちそんな計算するのはめんどくさすぎる。メンテナンスし辛そうだし、コードの量が多くなりそうだし、真面目に書く気しないです(大学生の頃だったらやってたかもだけどね……時間は有り余ってたから)。

で、代わりに以下のようなやり方を思いついた。

  • アニメーション開始時点やアニメーション実行中は、個々のタブの属性値として本来の座標からのオフセット値を持たせておく。
  • タブの座標が必要になった時には、「特定の条件にマッチするタブの当該属性値を収集して合計した値」をXPathで取得して、それを現在の座標に足してやる。

コードにするとこんな感じ。

getYOffsetOfTab : function(aTab)
{
  return document.evaluate(
    'sum((self::* | preceding-sibling::*[not(@tab-collapsed="true")])'+
         '/attribute::tab-y-offset)',
    aTab,
    null,
    XPathResult.NUMBER_TYPE,
    null
  ).numberValue;
},

sum()は、渡されたノードセットの値を数値として合計したものを返すXPathの関数。受け取る結果の型をXPathResult.NUMBER_TYPEと指定すれば、計算結果の数値を直接得られる。XPathはうまく使えばこんな風に、処理対象のノードの絞り込みだけじゃなくその後の処理(ここでは計算)まで一発で済ませられるので面白い。

アドオン用のミニマルなアニメーション実行用マネージャ - Apr 08, 2009

トゥイーンの話を受けてごろたんがさらに発展的な話を書いてくれた。で、「ほうほう、こういうのをeasingと言うのか……」と自分の無知っぷり&何も知らんくせに偉そうなことを語る厚顔さに恥じ入りながら先のエントリを少し手直しした。

で、JSTweener という Tweener (as3 のモーショントィーンライブラリ) 互換のライブラリを使うと、標準で様々な easing 関数が利用できたり、タイマーが一つなので、数百, 数千オブジェクトをモーションさせるときにはだいぶ高速になる。というのを読んで、確かに先のエントリに書いた物はいっこいっこのタブごとにタイマー走らせるから効率悪いよなあ、と思い、1個のタイマーだけで複数のアニメーションを走らせるための簡単なライブラリを作ってみた。ツリー型タブの開発版にも早速組み込んでる。

  • window['piro.sakura.ne.jp'].animationManager.addTask( )にアニメーション用の関数その他の引数を渡してやると、それを共通のタイマーで処理する、というごく単純なもの。
  • easing関数を使いやすいように、アニメーション用関数にはeasing関数が受け取るのと同じ形式の引数が渡る。
  • アニメーションを途中で止めたい時はwindow['piro.sakura.ne.jp'].animationManager.removeTask( )で関数の登録を解除してやる。

クロージャ使って書くこと前提の不親切なAPIですんません……

今更聞けない可変フレームレートなトゥイーンの基本 - Apr 08, 2009

1つ前のエントリに書いた、可変フレームなトゥイーン効果の実装の話。

今時は便利なJavaScriptのアニメーション用ライブラリが色々あるからわざわざ自分で書くような必要はないんだろうけど、自分はほんの一箇所だけのためにライブラリ全部突っ込むというのは気が引けるタイプなので、ピンポイントな実装とその理屈を(雑学として)書いておこう。

先に、タブのインデント幅変更の処理の完成したものを貼っておく。説明を簡単にするためにちょっと省略してる。

indentDuration : 200,

updateTabIndent : function(aTab, aProp, aIndent)
{
  this.stopTabIndentAnimation(aTab);

  var startIndent = this.getPropertyPixelValue(aTab, aProp);
  var delta       = aIndent - startIndent;
  var duration    = this.indentDuration;
  var startTime   = Date.now();

  aTab.__treestyletab__updateTabIndentTimer = window.setInterval(function(aSelf) {
    var progress = Math.min(1, (Date.now() - startTime) / duration);
    var powerForStyle = Math.sin(90 * power * Math.PI / 180);
    var indent = (progress == 1) ?
                aIndent :
                startIndent + (delta * powerForStyle);
    aTab.setAttribute(
      'style',
      aTab.getAttribute('style')+';'+
        aProp+':'+indent+'px !important;'
    );

    if (progress == 1) aSelf.stopTabIndentAnimation(aTab);
  }, 10, this);
},

stopTabIndentAnimation : function(aTab)
{
  if (!aTab.__treestyletab__updateTabIndentTimer) return;
  window.clearInterval(aTab.__treestyletab__updateTabIndentTimer);
  aTab.__treestyletab__updateTabIndentTimer = null;
},

getPropertyPixelValue : function(aElement, aProp) 
{
  var style = window.getComputedStyle(aElement, null);
  return Number(style.getPropertyValue(aProp).replace(/px$/, ''));
},

なんでCSSOM使ってないの、って所にはツッコまないように。

アニメーションというと、つまりはちょっとずつ値を変えて再描画するということで、単純に考えたら多分こうなる。

function doAnimation(aElement, aStart, aEnd)
{
  for (var i = aStart; i < aEnd; i++)
  {
    aElement.style.marginLeft = i+'px';
  }
}

でもこれはダメ。CSSのプロパティを変更しても、その状態が描画されるより前に次の値がセットされてしまうので、間のアニメーションがアニメーションにならない。(CSSのプロパティが変更された瞬間に再描画する実装だったらこれでもいいかもだけど、少なくともGeckoではダメ。)

もうちょっと改良すると、こう。

function doAnimation(aElement, aStart, aEnd)
{
  var margin = aStart;
  var timer = window.setInterval(function() {
            aElement.style.marginLeft = (margin++)+'px';
            if (margin == aEnd) {
              window.clearInterval(timer);
            }
          }, 10);
}

こういう風にタイマーを使ってやれば、きちんと各コマが描画されるようになる。

しかしこのやり方だと、最初の値から最後の値までの全コマが必ず描画される(=フレームレート固定)ので、貧弱な環境だとものすごい遅いアニメーションになってしまう。上の例だと、10ミリ秒ごとに1ピクセルずらして、というのを移動距離の分だけ繰り返すことになってしまう。

「高速な環境ではたくさん描画していいけど、低速な環境だと再描画を減らしてほしい。とにかく、1回のアニメーションは決まった時間の中できちんと終わらせたい。」これが可変フレームレートの基本的な考え方。

可変フレームレートにする場合、描画と描画の間にどれだけ時間がかかったか、というのが鍵になる。

JavaScriptのsetIntervalのタイマーは、(少なくともGecko/SpiderMonkeyでは、)仮に実行間隔を10ミリ秒にした場合、実際には「1回目の処理の時間」+「10ミリ秒のインターバル」+「2回目の処理の時間」+「10ミリ秒のインターバル」……という風な感じで時間が過ぎていく。アニメーションにかける全体の時間が分かっているなら、「n回目の描画開始時の時点で、アニメーションにかける時間全体の何%が過ぎたか」をまず計算して、移動量をそのパーセンテージから求めてやればいい。

function doAnimation(aElement, aStart, aEnd)
{
  var delta     = aEnd - aStart;
  var duration  = 200; // アニメーション効果全体を200ミリ秒で終わらせる
  var startTime = Date.now(); // (new Date()).getTime() と同等
  var timer = window.setInterval(function() {
        var progress = Math.min(
                      1,
                      (Date.now() - startTime) / duration
                    );
        var margin = (progress == 1) ?
                       aEnd :
                       aStart + (delta * progress) ;
        aElement.style.marginLeft = margin+'px';
        if (progress == 1) {
          window.clearInterval(timer);
        }
      }, 10);
}

これで、可変フレームレートのアニメーションになった。progressはアニメーションの進行状況を示していて、0(開始時点=0%)から1(終了時点=100%)の間の値を取る。開始時の値と終了時の値の差にこれをかけてやれば、「今の時点ではこれだけ移動してるはず(その位置に描画すればよい)」ということが分かるワケ。(ちなみにこの時点で、値が増加していくのか減少していくのかどっちなんだ、ということも気にせずに済むようになっている。)

この時点では、値の変化率は一定なので、理科の時間に習う「等速直線運動」ってやつになっている。実際のUIでこれをそのまま使うとちょっと味気ないので、もうちょっとかっこよくしてやりたくなるところだ。可変フレームレートならそれも簡単にできる。

よくあるのは、「最初はすごい速度で飛んできて、最後はフワッと着地する」みたいな効果だろう。これは三角関数のsin()やcos()を使えば簡単に計算できる。

円と複素数平面の勉強をしたことがあれば、0°から90°の間を1°ずつ動く間に、Xの値は「最初はゆっくりと、最後は急速に」Yの値は「最初は速く、最後はゆっくり」増加していくことが分かるだろう。sin(θ)やcos(θ)を使えば、この「XやYの値の変化率」を取り出して利用できる。

例えば「最初は速く、最後はゆっくり」のアニメーションなら、上の例の(delta * progress)の所を(delta * Math.sin( (progress * 90) * Math.PI / 180 ))にすればいい。これで、0°から90°までの間のYの値の変化率に応じた移動量になる。(Math.sin()Math.cos()は角度をラジアンで指定しないといけないので、θ×π÷180で度数をラジアンに変換している。)

以上、中学や高校で勉強する数学って案外無駄にならないんだよ、というお話でした。

ちなみに、aStart + (delta * progress)の所あたりを任意の式に置き換えれば、動き方を等速直線運動や等加速度運動やはね回る等の色んな形に変えることができる。これを、「最初の値Start値の最終的な変化量total Changeアニメーションにかけたい時間Duration現在までに過ぎた時間Time(最後の2つから現在の進度が分かる)という4つのパラメータを受け取り現在の値を返す関数」として定義した物をeasing関数と呼ぶそうで、世にある多くのアニメーションライブラリは、このeasing関数を入れ替えることで色んなエフェクトを得られるようにしたもの、と言うことができる。

……という風に読み解いてみると、普段使ってるライブラリが一体何をどのように処理しているのか分かっておもしろいんじゃないでしょうか。僕はライブラリ使ってませんが。

追記。Norahさんのこれって yield 使ったらダメなんだろうか?というコメントを見た。べつに使っちゃダメってことはないし、むしろ自分も一瞬「あれ、これyieldで書けるんじゃね?」と思ったんだけど、どっちにしろタイマーでループ回すという事には変わりないし、そうなるとここで書いてるくらいの規模だとコードの量が無駄に増えるだけって気がしたので、そのままタイマーだけで書いた次第です。

追記。easing関数の説明を間違えてたので修正しました……

getBoundingClientRect()とgetBoxObjectFor()で取れる座標の違い - Mar 31, 2009

trunkでとうとうgetBoxObjectForがエラーを吐くようになってしまった - alice0775のファイル置き場 - Yahoo!ジオシティーズ

これを見て焦って今頃になってやっと調べた。

パッチによると、nsIDOMNSDocumentからgetBoxObjectFor()が消えて、nsIXULDocument専用のメソッドになった。ということなので、HTMLDocumentでgetBoxObjectFor()を使っているコードは全滅だ。何とかして代わりの方法を見つけないといけない。

document.getBoxObjectFor(element)で取れるのはnsIBoxObjectelement.getBoundingClientRect()で取れるのはnsIDOMClientRectで、インターフェースが違う。

取りたい値nsIBoxObjectnsIDOMClientRect
ボックスの幅box.widthrect.right-rect.leftまたはrect.width
ボックスの高さbox.heightrect.bottom-rect.topまたはrect.height
ボックスの左上の点のX座標(ドキュメントの原点基準)box.x+左ボーダー幅rect.left+window.scrollX
ボックスの左上の点のY座標(ドキュメントの原点基準)box.y+上ボーダー幅rect.top+window.scrollY
ボックスの右下の点のX座標(ドキュメントの原点基準)box.x-左ボーダー幅+box.widthrect.right+window.scrollX
ボックスの右下の点のY座標(ドキュメントの原点基準)box.y-上ボーダー幅+box.heightrect.bottom+window.scrollY
ボックスの左上の点のX座標(ビューポートの原点基準)box.x+左ボーダー幅-window.scrollXrect.left
ボックスの左上の点のY座標(ビューポートの原点基準)box.y+上ボーダー幅-window.scrollYrect.top
ボックスの右下の点のX座標(ビューポートの原点基準)box.x-左ボーダー幅+box.width-window.scrollXrect.top
ボックスの右下の点のY座標(ビューポートの原点基準)box.y-上ボーダー幅+box.height-window.scrollYrect.bottom

nsIDOMClientRectのwidthheightはどうもGecko 1.9.1以降でしか使えないっぽい。

例えば今使ってる環境のdocument.getElementById('reload-button')の場合はこんな感じ。(スクリーンショット) この時の値は以下の通り。

nsIBoxObjectnsIDOMClientRect
box.width==36rect.width==36
box.height==36rect.height==36
box.x==83rect.left==82
box.y==23rect.top==22
rect.right==118
rect.bottom==58

xleftytopの間にずれがあるのは、nsIBoxObjectのxyがいわゆるborder-box(border + padding + contentのボックス)ではなくpadding-box(padding + contentのボックス)基準であるからということらしい。nsIBoxObjectからborder辺の座標を取るには、getComputedStyle()でborderの幅を取って計算してやらないといけない。nsIBoxObjectもnsIDOMClientRectも、これら以外のプロパティはborder-box基準のようだ

で、上の表には書いてないけど、nsIBoxObjectにはscreenXscreenYというプロパティがあって、こちらで取れる画面上の座標もborder-box基準。そして、nsIDOMClientRectにはこれらプロパティが無いので、画面上の座標を取ることができない。

大まかに言って、nsIBoxObjectのxはnsIDOMClientRectのleft、nsIBoxObjectのyはnsIDOMClientRectのtopに読み替えて差し支えない。となると、残る問題は、screenXscreenYに相当する値をどう取るかだ。

で、試行錯誤の結果、以下のようなコードができあがった。

function getBoxObjectFor(aNode)
{
  // getBoxObjectFor() がある時はそれを使う。
  if ('getBoxObjectFor' in aNode.ownerDocument)
    return aNode.ownerDocument.getBoxObjectFor(aNode);

  var box = {
      x       : 0,
      y       : 0,
      width   : 0,
      height  : 0,
      screenX : 0,
      screenY : 0
    };
  try {
    var rect = aNode.getBoundingClientRect();
    var frame = aNode.ownerDocument.defaultView;
    box.x = rect.left + frame.scrollX;
    box.y = rect.top + frame.scrollY;
    box.width  = rect.right-rect.left;
    box.height = rect.bottom-rect.top;

    // 親フレームの要素を辿っていく。
    box.screenX = rect.left;
    box.screenY = rect.top;
    var owner = aNode;
    while (true)
    {
      frame = owner.ownerDocument.defaultView;
      owner = getFrameOwnerFromFrame(frame);
      if (!owner) {
        // 最上位のフレームまで来てしまったら、仕方ないのでwindowのプロパティを使う。
        // でもウィンドウの枠の外側の座標なので、激しくずれる。
        box.screenX += frame.screenX;
        box.screenY += frame.screenY;
        break;
      }
      if (owner.ownerDocument instanceof Ci.nsIDOMXULDocument) {
        // XULのドキュメント中の要素なら画面上の正確な位置を取れる。
        let ownerBox = owner.ownerDocument.getBoxObjectFor(owner);
        box.screenX += ownerBox.screenX;
        box.screenY += ownerBox.screenY;
        break;
      }
      let ownerRect = owner.getBoundingClientRect();
      box.screenX += ownerRect.left;
      box.screenY += ownerRect.top;
    }
  }
  catch(e) {
  }
  return box;
}

function getFrameOwnerFromFrame(aFrame)
{
  // window.parentでは、<browser type="content"/> の内容の
  // フレームからは親を辿れない。
  // nsIDocShellTreeItemを経由すれば可能。
  var parentItem = aFrame
        .QueryInterface(Ci.nsIInterfaceRequestor)
        .getInterface(Ci.nsIWebNavigation)
        .QueryInterface(Ci.nsIDocShell)
        .QueryInterface(Ci.nsIDocShellTreeNode)
        .QueryInterface(Ci.nsIDocShellTreeItem)
        .parent;
  var isChrome = parentItem.itemType == parentItem.typeChrome;
  var parentDocument = parentItem
        .QueryInterface(Ci.nsIWebNavigation)
        .document;
  // フレームに結びついてるiframe要素を直接取る方法が分からないので、
  // 泥臭い方法を……
  var nodes = parentDocument.evaluate(
      '/descendant::*[contains(" frame FRAME iframe IFRAME browser tabbrowser ",'+
                              'concat(" ", local-name(), " "))]',
      parentDocument,
      null,
      XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
      null
    );
  for (let i = 0, maxi = nodes.snapshotLength; i < maxi; i++)
  {
    let owner = nodes.snapshotItem(i);
    if (isChrome && owner.wrappedJSObject) owner = owner.wrappedJSObject;
    if (owner.localName == 'tabbrowser') {
      let tabs = owner.mTabContainer.childNodes;
      for (let i = 0, maxi = tabs.length; i < maxi; i++)
      {
        let browser = tabs[i].linkedBrowser;
        if (browser.contentWindow == aFrame)
          return browser;
      }
    }
    else if (owner.contentWindow == aFrame) {
      return owner;
    }
  }
  return null;
}

これにさらにborder幅によるズレとかposition:fixed;の場合への対応とかも盛り込んだ物を、ライブラリにしてみた。見ての通りchrome特権をバリバリに使ってるので、このコードはアドオンの中でしか動かない。画面上の絶対位置が必要になる場面なんてのはアドオンの場合くらいだろうから、別に問題ないと思うけど。

screenXscreenYに相当する値を取るためにけっこう面倒なことをしているので、オーバーヘッドがきっと半端ない。nsIDOMClientRectの持ってる情報だけで済む場合はそれだけ使った方がいいと思う。

ちなみに、安直な発想でXULDocument.getBoxObjectFor.call(HTMLDocument, HTMLElement)というのも考えてみたけど、これは実際には使えない。残念。

テキストリンクとpopInとjQuery - Mar 31, 2009

テキストリンクpopInの競合、について調べてる。

popInが入っているとテキストリンクが動かない、ことの理由はどうも以下の2点によるみたい。

  • popInがdblclickイベントをstopPropagation()しているため、テキストリンクにイベントが渡されなくなっている。
  • popInがポップアップアイコンの挿入位置を決定するために(?)、選択位置のテキストノードを動的に分割しており、仮にstopPropagation()されない状態にしてテキストリンク側でイベントを拾って処理を行おうとしても、イベント発生時とテキストリンクが処理を行う時とでDOMツリーの構造が微妙に変わっていて、正常に動かない。

何か有効な対策が無いか考えてる。

ところでpopInは内部的にjQueryを使ってるようなんだけど、その旨の表記を僕には見つけられなかった。jQueryはMITライセンスとGPLのデュアルライセンスで、MITを選択した場合でもThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.(著作権表示とMITライセンスの許諾表示をソフトウェアの全コピーかもしくは重要な箇所で示す必要がある)ということなので、下手したらMITライセンスの違反ということになるような気が…… (一応フィードバックフォームから送ってはみた)

JavaScriptでsleepしたい、を実現する方法(require JavaScript 1.7) - Feb 20, 2009

中野さんが、JavaScriptにはsleep(一定時間待ってから次の処理に進むという命令文)が無いせいでテストを書くのに難儀したという話を書かれているけれども。まさにこれをどうにかしたくて、UxUは進化してきたようなものと言える。

知ってる人は知ってるだろうけど、Firefox/Thunderbirdアドオン向けの自動テスト実行ツール・UxUでは、テストを書く上でsleepに相当する機能を利用できる。これは、JavaScript 1.7でジェネレータ・イテレータ機能を実現するために追加されたyield式のトリッキーな使い方だ。

これを実現しているのは、lib/utils.jsdoIteration()と、test/test_case.jsrun()ということになる。ここから要点だけを取り出すと、以下のような事をしている。

まず、スクリプトの書き手は、「sleepを使いたい処理」を関数として定義する。この時、sleepの代わりにyieldを使う。

var task = function() {
  doSomething1();
  yield 1000;
  doSomething2();
  yield 1000;
  doSomething3();
}

次に、スクリプトの書き手は、この関数を後述するdoIteration()に渡す。すると、doIteration()がよしなに計らって、「doSomething1()を実行した後、1000ミリ秒待って、doSomething2()を実行し、また1000ミリ秒待って、doSomething3()を実行する」という風な形で先程の関数の内容を実行する。

この時のdoIteration()の内容は、以下のような感じ。

function doIteration(aTask) {
  if (typeof aTask == 'function') {
    // 渡されたのが関数だったら、まず、評価した返り値を得る。
    aTask = aTask();
  }
  if (!aTask ||
      !('next' in aObject) ||
      !('send' in aObject) ||
      !('throw' in aObject) ||
      !('close' in aObject) ||
      aObject != '[object Generator]') {
    // 渡されたオブジェクトまたは関数の返り値が
    // ジェネレータ・イテレータではない場合、何もしない。
    return;
  }

  // ここからがミソ!

  // 全部の処理が終わったかどうか、を示すオブジェクトを定義
  var finishFlag = { value : false, error : null };
  var last = 0; // スリープ開始時点の時刻を保持する変数
  var sleep = 0; // スリープの長さ(秒数)を保持する変数
  var timer = window.setInterval(function() {
    if (
        // スリープの長さがちゃんと指定されていて
        sleep > 0 &&
        // スリープ開始時点からの経過時間がスリープとして
        // 指定された時間未満であれば
        (Date.now() - last) < sleep
       ) {
      // ここで処理を終えて、100ミリ秒後まで待つ。
      return;
    }
    // スリープとして指定された時間が経過したので、処理を進める。
    try {
      // 次にyieldが登場するまでの間の処理を実行。
      sleep = aTask.next();
      // next()の返り値はyield式に渡された値。
      last = Date.now(); // スリープ開始時刻を保持して
      return; // 処理を一旦終えて100ミリ秒後を待つ。
    }
    catch(e if e instanceof StopIteration) {
      // 最後のyield式の後の内容が実行されて、定義された関数の内容が
      // 最後まですべて実行されると、StopIteration例外が発生する。
      // よって、処理完了とみなす。
      finishFlag.value = true;
    }
    catch(e) {
      // それ以外の未知の例外が発生した時は、処理中断とする。
      finishFlag.error = e;
    }
    // 100ミリ秒ごとの繰り返し処理を停止。
    window.clearInterval(timer);
  }, 100);
  return finishFlag;
}

UxUの内部でやってる事は基本的にはこういう事。ただ、実際にはもうちょっと使い勝手をよくするために細かい処理が加わってる。

doIteration()が中で何をやってるのかを知らなければ、パッと見は、sleepという命令文の名前がyieldに変わっただけのようにすら見えるんじゃないだろうか。そこが、このやり方の狙いだ。スクリプトの書き手はタイムアウトだのコールバックだのといった難しい事を何も考えなくても良くて、単に「sleep文に相当する機能が加わったJavaScript」として好きなように処理を書く事ができる。テストを書くための工数が大幅に削減される(かもしれない)ので、テストを書くのが苦にならず、ばりばりテストを書けるようになる(はず)。その結果、充実したテストのおかげでより安心して開発に専念できるようになる(はず)。という理屈です。

ちなみに、勘のいい人は気付くだろうけど、setInterval()に渡している関数の冒頭に以下の内容を挿入すれば、doIteration()をいくらでも入れ子にできるようになる。

    if (
        typeof sleep == 'object' &&
        (!sleep.value || !sleep.error)
       ) {
      return;
    }
var task = function() {
  doSomething1();
  yield 1000;
  yield doIteration(function() {
          doSomething2();
          yield 500;
          doSomething3();
          yield doIteration(function() {
            doSomething4();
            yield 100;
            doSomething5();
          });
        });
  doSomething6();
};
doIteration(task);

さらに、こんなこともできる。

var task = function() {
  doSomething1();
  yield doIteration(function() {
          var flag = { value : false; }
          frame.addEventListener('load', function() {
            frame.removeEventListener('load', arguments.callee, false);
            flag.value = true;
          }, true);
          frame.contentDocument
               .defaultView
               .location.href = 'http://www.example.com/';
          yield flag;
          // フレームの読み込みが終わったらここに進む
          doSomething2();
        });
  doSomething3();
};
doIteration(task);

この辺をもっと簡単に書けるようにヘルパーメソッドを色々と整備したのがUxUのテスト実行環境、と思ってもらえれば大体それで正解です。

25日追記。他にも色々やり方があるようです。(どっちもMozilla限定だけど)

Page 5/239: « 1 2 3 4 5 6 7 8 9 »

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のつぶやき

オススメ

Mozilla Firefox ブラウザ無料ダウンロード