In my html5 app, I do a lot of dynamic dom element creation/manipulation. In certain cases, I need to verify whether an element (e.g. a div) can be "clickable" by the user. "Clickable" means that both of the following conditions are met:
display
and visibility
properties of the element and all of its parents)I can use pure JS or jQuery. With jQuery it's easy to check the first part (i.e using .is(':visible')
. Yet, if I have an element, which is obscured by another element, this still returns true
.
How can I check whether the element is truly clickable?
The easiest way to access a single element in the DOM is by its unique ID. You can get an element by ID with the getElementById() method of the document object. In the Console, get the element and assign it to the demoId variable. Logging demoId to the console will return our entire HTML element.
To check if an element is connected or attached to the DOM or the document object ( or otherwise called the context ), you can use the isConnected property in the element's object in JavaScript. The isConnected element property returns boolean true if it connected to the DOM ( document object) and false if not.
The easiest way to find an HTML element in the DOM, is by using the element id.
To check if an element was clicked, add a click event listener to the element, e.g. button. addEventListener('click', function handleClick() {}) . The click event is dispatched every time the element is clicked.
This uses standard video-game style collision testing to determine whether or not an item takes up the full space that another item takes up. I won't bother explaining that part, you can see the other answer.
The hard part for me in figuring this out was trying to get the z-index of each element to determine if an element is actually on top of or underneath another element. First we check for a defined z-index, and if none is set we check the parent element until we get to the document. If we get all the way up to the document without having found a defined z-index, we know whichever item was rendered first (markup is higher in the document) will be underneath.
I've implemented this as a jQuery pluin.. $("#myElement").isClickable()
$.fn.isClickable = function() {
if (!this.length) return false;
const getZIndex = e => {
if (e === window || e === document) return 0;
var z = document.defaultView.getComputedStyle(e).getPropertyValue('z-index');
if (isNaN(z)) return getZIndex(e.parentNode);
else return z;
};
var width = this.width(),
height = this.height(),
offset = this.offset(),
zIndex = getZIndex(this[0]),
clickable = true,
target = this[0],
targetIsBefore = false;
$("body *").each(function() {
if (this === target) targetIsBefore = true;
if (!$(this).is(":visible") || this === target) return;
var e_width = $(this).width(),
e_height = $(this).height(),
e_offset = $(this).offset(),
e_zIndex = getZIndex(this),
leftOfTarget = offset.left >= e_offset.left,
rightOfTarget = width + offset.left <= e_width + e_offset.left,
belowTarget = offset.top >= e_offset.top,
aboveTarget = height + offset.top <= e_height + e_offset.top,
behindTarget = e_zIndex === zIndex ? targetIsBefore : e_zIndex > zIndex;
if (leftOfTarget && rightOfTarget && belowTarget && aboveTarget && behindTarget) clickable = false;
});
return clickable;
};
$(".clickme").click(function() {
alert("u clicked " + this.id)
});
$(".clickme").each(function() {
console.log("#"+this.id, $(this).isClickable() ? "is clickable" : "is NOT clickable");
})
#item1 {
background: rgba(230, 30, 43, 0.3);
position: absolute;
top: 3px;
left: 4px;
width: 205px;
height: 250px;
}
#item2 {
background: rgba(30, 250, 43, 0.3);
position: absolute;
top: 100px;
left: 50px;
width: 148px;
height: 50px;
}
#item3 {
background: rgba(30, 25, 110, 0.3);
position: absolute;
top: 23px;
left: 101px;
width: 32px;
height: 100px;
}
#item4 {
background: rgba(159, 25, 110, 0.3);
position: absolute;
top: 10px;
left: 45px;
width: 23px;
height: 45px;
z-index: -111
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="item1" class='clickme'></div>
<div id="item2" class='clickme'></div>
<div id="item3" class='clickme'></div>
<div id="item4" class='clickme'></div>
The following is a really rough implementation - it uses the document.elementFromPoint(x, y)
method and does a broad scan of each element's position to see if the element is clickable.
To keep it simple, and more performant, it surveys each element's position in 50-pixel grids. For example, if an element was 100x100 pixels, it would make 9 checks (0 0, 50 0, 100 0, 0 50, 50 50, 100 50, 0 100, 50 100, and 100 100). This value could be tweaked for a more detailed scan.
Another factor that you might want to account for, how much of an element is clickable. For example, if a 1 pixel line of the element is visible, is it really clickable? Some additional checks would need to be added to account for these scenarios.
In the following demo there are 5 squares - red, green, blue, yellow, cyan, black, and gray. The cyan element is hidden beneath the yellow element. The black element is beneath the gray element, but uses z-index
to display it above. So every element, except cyan and gray, will show as clickable.
Note: green shows as not clickable because it's hidden behind the console logs (I believe)
Here's the demo:
// Create an array of the 5 blocks
const blocks = Array.from(document.querySelectorAll(".el"));
// Loop through the blocks
blocks.forEach(block => {
// Get the block position
const blockPos = block.getBoundingClientRect();
let clickable = false;
// Cycle through every 50-pixels in the X and Y directions
// testing if the element is clickable
for (var x = blockPos.left; x <= blockPos.right; x+=50) {
for (var y = blockPos.top; y <= blockPos.bottom; y+=50) {
// If clickable, log it
if (block == document.elementFromPoint(x, y)) {
console.log('clickable - ', block.classList[1])
clickable = true;
break;
}
}
if (clickable) {
break;
}
}
if (!clickable) {
console.log('not clickable - ', block.classList[1]);
}
});
.el {
position: absolute;
width: 100px;
height: 100px;
}
.red {
top: 25px;
left: 25px;
background-color: red;
}
.green {
top: 150px;
left: 25px;
background-color: green;
}
.blue {
top: 75px;
left: 75px;
background-color: blue;
}
.yellow {
top: 50px;
left: 200px;
background-color: yellow;
}
.cyan {
top: 50px;
left: 200px;
background-color: cyan;
}
.black {
top: 25px;
left: 325px;
z-index: 10;
background-color: black;
}
.gray {
top: 25px;
left: 325px;
z-index: 1;
background-color: gray;
}
<div class="el red"></div>
<div class="el green"></div>
<div class="el blue"></div>
<div class="el cyan"></div>
<div class="el yellow"></div>
<div class="el black"></div>
<div class="el gray"></div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With