Why does jquery.blockUI swallow onClick event? - javascript

Summary Using jquery.blockUI seems to hide / swallow / mask button click event.
Tech Stach
backbone and marionette
backbone.radio
underscore
jquery
jquery.blockUI
(all latest version)
The App
The app consists of a text input and a button.
In terms of backbone/marionette terminology, there is
a top view which has 2 regions
container view which as the text input
footer view which has the button
The container view is backed by a model.
The footer has a button, Clicking the button sends a backbone.radio event.
This event is picked up in the top view.
When the user leaves the text input, an API (server / backend) is called. In the example a Promise / setTimeout is used to simulate the call.
In the example the button calls console.log.
Code
Here is the JSFiddle Example on JSFiddle and below the Javascript code
// ------------------------------------------------------------------
var Model = Backbone.Model.extend({
defaults: {
"SearchCriteria": {
"Min": { "value": "abc123", "ReadOnly": true }
}
},
async callBackend() {
//$.blockUI(); //<----- uncomment this and the button click is swallowed
await new Promise(resolve => setTimeout(resolve, 3000));
$.unblockUI();
}
});
// ------------------------------------------------------------------
// ------------------------------------------------------------------
var ContainerView = Marionette.View.extend({
template: _.template('<div><label>Container</label></div><div><input id = "min" name = "min" type = "text"/></div>'),
events: {
'change': 'onChangeData',
},
async onChangeData(data) {
console.log('start onChangeData');
await this.model.callBackend();
this.render();
console.log('end onChangeData');
}
});
// ------------------------------------------------------------------
// ------------------------------------------------------------------
var FooterView = Marionette.View.extend({
template: _.template('<div><button class="btn-footer-test">Footer</button></div>'),
events: {
"click .btn-footer-test": () => {
console.log('click test ...');
Backbone.Radio.channel("maske").trigger("select:test");
}
},
});
// ------------------------------------------------------------------
// ------------------------------------------------------------------
var TopView = Marionette.View.extend({
template: _.template("<div id='container'></div><div id='footer'></div>"),
regions: {
container: '#container',
footer: '#footer'
},
events: {
'change': 'onChangeData',
},
initialize() {
this.listenTo(Backbone.Radio.channel("maske"), "select:test", this.onTest, this);
},
onRender() {
this.showChildView('container', new ContainerView({
model: new Model()
}));
this.showChildView('footer', new FooterView());
},
onChangeData(data) {
},
onTest() {
//NOT called if jquery.blockUI present ******
console.log('onTest');
}
});
// ------------------------------------------------------------------
$(document).ready(function () {
console.log('Start');
const topView = new TopView();
topView.render();
$('body').append(topView.$el);
});
Use
The user uses the app like so. The user
changes the text input
and directly clicks the button (without tabbing out of the field first!)
Expected Behavior
the change to the text input triggers a change event.
jquery.blockUI
async call
jquery unblockUI
the click event to the button is executed
Actual Behavior
When the jquery.blockUI function is present the click event to the button is not executed. Commenting jquery.blockUI the button click event occurs, however before the await returns.
Questions
What am I doing wrong?
Why is the click event swallowed?

