How to make cursor tooltip not go offscreen - javascript

I'm working on a popup box that shows some text some position away from an element when the cursor hovers over it. You can see a simplified demo here: https://jsfiddle.net/xsvm2Lur/61/
Right now the popup box will squash up when it is near the bounding box. I want the popup to appear to the bottom-left (if the popup will overflow to the right) or top-left (if the popup will overflow to the bottom and to the right) of the hovered element's position if the popup will overflow.
The position and text that will be shown are dynamically generated (e.g. I don't know before rendering).
Right now I have a working version using Javascript. The way I go about it is:
Get the text that will be displayed. Count the number of characters that is going to be displayed x 0.25em for each character to get the width of the text.
Calculate the width of the displayed string + padding (left and right). Let's call it textLength. This will be set as the popup's width so all the text goes into 1 line.
If textLength + x position of the cursor > box width, "invert" the popup box on the x-axis by deducting the popup's "left" value by textLength and some distance away from the element.
Repeat the same check for the y position, i.e. if cursor position + line height (1em) + bottom padding > box height, invert y.
The solution works, but I'm wondering if there's a better way to do this without character counting, or if there is another way to do it elegantly, maybe CSS only without Javascript?

Sadly, I don't believe there is a way to do it with CSS only. However, by working on your fiddle, I've managed to add the functionality you wanted.
The way I went about it was just to include a reference to the container and check whether the popup position and size were inside the container BoundingClientRect.
This is the updated code for the popupShow function:
const showPopup = (top, left, text, container) => {
popup.textContent = text;
const containerBCR = container.getBoundingClientRect();
const popupBCR = popup.getBoundingClientRect();
const popupWidth = popupBCR.width,
popupHeight = popupBCR.height;
let popupTop = top + 20,
popupLeft = left + 20,
newPopupWidth;
console.log("height: ", popupHeight);
console.log("top: ", top);
console.log("bottomPopup: ", top + 20 + popupHeight);
console.log("bottomBoundary", containerBCR.bottom);
if (left + 20 + popupWidth > containerBCR.right) {
popupLeft = left - popupWidth;
if (popupLeft < containerBCR.left) {
popupLeft = containerBCR.left;
newPopupWidth = left - containerBCR.left;
}
}
if (top + 20 + popupHeight > containerBCR.bottom) {
popupTop = top - popupHeight;
if (popupTop < containerBCR.top) {
popupTop = containerBCR.top;
}
}
popup.style.top = popupTop + "px";
popup.style.left = popupLeft + "px";
popup.style.width = newPopupWidth;
popup.style.visibility = 'visible';
}
As you can see, I've also edited the popup to use "visibility: hidden" instead of "display: none". This is because if the display is set to "none", we won't be able to get his size (there might be workarounds for this, though).
Try checking out the updated fiddle and tell me what you think.
I've pushed one circle a little bit further down because the code doesn't currently check for the padding of the popup, so it was overflowing a little (few pixels).

This is based on quadrants, simple calculates if we are over 50% width and/or height and swaps the style to use the right or bottom instead. This doesn't care about the content of the popup, no measuring required.
const popup = document.getElementById("pop-up")
const parsePx = (px) => parseFloat(px.slice(0, -2))
const showPopup = (text, position) => {
popup.textContent = text;
popup.style.top = position.top;
popup.style.left = position.left;
popup.style.right = position.right;
popup.style.bottom = position.bottom;
popup.style.display = 'inline-block';
}
const hidePopup = () => {
popup.style.display = 'none';
}
const circles = document.querySelectorAll(".red-circle")
circles.forEach(el => el.addEventListener('mouseover', (e) => {
const hoveredEl = e.target;
const textContent = hoveredEl.getAttribute('data-content');
//get absolute position of elements
let elBounds = hoveredEl.getBoundingClientRect();
//get absolute position of parent;
let ctBounds = popup.parentElement.getBoundingClientRect();
//calculate relative positions
let left = elBounds.left - ctBounds.left + (elBounds.width / 2),
top = elBounds.top - ctBounds.top + (elBounds.height / 2),
width = ctBounds.width,
height = ctBounds.height
//prepare position settings
let position = { left: "auto", top: "auto", bottom: "auto", right: "auto" }
//calculate if we're over 50% of box size
if(top>ctBounds.height/2) position.bottom = ctBounds.height - top + 20 + 'px'; else position.top = top + 20 + 'px';
if(left>ctBounds.width/2) position.right = ctBounds.width - left + 20 + 'px'; else position.left = left + 20 + 'px';
showPopup(textContent, position);
}))
circles.forEach(el => el.addEventListener('mouseout', (e) => { hidePopup() }))
.container { width: 200px; height: 200px; border: 1px solid black; position: relative;}
.red-circle { border-radius: 50%; background: red; width: 20px; height: 20px; position: absolute;}
#pop-up { background-color: #EFEFEF; padding: 0.25em; position: absolute;}
<div class="container">
<div style="top:20px;left:20px;" class="red-circle" data-content="This is a red circle"></div>
<div style="top:10px;left:150px;" class="red-circle" data-content="This is the top-right red circle"></div>
<div style="top:140px;left:150px;" class="red-circle" data-content="This is the bottom-right red circle"></div>
<div style="top:140px;left:15px;" class="red-circle" data-content="This is the bottom-left red circle"></div>
<span style="display:hidden" id="pop-up"></span>
</div>

Related

Animations in Javascript

I'm working on my Javascript project.
In this project I have to create some animations.
In this specific case, I have to make a ball bounce up and down.
The code below works great just from the top to the bottom, but not viceversa.
var div = document.getElementById('container-ball');
function createBall(event)
{
var x = event.clientX;
var y = event.clientY;
var newBall = document.createElement('div');
newBall.style.position = "absolute"
newBall.style.width = '15px';
newBall.style.height = '15px';
var bx = newBall.style.left = x + 'px';
var by = newBall.style.top = y + 'px';
newBall.style.borderRadius = '10px';
newBall.style.backgroundColor = 'white';
var incrementPos = 0;
var id = setInterval(bounce, 5);
function bounce()
{
incrementPos++;
by = newBall.style.top = incrementPos + y + "px";
if(by == 650 + "px")
{
clearInterval(id)
var id2 = setInterval(function bounceUp()
{
incrementPosYMax -= 650
by = newBall.style.bottom = by + "px" - incrementPosYMax
}, 5)
}`/*Function that make the ball bounce down and up(but when it came at 650 px it stopped )*/ì
} /*End of the set interval */`
div.appendChild(newBall);
}
div.addEventListener("click", createBall);
This down below is the HTML CODE
<html>
<head>
<link rel= "stylesheet" href="style/style.css">
</head>
<body>
<div id ="container-ball">
</div>
<script src="js/main.js"></script>
</body>
Working example (comments see below):
const areaHeight = 150; // it is 650px in the original question
var div = document.getElementById('container-ball');
function createBall(event) {
var x = event.clientX;
var y = event.clientY;
var newBall = document.createElement('div');
newBall.className = 'ball';
var bx = newBall.style.left = x + 'px';
var by = newBall.style.top = y + 'px';
var incrementPos = 0;
var id = setInterval(bounce, 5);
let direction = 1; // 1 = down, -1 = up
function bounce() {
incrementPos += direction;
by = newBall.style.top = incrementPos + y + "px";
if (by == areaHeight + "px" || by == y + 'px') {
direction = -direction;
}
}
div.appendChild(newBall);
}
#container-ball {
width: 300px;
height: 157px;
background: gray;
}
#container-ball .ball {
position: absolute;
width: 15px;
height: 15px;
border-radius: 10px;
background-color: white;
border: 1px solid #333;
box-sizing: border-box;
}
<div id="container-ball" onclick="createBall(event)"></div>
Click on the grey box
Now, the explanation.
I've moved ball's styles to CSS - this is easier to control in the future. So when I have a class for ball, I can write in my code: newBall.className = 'ball';
I removed incrementPosYMax because I do not really understand if you need it
I understand your 'bounce' as bounce, so my ball just fall to the floor and then return to the original position. I do not really understand if you mean that (please, comment if it is wrong).
Your program is quite small, so I do not see the need for another setInterval, so all the animation in my example is inside only one setInterval
I've added new variable direction to control the direction for the ball (1 = down, -1 = up)
I do not like the parts with by == areaHeight + "px", but I keep them for you, because you use it in your code.
This code have some bugs, that you (or me if you ask) can fix. I just need to understand that my approach is correct
How the direction works:
Take a look at this line by = newBall.style.top = incrementPos + y + "px"; here you set new "y" coordinate for the ball as sum of 'original' "y" coordinate (in y) and offset (the distance that ball moved over the time) in incrementPos. So, if you increase incrementPos, then the ball's position will be lower (because "zero" in browser is at the top left corner, bigger "y" means lower the element).
Before my change, in your code you changed the offset with this line: incrementPos++; (means you increase incrementPos by 1 on every bounce step).
To move to another direction, you need to subtract 1 on every bounce step.
To reflect the "direction" of that move, I've added direction variable (so 1 means move down, and -1 means move up)
Now the offset is changed by: incrementPos += direction; (so I add this direction, not always 1)
Sometimes we need to change the "direction" with this code: direction = -direction;
The "levels" where we need to change direction is checked by this code: if (by == areaHeight + "px" || by == y + 'px') - here we check bottom (areaHeight) and top (y - it is where user clicks the mouse)

