2007-12-12

IE/DOMLoad : behavior expression

IEの独自実装による document.write を使わない DOMロード の検知方法を考えました。

CSSのbehaviorとexpressionを使います。
behaviorは画面描画のたびに頻繁に評価されます。
expressionを用いると、javascriptを記述できます。

上記を利用することで、Diego Perini氏による doScroll の動作チェックコードの頻度をあげるというアイデアです。
アイデアの中心となるコードは次のようなものです。

document.documentElement.style.setExpression('behavior', 'document.documentElement.polling()');

これを利用したDOMロードイベントのライブラリを公開します。
利用方法は、関数 onready の引数に、DOMロード後に実行したい関数を渡すだけです。

HTML :
<script src="http:// ... /onready.js" type="text/javascript" charset="utf-8"></script>
javascript :
onready(function(){
    alert('how are you?');
});
onready(function(){
    alert('fine! and you?');
});
onready(function(){
    alert('me, too!');
});

onready.js :
/*
    onready.js : Cross-Browser DOMContentLoaded Event Listener

    Copyright (c) 2007 latchet (http://juce6ox.blogspot.com/)
    Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
*/
var onready = (function(){
    var id,
        ua   = navigator.userAgent.toLowerCase(),
        fs   = [],
        i    = 0,
        done = false,
    /*@cc_on
        oldExp,
    @*/
        init = function(){
                   var f, j = 0;
                   if(done) return;
                   done = true;
                   /*@cc_on
                   if(oldExp) document.documentElement.style.setExpression('behavior', oldExp);
                   else       document.documentElement.style.removeExpression('behavior');
                   window.detachEvent('onload', init);
                   document.onreadystatechange = null;
                   /*@if(false)@*/
                   document.removeEventListener('DOMContentLoaded', init, false);
                   window.removeEventListener('load', init, false);
                   /*@end@*/
                   window.onload = null;
                   while(f = fs[j++]) f();
               },
        set  = function(){
                   var ok;
                   /*@cc_on
                   oldExp = document.documentElement.style.getExpression('behavior');
                   document.documentElement.polling = function(){
                       try{
                           document.documentElement.doScroll('left');
                       }catch(e){
                           return setTimeout(document.documentElement.polling, 0);
                       };
                       document.documentElement.polling = null;
                       init();
                   };
                   document.documentElement.style.setExpression('behavior', 'document.documentElement.polling()');
                   window.attachEvent('onload', init);
                   document.onreadystatechange = function(){
                       if(document.readyState == 'complete') init();
                   };
                   /*@if(false)@*/
                   if(/mozilla/.test(ua) && !/compatible|webkit/.test(ua) || /opera/.test(ua))
                       document.addEventListener('DOMContentLoaded', init, false);
                   else if(/webkit|khtml/.test(ua) && (ok = /loaded|complete/))
                       (function(){
                           return ok.test(document.readyState) ? init() : setTimeout(arguments.callee, 0);
                       })();
                   window.addEventListener('load', init, false);
                   /*@end@*/
                   var old = window.onload
                   window.onload = (typeof old == 'function') ? function(){old();init();} : init;
               };
    return function(f){
        if(typeof f != 'function') return;
        if(done) return f();
        if(!i) set();
        fs[i++] = f;
    };
})();

参考資料

このアイデアを思いついてコードを書き終えたころに、細かい違いはありますが、 同じようにbehaviorとexpressionを使うアイデアを、doScrollハックの考案者であるDiego Perini氏がjQueryのメーリングリストに投稿していたことを検索の結果、知りました(笑)

Nabble - Re: document.ready firing too early within iframe

その時は正直、すこしがっくりしましたが、気付かなかったからこそ取り組めた面もあるのかもしれません。コードを整えつつ、その後、新しい別の方法が無いか再び試行錯誤し、(自分にとって)新しいアプローチを見つけました。document.writeは使わず、IEのDHTMLのondocumentreadyイベントを利用しつつ、新たな通信をしなくてもすむとおもわれる方法です。

