setExpression と removeExpression について調べ物

IE 5〜7 限定のメソッド setExpression と removeExpression について。IE8のmode8では CSS:expression は廃止された(setExpression と removeExpression も廃止されたかは不明)。

  • setExpression
    • element.setExpression(String:プロパティ名, String:式): undefined を返す
    • element.style.setExpression(String:CSSプロパティ名, String:式): undefined を返す
      • CSSプロパティ名はキャメルケース(backgroundImage)
  • getExpression
    • element.setExpression(String:プロパティ名): 式または undefined を返す
    • element.style.setExpression(String:CSSプロパティ名): 式または undefined を返す
  • removeExpression
    • element.removeExpression(String:プロパティ名): 削除成功でtrueを、失敗でfalseを返す
    • element.style.removeExpression(String:CSSプロパティ名): 削除成功でtrueを、失敗でfalseを返す
      • setExpression から呼ばれた関数内で removeExpression を行うと例外が発生する。そのようなケースではsetTimeout(func, 0)等を挟み別スレッドで実行する必要がある。
      • CSSで設定した expression も removeExpression で各要素ごとに削除できる。

CSSで設定した expression を removeExpression する

<style>
.alpha {
  width: expression(step1(this));
}
</style>
<script>
function step1(elm) {
  setTimeout(function() { step2(elm); }, 0);
}
function step2(elm) {
  alert(elm.style.getExpression("width")); // "step1(this)"
  elm.style.removeExpression("width");
  alert(elm.style.getExpression("width")); // undefined
}
</script>

behavior:expression との棲み分け

以下のようにすると、全ての要素について 一度だけ hoge(element) を呼び出すことができる

<style>
* {
  behavior:expression(hoge(this))
}
</style>
<script>
function hoge(elm) {
  elm.style.behavior = "none"; // この要素は、behavior:expression から呼ばれなくなる
}
</script>

他のライブラリ併用時に発生するコンフリクトを回避する仕組みが必要

上記の書き方でも自己完結した世界なら十分だが、同様の仕組みを持つ他のライブラリをロードすると問題が発生する(behaviorの取り合いが発生する)。

解決するには、要素の追加/削除(MutationEvent)イベントを通知する流れを作り、各ライブラリがこの流れに沿って行動を起こす必要がある。

MutationEvent は IEには実装されていないが、uupaa.js では MutationEvent をエミュレートすることでこの流れを作っている。

// === Custom Event ========================================
UU.CUSTOM_EVENT = {
  NOTIFY:           0x01, // without node
  ADD_ELEMENT:      0x02, // with node
  REMOVE_ELEMENT:   0x04, // with node
  UPDATE_ELEMENT:   0x08,
  RESIZE_VIEWPORT:  0x10, // resize window
  RESIZE_BODY:      0x20, // resize body (one shot)
  RESIZE_FONT:      0x40, // resize font
  ALL:              0xff
};

