can't get the click position of a div element - javascript

I want to create a minimap. So I have an accurate representation of div elements inside my minimap. I want the user to use the minimap to navigate around the site.
I get the correct position when I click inside my minimap (the gray box), but when I click on a "ghostly" or the green box, I get incorrect dimensions, which leads to an incorrect position setting.
here is a showcase:
function getClickPosition(e) {
// I need the click position of the gray box
// but when I click on the green or red box I get their values
console.log(e.layerX)
}
.minimap {
height: 100px;
width: 140px;
background-color: #999;
position: absolute;
z-index: 100;
}
.viewport-layer {
height: 20px;
width: 35px;
left: 20px;
top: 15px;
box-sizing: border-box;
position: absolute;
border: 2px solid green;
z-index: 101;
max-width: 100px;
}
.ghosty-box {
position: absolute;
top: 60px;
left: 90px;
height: 30px;
width: 40px;
background-color: red;
z-index: 0; // should be in the background
}
<div class="minimap" onclick="getClickPosition(event)">
<!-- user screen: green border -->
<div class="viewport-layer">
</div>
<!-- dynamic size of the minimap -->
<div class="relativeLayer">
<!-- representation of the visible elements -->
<div class="ghosty-box"></div>
</div>
</div>
my console of e says something like:
click { target: div.ghosty-box, ... layerX: 10, layerY: 11 }
or
click { target: div.viewportLayer, ... layerX: 33, layerY: 16 }
I was expecting that a z-index would help.
Do you have any suggestions to get the click position of the .minimap or .relativeLayer with elements behind it?
So the target is always the gray box?

I think you want relative values of clicked elements inside of minimap. This will give you the relative X and Y of the clicked item - as well as the relative mouseX and mouseY (along with the relative percentage position)
function getClickPosition(e) {
// I need the click position of the gray box
// but when I click on the green or red box I get their values
if (e.target.classList.contains('minimap')) {
console.log('clicked on minimap background');
return;
}
let ref = e.target.closest('.minimap').getBoundingClientRect()
let pos = e.target.getBoundingClientRect();
let posY = pos.top - ref.top
let posX = pos.left - ref.left
let mouseY = e.clientY - ref.top
let mouseYPerc = ((mouseY / ref.height) * 100).toFixed(2);
let mouseX = e.clientX - ref.top
let mouseXPerc = ((mouseX / ref.width) * 100).toFixed(2)
console.log('my relative position X:' + posX + ' Y:' + posY);
console.log("relative mouseX:" + mouseX + " (" + mouseXPerc + "%) horiz");
console.log("relative mouseY:" + mouseY + " (" + mouseYPerc + "%) vert");
}
.minimap {
height: 100px;
width: 140px;
background-color: #999;
position: absolute;
z-index: 100;
margin: 50px;
opacity: .2;
}
.viewport-layer {
height: 20px;
width: 35px;
left: 20px;
top: 15px;
box-sizing: border-box;
position: absolute;
border: 2px solid green;
z-index: 101;
max-width: 100px;
}
.ghosty-box {
position: absolute;
top: 60px;
left: 90px;
height: 30px;
width: 40px;
background-color: red;
z-index: 0; // should be in the background
}
<div class="minimap" onclick="getClickPosition(event)">
<!-- user screen: green border -->
<div class="viewport-layer">
</div>
<!-- dynamic size of the minimap -->
<div class="relativeLayer">
<!-- representation of the visible elements -->
<div class="ghosty-box"></div>
</div>
</div>

I'm not absolutely sure what readings you want out of the various layers, but a couple of comments:
According to MDN:
This feature is non-standard and is not on a standards track. Do not use it on production sites facing the Web: it will not work for every user. There may also be large incompatibilities between implementations and the behavior may change in the future.
If you want the position relative to the gray box, as in your example, you may want to look at event.pageX or event.clientX. As in this snippet:
function getClickPosition(e) {
// I need the click position of the gray box
// but when I click on the green or red box I get their values
console.log('pageX = ' + e.pageX);
console.log('clientX = ' + e.clientX);
}
.minimap {
height: 100px;
width: 140px;
background-color: #999;
position: absolute;
z-index: 100;
}
.viewport-layer {
height: 20px;
width: 35px;
left: 20px;
top: 15px;
box-sizing: border-box;
position: absolute;
border: 2px solid green;
z-index: 101;
max-width: 100px;
}
.ghosty-box {
position: absolute;
top: 60px;
left: 90px;
height: 30px;
width: 40px;
background-color: red;
z-index: 0; // should be in the background
}
<div class="minimap" onclick="getClickPosition(event)">
<!-- user screen: green border -->
<div class="viewport-layer">
</div>
<!-- dynamic size of the minimap -->
<div class="relativeLayer">
<!-- representation of the visible elements -->
<div class="ghosty-box"></div>
</div>
</div>
Alternatively, when you process the click event you may want to check which actual element has been clicked when you have nested elements and/or you may or may not want to stop propagation. It's worth looking at an event object console.log(e) for example to see what the target is and the other settings you are given to get the right ones.

