l'essentiel est invisible pour les yeux

Saturday, September 02, 2006

Prototype.jsのCHANGELOGまとめと自分なり拡張

Prototype.jsが Updateされいくつかの機能が追加されました。
メソッドチェーンによるメソッドの呼び出しが完全にサポートされたのは大きな変更かと思います。

CHANGELOG

Element, Form, Form.Elementメソッドの仕様変更
Elment, Form, Form.Elementが配列ではなく、一つの要素(or ID)を引数に取るように仕様変更され、返り値として引数で渡したオブジェクトを返すようになりました。

この修正により、

$('div').each(function(val) {Element.show(val);}); // Before
$('div').each(Element.show); // After

$('input[type="text"]').each(function(val) {Field.clear(val);}); // Before
$('input[type="text"]').each(Form.Element.clear); // After
eachメソッドにElement.*関数を高階関数と渡すだけで全要素に処理を適用できます。

ただし、この仕様変更により次のようなレガシーなコードは変更が必要になります。


Element.show('page', 'sidebar', 'content');// Before
['page', 'sidebar', 'conent'].each(Element.show); // After



Field.*はForm.Methodsに統一
Fieldオブジェクトに定義されていたメソッドは、Form.Elementに統一されました。FieldはForm.Elementへの参照として定義されているため、後方互換性は保たれています。


Form中の要素を全て有効化/無効化するメソッドの追加
Form中の要素全てを有効化/無効化するためのメソッドとして、Form.enable, Form.disableが追加されました。


$('form').disable();
$('form').enable();


$(), $()で返される要素にForm.Methods, Form.Elementがmixinされるようになった
Element.extendメソッドに、FORM要素にはForm.Methodsが、INPUT, TEXTAREA, SELECTの場合には、Form.Element.Methodsがmixinされるようになった。
この変更により、$()の結果に対しての完全なメソッドチェーンでのメソッド呼び出しがサポートされた。


$('input1').clear();
$('input2').focus();



Objectのコピーを生成する、Object.cloneの追加
オブジェクトのコピーを生成し返すメソッドが追加されました。

clone: function(object) {
return Object.extend({}, object);
}


Object中に定義されているキーと値を取得するメソッド
Object.keys, Object.valuesメソッドが追加され、それぞれ引数に渡したオブジェクト中に定義されているキーとその値を取得して配列で返します。

Object.keys(Array); // => ["from","bind","bindAsEventListener"]
Object.values(Array); // => [function (iterable) {...},function () {...},function (object) {...}]



$()関数で参照した要素の子要素に対してのクラス名・CSSセレクタでの参照が可能に。
Element.Methods.getElementByClassName, Element.Methods.getElementsBySelectorが追加され、$()で参照した要素の子要素に対してクラス名・CSSセレクタでの参照が可能になりました。

$('form').getElementsByClassName('item');
$('form').getElementsBySelec('input[type="text"]'); // => テキスト入力フォームのみ取得


Array.reduceメソッドの追加
配列中の要素が一つの場合は、要素を返し、複数要素を含む場合には配列自身を返すメソッドです。
配列内の要素が1つかどうかいちいち確認するのをDRYにします。


[1, 2].reduce() // [1, 2]
[1].reduce() // 1
[].reduce() // undefined


PUT/DELETEメソッドをPOSTメソッドでエミュレートする
DHHによる変更です。Ajax.Requestに渡すオプション中で、method: put, method: deleteと指定している場合は、methods: postに置き換え、_method: put, _method: _deleteを定義して、PUT/DELETEをエミュレートします。
ActiveResourceをJavascriptからも利用しやすくするための変更だと思われます。

個人的に機能拡張しているところ。
prototype.jsは、便利で活用しているのですが、細かいところで「こんな機能あったらなぁ・・」と思って、自分で拡張して実装しています。そのいくつかを書いておきます。

Hash.mapの変形バージョン
HashクラスもEnumerableクラスをmixinしているため、Hash.mapが使えるのですが、ハッシュに高階関数を適用すると次のように適用した結果の値を取り出した配列が返されます


Hash.collect2 = function(iterator) {
var result = $H({});

this.each(function(value, index){
result[value.key] = iterator(value, index);
});
return result;
}
Hash.map2 = Hash.collect2;
function inc_value(val){return val.value+1;};

$H({fruits: 2, apple: 3, melon: 4}).map(inc_value);
//=> [3,4,5]

console.log($H({fruits: 2, apple: 3, melon: 4}).map2(function(val){return val.value+1;}).inspect());
// =>
#<hash:{'fruits':3, apple: 4, melon: 5}>


