Collapse div when I click off element - javascript

I've made a collapsible menu with Vanilla JS which works fine, but I'd like for the menu to collapse when you click off it. I thought adding an event listener to the window would work, but I must have done something wrong.
const pageTitleDrop = document.getElementById('page-title');
const dropMenu = document.getElementById('dropdown');
pageTitleDrop.addEventListener('click', () => {
dropMenu.classList.toggle('collapsed');
})
window.addEventListener('click', (e) => {
if (e.target !== dropMenu) {
if (dropMenu.classList.contains('collapsed')) {
return;
} else {
dropMenu.classList.toggle('collapsed');
}
}
})

e.target can be the dropMenu but it could also be any child element of that container instead. You need to check ancestors of e.target for the dropdown. Thankfully, this is easy:
if( !e.target.closest("#dropdown")) {
dropMenu.classList.add("collapsed");
}
Note that I've also simplified your code. Rather than "if the class is set, do nothing, else toggle the class", it's now just "add the class" -- if it's already there, nothing happens.

Related

How to detect div and its children using window.onclick?

Example code:
window.onclick = function(event) {
if(event.target.matches('.myDiv')){
DO THIS
}
}
Understandably, this only checks if a click is made on myDiv specifically.
Without using <div onclick="function()"> and without hardcoding each of myDiv's children into the if statement, is there a way to check if a click is made on a div and any of its children?
Instead you should do:
var parent_div = document.getElementsByClassName(".myDiv");
parent_div.onclick = function(e) { /*DO SOMETHING*/ };
This way you directly check for a click on an element and all of its children instead of getting the click event from window and then checking whether it is the element which you want or not.
try like this:
window.onclick = function (event) {
if (event.target.classList.contains('myDiv')) {
event.target.remove();
}
}

Logic for showing elements and hiding them on body click

I have some piece of code.
This code on button click open menu.
When i click on button again, menu is hidden (i remove .show class, show class has display:block rule, so i toggle visibility of this item by clicking on button).
In next line, i have event, which check what element is clicked. If i "click" outside" of menu, menu become hidden, beacuse i remove .show class.
And now i have a problem, it looks like first part of code dont work anymore (button.on('click')) - i mean, work, but second part of code is also executed, and this logic is now broken.
Have you got any idea for workaround?
Thanks
var menu = $('.main-menu');
var button = $('.burger');
button.on('click',function() {
if (menu.hasClass('show')) {
menu.removeClass('show');
$(this).removeClass('opened');
} else {
menu.addClass('show');
$(this).addClass('opened');
}
});
$(document).bind( "mouseup touchend", function(e){
var container = menu;
if (!container.is(e.target)
&& container.has(e.target).length === 0) {
container.removeClass('show');
button.removeClass('opened');
}
});
maybe use jQuery toggle() method ? For example:
button.on('click',function() {
menu.toggle();
});
You need to bind an outer click event only when the button click event has been triggered, and remove the outer click event when the outer click event has been triggered:
var menu = $('.main-menu');
var button = $('.burger');
button.on('click',function() {
if (menu.hasClass('show')) {
menu.removeClass('show');
$(this).removeClass('opened');
} else {
menu.addClass('show');
$(this).addClass('opened');
}
var butbindfunc = function(e){
var container = menu;
container.removeClass('show');
button.removeClass('opened');
$(this).unbind("mouseup touchend", butbindfunc);
};
$(document).not(button).bind( "mouseup touchend", butbindfunc);
});
Note, that I have removed your condition in the document binding callback, and simple excluded it from the select set.

Vanilla javascript Trap Focus in modal (accessibility tabbing )

