prototype.jsを読み解く

第6回 Prototypeライブラリ(1609~2051行目)

この記事を読むのに必要な時間:およそ 18 分

1684: if (Prototype.Browser.Opera) {
1685:   Element.Methods._getStyle = Element.Methods.getStyle;
1686:   Element.Methods.getStyle = function(element, style) {
1687:     switch(style) {
1688:       case 'left':
1689:       case 'top':
1690:       case 'right':
1691:       case 'bottom':
1692:         if (Element._getStyle(element, 'position') == 'static') return null;
1693:       default: return Element._getStyle(element, style);
1694:     }
1695:   };
1696: }

ここからは,Element.Methodsの関数を,ブラウザごとに微調整します。

まずはOpera用にgetStyle()を再定義します。

元のgetStyleを_getStyleとして保存しておき,新たにgetStyleを定義します。style.positionが'static'で,'left', 'top', 'right', 'bottom' のスタイル情報を取得しようとするとnullを返し,それ以外は元の_getStyle()を呼び出して値を返しています。

先ほどと似ていますが,Operaではposition指定が無い要素に明示的にposition:staticを指定すると,top, leftなどが自分の親のうちposition:static以外のものからの相対座標,に設定されます。これを嫌ってPrototypeライブラリのgetStyle()を使う場合はIEと同じようにnullを返すようにしています。

1697: else if (Prototype.Browser.IE) {
1698:   Element.Methods.getStyle = function(element, style) {
1699:     element = $(element);
1700:     style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
1701:     var value = element.style[style];
1702:     if (!value && element.currentStyle) value = element.currentStyle[style];
1703: 
1704:     if (style == 'opacity') {
1705:       if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
1706:         if (value[1]) return parseFloat(value[1]) / 100;
1707:       return 1.0;
1708:     }
1709: 
1710:     if (value == 'auto') {
1711:       if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
1712:         return element['offset'+style.capitalize()] + 'px';
1713:       return null;
1714:     }
1715:     return value;
1716:   };
1717: 

IE用の修正として,getStyle(),setOpacity(),update()を入れ替えています。

1698行目のgetStyle()では,floatスタイルの取得に'float', 'cssFloat'ではなく'styleFloat'を使うようにしています。

また,IEではdocument.defaultView.getComputedStyle()が使えないので,その代わりにcurrentStyleプロパティを参照しています。こちらも要素に指定されたスタイルではなく,実際に描画されているスタイル情報を取得することになります。

'opacity'を取得しようとしている場合は,'filter'に'alpha(opacity=50)'のような指定が入っているかどうかをチェックし,その値をFirefoxなどと合わせる為に100で割って返しています(IEは100で不透明⁠⁠。

このalpha()の内側は,opacity=50などの他にもいくつかのパラメータが利用できます。また大文字で記述しても効果があるので,この正規表現だとカバーできない条件が多いでしょう。

得られた値が'auto'の場合,元のgetStyle()では単にnullを返していましたが,ここでは取得しようとしているスタイルが'width'か'height'でその要素のdisplayスタイルが'none'以外の場合,offsetWidthかoffsetHeightで値が取れるのでそれを使って返します。

1718:   Element.Methods.setOpacity = function(element, value) {
1719:     element = $(element);
1720:     var filter = element.getStyle('filter'), style = element.style;
1721:     if (value == 1 || value === '') {
1722:       style.filter = filter.replace(/alpha\([^\)]*\)/gi,'');
1723:       return element;
1724:     } else if (value < 0.00001) value = 0;
1725:     style.filter = filter.replace(/alpha\([^\)]*\)/gi, '') +
1726:       'alpha(opacity=' + (value * 100) + ')';
1727:     return element;
1728:   };
1729: 

1718行目からはsetOpacity()の上書きです。

半透明化はIEではfilter: alpha(opacity=<integer>)を使うので,その形で実装しなおします。

まず,現状のfilterスタイルの値を取得しておきます。指定しようとしている値が1もしくは空文字列の場合,完全な不透明を指定していることになるので,filter文字列からalpha()関数呼び出しをまるごと削除します。

