Dispatch event to element bypassing capturing and bubbling - javascript

Since dispatchEvent, as per the docs, will apply the:
normal event processing rules (including the capturing and optional
bubbling phase)
I'm looking for something similar but with a way to skip this process and trigger the event directly on the element. To trigger the default element event behavior while bypassing the processing stage.
As in, to capture the event at window level (before it reaches the other capture triggers) and pass it straight to the component (text area) invoking it directly.
(For example to trigger the default keydown of a text area without going through the hierarchy)
I've been trying to do it like this but if there is another event at window level this will not work:
window.addEventListener("keydown", this.keyDown, true);
keyDown = (event) => {
event.preventDefault();
event.nativeEvent && event.nativeEvent.stopImmediatePropagation();
event.stopImmediatePropagation && event.stopImmediatePropagation();
event.stopPropagation();
// Pass event straight to the element
return false;
};
I'm looking to trigger the default element event behavior while bypassing the processing

There may well be a more elegant way to do this, but one option is to remove the element from the DOM first, dispatch the event to it, then put it back into the DOM:
document.body.addEventListener('keydown', () => {
console.log('body keydown capturing');
}, true);
document.body.addEventListener('keydown', () => {
console.log('body keydown bubbling');
});
const input = document.querySelector('input');
input.addEventListener('keydown', () => {
console.log('input keydown');
});
const node = document.createTextNode('');
input.replaceWith(node);
input.dispatchEvent(new Event('keydown'));
node.replaceWith(input);
<input>
Since the element isn't in the DOM when the event is dispatched, the elements which used to be its ancestors won't see the event.
Note that events dispatched to detached elements do not capture/bubble regardless, not even to parents or children of element the event was dispatched to.
Without removing the element from the DOM entirely beforehand, if the input can exist in a shadow DOM, you can also dispatch an event to it there, and the event won't capture down or bubble up (though user input, not being custom events, will propagate through):
document.body.addEventListener('keydown', () => {
console.log('body keydown capturing');
}, true);
document.body.addEventListener('keydown', () => {
console.log('body keydown bubbling');
});
outer.attachShadow({mode: 'open'});
const input = document.createElement('input');
outer.shadowRoot.append(input);
input.addEventListener('keydown', () => {
console.log('input keydown');
});
input.dispatchEvent(new Event('keydown'));
<div id="outer"></div>
Another approach would be to call stopPropagation and stopImmediatePropagation in the capturing phase, at the very beginning, when the event is at the window, and then manually call the listener function you want. Make sure to attach your window listener before any other scripts on the page run, to make sure none of the page's listeners can see the event first:
// Your script:
const input = document.body.appendChild(document.createElement('input'));
input.className = 'custom-extension-element';
const handler = (e) => {
setTimeout(() => {
console.log(e.target.value);
});
};
window.addEventListener(
'keydown',
(e) => {
if (e.target.closest('.custom-extension-element')) {
e.stopImmediatePropagation(); // Stop other capturing listeners on window from seeing event
e.stopPropagation(); // Stop all other listeners
handler(e);
}
},
true // add listener in capturing phase
);
// Example page script
// which tries to attach listener to window in capturing phase:
window.addEventListener(
'keydown',
(e) => {
console.log('page sees keydown');
},
true
);
document.body.addEventListener('keydown', () => {
console.log('body keydown capturing');
}, true);
document.body.addEventListener('keydown', () => {
console.log('body keydown bubbling');
});

The best way around propagation issues is to just execute the function:
function tester(){
console.log("Just fire the function! Don't dispatchEvent!");
}
tester();
document.getElementById('test').onkeyup = function(e){
e.stopPropagation();
console.log(this.id); console.log(e.target); tester();
}
document.body.onkeyup = ()=>{
console.log("This shouldn't fire when on #test");
}
<input id='test' type='text' value='' />

Related

How can I stop this document attached event listener from emitting duplicate events?

I'd like for a game to fire events upon key presses, but when I navigate away from and back to the game page the event listener fires twice. I found another issue suggesting I remove the listener before adding it, but that isn't helping.
The listener is added during mounted
mounted() {
this.setupInputListener();
},
And the keydown event listener is added to the document
keydownListener() {
const action = handleKeyDown(event.key);
if ( action ) {
this.SEND_INPUT({
gameId: this.gameId,
action: action
});
}
},
setupInputListener() {
document.removeEventListener('keydown', this.keydownListener);
document.addEventListener('keydown', this.keydownListener);
}
How can I prevent this keydown listener from emitting duplicate events?
You cannot add an event listener multiple times, that will cause it to fire once or many times. To avoid this, try using onkeydown. Once we do this we no longer need the removeEventListener. For example, something like this should work:
keydownListener() {
const action = handleKeyDown(event.key);
if (action) {
this.SEND_INPUT({
gameId: this.gameId,
action: action
});
}
},
setupInputListener() {
document.onkeydown = () => {
this.keydownListener;
}
}
Hoped this helped!

