prototype.jsを読み解く

第9回Prototypeライブラリ(2621~2845行目)

Prototype 1.6.0とscript.aculo.us 1.8.0

script.aculo.us 1.8.0がアナウンスされ、Prototypeも1.6.0のタグが打たれたようです。

新機能の部分が枯れるまではまだしばらくは1.5.1.1でいいとは思いますが、Prototypeに依存するライブラリも徐々に1.6.0に移行していくと思われるので、早いうちから慣れておいたほうがよいかもしれません。

では、今回は少なめですが、主にForm関係のクラスたちを見ていきます。

$$() 関数

2621: function $$() {
2622:   return Selector.findChildElements(document, $A(arguments));
2623: }

2621行目からは$$()関数です。

先ほどのSelector.findChildElements()をdocumentトップから検索するようにしたショートカット関数となっています。

Form オブジェクト

2624: var Form = {
2625:   reset: function(form) {
2626:     $(form).reset();
2627:     return form;
2628:   },
2629: 

2624行目からはFormオブジェクトです。reset(), serializeElements()しか定義されていませんが、Element.addMethods()でObject.extend(Form, Form.Methods)されているので、最終的にはForm.Methodsのメソッドも持つことになります。

2625行目からのreset()は、ブラウザ側のHTMLFormElementインターフェイスのreset()を呼んでいるだけです。

2630:   serializeElements: function(elements, getHash) {
2631:     var data = elements.inject({}, function(result, element) {
2632:       if (!element.disabled && element.name) {
2633:         var key = element.name, value = $(element).getValue();
2634:         if (value != null) {
2635:                 if (key in result) {
2636:             if (result[key].constructor != Array) result[key] = [result[key]];
2637:             result[key].push(value);
2638:           }
2639:           else result[key] = value;
2640:         }
2641:       }
2642:       return result;
2643:     });
2644: 
2645:     return getHash ? data : Hash.toQueryString(data);
2646:   }
2647: };
2648: 

2630行目からはserializeElements() です。

渡された要素の配列を、Array.inject({})を使ってループ処理します。そのイテレータ関数内では、disabledプロパティが偽で、nameプロパティがセットされているものだけが対象になっています。また、同じnameを持つ要素が複数出てきた場合は、値を配列として格納するようになっています。

各フォーム要素の値を得るにはgetValue() ⁠定義は Form.Element.Methods.getValue())を用いています。

最後に、getHashフラグが真ならハッシュのまま、そうでなければtoQueryString()して文字列形式で返しています。

Form.Methods オブジェクト

2649: Form.Methods = {
2650:   serialize: function(form, getHash) {
2651:     return Form.serializeElements(Form.getElements(form), getHash);
2652:   },
2653: 

2649行目からはForm.Methodsオブジェクトです。結局FormにObject.extend()するので、なぜわざわざ分割しているのかは不明です。

2650行目からはserialize()です。これは次のgetElements()を使って指定されたフォーム以下の入力要素の一覧を取得し、それを先ほどのForm.serializeElements()に渡しています。

2654:   getElements: function(form) {
2655:     return $A($(form).getElementsByTagName('*')).inject([],
2656:       function(elements, child) {
2657:         if (Form.Element.Serializers[child.tagName.toLowerCase()])
2658:           elements.push(Element.extend(child));
2659:         return elements;
2660:       }
2661:     );
2662:   },
2663: 

2654行目からはgetElements()です。

フォーム要素を渡すと、それ以下でgetElementsByTagName('*')で全タグを取得し、Form.Element.Serializersにシリアライズ用ハンドラが存在するなら対応している要素とみなして返り値用の配列にpush()します。具体的には<input>, <textarea>, <select>に対応しています。

2664:   getInputs: function(form, typeName, name) {
2665:     form = $(form);
2666:     var inputs = form.getElementsByTagName('input');
2667: 
2668:     if (!typeName && !name) return $A(inputs).map(Element.extend);
2669: 
2670:     for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
2671:       var input = inputs[i];
2672:       if ((typeName && input.type != typeName) || (name && input.name != name))
2673:         continue;
2674:       matchingInputs.push(Element.extend(input));
2675:     }
2676: 
2677:     return matchingInputs;
2678:   },
2679: 

2664行目からはgetInputs()です。

typeNameに'radio'などと指定して限定したり、さらにnameを指定して絞り込んだりできます。

typeName, nameの両方とも指定されていなければ、指定されているフォーム要素以下の<input>タグすべてを返します。

そうでなければ、取得した<input>タグをループして、typeName, nameの指定に沿ったものを集めてそれだけを返しています。

2680:   disable: function(form) {
2681:     form = $(form);
2682:     Form.getElements(form).invoke('disable');
2683:     return form;
2684:   },
2685: 

2680行目はdisable()です。