Detect how much browser changed in width on resize and update position of element

There is one div element positioned absolutely in a relative container. It is initially positioned under one of four buttons. Based on which button is clicked, I calculate with JS the x coordinate of the button and move the div element underneath that button. Here's my markup and JS:
<div id='container-of-four-buttons' style="width: 100%; position: relative;">
<button>Foo</button>
<button>Buzz</button>
<button>Foo</button>
<button>Buzz</button>
</div>
<div id='followAlong-container' style="width: 100%; position: relative;">
<div class='followAlong-div' style="position:absolute; width: 15px; height: 13px;">Some stylized arrow</div>
</div>
var initialDiv = /* selected the second button to be initial */
var followAlongDiv = document.querySelector('followAlong-div');
var buttons = document.querySelectorAll('button');
followAlongDiv.style.left = initialDiv + 89 + 'px';
buttons.forEach(function(btn) {
btn.addEventListener('click', function() {
let xCoord = 0;
xCoord += (div.offsetLeft - div.scrollLeft + div.clientLeft);
followAlongDiv.style.left = xPos + 17 + 'px';
});
});
However, if I resize the browser, the four button elements shrink towards eachother, but the followAlongDiv stays at the same spot because it is positioned absolutely in its relative container.
How do I calculate the amount of width the browser has resized and update the position of the followAlongDiv?
I will use window.addEventListener('resize', updatePosition), but I just don't know how to approach the formulation of the updatePosition function.
First, 'followAlong-div' is a class (needs '.' before the className ):
var followAlongDiv = document.querySelector('.followAlong-div');
Second, I do not understand why you have this line:
xCoord += (div.offsetLeft - div.scrollLeft + div.clientLeft);
For resizing just needs:
window.onresize = ()=>{
var xCoord = 0;
xCoord += (selectedButton.offsetLeft);
followAlongDiv.style.left = xCoord + 'px';
}
Based on your code, I think this is what you want: https://jsfiddle.net/vua4eLhc/
Hope it helps!