uu.Class.Singleton("CustomEvent", {
  construct: function() {
    this._fn = []; // Array( [[callback function, customEvent], ... ] )
    this._enable = true;
    this._lastEvent = 0x0; // infinitely resize guard

    // set event handler
    var me = this;

    uu.customEventResizeFont.attach(function() {
      uu.style.unit(1);
      me.fire(UU.CUSTOM_EVENT.RESIZE_FONT);
    });

    uu.customEventResizeBody.attach(function() {
      me.fire(UU.CUSTOM_EVENT.RESIZE_BODY);
    });

    uu.event.attach(window, "resize", this);

    if (UU.IE && uu.ua.version < 8) {
      // hook add element for IE6, IE7
      if (uu.windowReady) {
        setAgent();
      } else {
        window.attachEvent("onload", setAgent);
      }
    }

    function setAgent() {
      uudoc.createStyleSheet().cssText =
          "*{behavior:expression(uu.Class.CustomEvent._addElement(this))}";
    }
  },

  handleEvent: function(evt) {
    if (this._enable) {
      if (evt.type === "resize") {
        this.fire(UU.CUSTOM_EVENT.RESIZE_VIEWPORT);
      }
    }
  },

  // uu.Class.CustomEvent.enable
  enable: function() {
    this._enable = true;
  },

  // uu.Class.CustomEvent.disable
  disable: function() {
    this._enable = false;
  },

  // uu.Class.CustomEvent.attach - attach event handler
  attach: function(fn,            // Function: callback function
                   customEvent) { // Number: customEvent, UU.CUSTOM_EVENT...
    this._fn.push([fn, customEvent]);
  },

  // uu.Class.CustomEvent.detach - detach event handler
  detach: function(fn) { // Function: callback function
    var idx = -1, i = 0, iz = this._fn.length;
    for (; i < iz; ++i) {
      if (i in this._fn) {
        if (this._fn[i][0] === fn) {
          idx = i;
          break;
        }
      }
    }
    if (idx >= 0) {
      this._fn.splice(idx, 1);
    }
  },

  // uu.Class.CustomEvent.fire - revalidate
  fire: function(customEvent, // Number(default: 0x01): customEvent
                 node) {      // Node(default: undefined):
    customEvent = customEvent === void 0 ? 0x01 : customEvent;

    if (this._enable) {
      if (customEvent & UU.CUSTOM_EVENT.RESIZE_VIEWPORT &&
          customEvent & this._lastEvent) {
        customEvent = customEvent & ~UU.CUSTOM_EVENT.RESIZE_VIEWPORT;
      }
      this._lastEvent = customEvent;

      var v, i = 0;
      while ( (v = this._fn[i++]) ) {
        if (customEvent & v[1]) {
          v[0](customEvent, node); // callback
        }
      }
    }
  }
});

// static function
uu.mix(uu.Class.CustomEvent, {
  // uu.Class.CustomEvent._addElement
  _addElement: function(elm) {
    // @see http://d.hatena.ne.jp/uupaa/20081129/1227951320
    if (elm.style.behavior === "none") { return; } // guard: text selection + drag

    uu.customEvent.disable();

    elm.style.behavior = "none"; // disable CSS::expression

    uu.customEvent.enable();

    if (elm.nodeType === 1) {
      // hook remove element
      (function(node) {
        var rm = node.removeChild;
        node.removeChild = function(oldChild) {
          var rv = rm(oldChild);
          uu.customEvent.fire(UU.CUSTOM_EVENT.REMOVE_ELEMENT, rv);
          return rv;
        }
      })(elm);

      uu.customEvent.fire(UU.CUSTOM_EVENT.ADD_ELEMENT, elm);
    }
  }
});

uu.customEvent = null;
uu.ready(function() {
  uu.customEvent = new uu.Class.CustomEvent();
}, 2); // 2: mid


要素が追加されたタイミング(正確にはブラウザが要素を初めて表示しようとしたタイミング)を知りたい場合は、クラスのコンストラクタ内で以下のように定義しておく。
uu.customEvent.attach() でアタッチした関数は、イベント種別(customEvent) と 追加された要素(elm) を引数にコールバックされる。

uu.Class.Singleton("IEBoostPNGBG", {
  construct: function() {
    var me = this;
    uu.customEvent.attach(function(customEvent, elm) {
      if (uu.className.has(elm, "alpha")) { // elm.className に "alpha" が含まれていればtrue
        me.init(elm); // 透過 png 処理開始
      }
    }, UU.CUSTOM_EVENT.ADD_ELEMENT);
  }
});
UU.IE && uu.ready(function() { // DOMContentLoaded でインスタンス化
  uu.ieboostPNGBG = new uu.Class.IEBoostPNGBG();
});

その先にある問題

IEに欠落している機能(CSS2.1, CSS3系)を補完するには、 expression, behavior, onpropertychange を最大限に利用する必要があるが、これらはたとえ使用方法を間違えなくても、何かのはずみでブラウザをフリーズさせる能力を持っている。

再描画の度に関数が呼び出され再評価が行われるため、本当に必要な条件を編み出し、不要な再描画を避けるようにガードしてあげないと、パフォーマンスが劇的に悪化する。

これらをクリアしたとしても、resize イベントの無限ループ地獄が きっとあなたを待っている。