もし指定しようとしている値が極端に小さければ0として扱うようにし,その後alpha()呼び出しを削除した後,指定された値でalpha(opacity=値)を追加しています。

先ほども書きましたが,alpha()にはopacity以外にもいろいろと指定できるため,alpha()まるごと削除,はちょっと乱暴でしょう。

1730:   // IE is missing .innerHTML support for TABLE-related elements
1731:   Element.Methods.update = function(element, html) {
1732:     element = $(element);
1733:     html = typeof html == 'undefined' ? '' : html.toString();
1734:     var tagName = element.tagName.toUpperCase();
1735:     if (['THEAD','TBODY','TR','TD'].include(tagName)) {
1736:       var div = document.createElement('div');
1737:       switch (tagName) {
1738:         case 'THEAD':
1739:         case 'TBODY':
1740:           div.innerHTML = '<table><tbody>' +  html.stripScripts() + '</tbody></table>';
1741:           depth = 2;
1742:           break;
1743:         case 'TR':
1744:           div.innerHTML = '<table><tbody><tr>' +  html.stripScripts() + '</tr></tbody></table>';
1745:           depth = 3;
1746:           break;
1747:         case 'TD':
1748:           div.innerHTML = '<table><tbody><tr><td>' +  html.stripScripts() + '</td></tr></tbody></table>';
1749:           depth = 4;
1750:       }
1751:       $A(element.childNodes).each(function(node) { element.removeChild(node) });
1752:       depth.times(function() { div = div.firstChild });
1753:       $A(div.childNodes).each(function(node) { element.appendChild(node) });
1754:     } else {
1755:       element.innerHTML = html.stripScripts();
1756:     }
1757:     setTimeout(function() { html.evalScripts() }, 10);
1758:     return element;
1759:   }
1760: }

1730行目からはupdate()の上書きです。

IEではテーブル関係の一部だけをinnerHTMLで書き換えることができないので,その場合の例外処理を行います。

書き換えようとしている要素のタグ名がthead,tbody,tr,td以外の場合は,元々のupdate()と挙動は同じです。

例外対象のタグの場合は,空の div 要素を作って,その中に必要な階層のテーブル関係のタグと,今回更新したい内容をくっつけて,まとめて innerHTML に代入します。その際,後で何階層下のタグを取り出すのか,ということを示す数値を depth 変数に入れておきます。

1751行目で,要素の子供ノードをすべて削除します。その後,depthで指定された回数だけdiv.firstChildを辿ります。これでdivは今回html変数に入った文字列から作った要素のひとつ親の要素を示すようになります。

最後に,divのchildNodesすべてを,elementにappendChild()して終了です。

1761: else if (Prototype.Browser.Gecko) {
1762:   Element.Methods.setOpacity = function(element, value) {
1763:     element = $(element);
1764:     element.style.opacity = (value == 1) ? 0.999999 :
1765:       (value === '') ? '' : (value < 0.00001) ? 0 : value;
1766:     return element;
1767:   };
1768: }
1769: 

1761行目からは Gecko(Mozilla系)用の修正です。元々の関数との変更点は,指定された値が1だったときに,style.opacityに''を設定していたのが0.999999を設定している,という点です。

この0.999999を渡す,という処理は他のライブラリでも見かけますが,由来が見つけられませんでした。簡単なテストでは1.0でも0.999999でもレンダリング結果は変わらないように見えます。

1770: Element._attributeTranslations = {
1771:   names: {
1772:     colspan:   "colSpan",
1773:     rowspan:   "rowSpan",
1774:     valign:    "vAlign",
1775:     datetime:  "dateTime",
1776:     accesskey: "accessKey",
1777:     tabindex:  "tabIndex",
1778:     enctype:   "encType",
1779:     maxlength: "maxLength",
1780:     readonly:  "readOnly",
1781:     longdesc:  "longDesc"
1782:   },
1783:   values: {
1784:     _getAttr: function(element, attribute) {
1785:       return element.getAttribute(attribute, 2);
1786:     },
1787:     _flag: function(element, attribute) {
1788:       return $(element).hasAttribute(attribute) ? attribute : null;
1789:     },
1790:     style: function(element) {
1791:       return element.style.cssText.toLowerCase();
1792:     },
1793:     title: function(element) {
1794:       var node = element.getAttributeNode('title');
1795:       return node.specified ? node.nodeValue : null;
1796:     }
1797:   }
1798: };
1799: 

