Abstract.EventObserverクラス、Form.Element.EventObserverクラス改修検討

Form.Element.EventObserverクラスでは、Form.Element.TimeObserver等とは違い、multipe指定のselectタグでも問題は発生しません。しかし、複数の同名radioやcheckboxを扱えないのは同じです。扱えようになると便利なのですが、そのためにはかなり改造する必要があります。

まず、Abstract.EventObserverのinitializeメソッド。

【改修案-1】Abstract.EventObserver.initializeメソッド
  initialize: function(element, callback) {
    var tags;
    element  = $(element);
    if(element.form){
      tags = element.form.elements[element.name];
    }else{
      tags = document.getElementsByName(element.name);
    }
    if(tags.length != null && tags.length > 1 && tags.type == null){
      this.element = tags;
    }else{
      this.element = element;
    }
  },

引数elementで、複数同名要素の集合も指定可能にしてあります。単要素指定の場合でも、同名要素を探して、あれば対象とします。

次に、Abstract.EventObserver.onElementEventメソッド。

【改修案-2】Abstract.EventObserver.onElementEventメソッド
  onElementEvent: function() {
    var isChanged = false;
    var value = this.getValue();
    if (this.lastValue instanceof Array
    &&  value instanceof Array){
      if(this.lastValue.length != value.length
      || this.lastValue.any(function(last, i){return (last != value[i]);})){
        isChanged = true;
      }
    }else{
      if (this.lastValue != value) {
        isChanged = true;
      }
    }
    if(isChanged){
      this.callback(this.element, value);
      this.lastValue = value;
    }
  },

Abstract.TimedObserver.onTimerEventメソッド改修検討 - Backstage of theater.js」で示した改修案と同じです。this.getValueメソッドが返却する値が配列の場合でも対応します。ただ、これが役に立つのは、チェック済みラジオボタンを再度クリックしたときに、関数実行を抑制するくらいなのですが・・・。

次に、既存Abstract.EventObserver.registerCallbackメソッドの名前を「_registerCallback」に変更します。

【改修案-3】Abstract.EventObserver.registerCallbackメソッド改名
  _registerCallback: function(element) {
(省略 既存と同じ)
  },

registerCallbackメソッドは新たに以下を設定します。

【改修案-4】Abstract.EventObserver.registerCallbackメソッド再設定
registerCallback: function(element) {
  if (element.length && !element.type) {
    $A(element).each(this._registerCallback.bind(this));
  }else{
    this._registerCallback(element);
  }
}

これで、複数の同名要素に対してもイベントが登録できるようになります。

次にForm.Element.EventObserverクラスのgetValueメソッド。

【改修案-5】Form.Element.EventObserver.getValueメソッド再設定
  getValue: function() {
    return Form.Element.getValue2(this.element);
  }

前述Form.Elementクラスの改修案の追加メソッドgetValue2を使用してます。なので、getValue2メソッドも必要です。

【参考】Form.Element.getValue2(改修追加案)
  getValue2: function(element){
    var retVal;
    element = $(element);
    if(element.length != null && element.type == null){
      retVal = $A(element).collect(function(elem){
        return this.getValue(elem);
      }.bind(this)).compact();
    }else{
      var tags;
      if(element.form){
        tags = element.form.elements[element.name];
      }else{
        tags = document.getElementsByName(element.name);
      }
      if(tags.length != null && tags.length > 1 && tags.type == null){
        retVal = $A(tags).collect(function(tag){
          return this.getValue(tag);
        }.bind(this)).compact();
      }else{
        retVal = this.getValue(element);
      }
    }
    return retVal;
  }

これで、複数の同名要素の値を配列として受け取ることができます。

以下、長くなりますが例です。大半は各メソッドの上書きです。

【参考】改修案を用いた場合の例。
        prototype.jsをそのまま読み込み後にメソッド追加・上書き。

<html>
<head>
<title></title>
<script language="javascript" src="prototype.js" charset="utf-8"></script>
<script>
<!--
//initializeメソッド上書き用関数
function initialize(element, callback) {
  var tags;
  element  = $(element);
  if(element.form){
    tags = element.form.elements[element.name];
  }else{
    tags = document.getElementsByName(element.name);
  }
  if(tags.length != null && tags.length > 1 && tags.type == null){
    this.element = tags;
  }else{
    this.element = element;
  }

  this.callback = callback;

  this.lastValue = this.getValue();
  if (this.element.tagName 
  &&  this.element.tagName.toLowerCase() == 'form')
    this.registerFormCallbacks();
  else
    this.registerCallback(this.element);
}