What am I doing wrong?
Your expectations are wrong. There is no implicit mechanism in JavaScript that serializes asynchronous events one after the other has completed. You (developer) are responsible for synchronization of asynchronous events.
Why is the click event swallowed?
Click event fires when a mousedown and mouseup event occur on the same element. And this is not your case. The order of events is as follows:
mousedown on <button>
change on <input>; causes displaying overlay <div> via blockUI
mouseup on overlay <div>
click on closest common parent element of elements that triggered mousedown and mouseup, which is <body>
Technically it seems to be impossible to click the button after the input changed, because overlay is displayed before mouseup, however there is one way. If you click and hold the mouse button while the overlay is being displayed and release afterwards, the click event will be triggered on the button, but this isn't something that you want anyway.
Try to play with this snippet. It logs every mousedown, mouseup, click and change event. It registers asynchronous change event handler on <input> thet does nothing for the first second, then displays an overlay, then sleeps for 3 seconds and finally hides the overlay. You can observe various behaviours based on how long you kept the button mouse depressed.
Change input text and quickly click the button
button.mousedown
input.change
button.mouseup
button.click
Change input text, click the button and hold for 1 sec, then release
button.mousedown
input.change
div.mouseup
body.click
Change input text, click the button and hold for 4 sec (until overlay disappears), then release
button.mousedown
input.change
button.mouseup
button.click
$(document).on('mousedown mouseup click change', e => {
console.log(e.target.tagName.toLowerCase() + '.' + e.type);
});
$('input').change(async _ => {
await new Promise(resolve => setTimeout(resolve, 1000));
const $div = $('<div style="position:fixed; left:0; top:0; right:0; bottom:0; background-color:rgba(0,0,0,0.5); z-index:999">').appendTo('body');
await new Promise(resolve => setTimeout(resolve, 3000));
$div.remove();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<input type="text" placeholder="type something, then click the button" style="width:250px">
<button>Button</button>
You will have pretty hard time getting around this, because in normal situations the click event won't be fired on the button and therefore I'd suggest you to rethink your user interface. Why do you even block user interface when you use async/promises? It was invented to avoid blocking.

Related

Javascript - Which event to listen to know if a popup menu comes up

I want to capture events in Javascript like link click, input type, button hit/submit etc and send it to an application (recording user actions). Later I will play those user events and do automation testing.
I have a specific case, that I do not know how to capture the user event. When I bring mouse cursor over menu region, a pop up menu shows up. Let's take an example. In dev.lmtools.com site, bring mouse cursor to "Test Environment" tab. You will see smething like image below. In that image, you can see the highlighted menu link "Calculate Endpoints" that user will click.
My job is to record user event when popup menu shows up and as well when user clicks link "Calculate Endpoints". I have explored mouseover, mouseexit, mouseenter, mouseleave event handlers without success. Mouseover generates so many events, so this I want to exclude. Mouseenter fires at page start, when it goes to menu area event does not fire.
I am interseted to know which event I should listen for, what is the unique identifier of element that pops up, identifier of link thats clicked by user, so that I can play afterwards accordingly.
Any help in this regard highly appreciated.
You can try Intersection Observer
The Intersection Observer API provides a way to asynchronously observe
changes in the intersection of a target element with an ancestor
element or with a top-level document's
For example, you can add a IDto the popup container and then, ( Note - I did not ran this code. May have some syntax errors )
// definition
function popupObserver(el, cb) {
let opt = {
root: document.documentElement
}
let observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
cb(entry.intersectionRatio > 0);
});
}, opt);
observer.observe(el);
}
then you use above function like,
popupObserver(document.querySelector("#popup_container_id"), trackPopup);
function trackPopup(isVisible){
let linkEl = null;
if(linkEl) {
linkEl.removeEventListener("click", trackMouseClick)
}
if(isVisible) {
// User hover over the link. capture popup. below code is a dummy code
tracker.track(EVENT.POPUP_HOVER);
linkEl = document.querySelector("#link_id");
linkEl.addEventListener("click", trackMouseClick)
}
}
function trackMouseClick() {
// dummy code to track link click
tracker.track(EVENT.LINK_CLICKED)
}

Javascript Event Queueing: XHR response in blur queued before parent onclick

For the following test case (I replaced the XHR call with setTimeout to represent an XHR call with a very fast response):
<html>
<body>
<div onclick="console.log('onclick parent')">
Event Queue Test:
<input type=text onblur="console.log('onblur'); setTimeout(function() {console.log('onblur timeout')})"/>
</div>
</body>
</html>
If I click in the text field and then click on the text preceding the text field, I get the following output in Chrome (v70):
onblur
onblur timeout
onclick parent
I would have expected the parent onclick handler to have been queued before the setTimeout event. In Chrome, at least, it does not. Is this behavior specified by the standard or implementation-dependent? I'm also interested in workarounds to always have the parent onclick handler run before the timeout (or XHR) handler.
In my actual use case, this issue is problematic because the events are not guaranteed to be queued in the same order, depending on how long the XHR request takes to run.
This won't necessarily help with your actual XHR problem, but I think I understand what's going on. I extended your sample code, using jQuery for convenience:
<div class=parent>Label: <input class=field></div>
and the JavaScript:
var $d = $(document);
var t = {
mousedown: -1,
mouseup: -1,
focus: -1,
click: -1,
blur: -1,
timer: -1
};
["mousedown", "mouseup", "focus", "click"].forEach(function(event) {
$d.on(event, ".parent", function() {
t[event] = Date.now();
})
});
$d.on("blur", ".field", function() {
t.blur = Date.now();
setTimeout(function() {
t.timer = Date.now();
}, 150);
});
$d.on("click", ".parent", function() {
setTimeout(function() {
var race = Object.keys(t).sort(function(k1, k2) { return t[k1] - t[k2];});
console.log("Result: ", race);
race.forEach(function(key) { t[key] = -1; });
}, 1000);
});
What that code does is order the events by the times they actually happen. In the situation of clicking on the text after clicking and focusing in the input field, the order with a short timeout (less than 100ms or so) is
focus
mousedown
blur
timer
mouseup
click
The "blur" and "timer" events are for the input field and its setTimeout() callback. If I make that timeout interval longer, like 150ms or so, then I get
focus
mousedown
blur
mouseup
click
timer
That, I think, makes some sense: the "mousedown" and "mouseup" events are driven by your actual physical interaction with the mouse button. The "click" event does not make sense until "mouseup" is received. With a short timer in the "blur" handler, the timer can fire before the mouse has physically communicated the "up" event through your OS etc.

