• atwiki
  • TEST
  • Simple Implementation of DOM Events in JavaScript

TEST

Simple Implementation of DOM Events in JavaScript

最終更新:

eriax

- view
管理者のみ編集可

制限

  • DOM Events のシンプル(?)な実装試案。名前空間ありの旧仕様。

ソースコード

if ('undefined' === typeof Array.prototype.indexOf) {
  Array.prototype.indexOf = function (ceil, floor) {
    return function (searchElement) {
      var fromIndex = arguments[1];
      var count = this.length;
      var i = Number(fromIndex) || 0;
      i = (i < 0) ? ceil(i) : floor(i);
      if (i < 0) {
        i += count;
      }
      for (; i < count; i++) {
        if (i in this) {
          if (this[i] === searchElement) {
            return i;
          }
        }
      }
      return -1;
    };
  }(Math.ceil, Math.floor);
}

////////////////////////////////////////////////////////////////////////

function DOMObject(arg) {
  if (arguments.length > 0) {
    var name;
    for (name in arg) {
      if (arg.hasOwnProperty(name)) {
        this[name] = arg[name];
      }
    }
  }
}

(function () {
  this.constructor = DOMObject;
}).call(DOMObject.prototype);

////////////////////////////////////////////////////////////////////////

(function () {
  // EventPhase
  this.CAPTURING_PHASE = 1;
  this.AT_TARGET = 2;
  this.BUBBLING_PHASE = 3;
}).call(DOMEvent);

function DOMEvent(arg) {
  if (arguments.length > 0) {
    if (arg) {
      DOMObject.apply(this, arguments);
    }
  }
}

DOMEvent.prototype = new DOMObject;

(function () {
  this.constructor = DOMEvent;
  // Level 2
  this.type = null;
  this.target = null;
  this.currentTarget = null;
  this.eventPhase = null;
  this.bubbles = null;
  this.cancelable = null;
  this.timeStamp = null;
  // Level 3
  this.namespaceURI = null;
  this.defaultPrevented = false;
  this.trusted = false;
  // extension
  this.ownerDocument = null;
  this.propagationStopped = false;
  this.immediatePropagationStopped = false;
}).call(DOMEvent.prototype);

(function () {
  this.stopPropagation = function () {
    this.propagationStopped = true;
  };
  this.preventDefault = function () {
    if (this.cancelable) {
      this.defaultPrevented = true;
    }
  };
  this.initEvent = function (eventTypeArg, canBubbleArg, cancelableArg) {
    this.initEventNS(null, eventTypeArg, canBubbleArg, cancelableArg);
  };
  this.stopImmediatePropagation = function () {
    this.immediatePropagationStopped = true;
  };
  this.toString = function () {
    return '[object Event]';
  };
  this.initEventNS = function (namespaceURI, eventTypeArg, canBubbleArg, cancelableArg) {
    this.namespaceURI = namespaceURI;
    this.type = eventTypeArg;
    this.bubbles = canBubbleArg;
    this.cancelable = cancelableArg;
  };
}).call(DOMEvent.prototype);

////////////////////////////////////////////////////////////////////////

function DOMEventTarget(arg) {
  if (arguments.length > 0) {
    if (arg) {
      DOMObject.apply(this, arguments);
    }
    var name = '[EventRegistry]';
    if (!this.hasOwnProperty(name)) {
      var memo = this[name] = [];
      memo[DOMEvent.CAPTURING_PHASE] = {
        '': []
      };
      memo[DOMEvent.BUBBLING_PHASE] = {
        '': []
      };
    }
  }
}

DOMEventTarget.prototype = new DOMObject;

(function () {
  this.constructor = DOMEventTarget;
  this['[EventRegistry]'] = null;
}).call(DOMEventTarget.prototype);