jquery get width of div to left side window

And I want to get the width or distance or pixel that between the div and left side of of window/viewport.
And another width again between the div to the right side of the window.
I will use the width to create a left line and right line.
But I am poor in jQuery, I try offset but seems nothing happen.
So I back to 0 again so I didn't include fiddle here since I got nothing inside.
But I have attached with the image link as below, to explain my question.
Please help me on try to get the width, I can create the line myself.
Thank you.
var of = $(ele).offset(), // this will return the left and top
left = of.left, // this will return left
right = $(window).width() - left - $(ele).width() // you can get right by calculate
Maybe this can help you.
After all, .width() isn't the only answer, like innerWidth() or outerWidth()
There is two options
One is you can use red line as image and you can place the div over the red line.
Second one,
If you want to calculate:
Left div width = parent div width - child div offset;
Right div width = parent div width - child div offset + child div width;
var parentdiv = document.getElementById("ParentDivID");
var parentWidth = parentdiv.offsetWidth;
var childdiv = document.getElementById("childDivID");
var childWidth = childdiv.offsetLeft;
This is easier to do with POJ (plain old javascript). Get the position of the element on the screen. Then evaluate its left property. That will be the width of your left line. Then subtract its right property from the width of the screen. That will be the width of your right line.
var x = document.getElementById('myDiv').getBoundingClientRect();
var myLeftLineWidth = x.left;
var myRightLineWidth = screen.width - x.right;
For more information see this post.
If you want the width of the window instead of the screen, change screen.width to window.innerWidth. If you don't want the scrollbar, etc. to be included in the width, use document.documentElement.clientWidth. (For more info on these, see this.)
We can work out that where the box starts with .offset().
Next, we can work out where the box ends with .offset() + .width().
We now know where our box sits on the x-axis.
Now let's see what we have to the left of our box with .left which can run on our .offset().
We've now worked out how much space there is to the left and how wide our box is.
Finally, we can put what we've worked out together, we can get the page width $(window).width() and minus what there is to the left of our box (stage 2) and the width of our box (stage 1) and that will only leave what is to the right of our box.
That's the theory anyway now let's have a look at some code. You'll see I'm working out all the bits from the theory and then adding some visual representation.
calcSizes = function() {
var boxPos = $(".box").offset(),
toLeft = boxPos.left,
toRight = $(window).width() - ($(".box").width() + toLeft);
$(".left").width(toLeft + "px");
$(".right").width(toRight + "px");
console.log("Right: " + toRight + "px");
console.log("Left: " + toLeft + "px");
console.log("Width: " + $(".box").width() + "px");
console.log(
$(window).width() + "px = " +
toRight + "px + " +
toLeft + "px + " +
$(".box").width() + "px"
);
console.log(" ");
}
calcSizes();
body {
margin: 0
}
.box,
.indicator {
padding: 10px 0;
text-align: center
}
.box {
width: 100px;
background: #FF5722;
margin-left: 60%
}
.indicator {
background: repeating-linear-gradient( 45deg, #F44336, #F44336 10px, #D32F2F 10px, #D32F2F 20px);
overflow: hidden;
transform: translatey(-100%);
opacity: .8
}
.left {
float: left
}
.right {
float: right
}
button {
position: fixed;
top: 55px;
left: 30px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="box">BOX</div>
<div class="left indicator">
LEFT
</div>
<div class="right indicator">
RIGHT
</div>
<button onclick="calcSizes()">
Recalculate
</button>
Hope this makes sense and helps you with your project.
You can do that with JavaScript, no need for jQuery:
var mydiv = document.getElementById('mydiv');
var offset = mydiv.getBoundingClientRect();
var offsetRight = document.documentElement.clientWidth - offset.right;
var offsetLeft = offset.left;
JSFiddle

Calculating child div top position - why does parent offsetHeight need to be subtracted?

I have created a magnifier in pure js. What I discovered in needing to translate the mouse position of a div relative to its parents is that in calculating the top for the overlaying magnifier div, the offsetTop works differently than the offsetLeft. After adjusting for what should be the top, I need to subtract the whole container div's offsetHeight.
The line in the code in question is this:
magnifier.style.top = yPosition - container.offsetHeight + "px";
Why do I need to subtract container.offsetHeight?
I know I've read something regarding this, but can't find it.
Disclaimers This code is working. I am asking so I (and those following) can understand how the box model works.
I know there are jQuery alternatives that are more cross browser reliable. I like to code it myself so that I can learn how it all works. If you see something which is not compatible for a modern browser, feel free to comment.
Lastly, For anyone using this, I removed code from this example to adjust for transforms. For example, if the wrapper has a transform: translate(-50%, 0); to center the wrapper horizontally, you will need to add the resulting amount of the translation (which translates to the wrapper's left position) back into the calculation.
I have created a jsfiddle here. I left more comments in the Fiddle as to methodology if anyone is interested.
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="../css/ms.js"></script>
<style type="text/css">
/********************/
body {
background-color: #FFF;
margin-left: 30px;
margin-top: 10px;
}
.wrapper {
position: absolute;
left: 100px;
}
#container {
width: 527px;
height: 450px;
border: 5px black solid;
overflow: hidden;
background-color: #F2F2F2;
cursor: pointer;
}
#image {
width: 527px;
height: 450px;
}
#magnifier {
width: 250px;
height: 250px;
overflow:hidden;
position:relative;
z-index: 1000;
border: solid 1px;
}
#magnifier img {
position: absolute;
}
</style>
</head>
<body>
<div class="wrapper" id="wrapper">
<div id="container">
<img id="image" src="../docs/grade-2/jpg/g2-bb-saints-francis.jpg">
<div id="magnifier" class="magnifier">
<img id="imagecopy">
</div>
<br>
</div>
<input type="button" value="Zoom" onClick="initmagnifier('magnifier', 'image', 'imagecopy');"><br>
</div>
<script>
function initmagnifier(magnifier, image, imagecopy){
var magnifier = document.getElementById("magnifier");
var container = document.getElementById("container");
var wrapper = document.getElementById("wrapper");
var img = document.getElementById(image);
var imgcopy = document.getElementById(imagecopy);
var zoom = 2;
container.addEventListener("mousemove",
function(e){
movemagnifier(e, img, imgcopy, magnifier, container, wrapper, zoom)
}, false);
var src = img.src;
imgcopy.src = src;
var src2 = imgcopy.src;
imgcopy.height = img.height * zoom;
imgcopy.width = img.width * zoom ;
}
function movemagnifier(e, img, imgcopy, magnifier, container, wrapper, zoom) {
// to get the left & top of the magnifier
// position needs to be adjusted for WRAPPER & CONTAINER top and left
// gets the top and left of the container
var containerPosition = getPosition(e.currentTarget);
// adjust out the CONTAINER's top / left
// Then takes 1/2 the hight of the MAGNIFIER and subtracts it from the MOUSE position to center MAGNIFIER around the MOUSE cursor
var xPosition = e.clientX - containerPosition.x - (magnifier.clientWidth / 2);
var yPosition = e.clientY - containerPosition.y - (magnifier.clientHeight / 2);
magnifier.style.left = xPosition + "px";
magnifier.style.top = yPosition - container.offsetHeight + "px";
// Adjust for zoom
// adjust the MAGNIFIER's top/left at an equal pace to the zoom amount
var yTravel = (e.clientY - containerPosition.y ) * (zoom - 1);
var yimgPosition = -(yPosition - container.clientTop + yTravel);
imgcopy.style.top = yimgPosition + "px";
var xTravel = (e.clientX - containerPosition.x) * (zoom - 1); // * 1.5
var ximgPosition = -(xPosition + xTravel);
imgcopy.style.left = ximgPosition + "px";
console.log('****');
console.log(e.clientY); // MOUSE POSTION
console.log(containerPosition.y);
console.log(wrapper.offsetTop);
console.log(wrapper.clientHeight);
console.log(container.offsetTop);
console.log(container.clientHeight);
console.log(yPosition);
console.log(container.offsetHeight);
console.log(magnifier.style.top);
}
function getPosition(element) {
var xPosition = 0;
var yPosition = 0;
// element is the CONTAINER
// This calculates the postion of the element (CONTAINER) TOP & LEFT relative to ALL parents
while (element) {
// if transform: translate in place for x and y,
// add it back as it skews the offsetLeft offsetTop values by the translate amount
xPosition += ((element.offsetLeft) - element.scrollLeft);
yPosition += ((element.offsetTop) - element.scrollTop);
element = element.offsetParent;
}
return { x: xPosition, y: yPosition };
}
</script>
</body>
</html>
It took me longer than I care to admit, but I have found the reason. In your fiddle you position the #magnifier-element relative, which means you have to move it from its 'natural' position, which is below the image inside the container.
So with every move you have to compensate for this, by pulling the #magnifier to the top/left position of the container, the left position already matches, but the 'natural' top position of the #magnifier is the full height of the container, as you calculate from the top/left position of the #container, you need to subtract the #container height.
A simple fix is to add position: relative to the #container and change position: relative on the #magnifier to position: absolute.
This will give you the expected coordinate system for the #magnifier as top: 0; left: 0 for the absolute positioned element is the top left corner of the its relative parent (the first positioned parent element, in this case #container).
a working example without the need to to subtract container.offsetHeight.
While I'm at it, you may want to look into the Element.getBoundingClientRect function, as you can get all information you need to determine the position in a single call.

