Jul 06, 2010

Minefield 4.0b2preではどうも同期的な処理がことごとく失敗するようになっている気がする

XUL/MigemoのMinefield 4.0b2pre対応のために色々検証していて、1つとても困った問題にぶち当たった。rangefindを使ってWebページ中の要素を装飾する時に、前から後ろに向かって処理を行うと検索が止まってしまう。

Components.utils.import('resource://gre/modules/debug.js'); 
const Cc = Components.classes;
const Ci = Components.interfaces;

function decorate() {
  var span = d.createElement('span');
  span.setAttribute('style','font-size:150%;');
  foundRange.surroundContents(span);
}

/* コンテンツ領域にテスト用の内容をロードする */
var d = Cc['@mozilla.org/appshell/window-mediator;1']
          .getService(Ci.nsIWindowMediator)
          .getMostRecentWindow('navigator:browser')
          .content.document;
d.documentElement.innerHTML = 'Firefox, Firefox, Firefox.';
d.documentElement.clientTop; /* ←伏線 */

/* rangefindを初期化する */
var find = Cc['@mozilla.org/embedcomp/rangefind;1'].createInstance(Ci.nsIFind);
find.findBackwards = false; /* 前から後ろに向かって検索 */
find.caseSensitive = false;

var findRange= d.createRange(); /* 検索する範囲 */
findRange.selectNodeContents(d.documentElement);
var startPoint = findRange.cloneRange(); /* 検索の始点 */
startPoint.collapse(true);
var endPoint = findRange.cloneRange(); /* 検索の終点 */
endPoint.collapse(false);

/* 検索を実行 */
var term = 'Firefox';
var foundRange = find.Find(term, findRange, startPoint, endPoint);
NS_ASSERT(foundRange !== null, '1回目で失敗');

decorate(); /* DOMツリーを編集して装飾する */

/* 検索の範囲を変える(編集した箇所より後を検索の範囲にする)*/
findRange.setStart(foundRange.endContainer, foundRange.endOffset);
startPoint.setEnd(foundRange.endContainer, foundRange.endOffset);
startPoint.collapse(false);

/* もう一度検索を実行 */
foundRange = find.Find(term, findRange, startPoint, endPoint);
NS_ASSERT(foundRange !== null, '2回目で失敗');

decorate(); /* DOMツリーを編集して装飾する */

エラーコンソールにこれをコピペして実行してみると、2回目の方で必ず失敗してしまう事が分かる。本当は「Firefox」という文字列が2箇所装飾されて欲しいのに、最初の1箇所だけで処理が止まってしまう。

これ、上のサンプルの中で伏線と書いている部分が鍵なんだけど、どうもこういうことらしい。

  • DOMツリーを編集すると、編集した箇所から先の範囲が「不確定」な状態になる。
  • 「不確定」な範囲に対しては、rangefindは一切の検索を行えない。
  • DOM要素のプロパティにアクセスするなどしてレイアウト情報を参照すると、状態が強制的に「確定」される。
  • または、setTimeout()等で少し遅らせて処理を行えば、その時には状態が「確定」されている。
  • 状態が「確定」されると、その範囲をまた検索できるようになる(最初の方にある d.documentElement.clientTop は、実はそのための物)。

1回目の検索結果のRangeの箇所でDOMツリーを切った貼ったしているので、その箇所より後の部分はどう頑張ってもそのままの流れでは検索できないようになってしまっている、ということのようだ。

なので、

  • 2回目以降の検索を実行する前に span.clientTop あたりにアクセスしてやれば(これ以外にもclientLeftでもoffsetWidthでもレイアウト系のプロパティなら何でもいいっぽい)、whileforのループを回し続ける事ができる。
  • 毎回setTimeout()で状態の「確定」を待ってやるというやり方でもよい。

という風な回避策があると言える。でも、後者は言わずもがな、前者も毎回レイアウト情報を参照するからクソ重くなりそうで、できればどっちの方法もとりたくない所だ。

将来的にどうなるのかは知らんけど、とりあえず今のところは、後方検索(Rangeの後ろの方から前の方に向かって検索する)ならこの問題に引っかからずに済むみたい。編集した箇所から先の部分が「不確定」になっても、後方検索だと「編集した箇所から先=もう検索が終わった範囲」なので。(→と思ってたけどやっぱり動作が怪しいので安全めな方に倒すということで毎回clientTopにアクセスする方法を使う事にした。なんか負けた気分。)

この「rangefindでループを回してDOMツリーを切った貼ったして装飾する」というやり方はFirefox 2以前のページ内検索における「すべて強調表示」の実装方法だったんだけど、今のFirefoxではDOMツリーはいじらずに強調箇所を選択範囲として処理するようになってて、この問題は問題にならないようだ。今でもFirefox本体でこういうことをやってるところがあれば「これってregressionなんじゃないの」とbugzillaに報告できるところだと思うんだけど……

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能