Hash.toQueryStringの拡張
prototyp.js中に定義されている、Hash.toQueryStringはハッシュをURLパラメータ形式の文字列にして返してくれます。しかし、ハッシュ中に配列が含まれたり、ハッシュがネストしている場合には、次のようになります。

$H({fruits: ["apple", "banana", "melon"]}).toQueryString();
// => "fruits=apple%2Cbanana%2Cmelon"
$H({fruits: {name: "apple", size: 3, color: "red"}}).toQueryString();
// =>
"fruits=%5Bobject%20Object%5D"


人により差異はあるかもしれませんが、これは意図した通りにサーバ側でパースされません。
そこで、Hash.toQueryStringを変更し、次のように返すようにしています。

$H({fruits: ["apple", "banana", "melon"]}).toQueryString();
// =>"fruits[0]=apple&fruits[1]=banana&fruits[2]=melon"
$H({fruits: {name: "apple", size: 3, color: "red"}}).toQueryString();

// => "fruits[name]=apple&fruits[size]=3&fruits[color]=red"


と返されるように、Hash.toQueryStringを次のように定義しています。


Hash.toQueryString = function(){
return this.map(function(pair){
if(pair.value instanceof Array) {
var key = pair.key;
return pair.value.length == 0 ? "" : $A(pair.value).map(function(val){return key+"[]="+val}).join('&');
} else if(pair.value instanceof Object) {
var key = pair.key;
return $H(pair.value).map(function(_pair){return key+"["+_pair.key+"]="+_pair.value}).join('&');
} else {
return pair.map(encodeURIComponent).join('=');
}
}).compact2().join('&');
};


Array.compact2については下記で説明します。

Array.compact2-NULLとundefinedと空文字列(==falseと評価される値)を配列から取り除く
Array.compactの変形バージョンです。


Array.prototype.compact2 = function(){
return this.select(function(value) {
return value != undefined && !(value == false);
});
};



Array.unique - 重複した要素を取り除いたユニークな配列を返す。
Array.prototype.unique = function(){
var ret = [], sorted = this.sort(), cur;
for(var i=0,l=this.length;i<l;++i) i="" return="" ret="">


String関連の拡張
全角スペースを半角スペースにとか。

String.prototype.empty = function(){
return (this == "");
};
String.prototype.full2harf = function(){
return this.gsub("\u3000", "\u0020");
};



書式付き日付出力
そういえば、Prototype.jsには日付関連の拡張は一切無い。
/**
* Date.strftime
*
* Inspired by
* http://www.mattkruse.com/javascript/date/date.js
* Author: Matt Kruse <matt@mattkruse.com>
*/

Date.MONTH_NAMES = 'January February March April May June July August September October November December Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');
Date.DAY_NAMES = 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sun Mon Tue Wed Thu Fri Sat'.split(' ');
Date.prototype.strftime = function(format) {
function LZ(x) {return(x<0||x>9?"":"0")+x}
format=format+"";
var result="";
var i_format=0;
var c="";
var token="";
var y=this.getYear()+"";
var M=this.getMonth()+1;
var d=this.getDate();
var E=this.getDay();
var H=this.getHours();
var m=this.getMinutes();
var s=this.getSeconds();
var yyyy,yy,MMM,MM,dd,hh,h,mm,ss,ampm,HH,H,KK,K,kk,k;
// Convert real date parts into formatted versions
var value=new Object();
if (y.length < y="" h="=">12){value["h"]=H-12;}
else {value["h"]=H;}
value["hh"]=LZ(value["h"]);
if (H>11){value["K"]=H-12;} else {value["K"]=H;}
value["k"]=H+1;
value["KK"]=LZ(value["K"]);
value["kk"]=LZ(value["k"]);
if (H > 11) { value["a"]="PM"; }
else { value["a"]="AM"; }
value["m"]=m;
value["mm"]=LZ(m);
value["s"]=s;
value["ss"]=LZ(s);
while (i_format < c="format.charAt(i_format);" token="" result="result" result="result">


タグベースのイテレータ

ネタ元は、Enctymedia blog.

/**
* Tag base iterator
*/
(function() {
/* Tag base iterator */
var tags = "div p span ul ol li span form input select textarea h1 h2 h3 h4 h5 h6 dl dt em strong a";
var methods = {};
$A(tags.split(' ')).each(function(tag){

methods["each"+tag.charAt(0).toUpperCase()+tag.substring(1)] = function(element, iterator) {
element = $(element);
element.cleanWhitespace();
$A((element).getElementsByTagName(tag)).each(iterator);
}
});
Object.extend(Element, methods);
})();


正規表現文字のエスケープ

RegExp.quote = function(str){
return str.replace(/([\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:\\\-])/g, '\\$1');
};