//onElementEventメソッド上書き用関数
function onElementEvent() {
  var isChanged = false;
  var value = this.getValue();
  if (this.lastValue instanceof Array
  &&  value instanceof Array){
    if(this.lastValue.length != value.length
    || this.lastValue.any(function(last, i){return (last != value[i]);})){
      isChanged = true;
    }
  }else{
    if (this.lastValue != value) {
      isChanged = true;
    }
  }
  if(isChanged){
    this.callback(this.element, value);
    this.lastValue = value;
  }
}

//_registerCallbackメソッド追加用関数
function _registerCallback(element) {
  if (element.type) {
    switch (element.type.toLowerCase()) {
      case 'checkbox':
      case 'radio':
        Event.observe(element, 'click', this.onElementEvent.bind(this));
        break;
      case 'password':
      case 'text':
      case 'textarea':
      case 'select-one':
      case 'select-multiple':
        Event.observe(element, 'change', this.onElementEvent.bind(this));
        break;
    }
  }
}

//registerCallbackメソッド上書き用関数
function registerCallback(element) {
  if (element.length && !element.type) {
    $A(element).each(this._registerCallback.bind(this));
  }else{
    this._registerCallback(element);
  }
}

//Form.Element.EventObserver メソッド上書き・追加
Form.Element.EventObserver.prototype 
 = Object.extend(Form.Element.EventObserver.prototype, {
  initialize: initialize,
  onElementEvent: onElementEvent,
  _registerCallback: _registerCallback,
  registerCallback: registerCallback,
  getValue: function() {
    return Form.Element.getValue2(this.element);
  }
});

//Form.EventObserver メソッド上書き・追加
Form.EventObserver.prototype = Object.extend(Form.EventObserver.prototype, {
  initialize: initialize,
  onElementEvent: onElementEvent,
  _registerCallback: _registerCallback,
  registerCallback: registerCallback
});

//Form.Element.getValue2メソッド追加
Form.Element.getValue2 = function(element){
  var retVal;
  element = $(element);
  if(element.length != null && element.type == null){
    retVal = $A(element).collect(function(elem){
      return this.getValue(elem);
    }.bind(this)).compact();
  }else{
    var tags;
    if(element.form){
      tags = element.form.elements[element.name];
    }else{
      tags = document.getElementsByName(element.name);
    }
    if(tags.length != null && tags.length > 1 && tags.type == null){
      retVal = $A(tags).collect(function(tag){
        return this.getValue(tag);
      }.bind(this)).compact();
    }else{
      retVal = this.getValue(element);
    }
  }
  return retVal;
}

function init(){
  new Form.Element.EventObserver(document.FORMNAME.TEXT, test1);
  new Form.Element.EventObserver(document.FORMNAME.CHECK, test1);
  new Form.Element.EventObserver(document.FORMNAME.RADIO, test1);
  new Form.Element.EventObserver(document.FORMNAME.SELECT, test1);
  new Form.Element.EventObserver(document.FORMNAME.TEXTALONE, test1);
  new Form.EventObserver(document.FORMNAME, test2);
}
function test1(element, value){
  Element.update('test1', (element.name||element[0].name) 
                           + " cahnged to " + Object.inspect(value));
}
function test2(element, value){
  Element.update('test2', element.name
	                       + " cahnged to " + Object.inspect(value));
}
//-->
</script>
</head>
<body onload="init();">
<form name="FORMNAME" onsubmit="return false;">
<input type="text" name="TEXT" value="テキストボックス1"/>
<input type="text" name="TEXT" value="テキストボックス2"/>
<input type="checkbox" name="CHECK" value="CHEKED1" checked/>
<input type="checkbox" name="CHECK" value="CHEKED2"/>
<input type="radio" name="RADIO" value="CHEKED1" checked/>
<input type="radio" name="RADIO" value="CHEKED2"/>
<select size="3" name="SELECT" multiple>
  <option value="OPTION1" selected>オプション1</option>
  <option value="OPTION2">オプション2</option>
  <option value="OPTION3" selected>オプション3</option>
  <option value="OPTION4">オプション4</option>
</select>
<input type="text" name="TEXTALONE" value="同名他要素なし"/>
</form>
<div id="test1">element</div>
<div id="test2">form</div>
</body>
</html>

かなり複雑になってしまった;;

余談ですが、prototype.js本体を改修せずに、自前のパッチファイルを作っておくといいかもしれません。そのうち、これまでの改修案をまとめたパッチファイルを作ってみようかと思います。