Using JQuery to .trigger('click') seems to unbind all actions for a modal

I am in the middle of writing some unit tests for a modal component I am developing. When I got around to testing that the action buttons, deny and ok, called the correct handler when clicked.
I thought an easy way to do this would be to call jquery .click(), .trigger('click'), or .tiggerHandler('click') on the button elements.
However doing so seems to unbind the click handler for not only the button being clicked but also the other action buttons and the dimmer. Making the modal impossible to exit.
let myModal = $('#confirmModalPanel');
myModal
.modal({
closable: false,
onDeny: function() {
console.log('onDeny was called');
},
onApprove: function() {
console.log('onApprove was called');
}
});
$('#btnReset').click(() => {
myModal.modal('show');
});
// Cannot trigger deny action from click in code.
// The first click correctly runs but seems to unbind the click event
// from the deny button and breaks the modal functionality. Commenting
// out these lines will make the modal behave normally.
console.log('Trying to click the accept button from code');
$('#positiveButton').trigger('click');
console.log('Trying to click the deny button from code');
$('#denyButton').trigger('click');
http://jsfiddle.net/TuckerD/fpt3rzkp/22/

Is there a way to stop all other events in a dojo onBlur event handler?

Maybe I'm on the wrong track...
The setup:
I have a rather complex full dojofied web application. The important part for this question is a longish form in the central region of a dijit.layout.BorderContainer with a navigation tree and some action buttons in the other regions.
What I want to do:
If the user did enter data and did not save, they should get a warning message if he is going to leave the form (if he navigates away, klicks the "new Element" button,...). For a better user experience, I wanted to give a modal dialog with the options "save", "leave anyway", "cancel".
May idea was to use the onBlur event of the form, stop all other events (most likely an onClick on some other widget), check for changes, if there are changes, display the dialog, otherwise let the other events continue.
I do not want to add a checkChanges method to all non-form active elements!
For a first test I just tried to stop the events...
This works
<div id="formArea" dojoType="dijit.form.Form" encoding="multipart/form-data" action="" class="ContentPane" region="center">
<script type="dojo/connect" event="onBlur" >
alert("I don't think so");
</script>
</div>
...but it's ugly and I can't easily continue
This doesn't
<div id="formArea" dojoType="dijit.form.Form" encoding="multipart/form-data" action="" class="ContentPane" region="center">
<script type="dojo/connect" event="onBlur" args="e">
console.log("blur"); // ok
e.preventDefault();//event.stopt(e)//return false //<--neither of these
</script>
</div>
the problem is that if I click on a button outside of the form, the onBlur triggers, but I can't stop the onClick on the button.
I know that onBlur doesn't deliver an event object - so the e.something can't really work...
Is there any way to catch the onClick on the other element?
Pause button event listener(s) in form's onBlur if data are not saved.
See it in action: http://jsfiddle.net/phusick/A5DHf/
You have some button event listeners, register them via on.pausable(node, event, callback) instead of on():
var b1Handler = on.pausable(button1Node, "click", function() {
console.log("b1.onClick");
});
var b2Handler = on.pausable(button2Node, "click", function() {
console.log("b2.onClick");
});
Gather handlers into an array:
var handlersToPause = [b1Handler, b2Handler];
Add onBlur event listener:
on(form, "blur", function(e) {
if (this.isDirty()) {
// if data are not saved, pause button event listeners
handlersToPause.forEach(function(handler) {
handler.pause();
});
// display modal dialog here
}
});
Add e.g. onFocus event listener to resume button event listeners:
on(form, "focus", function(e) {
handlersToPause.forEach(function(handler) {
handler.resume();
});
});
Please note, that handler.pause() is pausing an onclick listener, not an event. The onclick event is waiting in the Event queue and therefore is not accessible in the execution time of onblur.
I would work out some more robust solution, but this is quick and answers your question. Anyway, have a look at dojo/aspect and its around advice to call your checkChanges without the need to change all non-form active elements.
there is afaik only confirm('question?') that will 'deadlock' the events of your page like that.
I have made a similar setup though, the way I came around this (except if user enters url in addressbar and hits enter) was a popup dialog whenever the navigation tree is clicked, sending user to a new view. Consider:
----------------------------------------
| Nav 1 | Asset1 ( view controller ) |
| Nav 2 | Asset2 ( hidden ) |
----------------------------------------
Nav 1 is the default onload view, Asset 1 is loaded, contains a 'setup page' form or similar and can be changed. The trick is, Asset1 and Asset2 is derivative from AbstractAsset which in turn is a simple ContentPane extension.
In AbstractAsset is a 'must-override-for-functionality' function called isDirty
var Viewcontroller = declare("AbstractAsset", [dijit.ContentPane], {
isDirty: function() { return false; }
});
declare("Asset1", [Viewcontroller], {
startup: function() {
// sets up form
...
// and references inputfields to 'this'
this.inputfields = this.form.getChildren();
// and saves (clones) the state of each field
var self = this;
this.inputfields.forEach(function(inputWidget) {
self.states[inputWidget.id] = inputWidget.get("value");
});
},
isDirty: function() {
var self = this;
var dirty = false;
this.form.getChildren().some(input) {
if(self.states[input.id] != input.get("value")) {
dirty = true;
return false; // breaks .some loop
}
return true;
});
return dirty;
}
})
Then in turn, every navigation click must call the currently visible view controller's isDirty function in order to procede. Lets say user clicks the nav-tree (dijit.Tree) row node Nav 2.
var navigation = dojo.declare("NavigationController", [dijit.Tree], {
currentView : null,
onLoad: function() {
// start Asset1 in viewNode by default
this.currentView = new Asset1({ }, this.viewNode);
},
onClick : function() {
if(this.currentView.isDirty()) alert("I Dont Think So");
else {
this.loadFunction(this.model.selection.getSelected());
}
}
});
This is the general idea of implementing the on-unload-check, you Will need to hook any onClick events through your 'master application controller' to determine what should happen. Check this application which serves as cms navigation controller and its page.js:587 for isDirty example