IE/DOMLoad : cached htc

window.onload Problem

javascript の window.onload イベントはページの解析、画像などのファイル読込が全て完了してから発生するようです。
そのため、ロードイベント前の文書の解析後に迅速にページのDOM要素に対してjavascriptで操作を行ないたいという要求がおこるのは、ユーザーの利便性を考慮すると当然といえそうです。

Ajaxという言葉が注目され、Googleによるjavascriptを活用したアプリケーションが登場し、 回線速度の向上、インフラ整備、ブログ作成者・閲覧者の増加、ウェブAPIの公開、 マッシュアップサイトが数多く登場しています。

そのような背景の中でjavascriptによる DOM ロードイベント検知の方法もいろいろ探究されてきたようです。今までの流れを書き出してみました。


枯れた技術が活用され、以前は想定されなかった問題が解決されていく様子に見えます。
HTML5草案では、DOMContentLoadedイベント(FirefoxとOperaは実装済)が提案されているそうです。
将来、SafariとIEにも採用されたら多くの人に喜ばれそう。

2007-11-17

CSS/DOM : 動的にスタイルシートへルールを追加する

スタイル属性を動的に一括して設定します。
引数にはセレクタとルールを指定します。

//使用例
//addCSSRule(selector, declaration);

addCSSRule('div.test', 'width:200px; height:100px;');
addCSSRule('div.test', 'background:#ccc;');

var addCSSRule = (/*@cc_on!@*/false)
    ? (function(sheet){
          return function(selector, declaration){
              sheet.addRule(selector, declaration);
          };
      })(document.createStyleSheet())
    : (function(sheet){
          return function(selector, declaration){
              sheet.insertRule(selector + '{' + declaration + '}', sheet.cssRules.length);
          };
      })((function(e){
            e.appendChild(document.createTextNode(''));
            (document.getElementsByTagName('head')[0] || (function(h){
                document.documentElement.insertBefore(h, this.firstChild);
                return h;
            })(document.createElement('head'))).appendChild(e);
            return e.sheet;
        })(document.createElement('style')))
    ;


資料

2007-11-13

XPath/DOM : javascriptファイルの呼び出し元script要素を取得する

XPathを用いると、きめ細かく要素を指定できるようです。
たとえばamachangさんによるscript要素取得コードは以下のようなXPath式で表現可能です。
createElementで動的に生成されたscript要素を、その時点でのDOMツリーの末尾以外の任意の位置にappendChildしている場合には要素を取得できずにnullを返すか、ブラウザが解析中にたまたま末尾に存在する別のscript要素が返されます。
静的にHTMLで記述されたscript要素についてはその心配はありません。

var currentScript = !!(document.implementation && document.implementation.hasFeature && document.implementation.hasFeature('XPath', '3.0'))
    ? document.evaluate(
          '//*[local-name()="SCRIPT" or local-name()="script"][not(ancestor-or-self::*[following-sibling::*])]',
          document,
          null,
          XPathResult.FIRST_ORDERED_NODE_TYPE,
          null
      ).singleNodeValue
    : (function(e){
          return (!e) ? null
               : (e.nodeName.toLowerCase() == 'script') ? e
               : arguments.callee(e.lastChild);
      })(document)
    ;

探索対象文書(XML, HTML)、ブラウザの種類によってXPath式で判定可能な要素名の大文字・小文字の種別に影響があるようです。

たとえば、このブログのアドレスバーで以下のブックマークレットを実行した場合、Firefox2とOpera9で挙動が異なります。IEは現時点でXPathをサポートしていないため両方ともエラーとなります。
XPathでscript要素を抽出し、成功したらnodeNameを表示します。

■大文字(SCRIPT)でXPathにより要素抽出
○ Firefox2
× Opera9
× IE
javascript: try{alert(document.evaluate('//*[local-name()="SCRIPT"]',document,null,9,null).singleNodeValue.nodeName);}catch(e){alert(e);};



