Wie klone ich DOM-Ereignisse oder versende sie erneut?

70

Ich suche nach einer einfachen und abstrakten Möglichkeit, nur DOM-Ereignisse zu klonen oder erneut zu versenden. Ich bin nicht daran interessiert, DOM-Knoten zu klonen.

Ich habe ein bisschen experimentiert, die DOM-Ereignisspezifikation gelesen und keine klare Antwort gefunden.

Im Idealfall suche ich etwas so Unkompliziertes wie:

handler = function(e){
  document.getElementById("decoy").dispatchEvent(e)
}
document.getElementById("source").addEventListener("click", handler)

Dieses Codebeispiel funktioniert natürlich nicht. Es gibt eine DOM-Ausnahme, die besagt, dass das Ereignis derzeit ausgelöst wird - offensichtlich.

Ich möchte vermeiden, neue Ereignisse manuell mit zu erstellen document.createEvent(), zu initialisieren und zu versenden.

Gibt es eine einfache Lösung für diesen Anwendungsfall?

Razvan Caliman
quelle
Warum müssen Sie Ereignisse neu verteilen?
Josh3736
Ich benötige dies als Problemumgehung für CSS-Regionen, die derzeit keine untergeordneten Knotenereignisse auslösen. Regionen rendern nur den Inhalt des untergeordneten Knotens, sie fungieren nicht als übergeordnete Knoten.
Razvan Caliman

Antworten:

131

Ich weiß, die Frage ist alt und das OP wollte vermeiden, einen Ansatz zu erstellen / zu initialisieren, aber es gibt eine relativ einfache Möglichkeit, Ereignisse zu duplizieren:

new_event = new MouseEvent(old_event.type, old_event)

Wenn Sie mehr als nur Mausereignisse möchten, können Sie Folgendes tun:

new_event = new old_event.constructor(old_event.type, old_event)

Und im ursprünglichen Kontext:

handler = function(e) {
  new_e = new e.constructor(e.type, e);
  document.getElementById("decoy").dispatchEvent(new_e);
}
document.getElementById("source").addEventListener("click", handler);

(Für jQuery-Benutzer: Möglicherweise müssen Sie e.originalEvent.constructorstatt verwenden e.constructor)