Unable to add click event to button that isn't created until the object is created

I'm trying to select to element class 'remove-book' that isn't created until after display books is ran..how do I go about selecting this remove button AFTER it's created?
Here is link to the github https://github.com/Cluelesshint/library the 'displayBooks()' function is what creates that class..please help!
buttons.forEach((button) => {
if (button.className === 'new-book') {
button.addEventListener('click', function () {
const input = document.querySelector('.book-input');
openInput(input);
});
}
else if (button.className === 'add-input') {
button.addEventListener('click', addABook);
}
else if (button.className === 'remove-book') {
button.addEventListener('click', doThis);
}
console.table(buttons);
});
Try to use event delegation. The idea is simple: instead of listening events on the target element, listening events on it's ancestor elements. If an event is happening, the browser would work like the following.
The browser checks to see if the direct parent of the element selected
has an onclick event handler registered on it for the bubbling phase,
and runs it if so.
Then it moves on to the next immediate ancestor
element and does the same thing, then the next one, and so on until it
reaches the element.
So, when the element is clicked, the event is bubble up to its ancestor element. It doesn't care when and how the element is created.
Refactor your code with event delegation would be something like the following.
document.addEventListener('click', (event) => {
if (event.target.tagName == 'BUTTON') { // make sure the target is a button element
const button = event.target; // this is the button clicked.
const classNames = event.target.classList
if (classNames.contains('new-book')) {
const input = document.querySelector('.book-input');
openInput(input);
} else if (classNames.contains('add-input')) {
button.addEventListener('click', addABook);
} else if (classNames.contains('remove-book')) {
button.addEventListener('click', doThis);
}
}
})
note: I'm not sure why you add another event listener when a button is clicked. This is just a refactor in a perspective of event delegation

Did I convert my Jquery event listener into Javascript properly? I don't understand how the Javascript works without targeting the id?

I'm trying to convert jquery into javascript. My app is a simple to do list and I'm targeting a button with an id called #clear-completed. Whenever I click that button on my app, it deletes the completed todo items, but I don't understand where it is being targeting in my new Javascript code.
Here is the original Jquery code
$('#footer').on('click', '#clear-completed', this.destroyCompleted.bind(this));
So I changed it to Javascript and this code worked
var footer = document.getElementById('footer');
footer.addEventListener('click', this.destroyCompleted.bind(this))
What I don't understand is what happened the the #clear-completed id and how does my new javascript code still work, even though I am not specifying to target the #clear-completed button?
Here is the code for the destroyCompleted function
destroyCompleted: function () {
this.todos = this.getActiveTodos();
this.filter = 'all';
this.render();
},
In the debugger it runs through the activeTodos function, but I don't see anywhere where the id #clear-completed is targeted?
getActiveTodos: function () {
return this.todos.filter(function (todo) {
return !todo.completed;
});
},
getCompletedTodos: function () {
return this.todos.filter(function (todo) {
return todo.completed;
});
},
Did I write my Jquery into Javascript properly? Or did I miss something?
Also, if the id had more than one event listener how would you code that properly? for example
$('#todo-list')
.on('change', '.toggle', this.toggle.bind(this))
.on('dblclick', 'label', this.edit.bind(this))
.on('keyup', '.edit', this.editKeyup.bind(this))
.on('focusout', '.edit', this.update.bind(this))
.on('click', '.destroy', this.destroy.bind(this));
The equivalent JavaScript would be:
document.querySelector('#footer').addEventListener('click', event => {
const element = event.target.closest('#clear-completed');
if (
event.currentTarget !== element &&
event.currentTarget.contains(element)
) {
this.destroyCompleted(event);
}
});
The signature $(target).on(event, selector, handler) that you're using is called a delegated event handler, so the handler is invoked on the target element as you have correctly reproduced, but it is only invoked when the event targets an element matching selector which is descendant of target, not including target itself.
Matching the selector is reproduced above by checking that event.currentTarget .contains() the element returned by event.target .closest(selector).
You could even break this logic out into a helper function to make it more readable:
document.querySelector('#footer').addEventListener('click', event => {
const matches = selector => {
const element = event.target.closest(selector);
return (
event.currentTarget !== element &&
event.currentTarget.contains(element)
);
};
if (matches('#clear-completed')) {
this.destroyCompleted(event);
}
});
Since you need this pattern multiple times, it makes sense to move it into another reusable function:
function delegate (target, type, selector, handler) {
const matches = event => {
const element = event.target.closest(selector);
return (
event.currentTarget !== element &&
event.currentTarget.contains(element)
);
};
target.addEventListener(type, event => {
if (matches(event)) {
handler(event);
}
});
}
const element = document.querySelector('#todo-list');
delegate(element, 'change', '.toggle', e => this.toggle(e));
delegate(element, 'dblclick', 'label', e => this.edit(e));
delegate(element, 'keyup', '.edit', e => this.editKeyup(e));
delegate(element, 'focusout', '.edit', e => this.update(e));
delegate(element, 'click', '.destroy', e => this.destroy(e));
You targeted '#clear-completed' in the jquery function by passing the argument,'#clear-completed', to your on event handler. Jquery on says:
https://api.jquery.com/on/
.on( events [, selector ] [, data ], handler )
selector
Type: String
A selector string to filter the descendants of the selected elements that trigger the event. If the selector is null or omitted, the event is always triggered when it reaches the selected element.

