There is no other bullet-proof way, no.
To give one case that would discard any other method, think of the CSS clip-path rule, which is able to dig real holes inside an element through which click events would go.
If this clip-path is defined by an external SVG, it may be impossible for your script to read its path definition and thus to compute where the hole is.
div {
position: absolute;
width: 300px;
height: 150px;
}
.target {
background: red;
cursor: pointer;
left: 50px;
top: 25px;
}
.over {
background: green;
--rect-size: 75px;
clip-path: polygon( evenodd,
/* outer rect */
0 0, /* top - left */
100% 0, /* top - right */
100% 100%, /* bottom - right */
0% 100%, /* bottom - left */
0 0, /* and top - left again */
/* do the same with inner rect */
calc(50% - var(--rect-size) / 2) calc(50% - var(--rect-size) / 2),
calc(50% + var(--rect-size) / 2) calc(50% - var(--rect-size) / 2),
calc(50% + var(--rect-size) / 2) calc(50% + var(--rect-size) / 2),
calc(50% - var(--rect-size) / 2) calc(50% + var(--rect-size) / 2),
calc(50% - var(--rect-size) / 2) calc(50% - var(--rect-size) / 2)
);
}
<div class="target"></div>
<div class="over"></div>
However you could improve a bit your algorithm.
First, check that the element you are trying to click is in the viewport, if it isn't, elementFromPoint()
won't find it, and I doubt your click method will be able to reach it either.
Then, instead of linearly walking over all the coordinates in the element, it should be more performant in most of the cases to search randomly in these coordinates. The assumption here is that if an element is over a pixel, there are big chances it will also cover its neighbors. So better search in an other place altogether.
A final point which reaches back the first one is to not check all the coordinates of the target element, but only the ones visible in the viewport, to do this, we can simply calculate the intersection rectangle between our element's BBox and the viewport's.
document.querySelectorAll( "div:not(.cursor)" ).forEach( (el) => {
el.style.cssText = `
width: ${ (Math.random() * 100) + 50 }px;
height: ${ (Math.random() * 100) + 50 }px;
left: ${ (Math.random() * 250) }px;
top: ${ (Math.random() * 250) }px;
`;
} );
const target = document.querySelector(".target");
target.onclick = (evt) => {
console.log( "clicked" );
}
const point = findClickPosition( target );
if( !point ) {
console.log( "no clickable point found" );
}
else {
const cursor = document.querySelector(".cursor");
cursor.style.cssText = `
left: ${ point.x }px;
top: ${ point.y }px;
`;
}
function findClickPosition( el ) {
let i = 0; // for metrics only
const el_rect = el.getBoundingClientRect();
const win_rect = { right: window.innerWidth, bottom: window.innerHeight };
const { top, left, width, height } = intersectRect( el_rect, win_rect );
if( width <= 0 || height <= 0 ) {
console.log( "early exit" ); // for metrics only
return null; // out of screen
}
// build an Array of all the points
const coords = Array.from( { length: width * height }, (_,i) => {
const float = i / width;
const y = Math.floor( float );
const x = Math.round( (float - y) * width );
return { x: x + left | 0, y: y + top | 0 };
} );
while( coords.length ) {
// grab a random point in the list
const [{ x, y }] = coords.splice( Math.random() * coords.length, 1 );
i++; // for metrics only
if (el === document.elementFromPoint(x, y)) {
console.log( "looped", i ); // for metrics only
return { x, y };
}
}
console.log( "looped", i ); // for metrics only
return null;
}
function intersectRect( rect1, rect2 ) {
const top = Math.max( rect1.top || 0, rect2.top || 0 );
const left = Math.max( rect1.left || 0, rect2.left || 0 );
const bottom = Math.min( rect1.bottom || 0, rect2.bottom || 0 );
const right = Math.min( rect1.right || 0, rect2.right || 0 );
const width = right - left;
const height = bottom - top;
return { width, height, top, left, bottom, right };
}
.viewport > div {
position: absolute;
background: rgba(0,255,0,.2);
}
.viewport > .target { background: rgba(255,0,0,1); }
.viewport > .cursor {
pointer-events: none;
background: transparent;
border: 1px solid;
width: 10px;
height: 10px;
transform: translate(-5px, -5px);
border-radius: 50%;
}
/* stacksnippet's console */
.as-console {
pointer-events: none;
opacity: 0.5;
}
<div class="viewport">
<div class="target"></div>
<div class="over"></div>
<div class="over"></div>
<div class="over"></div>
<div class="over"></div>
<div class="over"></div>
<div class="over"></div>
<div class="over"></div>
<div class="over"></div>
<div class="over"></div>
<div class="cursor"></div>
</div>
We could probably still further improve it by using a smarter algorithm than randomness, but I'll leave it as an exercise for whoever wants to do so.
Now, I feel obliged to note that the fact you have to resort on this sort of checks is a code smell. In your position, I would triple check the reasons that made me believe I needed this.