Alexis
quelle
3
Beachten Sie, dass dies nur in modernen Versionen von Firefox und Chrome funktioniert und in keiner Version von IE
Quinn Strahl
3
@ Alexis Ich bekomme: Uncaught TypeError: Illegal constructor:(
niieani
2
Ich habe gerade ein Modul auf npm mit dem Namen clone-event zusammengestellt, das im Wesentlichen die in dieser Antwort beschriebene Funktionalität umschließt. Diese verwende ich derzeit, wenn ich ein Ereignis erneut auslösen möchte. Ich habe diese Methode in Chrome v37 grundlegend getestet und sie scheint zu funktionieren. Ich würde es begrüßen, wenn jemand einen Fehler findet oder eine Umgebung, in der es nicht funktioniert.
KP MacGregor
2
Sehr clever, wie Sie das alte Ereignis als Init-Argument des Konstruktors übergeben!
Anständiger Dabbler
2
@ Gyro, ja, gemäß den Spezifikationen können nicht vertrauenswürdige Ereignisse in den meisten Fällen keine Standardaktionen auslösen, "als ob die Methode präventiv () für sie aufgerufen worden wäre". Leider glaube ich nicht, dass wir etwas dagegen tun können.
Alexis
2

Ein Fix für Internet Explorer

Alexis veröffentlicht eine nette Lösung, aber seine Lösung funktioniert nicht im Internet Explorer. Die folgende Lösung wird. Leider gibt es in Internet Explorer kein System, das so konsistent ist wie Ereigniskonstruktoren. Daher ist der unten aufgeblähte Code erforderlich.

var allModifiers = ["Alt","AltGraph","CapsLock","Control",
                    "Meta","NumLock","Scroll","Shift","Win"];
function redispatchEvent(original, newTargetId) {
  if (typeof Event === "function") {
    var eventCopy = new original.constructor(original.type, original);
  } else {
    // Internet Explorer
    var eventType = original.constructor.name;
    var eventCopy = document.createEvent(eventType);
    if (original.getModifierState)
      var modifiersList = allModifiers.filter(
        original.getModifierState,
        original
      ).join(" ");
    
    if (eventType === "MouseEvent") original.initMouseEvent(
      original.type, original.bubbles, original.cancelable,
      original.view, original.detail, original.screenX, original.screenY,
      original.clientX, original.clientY, original.ctrlKey,
      original.altKey, original.shiftKey, original.metaKey,
      original.button, original.relatedTarget
    );
    if (eventType === "DragEvent") original.initDragEvent(
      original.type, original.bubbles, original.cancelable,
      original.view, original.detail, original.screenX, original.screenY,
      original.clientX, original.clientY, original.ctrlKey,
      original.altKey, original.shiftKey, original.metaKey,
      original.button, original.relatedTarget, original.dataTransfer
    );
    if (eventType === "WheelEvent") original.initWheelEvent(
      original.detail, original.screenX, original.screenY,
      original.clientX, original.clientY, original.button,
      original.relatedTarget, modifiersList,
      original.deltaX, original.deltaY, original.deltaZ, original.deltaMode
    );
    if (eventType === "PointerEvent") original.initPointerEvent(
      original.type, original.bubbles, original.cancelable,
      original.view, original.detail, original.screenX, original.screenY,
      original.clientX, original.clientY, original.ctrlKey,
      original.altKey, original.shiftKey, original.metaKey,
      original.button, original.relatedTarget,
      original.offsetX, original.offsetY, original.width, original.height,
      original.pressure, original.rotation,
      original.tiltX, original.tiltY,
      original.pointerId, original.pointerType,
      original.timeStamp, original.isPrimary
    );
    if (eventType === "TouchEvent") original.initTouchEvent(
      original.type, original.bubbles, original.cancelable,
      original.view, original.detail, original.screenX, original.screenY,
      original.clientX, original.clientY, original.ctrlKey,
      original.altKey, original.shiftKey, original.metaKey,
      original.touches, original.targetTouches, original.changedTouches,
      original.scale, original.rotation
    );
    if (eventType === "TextEvent") original.initTextEvent(
      original.type, original.bubbles, original.cancelable,
      original.view,
      original.data, original.inputMethod, original.locale
    );
    if (eventType === "CompositionEvent") original.initTextEvent(
      original.type, original.bubbles, original.cancelable,
      original.view,
      original.data, original.inputMethod, original.locale
    );
    if (eventType === "KeyboardEvent") original.initKeyboardEvent(
      original.type, original.bubbles, original.cancelable,
      original.view, original.char, original.key,
      original.location, modifiersList, original.repeat
    );
    if (eventType === "InputEvent" || eventType === "UIEvent")
      original.initUIEvent(
        original.type, original.bubbles, original.cancelable,
        original.view, original.detail
      );
    if (eventType === "FocusEvent") original.initFocusEvent(
        original.type, original.bubbles, original.cancelable,
        original.view, original.detail, original.relatedTarget
    );
  }
  
  document.getElementById(newTargetId).dispatchEvent(eventCopy);
  if (eventCopy.defaultPrevented)  newTargetId.preventDefault();
}
<button onclick="redispatchEvent(arguments[0], '2nd')">Click Here</button>
<button id="2nd" onclick="console.log('Alternate clicked!')">Alternate Button</button>

Eine allgemeinere Lösung

Abhängig von Ihren Anforderungen ist die Weitergabe synthetischer Ereignisse möglicherweise eine viel bessere Lösung als das erneute Versenden des ursprünglichen Ereignisses. Wir erstellen spezielle Möglichkeiten zum Registrieren von Ereignis-Listenern, die diese Listener auch unserem Code aussetzen, damit wir sie manuell aufrufen können. In der Tat gibt es eine getEventListenersFunktion, mit der aktuelle Ereignis-Listener abgerufen werden können. Wird getEventListenersjedoch nur von Chrome / Safari unterstützt. Daher habe ich den folgenden Ersatz entworfen. Obwohl der folgende Code viel zu groß aussieht, handelt es sich bei dem folgenden Code hauptsächlich um Variablennamen, sodass er nach der Minimierung sehr klein ist.

/**@type{WeakMap}*/ var registeredListeners = new WeakMap();

hearEvent(document.getElementById("1st"), "click", function propagate(evt) {
  fireEvent(document.getElementById("2nd"), evt, propagate);
});

hearEvent(document.getElementById("2nd"), "click", function(evt) {
  console.log( evt.target.textContent );
});


/**
 * @param{Element} target
 * @param{string} name
 * @param{function(Event=):(boolean|undefined)} handle
 * @param{(Object<string,boolean>|boolean)=} options
 * @return {undefined}
 */
function hearEvent(target, name, handle, options) {
  target.addEventListener(name, handle, options);
  var curArr = registeredListeners.get(target);
  if (!curArr) registeredListeners.set(target, (curArr = []));
  
  curArr.push([
    "" + name,
    handle,
    typeof options=="object" ? !!options.capture : !!options,
    target
  ]);
}

/**
 * @param{Element} target
 * @param{string} name
 * @param{function(Event=):(boolean|undefined)} handle
 * @param{(Object<string,boolean>|boolean)=} options
 * @return {undefined}
 */
function muteEvent(target, name, handle, options) {
  name += "";
  target.removeEventListener(name, handle, options);
  var capturing = typeof options=="object"?!!options.capture:!!options;
  var curArr = registeredListeners.get(target);
  if (curArr)
    for (var i=(curArr.length|0)-1|0; i>=0; i=i-1|0)
      if (curArr[i][0] === name && curArr[i][2] === capturing)
        curArr.splice(i, 1);
  
  if (!curArr.length) registeredListeners.delete(target);
}

/**
 * @param{Element} target
 * @param{Event} eventObject
 * @param{Element=} caller
 * @return {undefined}
 */
function fireEvent(target, eventObject, caller) {
  var deffered = [], name = eventObject.type, curArr, listener;
  var immediateStop = false, keepGoing = true, lastTarget;
  var currentTarget = target, doesBubble = !!eventObject.bubbles;
  
  var trueObject = Object.setPrototypeOf({
    stopImmediatePropagation: function(){immediateStop = true},
    stopPropagation: function(){keepGoing = false},
    get target() {return target},
    get currentTarget() {return currentTarget}
  }, eventObject);
  
  do {
    if (curArr = registeredListeners.get(currentTarget))
      for (var i=0; i<(curArr.length|0) && !immediateStop; i=i+1|0)
        if (curArr[i][0] === name && curArr[i][1] !== caller) {
          listener = curArr[i];
          if (listener[2]) {
            listener[1].call(trueObject, trueObject);
          } else if (doesBubble || currentTarget === target) {
            deffered.push( listener );
          }
        }
    
    if (target.nodeType === 13) {
      // for the ShadowDOMv2
      deffered.push([ target ]);
      currentTarget = target = currentTarget.host;
    }
  } while (keepGoing && (currentTarget = currentTarget.parentNode));
  
  while (
    (listener = deffered.pop()) &&
    !immediateStop &&
    (lastTarget === listener[3] || keepGoing)
  )
    if (listener.length === 1) {
      // for the ShadowDOMv2
      target = listener[0];
    } else {
      lastTarget = currentTarget = listener[3];
      listener[1].call(trueObject, trueObject);
    }
}
<button id="1st">Click Here</button>
<button id="2nd">Alternate Button</button>

Beachten Sie, dass nach der Minimierung der gesamte Code genau in ein einzelnes Kilobyte passt (vor gzip).

var k=new WeakMap;m(document.getElementById("1st"),"click",function q(a){r(document.getElementById("2nd"),a,q)});m(document.getElementById("2nd"),"click",function(a){console.log(a.target.textContent)});function m(a,c,f,b){a.addEventListener(c,f,b);var d=k.get(a);d||k.set(a,d=[]);d.push([""+c,f,"object"==typeof b?!!b.capture:!!b,a])}
function r(a,c,f){var b=[],d=c.type,n=!1,p=!0,g=a,t=!!c.bubbles,l=Object.setPrototypeOf({stopImmediatePropagation:function(){n=!0},stopPropagation:function(){p=!1},get target(){return a},get currentTarget(){return g}},c);do{if(c=k.get(g))for(var h=0;h<(c.length|0)&&!n;h=h+1|0)if(c[h][0]===d&&c[h][1]!==f){var e=c[h];e[2]?e[1].call(l,l):(t||g===a)&&b.push(e)}13===a.nodeType&&(b.push([a]),g=a=g.host)}while(p&&(g=g.parentNode));for(;(e=b.pop())&&!n&&(u===e[3]||p);)if(1===e.length)a=e[0];else{var u=g=
e[3];e[1].call(l,l)}}function z(a,c,f,b){c+="";a.removeEventListener(c,f,b);f="object"==typeof b?!!b.capture:!!b;if(b=k.get(a))for(var d=(b.length|0)-1|0;0<=d;d=d-1|0)b[d][0]===c&&b[d][2]===f&&b.splice(d,1);b.length||k.delete(a)}
<button id="1st">Click Here</button>
<button id="2nd">Alternate Button</button>

Jack Giffin
quelle
SEHR cool - obwohl ich denke, dass die letzte Zeile Ihrer ersten Lösung lauten sollte : if (eventCopy.defaultPrevented) original.preventDefault();?
Sree