Using jQuery .on() when selector element is obscured by another element - javascript

I'm trying to get my jQuery event callback to trigger correctly, but I can't seem to get around the fact the element I am interested in not receiving the event because of another element that covers it on the page. I can summarise it as follows (I've styled the elements so they show up in a jsfiddle):
<div id='mydiv'>
<div style="border: 1px solid; border-color: red; position: absolute; left: 0px; top: 0px; width: 200px; height: 200px; z-index: 100">Hello</div>
<canvas style="border: 1px solid; border-color: yellow; position: absolute; left: 50px; top: 50px; width: 150px; height: 150px"></canvas>
</div>​
With the segment above, if I try to listen to mouse clicks on the <canvas>, the event never gets called:
$('#mydiv').on('mousedown', 'canvas', this, function(ev) {
console.log(ev.target);
});
However, if I modify my event handler to listen to the <div> element instead, the callback is triggered as expected:
$('#mydiv').on('mousedown', 'div', this, function(ev) {
console.log(ev.target);
});
How can I coerce my <canvas> to receive events, whilst leaving the offending <div> block in the fore-front?

This should be the simplest solution, as already proposed:
http://jsfiddle.net/CUJ68/4/
$('canvas').on('mousedown', function(ev) {
console.log(ev.target);
});
$('ul').on('mousedown', function(ev){
$('canvas').mousedown();
});
if you need the original eventdata:
$('canvas').bind('mousedown', function(ev, parentEV) {
if(parentEV){
console.log(parentEV);
alert("Canvas INdirectly clicked!");
}else{
console.log(ev);
alert("Canvas directly clicked!");
}
});
$('ul').on('mousedown', function(ev){
$('canvas').trigger('mousedown', ev);
});

You can't. Objects on top get the click events. That's how the DOM works.
If you want to handle that click event, you will need to handle in the object that is on top or use bubbling and handle it in a parent object. You can handle it in the top object and "forward" it to the other object if you want by triggering a click on that other object or by just calling a click handler directly.
Or, you can move the canvas element above the ul by setting it's z-index to a higher value and it will then get the click event.
Or, you can make a new transparent canvas object that is on top that gets the event, leaving the other two objects where they are for the desired visual effect.

You can bind the element to the closest common parent, and check whether the X and Y coordinates of the mouse are within the range of the canvas.
In the example below, I have cached the dimensions (height and width) of the canvas, because I assume these to be constant. Move this inside the function if the dimensions are not constant.
I use the .offset() method to calculate the real X and Y coordinates of the <canvas>s top-left corner. I calculate the coordinates of the bottom-right corner by adding the values of outerWidth() and .outerHeight().
Basic demo: http://jsfiddle.net/75qbX/2/
var $canvas = $('canvas'), /* jQuery reference to the <canvas> */
$canvasWidth = $canvas.outerWidth(), /* assuming height and width to be constant */
$canvasHeight = $canvas.outerHeight();
function isCanvasClicked(x, y, target) {
if (target.tagName === 'CANVAS') return true;
var offset = $canvas.offset(),
left = offset.left,
top = offset.top;
return x >= left && x <= left + $canvasWidth &&
y >= top && y <= top + $canvasHeight;
}
$('#mydiv').on('mousedown', '*', this, function(ev) {
if (isCanvasClicked(ev.pageX, ev.pageY, ev.target)) {
$canvas.fadeOut().fadeIn();
}
});

Here you have a solution that consists in capture click event on above element, and triggering the event on the other: registering clicks on an element that is under another element

Related

How to trigger events in Javascript based on mouse click location parameters?

I am trying to create a drawing app with HTML canvas and javascript. But I would like images to be shown based on where the user draws on the canvas. For example if the user draws in the area above the head, a picture of a hat is shown.
My solution so far has been a div which is placed where I want the parameters to be that hides on mouseclick and after a delay (hopefully long enough for user to finish drawing) shows a image in another div.
$(document).ready(function(){
$(".hat").click(function(){
$("#hathide").delay( 3000 ).fadeIn( 200 );
});
});
$(document).ready(function(){
$(".hat").click(function(){
$(".hat").hide(0);
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<main class="main-content">
<div class="left">
<div class="grid-container">
<div><img id="hathide" src="img/hat.gif" style="display:none"></div>
</div>
</div>
<div class="right">
<div class="hat">draw a hat</div>
<div class="drawingcanvas"><div>
<div class="image-container">
<img src="img/person.jpg">
</div>
</div>
This doesn't suit my needs because this means the user needs to first click on the div to hide it and then draw on the canvas. I would like it so that the user draws directly on the canvas and for javascript to detect whether the drawing was done within the certain parameters to trigger the image to show.
I've tried to style the div with pointer-events:none; so the user can directly draw on the canvas but that meant the div wouldn't be targetable and wouldn't hide.
Additionally, this method doesn't truly listen for the user's input it just seems like it does. I would want it so an event is called when the user mouse is pressed within the parameters and the mouse is released. Ideally, it would be independent of the javascript code used for drawing so there would be no interference?
I am not too familiar with JS or jQuery enough to write my own syntax but I would think something along the lines of
$canvas.mouseup(
If 100 < page.x < 400, and 100 < page.y < 400: then show div;
Else none
)
Sorry, that is not real code.
I've been looking around this website and trying to find alternative solutions but I can't seem to figure it out on my own.
Any help and solutions would be greatly appreciated. It doesn't necessarily have to be Javascript/jQuery. Thank you!
The logic to determine weather you are in a certain area of the canvas or not is fairly simple. The method getMousePos() returns the x and y position of the mouse, relative to the top left corner of the target element of the given Event.
/**
* Get the mouse position relative to the top left corner
* of the target element of the given event
*/
function getMousePos( e ) {
let $el = $(e.target);
let elX = e.pageX - $el.offset().left;
let elY = e.pageY - $el.offset().top;
return {
x: elX,
y: elY
};
}
function inTarget( mousePos ) {
// consider using a map to use multiple target areas
return mousePos.x > 100 && mousePos.x < 200 && mousePos.y > 100 && mousePos.y < 200
}
function doSomething() {
$('#hat').show();
}
function doSomethingElse() {
$('#hat').hide();
}
$('canvas').on('mousedown', function() {
// enable draw mode
$(this).data('drawMode', true);
});
$('body').on('mouseup', function() {
$('canvas').data('drawMode', false);
})
$('canvas').on('mousemove', function( e ) {
if(!$(this).data('drawMode')) {
return;
}
let mousePos = getMousePos( e );
if( inTarget( mousePos ) ) {
doSomething();
}else{
doSomethingElse();
}
});
canvas {
border: 1px solid #CECECE;
background-color: #F1F1F1;
}
#hat {
position: absolute;
width: 100px;
height: 100px;
background: red;
top: 100px;
left: 100px;
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="hat"></div>
<canvas width="300" height="300">
</canvas>
<br/>
<span>Click and move the mouse to draw</span>

Minimizing the number of event listeners to achieve a "Draggable" behaviour

Objective
Trying to replicate jQueryUI's .draggable behaviour.
$(function() {
$(".box").draggable();
});
.box {
width: 100px;
height: 100px;
background: pink;
}
<head>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
</head>
<body>
<div class="box"></div>
</body>
A not some much working solution
I have created a function draggable that does that. It takes in as argument an object of type HTMLElement: the element that needs to be draggable. The function need to follow these two points:
the function can be called on multiple elements, so that more that one element on the page can be dragged (only one element can be dragged at the time like jQueryUI does it)
the draggable elements can be dragged wherever the user wants it to move on the page (more precisely in the parent container).
I have started writing the function, it works as follows:
it adds an onmousedown event listener on the element
when the event is triggered the distance (on x and y) from the mouse position to the corner of the dragged element is saved inside relativeX and relativeY. Also the state of dragging is changed from false to true.
it adds an onmousemove event listener on document
The event keeps getting triggered when the user's mouse moves on the page. This allows to check if the state of dragging is true. When the state is indeed true, it means the user is dragging the element. Therefore the position of the element needs to be updated. I use the absolute positioning to get these positions right taking in account the relative positions (relativeX and relativeY saved earlier).
it adds both onmouseleave and onmouseup on document to end the behaviour when needed:
when the user stops dragging the element (LEFT Mouse Button up) or leaves the document the function changes the state of dragging back to false. So the onmousemove listener will keep listening but won't update the position until dragging is set back to true again.
Here's the code to better illustrate:
const relativePos = (e, p) => {
let rect = e.target.getBoundingClientRect()
return [e.clientX - rect.x, e.clientY - rect.y];
}
const draggable = draggedE => {
let isDragging, relativeX, relativeY;
// checking if the user is dragging
document.onmousemove = (event) => {
if (isDragging) {
draggedE.style.left = `${event.clientX - relativeX}px`
draggedE.style.top = `${event.clientY - relativeY}px`
}
}
// when the user initiate the dragging behaviour
draggedE.onmousedown = e => {
[isDragging, relativeX, relativeY] = [true, ...relativePos(e)]
}
// listener to end the behaviour
document.onmouseup = () => isDragging = false
document.onmouseleave = () => isDragging = false
}
draggable(document.querySelector('.box'))
.box {
width: 100px;
height: 100px;
background: pink;
position: absolute;
}
<div class="box"></div>
Problems with the event listeners
The code works perfectly. But here are my concerns:
I am using lots of draggable elements. For each one of them there is an event attached to the document to check for the state of dragging. This doesn't seem very good for performance.
Also if you look closely at the animation below (using jQueryUI's .draggable method):
You can see I'm able to get out of the page without the behaviour getting stopped: I can drag elements outside of the page. I found that I can only achieve such result by attaching my listeners to the document. Attaching it to the parent element or even the body won't work.
My question
How can I achieve the result described above without using that many event handlers (on the parent and worse on document) while still ensuring the same behaviour?
[EDIT] Possible solution
Teemu suggested that:
I create an object draggable containing all methods
Add a unique mousedown event handler on document
Here's how it looks like:
const draggable = {
startDrag: function(event) {
this.element = event.target
const rect = event.target.getBoundingClientRect();
this.offset = [event.clientX - rect.x, event.clientY - rect.y];
},
dragging: function(e) {
this.element.style.left = `${e.clientX - this.offset[0]}px`;
this.element.style.top = `${e.clientY - this.offset[1]}px`;
},
endDrag: function() {},
element: '',
offset: []
};
document.addEventListener('mousedown', e => {
if (e.target.classList.contains('draggable')) {
draggable.startDrag(e)
const _mousemove = draggable.dragging.bind(draggable)
document.addEventListener('mousemove', _mousemove);
document.addEventListener('mouseup', function _mouseup(event) {
document.removeEventListener('mousemove', _mousemove)
document.removeEventListener('mouseup', _mouseup)
});
}
});
.box {
width: 100px;
height: 100px;
background: pink;
position: absolute;
user-select: none;
}
.box:nth-child(1) {
background: lightblue;
top: 125px;
left: 125px;
}
.box:nth-child(2) {
background: lightgreen;
top: 250px;
left: 250px;
}
<div class="draggable box"></div>
<div class="draggable box"></div>
<div class="draggable box"></div>
Having to name the functions used by the handlers in order to later delete them troubles me. Is there no other way of going about deleting the events without necessarely calling bind?
Thanks to Teemu I have found a solution to my problem. The idea is to run a unique event listener when the user not dragging. When he does drag an element, other event listeners will be added and removed as soon as the user ends the dragging process either with mouseup or mouseleave.
draggable is an object containing all functions: startDrag, dragging and endDrag and information: element and offset regarding the dragging mechanism.
In order to remove the event listeners, I needed to save my event functions in variables.
Furthermore, I had to use bind() in order to have access to draggable's properties inside the different functions. So I came up with the property events which is filled with draggable's functions bound to draggable...
So I came up with:
(function() {
this.events = {
_mousemove: this.dragging.bind(this),
_mouseup: this.endDrag.bind(this)
}
}.bind(draggable))()
Instead of writing:
const _mousemove = draggable.dragging.bind(draggable)
const _mouseup = draggable.endDrag.bind(draggable)
So now I can stop the events linked to _mousemove and _mouseup inside endDrag by accessing them with this.events._mousemove and this.events._mouseup.
Now the event listeners can be added with:
document.addEventListener('mousemove', draggable.events._mousemove);
document.addEventListener('mouseup', draggable.events._mouseup);
Here is the full JavaScript code:
const draggable = {
events: {},
startDrag: function(event) {
this.element = event.target
const rect = event.target.getBoundingClientRect();
this.offset = [event.clientX - rect.x, event.clientY - rect.y];
},
dragging: function(e) {
this.element.style.left = `${e.clientX - this.offset[0]}px`;
this.element.style.top = `${e.clientY - this.offset[1]}px`;
},
endDrag: function() {
document.removeEventListener('mousemove', this.events._mousemove)
document.removeEventListener('mouseup', this.events._mouseup)
},
element: '',
offset: []
};
document.addEventListener('mousedown', e => {
(function() {
this.events = {
_mousemove: this.dragging.bind(this),
_mouseup: this.endDrag.bind(this)
}
}.bind(draggable))()
if (e.target.classList.contains('draggable')) {
draggable.startDrag(e)
document.addEventListener('mousemove', draggable.events._mousemove);
document.addEventListener('mouseup', draggable.events._mouseup);
}
});
.box {
width: 100px;
height: 100px;
background: pink;
position: absolute;
user-select: none;
}
.box:nth-child(1) {
background: lightblue;
top: 125px;
left: 125px;
}
.box:nth-child(2) {
background: lightgreen;
top: 250px;
left: 250px;
}
<div class="draggable box"></div>
<div class="draggable box"></div>
<div class="draggable box"></div>

Jquery set div at mouse position

I want to have an div under my mouse at all times, this div I want to use to display tool-tips.
This is the code i'm trying to use. But this code gives an error:
" Cannot read property 'pageX' of undefined"
My question is why is pageX undefined, and how do I fix this problem?
$( document ).ready(function() {
AppentMouse(); //Setup div that is used for mouse icon
window.setInterval(function(){
SetMouse(); //Set div that is used for mouse icon at mouse location
}, 5);
});
function AppentMouse(){
$( 'Body' ).append( "<div id='Mouse'>Mouse</div>");
}
function SetMouse(){
var e = window.event || e;
var left = e.pageX + "px";
var top = e.clientY + "px";
var div = document.getElementById('Mouse');
div.style.left = left;
div.style.top = top;
}
Considering this is your html code:
<body>
<div>Your content</div>
</body>
And you have these styles for the div:
div {
position: absolute;
border: solid 1px #fc0;
}
Using jQuery, attach mousemove event listener to the document and make the div to have top and left styles changed on every move:
$(document).on('mousemove', function(e){
$('div').css('top', e.pageY);
$('div').css('left', e.pageX);
});
See this JSFiddle
EDIT:
Considering your code, variable e is undefined. And the error says that an undefined value does not have a pageX property.
That is because you need an mouse event (for your case) to have object event defined. And that object is received by the event listener that we add in the code that I provided.
As for your code, you will have to bind the event to the div.
An easy way to do this would be to not dynamically generate the div, just show and hide it. (As in my example). This is faster as well.
Alternatively, each time you generate the div, define and trigger the set mouse event from within the function that generates it.
Providing an alternate way of doing this:
Firstly, the HTML. Add the following anywhere inside the body.
<div id="tooltip"></div>
Now the CSS (add more to make it look pretty):
#tooltip {
position: absolute;
display: inline-block;
opacity: 0;
}
Make a class called tips and have all elements that you wish to provide tool tips for, belong to that class.
And then the jQuery:
//For every element in ".tips" have an attribute "tooltiptext"
$('.tips').mouseenter(function(e) {
$("#tooltip").css("left", e.pageX + 10);
$("#tooltip").css("top", e.pageY+ 10);
$("#tooltip").html($(this).attr("tooltiptext"));
$("#tooltop").show();
});
$('.tips').mouseout(function() {
$("#tooltip").hide();
});
Do tell me if this works.

how to create a popup next to the mouse according to a container div

I'm trying to display a div when mouse is over an element but the div is always inside the container div. Example (hover over any model at the bottom of this page) https://3dexport.com/
I've tried to get mouse position in the page and the mouse position inside the div but didn't work. Thanks in advance.
This is the main code I've used to display and hide a big div but the hidden process is not working though the alert is displayed (the black div is hidden by default)
$(".homeModelItem").mouseenter(function(){
var mouse = {x: 0, y: 0};
document.addEventListener('mousemove', function(e){
mouse.x = e.clientX || e.pageX;
mouse.y = e.clientY || e.pageY;
console.log(mouse.x);
if(mouse.x<400 && mouse.x>0){
$(".black").css({"left":"200px","display":"block"});
}
});
});
$(".homeModelItem").mouseout(function(){
alert("xxx");
$(".black").css({"display":"none","left":"0"});
});
You're adding a new mousemove listener every time the mouse enters a .homeModelItem. In that handler you set display: block for .black, and this will override the hiding in mouseleave handler.
It looks like you want to position .black related to the currently hovered .homeModelItem. You can do it for example like this:
$(".homeModelItem").mouseenter(function (e) {
$(e.target).append($('.black')); // Move the .black to the target element
$(".black").css({
display: "block"
});
});
$(".homeModelItem").mouseleave(function (e) {
$(".black").css({
display: "none"
});
});
Addition to .homeModelItem CSS:
position: relative;
and to .black CSS:
left: 100px;
top: 0px;
z-index: 100;
A live demo at jsFiddle.
If you'll need the mousemove somewhere, you can add it, but outside of any event handler (unles you will remove it in another handler).

How to propagate a click to all divs under cursor?

I have a bunch of divs positioned absolutely on top of each other. When I bind a click event to all of them, only the top div responds. How can I send the event to all divs under the cursor?
Taking FelixKling's suggestion to use document.elementFromPoint() and Amberlamps's fiddle, and employing jQuery for the DOM interactions, I ended up with the following :
$divs = $("div").on('click.passThrough', function (e, ee) {
var $el = $(this).hide();
try {
console.log($el.text());//or console.log(...) or whatever
ee = ee || {
pageX: e.pageX,
pageY: e.pageY
};
var next = document.elementFromPoint(ee.pageX, ee.pageY);
next = (next.nodeType == 3) ? next.parentNode : next //Opera
$(next).trigger('click.passThrough', ee);
} catch (err) {
console.log("click.passThrough failed: " + err.message);
} finally {
$el.show();
}
});
DEMO
try/catch/finally is used to ensure elements are shown again, even if an error occurs.
Two mechanisms allow the click event to be passed through or not :
attaching the handler to only selected elements (standard jQuery).
namespacing the click event, click.passThrough analogous to event.stopPropagation().
Separately or in combination, these mechanisms offer some flexibility in controlling the attachment and propagation of "passThrough" behaviour. For example, in the DEMO, try removing class p from the "b" element and see how the propagation behaviour has changed.
As it stands, the code needs to be edited to get different application-level behaviour. A more generalized solution would :
allow for programmatic attachment of app-specific behaviour
allow for programmatic inhibition of "passThrough" propagation, analogous to event.stopPropagation().
Both of these ambitions might be achieved by establishing a clickPassthrough event in jQuery, with underlying "passThrough" behaviour, but more work would be involved to achieve that. Maybe someone would like to have a go.
This is not as easy as you might think. This is a solution that I came up with. I only tested it in Chrome and I did not use any framework.
The following snippet is just for add a click event to every div in the document, that outputs its class name when triggered.
var divs = document.getElementsByTagName("div");
for(var i = 0; i < divs.length; i++) {
divs[i].onclick = function() {
console.log("class clicked: " + this.className);
};
}
Attaching a click event to the body element so that every single click event is noticed by our script.
if(document.addEventListener) {
document.body.addEventListener("click", countDivs);
} else if(document.attachEvent) {
document.attachEvent("onclick", countDivs);
}
Iterate through all divs that you want to check (you might want to adjust here to your preferred range of divs). Generate their computed style and check whether the mouse coordinates are within the range of the div´s position plus its width and height. Do not trigger click event when the div is our source element because the click event has already been fired by then.
function countDivs(e) {
e = e || window.event;
for(var i = 0; i < divs.length; i++) {
var cStyle = window.getComputedStyle(divs[i]);
if(divs[i] !== e.target && e.pageX >= parseInt(cStyle.left) && e.pageX <= (parseInt(cStyle.left) + parseInt(cStyle.width)) && e.pageY >= parseInt(cStyle.top) && e.pageY <= (parseInt(cStyle.top) + parseInt(cStyle.height))) {
divs[i].click();
}
}
}
CSS:
.a, .b, .c {
position: absolute;
height: 100px;
width: 100px;
border: 1px #000 solid
}
.a {
top: 100px;
left: 100px;
}
.b {
top: 120px;
left: 120px;
}
.c {
top: 140px;
left: 140px;
}
HTML:
<div class="a"></div>
<div class="b"></div>
<div class="c"></div>
I also added a jsFiddle
A simple way could be to use elementFromPoint():
http://jsfiddle.net/SpUeN/1/
var clicks = 0,cursorPosition={};
$('div').click(function (e) {
if(typeof cursorPosition.X === 'undefined') {
cursorPosition.X = e.pageX;
cursorPosition.Y = e.pageY;
}
clicks++;
e.stopPropagation();
$(this).addClass('hided');
var underELEM = document.elementFromPoint(cursorPosition.X, cursorPosition.Y);
if (underELEM.nodeName.toUpperCase() === "DIV") $(underELEM).click();
else {
$('#clicks').html("Clicks: " + clicks);
$('.hided').removeClass('hided');
clicks=0;
cursorPosition = {};
}
});
If you are stacking elements absolutely it may be simpler to stack them all in a positioned container, and handle the events from this parent. You can then manipulate its children without having to measure anything.

Categories

Resources