Determine if user clicked outside shadow dom - javascript

I'm trying to implement a dropdown which you can click outside to close. The dropdown is part of a custom date input and is encapsulated inside the input's shadow DOM.
I want to write something like:
window.addEventListener('mousedown', function (evt) {
if (!componentNode.contains(evt.target)) {
closeDropdown();
}
});
however, the event is retargeted, so evt.target is always the outside the element. There are multiple shadow boundaries that the event will cross before reaching the window, so there seems to be no way of actually knowing if the user clicked inside my component or not.
Note: I'm not using polymer anywhere -- I need an answer which applies to generic shadow DOM, not a polymer specific hack.

You can try using the path property of the event object. Haven't found a actual reference for it and MDN doesn't yet have a page for it. HTML5Rocks has a small section about it in there shadow dom tutorials though. As such I do not know the compatibility of this across browsers.
Found the W3 Spec about event paths, not sure if this is meant exactly for the Event.path property or not, but it is the closest reference I could find.
If anyone knows an actual spec reference to Event.path (if the linked spec page isn't already it) feel free to edit it in.
It holds the path the event went through. It will contain elements that are in a shadow dom. The first element in the list ( path[0] ) should be the element that was actually clicked on. Note you will need to call contains from the shadow dom reference, eg shadowRoot.contains(e.path[0]) or some sub element within your shadow dom.
Demo: Click menu to expand, clicking anywhere except on the menu items will close menu.
var host = document.querySelector('#host');
var root = host.createShadowRoot();
d = document.createElement("div");
d.id = "shadowdiv";
d.innerHTML = `
<div id="menu">
<div class="menu-item menu-toggle">Menu</div>
<div class="menu-item">Item 1</div>
<div class="menu-item">Item 2</div>
<div class="menu-item">Item 3</div>
</div>
<div id="other">Other shadow element</div>
`;
var menuToggle = d.querySelector(".menu-toggle");
var menu = d.querySelector("#menu");
menuToggle.addEventListener("click",function(e){
menu.classList.toggle("active");
});
root.appendChild(d)
//Use document instead of window
document.addEventListener("click",function(e){
if(!menu.contains(e.path[0])){
menu.classList.remove("active");
}
});
#host::shadow #menu{
height:24px;
width:150px;
transition:height 1s;
overflow:hidden;
background:black;
color:white;
}
#host::shadow #menu.active {
height:300px;
}
#host::shadow #menu .menu-item {
height:24px;
text-align:center;
line-height:24px;
}
#host::shadow #other {
position:absolute;
right:100px;
top:0px;
background:yellow;
width:100px;
height:32px;
font-size:12px;
padding:4px;
}
<div id="host"></div>

Can't comment because of reputation, but wanted to share how it should look using composedPath. See Determine if user clicked outside shadow dom
document.addEventListener("click",function(e){
if(!e.composedPath().includes(menu)){
menu.classList.remove("active");
}
});

The event.target of the shadowRoot would be the host element. To close a <select> element within shadowDOM if event.target is not host element you can use if (evt.target !== hostElement), then call .blur() on hostElement
var input = document.querySelector("input");
var shadow = input.createShadowRoot();
var template = document.querySelector("template");
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);
window.addEventListener("mousedown", function (evt) {
if (evt.target !== input) {
input.blur();
}
});
<input type="date" />
<template>
<select>
<option value="1999">1999</option>
<option value="2000">2000</option>
</select>
</template>

Another option is to check the event cursor offsets against the target element:
listener(event) {
const { top, right, bottom, left } = targetElement.getBoundingClientRect();
const { pageX, pageY } = event;
const isInside = pageX >= left && pageX <= right && pageY >= top && pageY <= bottom;
}

Related

How to focus a div to scroll on default keys? [duplicate]