■小文字(script)でXPathにより要素抽出
× Firefox2
○ Opera9
× IE
javascript: try{alert(document.evaluate('//*[local-name()="script"]',document,null,9,null).singleNodeValue.nodeName);}catch(e){alert(e);};



資料

2007-11-08

XPath/DOM : Class属性による要素抽出 - クロスブラウザ document.getElementsByClassName

javascriptの優れたライブラリが近年続々と公開されています。

幸いな事にprototype.jsjQuery、そして最近ではさらにamachangさんによるライブラリによってjavascriptによるDOM要素抽出が気軽に見通しよく記述することができる環境が整っているようです。

車輪の再実装ですがタグ名とクラス名を指定してDOM要素抽出を行なうという用途のための小さなコードを書きました。既存ライブラリを使わない小さなサイズのスクリプトでちょっとしたDOM操作のために。

使い方はgetBoxes(klass, tag) でクラス属性値klass、タグ名tagのDOM要素を配列にして抽出します。tagを省略すると、指定されたクラス属性値を持つ全てのタグを抽出します。 たとえば、getBoxes('test', 'div') では、testをクラス属性値に持つタグ名divの要素を配列で返します。

■追記:2007/11/12
getBoxes(klass, tag, context) のように第3引数にDOM要素を指定すると、その要素と子孫要素が探索対象範囲となる機能を追加しました。第3引数を指定しなければページ内の全要素が探索対象となります。
XPathを使用する場合、XPath式とnamespace resolver(ここでは、nullです)から生成されるオブジェクトを保存し再利用するようにしました。

var getBoxes
    = (/Firefox\/(\d+)/.test(navigator.userAgent) && RegExp.$1 > 2) ?
          (function(){
              var toChangeCase = (document.documentElement.nodeName == 'HTML')
                               ? 'toUpperCase' : 'toLowerCase';
              return function(klass, tag, context){
                  var boxes = (context || document).getElementsByClassName(klass);
                  return (tag && (tag = tag[toChangeCase]()))
                       ? Array.filter(boxes, function(e){return e.nodeName == tag;})
                       : boxes;
              };
          })()
    : (document.implementation && document.implementation.hasFeature && document.implementation.hasFeature('XPath', '3.0')) ?
          (function(){
              var toChangeCase = (document.evaluate('//*[local-name()="HTML"]', document, null, XPathResult.BOOLEAN_TYPE, null).booleanValue)
                               ? 'toUpperCase' : 'toLowerCase',
                  cache = {};
              return function(klass, tag, context){
                  var boxes = [],
                      key   = (tag || '*') + ':' + klass,
                      exp   = cache[key] || 
                             (cache[key] = document.createExpression('//*'
                            + (tag ? ('[local-name()="' + tag[toChangeCase]() + '"]') : '')
                            + '[contains(concat(" ",@class," ")," ' + klass + ' ")]', null)),
                      nodes = exp.evaluate(context || document, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null),
                      node,
                      i = 0;
                  if(nodes)
                      while(node = nodes.iterateNext())
                          boxes[i++] = node;
                  return boxes;
              };
          })()
    :     function(klass, tag, context){
              var boxes = [],
                  regex = new RegExp('(?:^|\\s)' + klass + '(?:\\s|$)'),
                  i = 0;
              /*@cc_on
              if(@_jscript_version > 5.5 || @_mac || tag || !context){
                @*/
              var nodes = /*@cc_on (@_jscript_version < 5.6 && !@_mac && !tag && !context) ? document.all : @*/
                          (context || document).getElementsByTagName(tag && tag.toLowerCase() || '*'),
                  node,
                  j = 0;
              if(nodes)
                  while(node = nodes[i++])
                      if(regex.test(node.className))
                          boxes[j++] = node;
              /*@cc_on
              }else{
                  (function walk(e){
                      if(e.nodeType == 1 && regex.test(e.className)) boxes[i++] = e;
                      e = e.firstChild;
                      while(e){
                          walk(e);
                          e = e.nextSibling;
                      };
                  })(context);
              };@*/
              return boxes;
          };


資料