I've created a simple modal that is allowed to be closed when you click outside of the content area. This is by design but it has an unintended side-effect. If I click anywhere in the content area (for example in a text field) and drag the mouse to beyond the content area and then release the click it will close the modal. I often have a habit of doing this and I can see how average users will perceive this as a bug so I'm trying to nip it prior to release.
var modal = document.getElementById("modal-container");
function openModal() { modal.classList.add("active"); }
function closeModal() { modal.classList.remove("active"); }
window.onclick = function (event) {
if (event.target == modal)
closeModal();
}
html, body {
margin: 0;
height: 100%;
}
.modal-container.active { top: 0; }
.modal-container {
position: absolute;
top: -500vh;
left: 0;
width: 100%;
height: 100%;
display: grid;
background-color: rgba(0, 0, 0, 0.75);
}
.modal-content {
height: 50%;
width: 50%;
margin: auto;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
<button onclick="openModal();">Open the Modal</button>
<div id="modal-container" class="modal-container">
<div class="modal-content">
<input type="text" />
</div>
</div>
To test it properly:
Click the 'Open the Modal' button.
Click in the text box at the center of the white panel.
Enter some text.
Press the left mouse button down in the text box.
Drag the mouse beyond the bounds of the white panel.
Release the mouse button.
The modal should now be closed.
Is there a way to prevent this without tracking the coordinates of the mouse?
Perhaps onmousedown instead of click?
That worked! Just need more coffee this morning I suppose. Going to write up a thorough answer later today for future readers.
Before you answer yourself with a valid cause (as noted in your Question Edit) -
take in consideration:
onmousedown might not always be the desired UX. (Sometimes experienced users to undo a mousedown not being registered as a click they on purpose move the mouse over another element for the mouseup event just to retain the current state.)
Remove inline JavaScript
Assign listeners using Element.addEventListener() to any button having the data-modal attribute
Use data-modal="#some_modal_id" even no the container element
Finally: use if (evt.target !== this) return;
const el_dataModal = document.querySelectorAll('[data-modal]');
function toggleModal(evt) {
if (evt.target !== this) return; // Do nothing if the element that propagated the event is not the `this` button which has the event attached.
const id = evt.currentTarget.getAttribute('data-modal');
document.querySelector(id).classList.toggle('active');
}
el_dataModal.forEach(el => el.addEventListener('click', toggleModal));
html, body {
margin: 0;
height: 100%;
}
.modal-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: grid;
background-color: rgba(0, 0, 0, 0.75);
opacity: 0; /* ADDED */
transition: 0.26s; /* ADDED */
visibility: hidden; /* ADDED */
}
.modal-container.active {
opacity: 1; /* ADDED */
visibility: visible; /* ADDED */
}
.modal-content {
height: 50%;
width: 50%;
margin: auto;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
<button data-modal="#modal-container">Open the Modal</button>
<div id="modal-container" class="modal-container" data-modal="#modal-container">
<div class="modal-content">
<input type="text">
<br><br>
<button data-modal="#modal-container">CLOSE MODAL TEST</button>
</div>
</div>
This is working example. Think, it matches that one you need))
var clickTarget = null;
var modal = document.getElementById("modal-container");
function openModal() {
modal.classList.add("active");
document.body.addEventListener('mousedown', onModalMouseDown, false);
document.body.addEventListener('mouseup', onModalMouseUp, false);
}
function closeModal() {
modal.classList.remove("active");
document.body.removeEventListener('mousedown', onModalMouseDown);
document.body.removeEventListener('mouseup', onModalMouseUp);
}
function onModalMouseDown(event) {
clickTarget = event.target;
}
function onModalMouseUp() {
if (clickTarget === modal) {
closeModal();
}
}
html, body {
margin: 0;
height: 100%;
}
.modal-container.active { top: 0; }
.modal-container {
position: absolute;
top: -500vh;
left: 0;
width: 100%;
height: 100%;
display: grid;
background-color: rgba(0, 0, 0, 0.75);
}
.modal-content {
height: 50%;
width: 50%;
margin: auto;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.modal-trigger-btn {
margin: 20px;
font-size: 16px;
}
<button onmousedown="openModal();" class="modal-trigger-btn">Open the Modal</button>
<div id="modal-container" class="modal-container">
<div class="modal-content">
<input type="text" placeholder="Start to drag outside..."/>
</div>
</div>
To answer this question myself, I thought about how the onclick event was working. A click is defined as the mouse button being pressed down, and then released. Both of those points have to occur to cause the onclick event to be raised (though you can't really have one without the other happening at some point before or after).
I haven't found any real documentation on the execution path below so it based on logical deduction. If you have any documentation on this please link it in a comment so that I can review it and adjust my answer for future readers.
User presses down the mouse button.
The onmousedown event is raised.
User releases the mouse button.
The onmouseup event is raised.
The onmouseclick event is raised.
I did write a test up to verify these results:
var ePath = document.getElementById("executionPath");
document.body.onmousedown = function (event) { ePath.innerHTML += "On Mouse Down<br>"; }
document.body.onmouseup = function (event) { ePath.innerHTML += "On Mouse Up<br>"; }
document.body.onclick = function (event) { ePath.innerHTML += "On Click<br>"; }
html, body { height: 100%; }
<p id="executionPath">Click the Window<br></p>
I believe the unintended behavior is caused by when the target is set for the onclick event. I think there are three possibilities (below from most to least likely) for when this is set, none of which I can confirm or deny:
The target is set when the mouse button is released.
The target is set when the mouse button is pressed down, then again when the mouse button is released.
The target is set continuously.
After analyzing my thoughts I determined that for my scenario onmousedown is likely to be the best solution. This will ensure that the modal closes only if the user initiates the click outside of the content area. A good way to couple this with onmouseup to ensure a full click is still achieved is demonstrated below. Though in my case I am okay with simply using onmousedown:
var initialTarget = null;
var modal = document.getElementById("modal-container");
function openModal() { modal.classList.add("active"); }
function closeModal() { modal.classList.remove("active"); }
window.onmousedown = function (event) { initialTarget = event.target; }
window.onmouseup = function (event) {
if (event.target == initialTarget)
closeModal();
}
html, body { height: 100%; }
.modal-container.active { top: 0; }
.modal-container {
position: absolute;
top: -500vh;
left: 0;
width: 100%;
height: 100%;
display: grid;
background-color: rgba(0, 0, 0, 0.75);
}
.modal-content {
height: 50%;
width: 50%;
margin: auto;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
<button onclick="openModal();">Open the Modal</button>
<div id="modal-container" class="modal-container">
<div class="modal-content">
<input type="text" />
</div>
</div>
The snippet above ensures that the click starts and ends on the modal container prior to closing the modal. This prevents the modal from closing if the user accidentally initiates a click outside of the content area and drags their mouse into the content area to complete the click. The same is true in the reverse order, and the modal will only close if the click is initiated and completed on the modal container.
The only thing I can't figure out is when the target for onclick is set which is probably more important in a proper explanation on the root cause of this issue so feel free to let me know!
Related
I have a screen
where I want to disable all the events when execution is going on.
When I click on the Execute button, an API is called which probably takes 4-5 minutes to respond. During that time, I don't want the user to click on the calendar cells or Month navigation arrows.
In short, I want to disable all the events on the center screen but not the left menu.
Is it possible to do that?
Yes sure, you can add a class with css pointer-events rule. Set it on the whole table and it will disable all events. Just add it when request starts and remove it when it ends. You can achieve that, by having a boolean variable isLoading in your state, or redux store and based on that add or remove the no-click class.
.no-click {
pointer-events: none;
}
You can use classic loading overlay box over your content when some flag (i.e. loading) is true.
Other way to do it is to use pointer-event: none in CSS. Use same flag to set class to your content block.
Here is a working example in codesanbox:
https://codesandbox.io/s/determined-dirac-fj0lv?file=/src/App.js
Here is code:
export default function App() {
const [loadingState, setLoadingState] = useState(false);
const [pointerEvent, setPointerEvent] = useState(false);
return (
<div className="App">
<div className="sidebar">Sidebar</div>
<div
className={classnames("content", {
"content-pointer-event-none": pointerEvent
})}
>
<button onClick={() => setLoadingState(true)}>
Show loading overlay
</button>
<button onClick={() => setPointerEvent(true)}>
Set pointer event in css
</button>
{loadingState && <div className="loading-overlay"></div>}
</div>
</div>
);
}
.App {
font-family: sans-serif;
text-align: center;
display: flex;
position: absolute;
width: 100%;
height: 100%;
}
.content {
flex: 1;
position: relative;
}
.loading-overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
z-index: 1;
}
.content-pointer-event-none {
pointer-events: none;
}
I've been trying to figure out how to close the modal I made by a window click but have no success. My modal is activated by adding a class and removing the 'show-modal' class. So I created the window event listener and seems like whenever I press the "a" tag in my HTML, it also considers that click as part of the window so the modal won't even open.
However, once I remove the window event listener, the modal is working fine; but the modal does not close unless I hit the "closeModal" button.
Is there something I am doing wrong or improve on? I tried googling around but their idea is different than mine.
I know I can do this using React & Boostrap but I am also trying to learn the vanilla JS way so I want to do this right!
const contact = document.querySelector('.contact');
const modal = document.querySelector('.modal');
const closeModal = document.querySelector('.closeBtn');
contact.addEventListener('click', showModal);
closeModal.addEventListener('click', modalClose);
window.addEventListener('click', modalClose);
function showModal(){
modal.classList.add('show-modal');
console.log('clicked')
}
function modalClose(){
modal.classList.remove('show-modal');
console.log('closed')
}
One way to get around this would be to add an "underlay" to the modal and listen for clicks on that instead of the window element.
Also I like to add the modal open class to the body so then it's easy to adjust styling on both the modal and modal underlay.
document.querySelector('button#open').addEventListener('click', e => {
document.body.classList.add('show-modal')
})
document.querySelector('button#close').addEventListener('click', e => {
document.body.classList.remove('show-modal')
})
document.querySelector('.underlay').addEventListener('click', e => {
document.body.classList.remove('show-modal')
})
.modal {
height: 100px;
width: 100px;
background-color: white;
display: none;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.underlay {
display: none;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.show-modal .modal {
display: block;
}
.show-modal .underlay {
display: block;
}
<button id="open">Open Modal</button>
<div class="underlay"></div>
<div class="modal">
<button id="close">Close Modal</button>
</div>
</div>
You can use event.stopPropagation() to avoid event propagation, try this:
function showModal(){
event.stopPropagation();
modal.classList.add('show-modal');
console.log('clicked')
}
This question already has answers here:
Transitions on the CSS display property
(37 answers)
Closed 3 years ago.
I'm sure that I'm making a rookie mistake here, but I've researched this solution all day, and I can't seem to understand why my code below isn't working.
The use case is a button that opens up a modal box inside a semi-transparent overlay, with the overlay covering everything else on the screen, including the button that opened it. The button is currently opening the modal and overlay just fine, and clicking anywhere outside of the modal box does indeed close it. But I don't understand why my set CSS transition isn't working.
I'm at a loss on this one, so I'd very much appreciate any advice that more seasoned developers can offer. Thank you so much in advance!
Best,
Josh
var modalOverlay = document.getElementById('modalOverlay');
var modalButton = document.getElementById('modalButton');
modalButton.addEventListener('click', openModal);
window.addEventListener('click', closeModal);
function openModal() {
modalOverlay.style.display = "flex";
modalOverlay.style.opacity = "1";
}
function closeModal(event) {
if (event.target == modalOverlay) {
modalOverlay.style.opacity = "0";
modalOverlay.style.display = "none";
}
}
.modal-overlay {
display: none;
opacity: 0;
position: fixed;
top: 0;
left: 0;
z-index: 10;
width: 100vw;
height: 100vh;
align-items: center;
justify-content: center;
background-color: rgba(0,0,0,0.5);
transition: all 1s ease-in-out;
}
.modal-box {
width: 200px;
height: 200px;
background-color: blue;
}
<button id="modalButton" class="modal-button">Open Modal</button>
<div id="modalOverlay" class="modal-overlay">
<div id="modalBox" class="modal-box"></div>
</div>
display is not a property that can be transitioned. You need to make your animation take multiple steps. When you click the button, the modal should be made flex, but it should still be transparent. Then you need to transition the opacity up to 1 which is what CSS transitions can do.
You need to do the inverse whenever you close the modal. Transition back to opacity 0 and after the transition is done, mark it display: none.
var modalOverlay = document.getElementById('modalOverlay');
var modalButton = document.getElementById('modalButton');
modalButton.addEventListener('click', openModal);
window.addEventListener('click', closeModal);
function openModal() {
// This will cause the browser to know
// that the element is display flex for a frame
requestAnimationFrame(() => {
modalOverlay.classList.add("modal-overlay--open");
// Then when we wait for the next frame
// the browser will now know that it needs
// to do the transition. If we don't make
// them separate actions, the browser
// will try to optimize the layout and skip
// the transition
requestAnimationFrame(() => {
modalOverlay.classList.add("modal-overlay--open-active");
});
});
}
function closeModal(event) {
if (event.target == modalOverlay) {
modalOverlay.classList.remove("modal-overlay--open-active");
setTimeout(() => {
modalOverlay.classList.remove("modal-overlay--open");
}, 1100);
}
}
.modal-overlay {
display: none;
opacity: 0;
position: fixed;
top: 0;
left: 0;
z-index: 10;
width: 100vw;
height: 100vh;
align-items: center;
justify-content: center;
background-color: rgba(0,0,0,0.5);
transition: all 1s ease-in-out;
}
.modal-overlay.modal-overlay--open {
display: flex;
}
.modal-overlay.modal-overlay--open-active {
opacity: 1;
}
.modal-box {
width: 200px;
height: 200px;
background-color: blue;
}
<button id="modalButton" class="modal-button">Open Modal</button>
<div id="modalOverlay" class="modal-overlay">
<div id="modalBox" class="modal-box"></div>
</div>
Let's draw a little insight from how some other frameworks deal with these kinds of transitions. For example, Vue.js separates its enter/leave transitions into 6 sorts of phases:
Enter: Starting state, added before entry (for you, display: flex and fully transparent)
Enter Active: The transitioning state that sets the transition "target" (opacity towards 1 in your case)
Enter To: What it should be after the transition is complete (we aren't going to bother with this)
Leave: About to start leaving (nothing really needs to change here for us)
Leave Active: Set the target state for your element so that it knows what to transition to (for us, we just remove the class that says opacity: 1)
Leave To: We don't need this either
The main thing that we need to consider is that we need the browser to have the element in the page and "rendered" so that it will consider it for transitioning. That's why we add, in our example, the modal-overlay--open class which makes it flex. We then wait just a second and add the transition target class modal-overlay--open-active which causes the element to actually transition.
Then we do the same thing in reverse: remove modal-overlay--open-active so the browser knows to transition the element back to the "normal" style. We set a timeout to remove the display: flex class after the transition is done. You could use event listeners for this, but it's overkill for such an example.
The display property doesn't play well with transition. Instead, just toggle between opacity 1 and 0.
const modalOverlay = document.getElementById('modalOverlay');
const modalButton = document.getElementById('modalButton');
modalButton.addEventListener('click', openModal);
window.addEventListener('click', closeModal);
function openModal() {
modalOverlay.style.opacity = "1";
modalButton.style.opacity = "0";
}
function closeModal(event) {
if (event.target == modalOverlay) {
modalOverlay.style.opacity = "0";
modalButton.style.opacity = "1";
}
}
.modal-overlay {
opacity: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-overlay,
.modal-button {
transition: opacity 1s ease-in-out;
}
.modal-box {
width: 200px;
height: 200px;
background-color: blue;
}
.modal-button {
position: absolute;
z-index: 2;
}
<button id="modalButton" class="modal-button">Button</button>
<div id="modalOverlay" class="modal-overlay">
<div id="modalBox" class="modal-box"></div>
</div>
I am trying to hide the popup if the background is clicked, but NOT the div.
Basically, when the user clicks the background it will hide the div; yet, if the user clicks the actual div it will still hide it. I would only like the div to be hidden on the clicking of the background.
Here is my code:
HTML
<div id="linkinputholder">
<div id="linkinputbox">
Title
</div>
</div>
<button onclick="displaylinkinput()" type="button"> Display </button>
CSS
#linkinputholder {
display: none;
position: fixed;
z-index: 100;
width: 100%;
min-height: 100%;
left: 0px;
top: 0px;
background: rgba(0, 0, 0, 0.2);
}
#linkinputbox {
display: block;
background-color: red;
width: 500px;
height: 100px;
position: fixed;
margin: auto;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
JS/Jquery
function displaylinkinput() {
document.getElementById('linkinputholder').style.display = "block";
}
$('#linkinputholder').click(function() {
document.getElementById('linkinputholder').style.display = "none";
});
I'm assuming by background you mean your linkinputholder div, which is 100% wide by 100% tall. Your jquery code was missing the call to displaylinkinput, so i added a click event handler to call it. When you click on the linkinputbox div, the click event passes down through to linkinputholder. To prevent this just stop the event propagation.
$('#linkinputbox').click(function (evt) {
evt.stopPropagation();
});
I have created a JSFIDDLE for you here: http://jsfiddle.net/seadonk/oLgex1pq/
Here is the corrected javascript:
function displaylinkinput() {
$('#linkinputholder').show();
}
$(function () {
$('button').click(function () {
displaylinkinput();
});
$('#linkinputholder').click(function () {
$('#linkinputholder').hide();
});
$('#linkinputbox').click(function (evt) {
evt.stopPropagation();
});
})();
Edit
Check if div is target
$('#linkinputholder').click(function(event) {
if (jQuery(event.target).is('.linkinputholder')) return;
document.getElementById('linkinputholder').style.display = "none";
});
I have div containing a form. When user clicks outside of the div it hides - this part works bit to good.
The problem is that while user is selecting text inside div(input, paragraph,...) and his mouse
leaves the modal(clicked state), mouseup event is triggered which causes my div to hide.
How do I ignore the mouseup event when user is selecting text?
Here is what my HTML mark up looks like:
<div class="body">
<button id="show-modal">Toggle modal</button>
<div class="modal">
<input type="text" name="opportunity-name" \>
<button>Submit</button>
</div>
</div>
CSS:
.body {
position: absolute;
width: 100%;
bottom: 0;
top: 0;
height: 100%;
background: green;
}
div.modal {
background: blue;
width: 200px;
height: 130px;
margin: 0 auto;
text-align: center;
padding-top: 70px;
}
JS:
var $modal = $('div.modal');
$('#show-modal').click(function () {
$modal.fadeIn();
});
$(document).mouseup(function (e) {
if (!$(e.target).is('div.modal *, div.modal')) {
$modal.fadeOut(100);
}
});
Here's a fiddle
How do I ignore the mouseup event when user is selecting text?
Check if the text input has focus. Ex:
if ( $(input).is(':focus') ) { ... }