Is it possible to focus on a <div> using JavaScript focus() function?
I have a <div> tag
<div id="tries">You have 3 tries left</div>
I am trying to focus on the above <div> using :
document.getElementById('tries').focus();
But it doesn't work. Could someone suggest something....?
Yes - this is possible. In order to do it, you need to assign a tabindex...
<div tabindex="0">Hello World</div>
A tabindex of 0 will put the tag "in the natural tab order of the page". A higher number will give it a specific order of priority, where 1 will be the first, 2 second and so on.
You can also give a tabindex of -1, which will make the div only focus-able by script, not the user.
document.getElementById('test').onclick = function () {
document.getElementById('scripted').focus();
};
div:focus {
background-color: Aqua;
}
<div>Element X (not focusable)</div>
<div tabindex="0">Element Y (user or script focusable)</div>
<div tabindex="-1" id="scripted">Element Z (script-only focusable)</div>
<div id="test">Set Focus To Element Z</div>
Obviously, it is a shame to have an element you can focus by script that you can't focus by other input method (especially if a user is keyboard only or similarly constrained). There are also a whole bunch of standard elements that are focusable by default and have semantic information baked in to assist users. Use this knowledge wisely.
window.location.hash = '#tries';
This will scroll to the element in question, essentially "focus"ing it.
document.getElementById('tries').scrollIntoView() works. This works better than window.location.hash when you have fixed positioning.
You can use tabindex
<div tabindex="-1" id="tries"></div>
The tabindex value can allow for some interesting behaviour.
If given a value of "-1", the element can't be tabbed to but focus
can be given to the element programmatically (using element.focus()).
If given a value of 0, the element can be focused via the keyboard
and falls into the tabbing flow of the document. Values greater than
0 create a priority level with 1 being the most important.
<div id="inner" tabindex="0">
this div can now have focus and receive keyboard events
</div>
document.getElementById('test').onclick = function () {
document.getElementById('scripted').focus();
};
div:focus {
background-color: Aqua;
}
<div>Element X (not focusable)</div>
<div tabindex="0">Element Y (user or script focusable)</div>
<div tabindex="-1" id="scripted">Element Z (script-only focusable)</div>
<div id="test">Set Focus To Element Z</div>
I wanted to suggest something like Michael Shimmin's but without hardcoding things like the element, or the CSS that is applied to it.
I'm only using jQuery for add/remove class, if you don't want to use jquery, you just need a replacement for add/removeClass
--Javascript
function highlight(el, durationMs) {
el = $(el);
el.addClass('highlighted');
setTimeout(function() {
el.removeClass('highlighted')
}, durationMs || 1000);
}
highlight(document.getElementById('tries'));
--CSS
#tries {
border: 1px solid gray;
}
#tries.highlighted {
border: 3px solid red;
}
To make the border flash you can do this:
function focusTries() {
document.getElementById('tries').style.border = 'solid 1px #ff0000;'
setTimeout ( clearBorder(), 1000 );
}
function clearBorder() {
document.getElementById('tries').style.border = '';
}
This will make the border solid red for 1 second then remove it again.

How to temporarily add an outline to :hover elements

I'm creating a bookmarklet that displays some information on the first element you click on after running the bookmarklet. I would love to have it so the element you are hovering over has an outline, but only before you click. After you have selected an element, the outline would no longer appear when hovering (except if it already did so before my bookmarklet).
To get the clicked element, this works fine for me:
function getClickedElement(e) {
document.removeEventListener("click", getClickedElement);
e.preventDefault();
clickedElement = e.target || e.srcElement;
// Some code that displays information on clickedElement...
}
document.addEventListener("click", getClickedElement);
But I don't know how to do the CSS. It would work like all elements gain this CSS:
:hover {
outline: 1px solid black;
}
while selecting an element, but that stops once an element has been selected. Hope that all made sense.
Small example with the principle explained!
If the user clicks on the element, add a specific class.
CSS Rule adds outline-border only if the element does not match the selector inside the :not pseudo class!
document.addEventListener('click', function(evt) {
var target = evt.target || evt.source;
if(!target.classList.contains('element')) return;
if(target.classList.contains('selected'))
target.classList.remove('selected');
else
target.classList.add('selected');
}, true);
div.element {
width:100px;
height:100px;
background-color:silver;
display:inline-block;
}
.element.selected {
background-color:black;
}
.element:not(.selected):hover {
outline: 1px solid red;
}
<div class="element"></div>
<div class="element"></div>
<div class="element"></div>

Swapping text equal in different divs and classes