Form.getElements()でフォーム内のすべての入力要素を配列で取得し、Enumerable.invoke()を使って各々の要素に対してdisable()メソッドを起動しています。

2686:   enable: function(form) {
2687:     form = $(form);
2688:     Form.getElements(form).invoke('enable');
2689:     return form;
2690:   },
2691: 

2686行目からは enable() です。

disable()と同様で、disable()の代わりにenable()メソッドを起動しています。

2692:   findFirstElement: function(form) {
2693:     return $(form).getElements().find(function(element) {
2694:       return element.type != 'hidden' && !element.disabled &&
2695:         ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
2696:     });
2697:   },
2698: 

2692行目からはfindFirstElement()です。

getElements()で入力要素すべてを取得し、イテレータ内で真を返す最初のものだけを返すEnumerable.find()メソッドを使っています。

イテレータ内の条件は<input type="hidden">ではなく、プロパティdisabledが真でもなく、タグが<input>, <select>, <textarea>のいずれかである場合に真となります。最後の条件はgetElements()でやっている形に合わせてForm.Element.Serializers 以下のプロパティ存在チェックを使う方が(速度的にも、同じような条件判断を一箇所に集めるためにも)よいかもしれません。

2699:   focusFirstElement: function(form) {
2700:     form = $(form);
2701:     form.findFirstElement().activate();
2702:     return form;
2703:   },
2704: 

2699行目はfocusFirstElement()です。単純に先ほどのfindFirstElement()を呼んで、その要素に対してForm.Element.Methods.activate()メソッドを呼んでいるだけです。

2705:   request: function(form, options) {
2706:     form = $(form), options = Object.clone(options || {});
2707: 
2708:     var params = options.parameters;
2709:     options.parameters = form.serialize(true);
2710: 
2711:     if (params) {
2712:       if (typeof params == 'string') params = params.toQueryParams();
2713:       Object.extend(options.parameters, params);
2714:     }
2715: 
2716:     if (form.hasAttribute('method') && !options.method)
2717:       options.method = form.method;
2718: 
2719:     return new Ajax.Request(form.readAttribute('action'), options);
2720:   }
2721: }
2722: 
2723: /*--------------------------------------------------------------------------*/
2724: 

2705行目からはrequest()です。

フォームを簡単にAjax.Request()経由で送信するための関数です。

まず、options.parametersで渡された送信パラメータと、フォームが持っている送信パラメータをマージします。2712行目で必要に応じて正規化してくれているので、options.parametersにはクエリ文字列形式の文字列でもハッシュ形式のオブジェクトでも構いません。

フォームの送信に使うHTTPメソッドは、options.methodが指定されていればそちらを優先し、そうでなければ<form>タグのmethod属性を使います。

最後にAjax.Request()を呼び出して終了です。

Form.Element オブジェクト

2725: Form.Element = {
2726:   focus: function(element) {
2727:     $(element).focus();
2728:     return element;
2729:   },
2730: 
2731:   select: function(element) {
2732:     $(element).select();
2733:     return element;
2734:   }
2735: }
2736: 

2725行目からはForm.Elementです。focus()とselect()しか定義されていませんが、後述するForm.Element.MethodsがObject.extend()されるので、実際にはそれらを合わせた関数が使えます。

focus(), select()共に渡された要素のfocus(), select()メソッドを呼んでいるだけです。両方続けて呼んでくれるForm.Element.Methods.activate()という関数もあります。

Form.Element.Methods オブジェクト