(function () {
  this['[IsValidEventListener]'] = function (listener) {
    return ('function' === typeof listener) || ('object' === typeof listener && 'function' === typeof listener.handleEvent);
  };
  this.addEventListener = function (type, listener, useCapture) {
    return this.addEventListenerNS(null, type, listener, useCapture);
  };
  this.removeEventListener = function (type, listener, useCapture) {
    return this.removeEventListenerNS(null, type, listener, useCapture);
  };
  this.dispatchEvent = function (evt) {
    evt.target = this;
    evt.timeStamp = new Date;
    PROPAGATION: {
      var targets = this['[TraceRoute]']();
      var targetCount = targets.length;
      var i;
      // capture
      evt.eventPhase = DOMEvent.CAPTURING_PHASE;
      for (i = targetCount - 1; 0 <= i; i -= 1) {
        if (!targets[i]['[CallEventListener]'](evt)) {
          break PROPAGATION; // stop (Immediate)Propagation () called
        }
      }
      // target
      evt.eventPhase = DOMEvent.AT_TARGET;
      if (!this['[CallEventListener]'](evt)) {
        break PROPAGATION; // stop (Immediate)Propagation () called
      }
      // bubble
      if (evt.bubbles) {
        evt.eventPhase = DOMEvent.BUBBLING_PHASE;
        for (i = 0; i < targetCount; i++) {
          if (!targets[i]['[CallEventListener]'](evt)) {
            break PROPAGATION; // stop (Immediate)Propagation () called
          }
        }
      }
    }
    return evt.defaultPrevented; // preventDefault () called?
  };
  this['[TraceRoute]'] = function () {
    var result = [];
    var node = this;
    if (node.nodeType > 0) {
      while ((node = node.parentNode)) {
        result[result.length] = node;
      }
    }
    return result;
  };
  this['[GetEventListener]'] = function (evt) {
    var type = evt.type;
    var eventPhase = evt.eventPhase;
    var namespaceURI = evt.namespaceURI;
    if (namespaceURI == null) {
      namespaceURI = '';
    }
    switch (eventPhase) {
    case DOMEvent.CAPTURING_PHASE:
    case DOMEvent.BUBBLING_PHASE:
      var registry = this['[EventRegistry]'][eventPhase];
      var listeners = registry[namespaceURI];
      if (listeners instanceof Object) {
        var handlers = listeners[type];
        if (handlers instanceof Array) {
          return handlers;
        }
        return listeners[type] = [];
      }
      listeners = registry[namespaceURI] = {};
      return listeners[type] = [];
    case DOMEvent.AT_TARGET:
      var listeners1 = this['[GetEventListener]']({
        namespaceURI: namespaceURI,
        type: type,
        eventPhase: DOMEvent.CAPTURING_PHASE
      });
      var listeners2 = this['[GetEventListener]']({
        namespaceURI: namespaceURI,
        type: type,
        eventPhase: DOMEvent.BUBBLING_PHASE
      });
      return listeners1.concat(listeners2);
    default:
      return [];
    }
  };
  this['[CallEventListener]'] = function (evt) {
    var listeners = this['[GetEventListener]'](evt);
    var listenerCount = listeners.length;
    var listener;
    var i;
    for (i = 0; i < listenerCount; i++) {
      listener = listeners[i];
      try {
        evt.currentTarget = this;
        if ('function' === typeof listener) {
          listener.call(this, evt);
        }
        else if ('function' === typeof listener.handleEvent) {
          listener.handleEvent(evt);
        }
      }
      catch (err) {
        ;
      }
      finally {
        if (evt.immediatePropagationStopped) { // stopImmediatePropagation () called
          return false;
        }
      }
    }
    return !evt.propagationStopped; // stopPropagation () called
  };
  this.toString = function () {
    return '[object EventTarget]';
  };
  this.addEventListenerNS = function (namespaceURI, type, listener, useCapture) {
    if (this['[IsValidEventListener]'](listener)) {
      var listeners = this['[GetEventListener]']({
        namespaceURI: namespaceURI,
        type: type,
        eventPhase: useCapture ? DOMEvent.CAPTURING_PHASE : DOMEvent.BUBBLING_PHASE
      });
      listeners.push(listener);
    }
  };
  this.removeEventListenerNS = function (namespaceURI, type, listener, useCapture) {
    if (this['[IsValidEventListener]'](listener)) {
      var listeners = this['[GetEventListener]']({
        namespaceURI: namespaceURI,
        type: type,
        eventPhase: useCapture ? DOMEvent.CAPTURING_PHASE : DOMEvent.BUBBLING_PHASE
      });
      if (listeners.length > 0) {
        var index = listeners.indexOf(listener);
        if (index >= 0) {
          listeners.splice(index, 1);
        }
      }
    }
  };
}).call(DOMEventTarget.prototype);

テスト。

<!DOCTYPE HTML>
<title>TEST</title>

<script type="text/javascript"></script>

<script type="text/javascript">