Referring to tooltip within event listener to keep it open on hover

Finding myself in a bit of a strange position where I have to reference the tooltip within an instantiation for all tooltips.
$('body').tooltip({
selector: '[data-toggle="tooltip"]',
html: true,
animation: false,
}).on("mouseenter", function (e) {
e.stopPropagation();
var _this = e.target;
$(_this).tooltip("show");
$(".tooltip").on("mouseleave", function () {
$(_this).tooltip('hide');
});
}).on("mouseleave", function (e) {
e.stopPropagation();
var _this = e.target;
setTimeout(function () {
if (!$(".tooltip:hover").length) {
$(_this).tooltip("hide");
}
}, 300);
});
That being said, how can I:
Reference the actual element that is triggering this jQuery call
Keep the tooltip open while either the actual tooltip or element that generated it are being hovered over?
Here is a link to a JSFiddle prototype:
https://jsfiddle.net/cwv57weu/8/
Within your '.on()' call, you can add an 'event' argument to your anonymous function. This will contain all of the data from the event including the element which triggered the event (it will be referenced as 'target').
}).on("mouseenter", function (event) {
$(event.target).tooltip("show");
})
the event argument contains a ton of data, I would play around with it by console.log(event) within your anonymous function to get a feel as to what data is available to you.
Use event.target.
$('body').tooltip({
selector: '[data-toggle="tooltip"]',
html: true,
animation: false,
}).on("mouseenter", function (e) {
var _this = e.target;
$(_this).tooltip("show");
$(".tooltip").one("mouseleave", function () {
$(_this).tooltip('hide');
});
}).on("mouseleave", function (e) {
var _this = e.target;
setTimeout(function () {
if (!$(".tooltip:hover").length) {
$(_this).tooltip("hide");
}
}, 300);
});
e.target is the actual element that the event originated on, while this is the element that the event listener was attached to (equivalent to e.currentTarget).
Note that because of event bubbling, the event will fire on all the containing elements up to body. You may want to use e.stopPropagation() to prevent bubbling, so you only process the deepest element.
I also changed the mouseleave handler on .tooltip to use .one(). Otherwise, every time you enter something, you'll add another mouseleave handler to all the tooltips, without removing the previous one, and soon there will be thousands of handlers running (this is why it's generally wrong to bind event handlers inside other event handlers). I'm not really sure you need both that mouseleave handler and the one you attach to body.

Interrupt all mousedown events to simulate like a mouse click

I want to trigger click event on a element when mousedown occurs. Also, I want to enable this feature for all elements in a html page.
Is it possible with jQuery on Chrome ?
Here's my first attempt;
$.fn.mousedown = function (onclick) {
this.bind("click", function (e) { onclick.call(this, e); });
return this;
};
But this mousedown elements fired after click occurs.
$(document).on('mousedown', function (e) { $(e.target).trigger('click') })
I'm not sure though for what this should be useful.
To prevent the second click (which is the normal click) you have to do some extra work
$(document).on('mousedown', function (e) {
$(e.target).trigger('click').once('click', function (e) {
e.preventDefault();
e.stopPropagation();
});
})

Categories