如何克隆或重新分派 DOM 事件?

我正在寻找一种简单而抽象的方法,只克隆或重新分派 DOM 事件。我对克隆 DOM 节点不感兴趣。

我做了一些实验,阅读了 DOM Events 规范,没有找到明确的答案。

理想情况下,我会找一些直截了当的东西,比如:

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

当然,这个代码示例不能工作。有一个 DOM 异常声明事件当前正在被调度-显然。

我希望避免使用 document.createEvent()手动创建新事件,初始化它们并分派它们。

这个用例是否有一个简单的解决方案?

30414 次浏览

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)

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>