if you don't really need to click the elements inside a good solution could be placing an absolute postioned empty layer covering the whole minimap just to capture the clicks. I added it to your snippet with the class .position-layer
function getClickPosition(e) {
// I need the click position of the gray box
// but when I click on the green or red box I get their values
console.log(e.layerX);
console.log(e.layerY);
}
.minimap {
height: 100px;
width: 140px;
background-color: #999;
position: absolute;
z-index: 100;
}
.viewport-layer {
height: 20px;
width: 35px;
left: 20px;
top: 15px;
box-sizing: border-box;
position: absolute;
border: 2px solid green;
z-index: 101;
max-width: 100px;
}
.ghosty-box {
position: absolute;
top: 60px;
left: 90px;
height: 30px;
width: 40px;
background-color: red;
z-index: 0; // should be in the background
}
.position-layer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index:1000;
}
<div class="minimap">
<!-- user screen: green border -->
<div class="viewport-layer">
</div>
<!-- dynamic size of the minimap -->
<div class="relativeLayer">
<!-- representation of the visible elements -->
<div class="ghosty-box"></div>
</div>
<div class="position-layer" onclick="getClickPosition(event)"></div>
</div>

MDN says (I'm paraphrasing) that layerX is position of the mouse cursor relative to the clicked element or one of it's parents that is absolutely positioned element
Your ghosty-box is position: absolute, which means that is it's clicked, the layerX is relative to it.
If you could position it relatively or using margins, that would solve the issue.
Other option is using pageX or screenX and computing the offset yourself, or positioning an overlay element over the minimap and catching the click on that.

Related

Safari selects wrong drag element when elements use transforms instead of left and top

When I have 3 draggable divs using position: absolute and positioning them using left and top the items are correctly draggable on Safari (and other browsers).
But when I set left and top to 0 and instead position them using transform.translateX and transform.translateY and try to drag them, Safari always selects the first item regardless of the mouse position.
The HTML code below shows this problem. Initially the items are positioned using left and top and you can drag any of the divs. When you click the button it moves them all to 0 (left and top) and repositions them using transform.translateX and transform.translateY so visually it looks the same, but when you try to drag them things get strange. It appears it's always selecting the topmost item at the origin (regardless of the cursor position) and it clips some of the content.
Is there a workaround for this, or can items not be dragged on Safari when they have transformations applied?
document.querySelector("button").addEventListener("click", (e) => {
document.querySelectorAll("div[draggable").forEach(elem => {
const style = getComputedStyle(elem);
const left = style.getPropertyValue("left");
const top = style.getPropertyValue("top");
elem.style.left = 0;
elem.style.top = 0;
elem.style.transform = `translateX(${left}) translateY(${top})`;
});
document.querySelector("p:nth-of-type(1)").style.display = "none";
document.querySelector("p:nth-of-type(2)").style.display = "";
e.target.style.display = "none";
});
body {
background-color: #444;
width: 720px;
height: 720px;
}
div[draggable] {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
color: #000;
border-radius: 16px;
width: 200px;
height: 200px;
}
div[draggable]:nth-child(1) {
background-color: #f80;
left: 20px;
top: 50px;
}
div[draggable]:nth-child(2) {
background-color: #8f0;
left: 240px;
top: 50px;
}
div[draggable]:nth-child(3) {
background-color: #08f;
left: 460px;
top: 50px;
}
button {
position: absolute;
left: 20px;
top: 10px;
padding: 4px 12px;
}
p {
color: white;
position: absolute;
left: 200px;
top: 0;
}
<div draggable="true">Item 1</div>
<div draggable="true">Item 2</div>
<div draggable="true">Item 3</div>
<p>Items use 'position: absolute' and are positioned using 'left' and 'top' properties.</p>
<p style="display: none">Items use 'position: absolute' and are positioned using 'transform.translateX' and 'transform.translateY' properties.</p>
<button>Position -> Transform</button>

Save position of the dragged widget

Using jquery ui's draggable widget to move an image up/down to show the desired part of the image inside the container (when image is too large to fit inside container). The Image can be dragged up/down to display the ideal part inside the container (initially the center region of image is visible inside the container). The position value is then saved for future reloads.
When image is dragged a yellow button appears at the bottom showing the current position set by function dragCover(). This is the value that is stored for future. For testing, when you click on that button, it updates the position of the image by setting the css top prop. If value is correct, image should not move because the value would be equal to the current position, since dragCover is setting an incorrect value, image moves away from its current position. Try dragging the image all the way down and then clicking on yellow box, it moves back up.
https://codepen.io/apprence/pen/KKVwKBN
It's a little unclear what you're going for in the end, in your click callback. I created the following pseudo view port type of example to see if this is the type of function / result you're looking to get.
$(function() {
function calcPerc(el, vp) {
el = $(el), vp = $(vp);
var y1 = el.position().top;
var h = el.height();
return parseFloat(((y1 / h) * 100).toFixed(2));
}
function showData(t, p) {
$(".top").html(t);
$(".perc").html(p);
}
$(".item").draggable({
scroll: false,
axis: "y",
cursor: "move",
containment: [0, -899, 0, 1],
drag: function(e, ui) {
showData(ui.position.top, calcPerc(this, $(".viewport")));
}
});
$(".perc").click(function() {
var offsetVal = parseFloat($(this).text()) * 10;
console.log(offsetVal);
$(".item").css({
top: offsetVal + "%"
});
});
$(".buttons button").click(function() {
$(".item").css("top", $(this).html());
showData($(".item").position().top, calcPerc($(".item"), $(".viewport")));
});
});
.viewport {
position: absolute;
border: 1px dashed #222;
top: 0;
}
.small {
width: 200px;
height: 100px;
}
.tall {
width: 201px;
height: 1000px;
}
.item {
background-color: #ccf;
position: absolute;
top: -299px;
}
.item span {
padding-bottom: 80px;
display: block;
text-align: center;
border-bottom: 1px solid #222;
}
.show {
position: absolute;
border: 1px solid #000;
border-radius: 3px;
width: 3em;
padding: 3px;
}
.show.buttons {
width: 100px;
}
.top {
top: 20px;
left: 220px;
}
.perc {
top: 50px;
left: 220px;
}
.buttons {
top: 80px;
left: 220px;
}
.buttons button {
width: 100%;
}
.ghost {
width: 201px;
background-color: #fff;
opacity: 0.75;
height: 900px;
position: absolute;
}
.after {
top: 101px;
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<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>
<div class="container">
<div class="small viewport">
<div class="tall item">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</div>
</div>
<div class="ghost after"></div>
<div class="show top">-299</div>
<div class="show perc">-29.9</div>
<div class="show buttons">
<button>0%</button>
<button>-25%</button>
<button>-500%</button>
<button>-75%</button>
<button>-900%</button>
</div>
</div>
https://developer.mozilla.org/en-US/docs/Web/CSS/top
When position is set to absolute or fixed, the top property specifies the distance between the element's top edge and the top edge of its containing block.
A <percentage> of the containing block's height.
So, for my example, the containing block is 100px tall. Therefore, a top set to -25%, we have to calculate that:
top = 100 * -.25
top = -25
Therefore, we need to increase this exponentially. To get the proper location, -29.12%, needs to be -291.2%.
top = 100 * -2.912
top = -291.2
This should help you with your image movement and view port. Personally, I would store and reload the exact pixel value, but if you desire to store the percentage, you can do so.

MouseEvent.clientX and MouseEvent.clientY Unexpected Results With CSS Scale Transform

$('body').on('mousemove', function (ev) {
$('span').text(`x: ${ev.clientX}, y: ${ev.clientY}`)
})
body {
padding: 0;
margin: 0;
}
div {
display: inline-block;
width: 100px;
height: 100px;
background: #333;
transform: scale(2, 2);
}
span {
position: absolute;
left: 300px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="my-div"></div>
<span id="my-span"></span>
I was just wondering why the scaled div returns x and y MouseEvent coordinates in the range from 0 to 150 instead of from 0 to 200? The scale property is set to 2, so I thought it would be the second range instead of the first. Could someone explain? Here's a link to the js fiddle page.
I noticed a lot of similar questions on Stackoverflow, so this might be a duplicate. However, I couldn't find anything that specifically asked this question about pixels, coordinates, and the scale transformation in CSS. I may have missed something, though...
Thanks!
because transform-origin is center by default so half the div is outside the screen from the top/left.
Either update the transform-origin:
$('body').on('mousemove', function (ev) {
$('span').text(`x: ${ev.clientX}, y: ${ev.clientY}`)
})
body {
padding: 0;
margin: 0;
}
div {
display: inline-block;
width: 100px;
height: 100px;
background: #333;
transform: scale(2, 2);
transform-origin:0 0;
}
span {
position: absolute;
left: 300px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="my-div"></div>
<span id="my-span"></span>
Or add some margin:
$('body').on('mousemove', function (ev) {
$('span').text(`x: ${ev.clientX}, y: ${ev.clientY}`)
})
body {
padding: 0;
margin: 0;
}
div {
display: inline-block;
width: 100px;
height: 100px;
background: #333;
transform: scale(2, 2);
margin:50px;
}
span {
position: absolute;
left: 300px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="my-div"></div>
<span id="my-span"></span>
The div is scaled relative to its center, so part of it ends up being off screen. (One way to notice this: add a border to the div and see that it doesn't go all the way around.)
Try using transform-origin: top left; on the div - I think that will do what you expect.

How can I get mouse coordinates relative to the page rather than relative to the screen in Javascript?

I want to get the location of the cursor relative to the page of my website rather than relative to the screen its being shown on
<script>
function show_coords(event) {
document.getElementById("sidebar").innerHTML = "X= " + event.clientX + "<br>Y= " + event.clientY;
}
</script>
<body>
<style>
body {
margin: 0;
}
.main {
padding: 0px;
width: 100%;
height: 100vh;
overflow: auto;
cursor: move;
}
.main img {
height: auto;
width: 100%;
}
#sidebar {
position: fixed;
display: block;
width: 150px;
height: 100%;
top: 15%;
left: 0%;
font-size: 100%;
z-index: 3;
}
</style>
<div class="main dragscroll" ondblclick="show_coords(event)">
<img id="map" src="https://cdn.glitch.com/f6ccc989-c4e6-4876-925d-
5c75e6d3cf19%2FRed-Dead-Redemption-2-Full-World-Map.png?1547325193157"/>
</div>
<div id="sidebar"></div>
</body>
V more details in this image V
You can try pageX which is relative to the top left of the fully rendered content area in the browser.
Also screenX is another option
The difference between the two is that screenX consider the top left of the physical monitor where as pageX is relative to the left edge of the entire document

Put overlay on document with transparent window

I would like to do something with my document which is quite unique (haven't seen it before) and thus maybe not even possible.
What I would like is to have a div which will overlay everything in the document, maybe give it background black so that nothing is visible. Second I would like to have a small squire window in the overlay which doesn't share the black background, in fact it is somewhat transparent and therefore it would be possible to 'peek' trough that window to see document content. But only the content where this window is. It would be kinda like those "zoom" plugins in which only a small portion is being zoomed, but in this case it would show specific content. Any idea how to create such a thing?
An example of what you can do is the following (it may not be the best but it works)
HTML
<div id='peakview'></div> <!-- This div is your view window -->
<div id='out'>
<div class='overlay'></div>
<div class='overlay'></div>
<div class='overlay'></div>
<div class='overlay'></div>
</div>
The <div> inside of #out will re-size accordingly to the position of #peakview creating the illusion of a full overlay. This can be done with simple css and some calculus.
Mainly what you need is the position of the element on screen.
var h = $(this).offset().top;
var l = $(this).offset().left;
var r = ($(window).width() - ($(this).offset().left + $(this).outerWidth()));
//right offset
var b = ($(window).height() - ($(this).offset().top + $(this).outerWidth()));
//bottom offset
In my example I used .draggable() from jQuery UI to move the div around. And while dragging the 4 divs shown above are adjusting their height and width to fill up the space between #peakview and document border.
An example for the first div
$('.overlay:eq(0)').css({
top: 0,
left: 0,
width: '100%',
height: h //the height is always changing depending on the #peakview .offset().top
});
In this fiddle you will see how the filling divs behave
Another ruff start:
http://jsfiddle.net/XDrSA/
This require some extra work, but it may suit your needs.
HTML:
<div id="yourContent" style="width: 300px; margin:100px auto;">
<input type="button" id="zoom" value="Click to zoom"/>
</div>
<div id="zoomer">
<div id="window">This is your "window"</div>
<div id="overlay_top"></div>
<div id="overlay_left"></div>
<div id="overlay_right"></div>
<div id="overlay_bottom"></div>
</div>
CSS:
body {
margin:0;
padding:0;
}
#zoomer {
height: 100%;
width: 100%;
position: absolute;
top: 0;
display: none;
}
#overlay_top {
height: 20%;
width: 100%;
background-color: black;
position: absolute;
top: 0
}
#overlay_right {
height: 100%;
width: 20%;
background-color: black;
position: absolute;
right: 0;
}
#overlay_left {
height: 100%;
width: 20%;
background-color: black;
position: absolute;
left: 0;
}
#overlay_bottom {
height: 20%;
width: 100%;
background-color: black;
position: absolute;
bottom: 0;
}
#window {
margin: 0 auto;
height: 100%;
width: 80%;
position: absolute;
background-color: rgba(0,0,0,.5);
}
And a piece of javascript:
$('#zoom').click(function() {
$('#zoomer').fadeIn();
});
You may need to stumble with the positioning, and the window will be a fixed size one. Not draggable though.

Categories