Display DIV only if user has been Idle for X amount of time

I would like to display a helpful DIV that basically shows the user how to accomplish something on a particular page, but only if the user has been idle for a period of time, say, 30seconds.
What I mean by "Idle" is:
Not clicking any links
Not right clicking anywhere
Exceptions:
I would like to exclude the following conditions from the Is User Idle rule:
User has scrolled up or down/left or right
User has pressed mouse button on an empty area on the site/ or on an element which has no source/link for example, an image with no hyperlink.
and, Pressing keyboard buttons
Can this be done? Or can we only detect when a particullar event occurs?
Any thoughts/suggestions/resources will be greatly appreciated.
Thank you
fairly basic...
var trigger = 30000
$.(function(){
setInterval('displayInf()',trigger );
$('body').bind('click dblclick keypress mousemove scroll', function(){
clearDisplayInf();
});
});
function displayInf()
{
$('body').append('<div>Your notification div</div>');
}
function clearDisplayInf()
{
trigger = clearInterval(trigger);
trigger = setInterval('displayInf()', 30000 );
}
that should do the trick - you could add some script to make the div removable and start the timer again once its removed but that just polishing up really..
Event in DOM would bubble from leaf to root, thus add a event listener on document would make sense.
But since we are possibiliy stop bubbling for click event in certain element, register click event on document may not work perfectly, in that case, register mousedown and mouseup event would help:
var timer; // create a timer at first
// restart timer on click
function startIdle() {
timer = setTimeout(function() { /* show div */ }, time);
}
if (document.addEventListener) {
document.addEventListener('mouseup', startIdle, false);
}
else {
document.attachEvent('onmouseup', startIdle);
}
// start the first timer
startIdle();

Categories

Resources