1770行目からはElement._attributeTranslationsです。

Element.readAttribute()でIE用の例外処理に用いられたり,Element.Methods.Simulated.hasAttribute()の中でプロパティ名の正規化に用いられたりします。

namesプロパティに入っているのは,要素の属性名の大文字小文字を正規化するための変換テーブルです。

valuesの方には,例外処理を必要とする要素の属性が含まれており,"style","title"は独自の関数を,それ以外は_getAttr()か_flag()が使われます。

_getAttr()内ではgetAttribute()関数の第二引数に2を渡しています。これは,IE独自の拡張かと思われますが,真だと大文字小文字を区別して属性を探すようです。

_flags()内では,checked="checked"というようなフラグ形式のものにおいて,適切な文字列が返されるようにするものです。

1800: (function() {
1801:   Object.extend(this, {
1802:     href: this._getAttr,
1803:     src:  this._getAttr,
1804:     type: this._getAttr,
1805:     disabled: this._flag,
1806:     checked:  this._flag,
1807:     readonly: this._flag,
1808:     multiple: this._flag
1809:   });
1810: }).call(Element._attributeTranslations.values);
1811: 

"href","src","type","disabled","checked","readonly","multiple"属性用に,内部用関数_getAttr,_flagを割り当てます。

Element._attributeTranslations.valuesのプロパティを,そのオブジェクト内の関数を値として設定するため,無名関数とcall()を使ってthisとして表記できるようにして短く記述しています。callを使わずに引数として渡してしまう方が素直かと思います。

1812: Element.Methods.Simulated = {
1813:   hasAttribute: function(element, attribute) {
1814:     var t = Element._attributeTranslations, node;
1815:     attribute = t.names[attribute] || attribute;
1816:     node = $(element).getAttributeNode(attribute);
1817:     return node && node.specified;
1818:   }
1819: };
1820: 

Element.Methods.Simulatedは,特定のブラウザでHTMLElementインターフェイスの実装が欠けているのを補うための関数が置かれています。

といっても今のところはIE 6, 7用のhasAttribute()関数のみです。Element.extend()で自動的に拡張されたり,Element.hasAttribute()から間接的に呼ばれたり,Element.addMethods()で拡張するのに使われています。

hasAttribute()では,Element._attributeTranslations.namesハッシュを使ってIE用にプロパティ名を正規化し,getAttributeNode()で属性ノードを取得しています。ここで,この関数が真を返すのはnodeが取得でき,かつnode.specifiedが真の場合です。このgetAttributeNode()が返すnodeはInterface Attrを実装する属性ノードで,そのspecifiedはユーザーが値を設定した場合に真となる,と定義されています(要素がその属性としてデフォルト値を持っていても,ユーザーが値を設定しなければ偽⁠⁠。一方hasAttribute()はその要素がデフォルト値を持っていても真となる,と定義されています。それに従うと,ここではnode.specifiedを使っているのは間違いかと思われます。

1821: Element.Methods.ByTag = {};
1822: 

1821行目では,Element.MEthods.ByTagオブジェクトを用意しています。この中身はElement.addMethods()で詰め込まれ,利用されています。

1823: Object.extend(Element, Element.Methods);
1824: 

ここまでで用意してきたElement.Methods内の拡張用のメソッド群を,Elementクラスに拡張しています。

著者プロフィール

栗山淳(くりやまじゅん)

S2ファクトリー株式会社株式会社イメージソース所属。
本業はWeb制作会社の裏方。得意分野はFreeBSDやPerlのはずだが,必要に迫られるとHTML/CSSやJavaScriptも書く。