I have several boxes of cards on one page, these boxes can come dynamically in different, not upper right corner has a text for the click to open the accordion type content, for each class I have to do an action as below, I think of something Regardless of the number of classes.
*new
I do not know how to explain it, I'll try a summary:
Change the text of only one div when clicking, because when I click on the item in the box it changes all the other texts of the
Other boxes.
$('.change-1').click(function () {
var $mudartxt = $('.mudartexto');
if ($mudartxt.text() == 'expandir')
$mudartxt.text('ocultar');
else {
$mudartxt.text('expandir');
}
});
You need to find the current clicked item.
For that you can use the event object
$('.change-1').click(function (e) {
// Get current target as jquery object
var $target = $(e.currentTarget);
// Find mudartexto in current target.
var $mudartxt = $target.find('.mudartexto');
if ($mudartxt.text() == 'expandir')
$mudartxt.text('ocultar');
else {
$mudartxt.text('expandir');
}
});
.change-1 {
display:inline-block;
width:200px;
height:50px;
text-align: center;
background-color:#dfdfdf;
clear: both;
float: left;
margin-top:10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="change-1">
<div class="mudartexto">expandir</div>
</div>
<div class="change-1">
<div class="mudartexto">expandir</div>
</div>
<div class="change-1">
<div class="mudartexto">expandir</div>
</div>
<div class="change-1">
<div class="mudartexto">expandir</div>
</div>
If you are asking how to change text of an element, inside a clicked box, this should do it.
$('.change-1').click(function () {
var $mudartxt = $(this).find('.mudartexto');
if ($mudartxt.text() == 'expandir')
$mudartxt.text('ocultar');
else {
$mudartxt.text('expandir');
}
});

How to prevent text selection from obstructing onMouseMove event?

I'm implementing a "resize handle" to change the width of my left navigation panel. It is a div that receives an onMouseDown() event, calculates the necessary widths and applies them to the right elements in the subsequent calls to onMouseMove(). But I'm having some problems.
1) The article element, to the right of the navigation panel and handle, does not activate the onMouseUp() if I release the mouse there. Is this because the onMouseDown() was activated in other element?
2) If I move the mouse fast to the right, I can't prevent the text in the article from being selected, even calling methods like preventDefault() and stopPropagation().
3) Even if there's no text in the article, the resizing only works if I move the mouse very slowly. If the mouse moves fast over the article element, the resize stops, unless I release the mouse button - in this case the resize goes smoothly (suggesting it was the text-selecting that was stopping the resize, even with no text at all). But if I release the mouse button, the resize should stop (see point 1).
I've seen some solutions using CSS user-select: none, but this would prevent any text from being selected inside article, which is obviously an overkill.
So, how can I make the resizing smooth, without selecting any text, when I move the mouse over any element in my document? (After pressing the button in the right div, of course.)
That's my HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>CSS Template</title>
</head>
<body>
<header>Header</header>
<main>
<nav id='nav'>
<div class='navcont' onmousemove='handMm(event)' onmouseup='handMu(event)'>
<p>nav 1</p>
<p>nav 2</p>
</div>
<div class='handle' onmousedown='handMd(event)' onmousemove='handMm(event)' onmouseup='handMu(event)'>
</div>
</nav>
<article id='article' onmousemove='handMm(event)' onmouseup='handMu(event)'>
</article>
</main>
<footer>Footer</footer>
</body>
</html>
That's my CSS:
html, body {
height:100%;
margin:0;
}
body {
display:flex;
flex-direction:column;
}
header {
text-align:center;
}
main {
flex:1;
display:flex;
min-height:0;
}
article {
background:#CCC;
width:auto;
overflow:auto;
padding:10px;
flex-grow:1;
}
nav {
width:300px;
height:auto;
overflow: hidden;
display:flex;
}
.navcont {
background:#8C8;
width:auto;
flex-grow:1;
}
.handle {
background:#333;
right:0px;
width:30px;
cursor:col-resize;
}
footer {
text-align:center;
}
That's my Javascript:
var mx,px,moving=false;
function handMd(e) {
mx = e.pageX;
px = document.getElementById('nav').clientWidth;
moving = true;
}
function handMm(e) {
if (moving) {
e.preventDefault();
e.stopPropagation();
e.cancelBubble = true;
e.returnValue = false;
var diff = e.pageX - mx;
document.getElementById('nav').style.width = (px + diff)+'px';
document.getElementById('article').style.width = (window.innerWidth-px-diff)+'px';
}
}
function handMu(e) {
moving = false;
}
And here is a working example: https://jsfiddle.net/45h7vq7u/
Another example, including Ryan Tsui's answer: https://jsfiddle.net/v1cmk2f6/1/ (the text-selection is gone, but the div still won't move smoothly, but only when moving fast to the right).
Catch the events of start moving and finish moving with your preferred method (onMouseDown and onMouseUp are fine). Add a CSS class to specify the moving state when the action starts. Remove the CSS class when the action finishes.
In your case, you may try the followings:
Add a new CSS class:
.moving {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
Extend your handMd() and handMu() functions:
function handMd(e) {
mx = e.pageX;
px = document.getElementById('nav').clientWidth;
moving = true;
document.getElementById('article').classList.toggle('moving', true); // Add this line. 'article' is the id of the element where you don't want the selection to occur.
}
function handMu(e) {
moving = false;
document.getElementById('article').classList.toggle('moving', false); // Add this line. 'article' is the id of the element where you don't want the selection to occur.
}
After analyzing how this was solved somewhere else, I came to the following changes.
HTML - only one event handler needed:
...
<main>
<nav id='nav'>
<div class='navcont'>
<p>nav 1</p>
<p>nav 2</p>
</div>
<div class='handle' onmousedown='handMd(event)'>
</div>
</nav>
<article id='article'>
</article>
</main>
...
Javascript - attach and detach the onmousemove event handler is way better than calling onmousemove every time the mouse moves (to only then test if the mouse button has been pressed). Also, the event is now attached to the document, not to each div on screen:
var mx,px,minW = 200;
function handMd(e) {
mx = e.pageX;
px = document.getElementById('nav').clientWidth;
document.addEventListener('mousemove',handMm);
document.addEventListener('mouseup',handMu);
document.getElementById('article').classList.toggle('moving',true);
document.getElementById('nav').classList.toggle('moving',true);
}
function handMm(e) {
var diff = e.pageX - mx;
if (px+diff >= minW && window.innerWidth-px-diff >= minW) {
document.getElementById('nav').style.width = (px+diff)+'px';
document.getElementById('article').style.width = (window.innerWidth-px-diff)+'px';
}
}
function handMu(e) {
document.removeEventListener('mousemove',handMm);
document.removeEventListener('mouseup',handMu);
document.getElementById('article').classList.toggle('moving',false);
document.getElementById('nav').classList.toggle('moving',false);
}
CSS - thanks to Ryan Tsui's answer, I eliminated the unwanted text selection while resizing:
.moving {
user-select:none;
-moz-user-select:none;
-webkit-user-select:none;
-ms-user-select:none;
}
I don't know how to stop the selection but I can tell you for some help that the event triggered when something is selected is onselect.

jQuery click event seems to be kind of late

I'm working on a Facebook reaction bar so it is pretty hard to copy the code here because it has a lot of events binded but all of you got facebook so if you want to check it by yourself - please do it.
The thing is that I managed to move the reaction bar under the react root and now I wanted to make the clicked reaction counter change the background color of itself to green.
And everything is working almost good excluding one thing: it is one click behind. To make you understand better I recorded little example how it looks. The red pulse ring appears when I click: https://vid.me/HqYp
Here is the changing code:
$(this).find('div._iu-[role="toolbar"]').bind('click',function(){
$(this).find('p.counter').each(function(){$(this).css('background-color','#48649F');});
$(this).find('span[aria-pressed="true"]').find('p.counter').css('background-color','green');
});
$(this) is div[id*="post"] so in $(this) I'm getting div with the whole post.
I thought that maybe I should use a callback function after changing-every-counter-to-default-color function but I don't know am I right and if it's right solution.
Thanks from above. (:
You can probably simplify this a bit. Although without the html structure I can't know for sure how the layout of the function works with respect to the event origin. Also I am not sure when the aria-pressed is set to true so I made the function a bit more generic. You simply add a data attribute to target the span you want to be targeted by the click.
<div class="_lu-" role="toolbar" data-target=".facebook-counter">
Later in your javascript you do the following
var $t = $(this);
var $t.target = $(this).data('target');
$t.on('click','div._lu-[role="toolbar"]', function() {
$t.find($t.target).css({
'background-color':'green'
}).siblings().css({'background-color','#48649F'});
});
This code is assuming first that your spans are in the same container, and second that the first $(this) refers to the parent container of this whole toolbar, and last that you have put data-target="" attributes with selectors for the appropriate target you want to affect.
This is a sample:
$(document).ready(function(){
$('.toolbar').on('click','.toolbar-item .icon', function(event) {
event.preventDefault();
event.stopPropagation();
if(!this.$) this.$ = $(this);
if(!this.parent) this.parent = this.$.parent();
if(!this.counter) this.counter = this.$.siblings('.counter');
this.parent.addClass('selected').siblings('.selected').removeClass('selected');
var count = this.counter.data('value');
count++;
this.counter.data('value',count);
this.counter.html(count);
});
});
.toolbar {
font-size:0;
text-align:center;
}
.toolbar-item .icon {
background:#FFF;
padding:30px;
border:1px solid #AAA;
border-radius:100%;
margin:0 20%;
transition:0.8s ease all;
}
.selected .icon {
background:#369;
}
.toolbar-item .counter {
background:#E0E0E0;
margin:0 10px;
transition:0.4s ease background;
}
.selected .counter {
background:#509050;
}
.toolbar-item {
font-size:10pt;
width:25%;
display:inline-block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div class="toolbar">
<div class="toolbar-item">
<div class="icon">Like</div>
<div class="counter" data-value="0">0</div>
</div>
<div class="toolbar-item">
<div class="icon">Wow</div>
<div class="counter" data-value="0">0</div>
</div>
<div class="toolbar-item">
<div class="icon">Sad</div>
<div class="counter" data-value="0">0</div>
</div>
<div class="toolbar-item">
<div class="icon">Angry</div>
<div class="counter" data-value="0">0</div>
</div>
</div>
As of jQuery 1.7 they introduced the .on('click', function().... method. Try that instead and see if you get the same results.
Quick answer without having tested or the time to test your code. I recently had a performance issue with a nested function, so maybe look at that second line with the .each() method.

Categories