This should be pretty simple but for some reason it isn't working, I'm getting the proper console.logs at the right time, but the focus isn't going to the correct place, please refer to my jsfiddle
https://jsfiddle.net/bqt0np9d/
function checkTabPress(e) {
"use strict";
// pick passed event of global event object
e = e || event;
if (e.keyCode === 9) {
if (e.shiftKey) {
console.log('back tab pressed');
firstItem.onblur=function(){
console.log('last a focus left');
lastItem.focus();
};
e.preventDefault();
}
console.log('tab pressed');
lastItem.onblur=function(){
console.log('last a focus left');
firstItem.focus();
};
e.preventDefault();
}
}
modal.addEventListener('keyup', checkTabPress);
I had to lock focus within a modal that we had used within a React component.
I added eventListner for KEY DOWN and collected Tab and Shift+Tab
class Modal extends Component {
componentDidMount() {
window.addEventListener("keyup", this.handleKeyUp, false);
window.addEventListener("keydown", this.handleKeyDown, false);
}
componentWillUnmount() {
window.removeEventListener("keyup", this.handleKeyUp, false);
window.removeEventListener("keydown", this.handleKeyDown, false);
}
handleKeyDown = (e) => {
//Fetch node list from which required elements could be grabbed as needed.
const modal = document.getElementById("modal_parent");
const tags = [...modal.querySelectorAll('select, input, textarea, button, a, li')].filter(e1 => window.getComputedStyle(e1).getPropertyValue('display') === 'block');
const focusable = modal.querySelectorAll('button, [href], input, select, textarea, li, a,[tabindex]:not([tabindex="-1"])');
const firstFocusable = focusable[0];
const lastFocusable = focusable[focusable.length - 1];
if (e.ctrlKey || e.altKey) {
return;
}
const keys = {
9: () => { //9 = TAB
if (e.shiftKey && e.target === firstFocusable) {
lastFocusable.focus();
}
if (e.target === lastFocusable) {
firstFocusable.focus();
}
}
};
if (keys[e.keyCode]) {
keys[e.keyCode]();
}
}
}
One of the problems is that you are using keyup instead of keydown. The keyup will only fire after the tab has already fired. However, making that change to your code results in the keyboard being trapped on one of the links. The code is flawed.
Here is some code that does what you want (using jQuery)
http://dylanb.github.io/javascripts/periodic-1.1.js
// Add keyboard handling for TAB circling
$modal.on('keydown', function (e) {
var cancel = false;
if (e.ctrlKey || e.metaKey || e.altKey) {
return;
}
switch(e.which) {
case 27: // ESC
$modal.hide();
lastfocus.focus();
cancel = true;
break;
case 9: // TAB
if (e.shiftKey) {
if (e.target === links[0]) {
links[links.length - 1].focus();
cancel = true;
}
} else {
if (e.target === links[links.length - 1]) {
links[0].focus();
cancel = true;
}
}
break;
}
if (cancel) {
e.preventDefault();
}
});
You can see a working version of this dialog here
http://dylanb.github.io/periodic-aria11-attributes.html
Click the text in one of the colored boxes to see the dialog pop up.
The e.preventDefault() has no effect on the keyup event (as the default browser action has already been fired)
Despite this, your example works. But only if there are links before and after the modal
If you change your HTML code with the following, adding one link before and one link after the modal; you will see that your focus is trapped in the modal:
other link
<div id="modal">
Link One
Link Two
</div>
other link
That's because there is no default browser action in such case, and then no action to prevent.
Trapping focus within a modal is very hard to do it on your own. If you're able to install third-party dependencies in your project, you can use the focus-trap package.
You can easily trap focus to any component with vanilla Javascript;
import { createFocusTrap } from 'focus-trap'
const modal = document.getElementById('modal')
const focusTrap = createFocusTrap('#modal', {
onActivate: function () {
modal.className = 'trap is-visible'
},
onDeactivate: function () {
modal.className = 'trap'
},
})
document.getElementById('show').addEventListener('click', function () {
focusTrap.activate()
})
document.getElementById('hide').addEventListener('click', function () {
focusTrap.deactivate()
})
or even React;
import React from 'react'
import ReactDOM from 'react-dom'
// Use the wrapper package of `focus-trap` to use with React.
import FocusTrap from 'focus-trap-react'
const Demo = () => {
const [showModal, setShowModal] = React.useState(false)
return (
<div>
<button onClick={() => setShowModal(true)}>show modal</button>
<FocusTrap active={showModal}>
<div id="modal">
Modal with with some{' '}
focusable elements.
<button onClick={() => setShowModal(false)}>
hide modal
</button>
</div>
</FocusTrap>
</div>
)
}
ReactDOM.render(<Demo />, document.getElementById('demo'))
I did a small write-up about the package here, which explains how to use it with either vanilla Javascript or React.
I thought I had solved trapping the focus on a modal by using tab, shift+tab, and arrow keys detection on keyup and keydown, focus, focusin, focusout on the first and last focusable elements inside the modal and a focus event for the window to set the focus back on the first focusable element on the form in case the focus "escaped" the modal or for situations like jumping from the address bar to the document using tab, but something weird happened. I had activated "Caret Browsing" in one of my browsers accidently, and that's when I realized all methods to trap focus failed miserably. I personally went on a rabbit whole to solve this for a modal. I tried focusin, focusout on the modal, matching focus-within pseudo classes, {capture: true} on the focus event from the modal and window, nothing worked.
This is how I solved it.
I recreated the modal to have a different structure. For the sake of simplicity, I am omitting a lot of things, like the aria attributes, classes, how to get all focusable elements, etc.
<component-name>
#shadow-root (closed)
<div class="wrapper">
<div class="backdrop"></div>
<div class="window>
<div tabindex="0" class="trap-focus-top"> </div>
<div class="content">
<div class="controls"><!-- Close button, whatever --></div>
<header><slot name="header"></slot></header>
<div class="body"><slot></slot></div>
<footer><slot name="footer"></slot></footer>
</div>
<div tabindex="0" class="trap-focus-bottom"> </div>
</div>
</div>
</component-name>
Search the contents div for focusable elements, to save the first and last one. If you find only one then that one will be first and last. If you find zero, then set the div for the body (.body) tabindex to "0" so that you have at least one element to set the focus on.
Before and after the content div we have two focusable divs, trap-focus-top and trap-focus-bottom, the first one when getting focus will jump the focus to the last focusable element detected on step one, and the second one will jump the focus to the first focusable element detected on step one. No need to capture any key events, just focus event on these elements. If you notice the non-breaking space on trap-focus elements, this is for mimicking content, because I noticed that the arrow keys went through these elements without firing any events when empty. When I realized this I added some content and everything worked, so I added a non-breaking space and styled the elements so that they do not occupy any space.
Capture all focus events from the window with the use capture flag set to true, so that every focus event whose target was different to the component (focus events inside the shadow-root wont't be captured with the actual target but the component itself) will result in the focus being set on the modal elements again.
Now there's another problem, let's say there's zero focusable elements on your modal outside of any controls, like a button to close the modal, then we set tabindex to 0 on the modal's body, your focus should go from the close button to the modal's body and vice versa, now, the caret browsing won't work on the content because the div.body will have the focus, not the actual content. This means I have to create another function that places the cursor at the beginning of the content whenever the body receives the focus.
startCursor = () => {
/* componentbody is a placeholder for the element with the actual content */
let text = componentbody.childNodes[0];
if (text) {
let range = new Range();
let selection = document.getSelection();
range.setStart(text, 0);
range.setEnd(text, 0);
selection.removeAllRanges();
selection.addRange(range);
componentbody.scrollTop = 0;/* In case the body has a scrollbar */
}
}
For anyone out there, this is what worked for me.

Determine if an element was clicked?

How can I determine if an element was clicked with jQuery?
Something like $('.ele').is(':clicked');
I am stuck with jQuery 1.7.2, and what I am trying to do is force the closing of some modal elements, when anywhere in the document is clicked and one is already open.
So I've got:
$('body').on('click', function () {
if ($('.calc-info').is(':visible') && !$('.mi').is(':clicked')) {
$('.calc-info').fadeOut('fast');
}
});
but it throws an error, because there is no :clicked expression
Notes
.calc-info is the modal
.mi is the button to click to open said modal
Do it the other way around, check if the clicked element (the event.target) matches a selector
$('body').on('click', function (event) {
if ( $(event.target).is('.ele') ) {
// do stuff
}
});
In your case I'd go with
$('body').on('click', function (event) {
if ( $('.calc-info').is(':visible') &&
!($(event.target).closest('.mi').length)
) {
$('.calc-info').fadeOut('fast');
}
});
Just check whether the clicked element is having the class that is to be excluded. and its just a kind of short circuit implementation. The following code will block the unnecessary dom traversal if user directly clicks on the target element.
Try this,
$('body').on('click', function (e) {
if ($('.calc-info').is(':visible')
&& !$(e.target).hasClass('mi')
&& !$(e.target).parents('.mi').length) {
$('.calc-info').fadeOut('fast');
}
});
Try something like this.
$('mi').on('click', function () {
$('mi').attr('rel',"clicked");
});
$('body').on('click', function () {
if ($('.calc-info').is(':visible') && !$('.mi').attr('rel')=='clicked') {
$('.calc-info').fadeOut('fast');
}
});`

X-Editable: stop propagation on "click to edit"

I have an editable element inside a div which itself is clickable. Whenever I click the x-editable anchor element, the click bubbles up the DOM and triggers a click on the parent div. How can I prevent that? I know it's possible to stop this with jQuery's stopPropagation() but where would I call this method?
Here's the JSFiddle with the problem: http://jsfiddle.net/4RZvV/ . To replicate click on the editable values and you'll see that the containing div will catch a click event. This also happens when I click anywhere on the x-editable popup and I'd like to prevent that as well.
EDIT after lightswitch05 answer
I have multiple dynamic DIVs which should be selectable so I couldn't use a global variable. I added an attribute to the .editable-click anchors which get's changed instead.
editable-active is used to know if the popup is open or not
editable-activateable is used instead to know if that .editable-click anchor should be treated like it is
$(document).on('shown', "a.editable-click[editable-activateable]", function(e, reason) {
return $(this).attr("editable-active", true);
});
$(document).on('hidden', "a.editable-click[editable-activateable]", function(e, reason) {
return $(this).removeAttr("editable-active");
});
The check is pretty much like you've described it
$(document).on("click", ".version", function() {
$this = $(this)
// Check that the xeditable popup is not open
if($this.find("a[editable-active]").length === 0) { // means that editable popup is not open so we can do the stuff
// ... do stuff ...
}
})
For the click on the links, simply catch the click event and stop it:
$("a.editable-click").click(function(e){
e.stopPropagation();
});
The clicks within X-editable are a bit trickier. One way is to save a flag on weather the X-editable window is open or not, and only take action if X-editable is closed
var editableActive = false;
$("a.editable-click").on('shown', function(e, reason) {
editableActive = true;
});
$("a.editable-click").on('hidden', function(e, reason) {
editableActive = false;
});
$("div.version").click(function(e) {
var $this;
$this = $(this);
if(editableActive === false){
if ($this.hasClass("selected")) {
$(this).removeClass("selected");
} else {
$(this).addClass("selected");
}
}
});
Fixed Fiddle
It's not pretty, but we solved this problem with something like:
$('.some-class').click(function(event) {
if(event.target.tagName === "A" || event.target.tagName === "INPUT" || event.target.tagName === "BUTTON"){
return;
}
We're still looking for a solution that doesn't require a specific list of tagNames that are okay to click on.

Categories

Resources