function TestController(controls) {
  var Log1 = [];

  // T1 を要素ノードとして作る
  var T1 = new DOMEventTarget({
    nodeType: 1,
    parentNode: null,
    childNodes: []
  });

  // T1 をイベントリスナで監視(キャプチャ)
  T1.addEventListener('click', function (e) {
    var log = [];

    log.push('T1: capturing_listener[0]');

    // デフォルトアクションを取り消す
    if (controls['preventDefault'].checked) {
      e.preventDefault();
      log.push('preventDefault() was called');
    }
    // イベント伝播を抑止する
    if (controls['stopPropagation'].checked) {
      e.stopPropagation();
      log.push('stopPropagation() was called');
    }
    // イベント伝播を直ちに抑止する
    if (controls['stopImmediatePropagation'].checked) {
      e.stopImmediatePropagation();
      log.push('stopImmediatePropagation() was called');
    }
    Log1.push(log.join(' | '));
  }, true);

  T1.addEventListener('click', function (e) {
    Log1.push('T1: capturing_listener[1]');
  }, true);
  T1.addEventListener('click', function (e) {
    Log1.push('T1: capturing_listener[2]');
  }, true);

  // T1 をイベントリスナで監視(バブル)
  T1.addEventListener('click', function (e) {
    Log1.push('T1: bubbling_listener[0]');
  }, false);
  T1.addEventListener('click', function (e) {
    Log1.push('T1: bubbling_listener[1]');
  }, false);
  T1.addEventListener('click', function (e) {
    Log1.push('T1: bubbling_listener[2]');
  }, false);

  ////////////////////////////////////////////////////////////////////////
  // T2 を要素ノード(T1 の子)として作る
  var T2 = new DOMEventTarget({
    nodeType: 1,
    parentNode: T1,
    childNodes: []
  });
  T1.childNodes.push(T2);

  // T2 をイベントリスナで監視(キャプチャ)
  T2.addEventListener('click', function (e) {
    Log1.push('T2: capturing_listener[0]');
  }, true);
  T2.addEventListener('click', function (e) {
    Log1.push('T2: capturing_listener[1]');
  }, true);
  T2.addEventListener('click', function (e) {
    Log1.push('T2: capturing_listener[2]');
  }, true);

  // T2 をイベントリスナで監視(バブル)
  var T2_listener0 = {
    handleEvent: function (e) {
      Log1.push('T2: bubbling_listener[0], this=' + this);
    },

    toString: function () {
      return '[object DOMEventListener]';
    }
  };
  T2.addEventListener('click', T2_listener0, false);
  T2.addEventListener('click', function (e) {
    Log1.push('T2: bubbling_listener[1], this=' + this);
  }, false);
  T2.addEventListener('click', function (e) {
    Log1.push('T2: bubbling_listener[2]');
  }, false);

  // T2 を監視している T2_listener0 を除去する(バブル)
  // T2.removeEventListener ('click', T2_listener0, false);

  ////////////////////////////////////////////////////////////////////////
  // T3 を要素ノード(T2 の子)として作る
  var T3 = new DOMEventTarget({
    nodeType: 1,
    parentNode: T2,
    childNodes: []
  });
  T2.childNodes.push(T3);

  // T3 をイベントリスナで監視(キャプチャ)
  T3.addEventListener('click', function (e) {
    Log1.push('T3: capturing_listener[0]');
  }, true);
  T3.addEventListener('click', function (e) {
    Log1.push('T3: capturing_listener[1]');
  }, true);
  T3.addEventListener('click', function (e) {
    Log1.push('T3: capturing_listener[2]');
  }, true);

  // T3 をイベントリスナで監視(バブル)
  T3.addEventListener('click', function (e) {
    Log1.push('T3: bubbling_listener[0]');
  }, false);
  T3.addEventListener('click', function (e) {
    Log1.push('T3: bubbling_listener[1]');
  }, false);
  T3.addEventListener('click', function (e) {
    Log1.push('T3: bubbling_listener[2]');
  }, false);

  ////////////////////////////////////////////////////////////////////////
  // イベント生成
  var e = new DOMEvent({
    ownerDocument: document
  });

  // イベント初期化
  e.initEvent('click', controls['bubbles'].checked, controls['cancelable'].checked);
  Log1.push('bubbles: ' + e.bubbles);
  Log1.push('cancelable: ' + e.cancelable);

  // イベント通知
  var defaultPrevented = T3.dispatchEvent(e);

  // デフォルトアクションが取り消されたか
  Log1.push('defaultPrevented: ' + defaultPrevented);

  ////////////////////////////////////////////////////////////////////////
  controls['OUTPUT-1'].value = Log1.join('.\n');
}

</script>

<!-- ここから -->

<p id="D1">T1 と、その子 T2 と、その子 T3 を作り、それぞれにイベントリスナを取り付けた後、T3 にイベントを送る。</p>

<pre role="img" aria-describedby="D1">
+ T1
  |
  + T2
    |
    + T3 <--- click!
</pre>

<form action="#" id="FORM-1">
  <p>
    <input type="button" value="試す" aria-describedby="D1" onclick="TestController(this.form.elements)">
    <label><input type="checkbox" name="bubbles" checked="checked">bubbles</label>
    <label><input type="checkbox" name="cancelable" checked="checked">cancelable</label>
    <label><input type="checkbox" name="preventDefault">preventDefault</label>
    <label><input type="checkbox" name="stopPropagation">stopPropagation</label>
    <label><input type="checkbox" name="stopImmediatePropagation">stopImmediatePropagation</label>
  </p>
  <p><textarea rows="20" cols="100" name="OUTPUT-1"></textarea></p>
</form>
  • 初出 2011-08-23/25
目安箱バナー