2737: Form.Element.Methods = {
2738:   serialize: function(element) {
2739:     element = $(element);
2740:     if (!element.disabled && element.name) {
2741:       var value = element.getValue();
2742:       if (value != undefined) {
2743:         var pair = {};
2744:         pair[element.name] = value;
2745:         return Hash.toQueryString(pair);
2746:       }
2747:     }
2748:     return '';
2749:   },
2750: 

2737行目からはForm.Element.Methodsです。

まずはserialize()です。単一要素の保持している値を、name=valueの形式の文字列として返します。

disabledプロパティが偽で、nameプロパティが設定されていて、値がundefinedでなければハッシュオブジェクトを作成しHash.toQueryString()で返します。

2751:   getValue: function(element) {
2752:     element = $(element);
2753:     var method = element.tagName.toLowerCase();
2754:     return Form.Element.Serializers[method](element);
2755:   },
2756: 

2751行目からはgetValue()です。

Form.Element.Serializersにタグ名ごとのハンドラが入っているので、それを呼び出して返り値を返しています。

2757:   clear: function(element) {
2758:     $(element).value = '';
2759:     return element;
2760:   },
2761: 

2757行目からはclear()です。

単にvalueプロパティに空文字列を設定しているだけです。そのため、<input type="text">や<textarea>で使われることが前提です。

2762:   present: function(element) {
2763:     return $(element).value != '';
2764:   },
2765: 

2762行目からはpresent()です。

valueプロパティが空文字でなければ真を返します。

2766:   activate: function(element) {
2767:     element = $(element);
2768:     try {
2769:       element.focus();
2770:       if (element.select && (element.tagName.toLowerCase() != 'input' ||
2771:         !['button', 'reset', 'submit'].include(element.type)))
2772:         element.select();
2773:     } catch (e) {}
2774:     return element;
2775:   },
2776: 

2766行目からはactivate()です。

まずfocus()を呼んで、その後selectプロパティが存在し、タグが<textarea>, <select>か、<input>のtypeがtext, password, hidden, radio, checkbox ならselect()メソッドを呼んでいます。

IEでは要素がdisplay:noneやvisibility:hiddenの場合に、focus()を呼ぶと例外が発生してしまいます。それを避けるために try {} で括って例外を無視するようにしています。

2777:   disable: function(element) {
2778:     element = $(element);
2779:     element.blur();
2780:     element.disabled = true;
2781:     return element;
2782:   },
2783: 

2777行目からはdisable()です。

要素のblur()メソッドを呼んでフォーカスを外し、disabledプロパティにtrueを設定して無効化しています。

2784:   enable: function(element) {
2785:     element = $(element);
2786:     element.disabled = false;
2787:     return element;
2788:   }
2789: }
2790: 
2791: /*--------------------------------------------------------------------------*/
2792: 

2784行目からはenable()です。

こちらはdisabledプロパティにtrueを設定しているだけで、フォーカスは操作しません。

2793: var Field = Form.Element;
2794: var $F = Form.Element.Methods.getValue;
2795: 
2796: /*--------------------------------------------------------------------------*/
2797: 

2793行目ではForm.Elementの別名としてFieldを作っています。今のところ他の場所ではこれは使われていないようです。

また、2794行目ではForm.Element.Methods.getValue()の別名として$F()関数を定義しています。

Form.Element.Serializers オブジェクト

2798: Form.Element.Serializers = {
2799:   input: function(element) {
2800:     switch (element.type.toLowerCase()) {
2801:       case 'checkbox':
2802:       case 'radio':
2803:         return Form.Element.Serializers.inputSelector(element);
2804:       default:
2805:         return Form.Element.Serializers.textarea(element);
2806:     }
2807:   },
2808: 

Form.Element.Serializersには、要素に対応したハンドラであるinput, textarea, selectプロパティと、それらから呼び出されるヘルパ関数からなっています。

2799行目からはinputハンドラです。

type別に処理を分けていて、checkbox, radioの場合はinputSelector()関数を、それ以外は単にvalueを取得するtextareaハンドラを呼び出しています。

2809:   inputSelector: function(element) {
2810:     return element.checked ? element.value : null;
2811:   },
2812: 
2813:   textarea: function(element) {
2814:     return element.value;
2815:   },
2816: 

inputSelector()では、checkedプロパティが真ならvalueプロパティの値を、そうでなければnullを返します。

textareaハンドラでは、<textarea>タグと<input>タグの一部を担当します。<textarea>でもvalueプロパティで値を取得できるので、それをそのまま返しています。

2817:   select: function(element) {
2818:     return this[element.type == 'select-one' ?
2819:       'selectOne' : 'selectMany'](element);
2820:   },
2821: 
2822:   selectOne: function(element) {
2823:     var index = element.selectedIndex;
2824:     return index >= 0 ? this.optionValue(element.options[index]) : null;
2825:   },
2826: 
2827:   selectMany: function(element) {
2828:     var values, length = element.length;
2829:     if (!length) return null;
2830: 
2831:     for (var i = 0, values = []; i < length; i++) {
2832:       var opt = element.options[i];
2833:       if (opt.selected) values.push(this.optionValue(opt));
2834:     }
2835:     return values;
2836:   },
2837: 
2838:   optionValue: function(opt) {
2839:     // extend element because hasAttribute may not be native
2840:     return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
2841:   }
2842: }
2843: 
2844: /*--------------------------------------------------------------------------*/
2845: 

2817行目からはselectハンドラです。<select>タグのtypeが'select-one'かどうかによって後述するselectOne(), selectMany()関数を使い分けています。例えばXHTMLで <select multiple="multiple"> と記述されている場合、ここではselectMany()関数が使われます。

selectOne()関数ではselectedIndexプロパティに選択されている<option>タグのインデックス値が入っているので、後述するoptionValue()関数を使って値を取得します。

selectMany()関数では、optionsプロパティを使ってすべての<option>タグを調べ、selectedプロパティが真のものを配列に集めて返します。

optionValue()関数 では、<option>タグにvalue属性があればその値を、無ければ<option>タグの内側の文字列が値となるのでそれをtextプロパティから取り出して返します。

おすすめ記事

記事・ニュース一覧