How to add a text box popup (Jquery tooltip or similar) to a Fabric JS image within a canvas?

I'm working on a Fabric JS project to map a floor with its rooms' locations.
At each room location I added an icon. I want to have a text box pop up (such as jquery tooltip) each time the mouse hover above the icon.
The text box should show room information (phone number \ person \ size \ etc.)
I found this google group post, but no one really described the solution beside sharing this link
Step 1: Set up your watchers
Step 2: Load the dialog
Step 3: Figure out where the bounding rect is on the page and move the dialog.
canvas.observe('mouse:over', function (e) {
console.log("Everyday I'm hovering");
showImageTools(e.target);
});
canvas.observe('mouse:out', function (e) {
$('#imageDialog').remove();
});
function showImageTools (e) {
var url = 'dialog/imageDialog.htm';
$.get(url, function(data) {
// Don't add it twice
if (!$('#imageDialog').length) {
$(body).append(data);
}
moveImageTools();
});
function moveImageTools () {
var w = $('#imageDialog').width();
var h = $('#imageDialog').height();
var e = canvas.getActiveObject();
var coords = getObjPosition(e);
// -1 because we want to be inside the selection body
var top = coords.bottom - h - 1;
var left = coords.right - w - 1;
$('#imageDialog').show();
$('#imageDialog').css({top: top, left: left});
}
function getObjPosition (e) {
// Get dimensions of object
var rect = e.getBoundingRect();
// We have the bounding box for rect... Now to get the canvas position
var offset = canvas.calcOffset();
// Do the math - offset is from $(body)
var bottom = offset._offset.top + rect.top + rect.height;
var right = offset._offset.left + rect.left + rect.width;
var left = offset._offset.left + rect.left;
var top = offset._offset.top + rect.top;
return {left: left, top: top, right: right, bottom: bottom};
}
That should be enough to get you started. Let me know if any of this doesn't make sense.
Add span element below the canvas
<span ref="toolTip" class="toolTip">ToolTip</span>
Add style for span element
NB: Visibility is hidden by default
.toolTip{
position: absolute;
z-index: 1;
background: rgb(119, 128, 0);
height: 30px;
width: 120px;
padding: 8px;
font-size: 13px;
color: #fff;
visibility: hidden;
}
Add mouse over and mouse out events
this.$data.canvas.on('mouse:over', function (e) {
// console.log(e.e.offsetX)
if (e.target && e.target.feature === 'Seat') {
self.$refs.toolTip.innerHTML =
'Seat: ' + e.target.label + ' Row: ' + e.target.rowLabel
self.$refs.toolTip.style.visibility = 'visible'
self.$refs.toolTip.style.top = e.e.offsetY + 'px'
self.$refs.toolTip.style.left = e.e.offsetX + 'px'
}
})
this.$data.canvas.on('mouse:out', function (e) {
self.$refs.toolTip.style.visibility = 'hidden'
})

Categories