97

I'm looking for a simple and abstract way of cloning or re-dispatching DOM events only. I am not interested in cloning DOM nodes.

I've experimented a bit, read the DOM Events specification and I found no clear answer.

Ideally, I'm looking for something as straight-forward as:

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

This code example, of course, does not work. There's a DOM exception stating that the event is currently being dispatched - obviously.

I'd like to avoid manually creating new events with document.createEvent(), initializing them and dispatching them.

Is there a simple solution to this use case?

Razvan Caliman
  • 4,509
  • 3
  • 21
  • 24
  • Why do you need to redispatch events? – josh3736 Aug 15 '12 at 17:42
  • I need this as a workaround for CSS Regions which currently don't dispatch child node events. Regions only render the child node content, they don't act as parentNodes. – Razvan Caliman Aug 15 '12 at 21:01
  • @josh3736 (I'm late to the party, but) if you make a system that dispatches new events (f.e. for commands based on keyboard shortcut) built on the existing events, you may want to still re-dispatch the original events as an escape hatch if someone still needs to do custom key handling that the new wrapper doesn't provide. – trusktr Aug 18 '23 at 00:09

3 Answers3

170

I know, the question is old, and the OP wanted to avoid creating / initializing approach, but there's a relatively straightforward way to duplicate events:

new_event = new MouseEvent(old_event.type, old_event)

If you want more than just mouse events, you could do something like this:

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

And in the original context:

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

(For jQuery users: you may need to use e.originalEvent.constructor instead of e.constructor)

Alexis
  • 4,317
  • 1
  • 25
  • 34
  • 3
    Note that this only works in modern versions of Firefox and Chrome, and does not work in any version of IE – Quinn Strahl Apr 25 '14 at 20:49
  • 4
    @Alexis I'm getting: ```Uncaught TypeError: Illegal constructor``` :( – niieani Dec 05 '14 at 02:09
  • @NIXin in which browser? – Alexis Dec 05 '14 at 09:39
  • @Alexis Chrome, both Stable and Dev Channel. Trying to clone a TouchEvent. – niieani Dec 05 '14 at 11:37
  • 3
    I've just put together a module on npm called clone-event which essentially wraps the functionality described in this answer, which is what I currently use when I want to re-dispatch an event. I have done basic testing of this method in Chrome v37, and it seems to work. I'd appreciate it if anyone finds a bug, or an environment in which it doesn't work. – K. P. MacGregor Mar 02 '15 at 20:41
  • 3
    Very clever, the way you pass the old event as the constructor's init argument! – Decent Dabbler Jul 23 '15 at 07:23
  • 4
    I'm trying to do the same with wheel events, but the new event gets 'isTrusted' false, and the OS ignores it instead of scrolling – Gyro Jun 28 '16 at 10:01
  • 2
    @Gyro, yeah, according to [specs](https://w3c.github.io/uievents/#trusted-events), in most cases untrusted events can't trigger default actions "as if the preventDefault() method had been called" on them. Unfortunately, I don't think there's anything we can do about it. – Alexis Jun 29 '16 at 15:46
  • 1
    I'm getting: Uncaught RangeError: Maximum call stack size exceeded. – sigmaxf Aug 17 '18 at 02:01
  • anyone has a fiddle that demonstrate how a secondary contenteditable-div can "retype itself" using event being duplicated from a main div? – Nathan B Apr 15 '19 at 11:05
  • @raphadko, this probably happends if you dispatch new event to the same target (the question and the answer assume there is a different target). – Stan Nov 13 '19 at 18:51
2

A Fix For Internet Explorer

Alexis posts a nice solution, but his solution will not work in Internet Explorer. The below solution will. Unfortunately, there is no system as consistent as event constructors in Internet Explorer, so the code bloat below is necessary.

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>

A More General Solution

Depending on your needs, a much better solution than redispatching the original event might be synthetic event propagation. We create special ways to register event listeners that also expose these listeners to our code so that we can call them manually. Indeed, there is a getEventListeners function that can be used to retrieve current event listeners. However, getEventListeners is only supported by Chrome/Safari. Thus, I designed the following replacement. Although the code below looks way too big, the code below is mostly variable names, so it will be very small after minification.

/**@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>

Observe that, after minification, all this code fits neatly into a single kilobyte (prior to 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 G
  • 4,553
  • 2
  • 41
  • 50
  • VERY cool - though I think the last line of your first solution should read: `if (eventCopy.defaultPrevented) original.preventDefault();` ? – sree Jul 30 '20 at 14:03
0

It's a little simpler to duplicate an object that you intend to retain and just one class of events to handle:

// base JS (far more complex in reality)
var click = document.querySelector("#clickme");
var txt = document.querySelector("#txt");
click.addEventListener("click", () => {
  txt.textContent++
});

// supplemental JS
var click2 = click.cloneNode(true);
click2.id = "click2"; // don't collide ids!
click2.addEventListener("click", () => {
  click.click();
});
document.querySelector("#parent").appendChild(click2);
<div id="parent">
  <button id="clickme">click me</button>
  <p>Your lucky number is <span id="txt">0</span>!</p>
</div>

The "supplemental JS" section demonstrates the cloning of the object. Rather than copying events, you can just trigger the events of the cloned node by proxy.

Adam Katz
  • 14,455
  • 5
  • 68
  • 83