onmouseenter flickering tooltip - javascript

I want to display a tooltip when hovering over a svg rect but the tooltip keeps on flickering, switching between display: none and display: flex erratically and unpredictably.
When I used the same code but hovered over a div the result was very smooth, so it seems to be related to the rect element.
I get different behaviours in the snippet below than I do when I open the HTML in Chrome.
In the snippet it seems to work if i enter the rect from left to right or top to bottom, but not the other way around.
In Chrome it just keeps flickering which ever direction I enter.
Why is it behaving this way and what can I do about it?
const tooltips = document.querySelectorAll('.tooltip');
const bars = document.querySelectorAll('rect');
document.addEventListener('mousemove', fn);
function fn(e) {
tooltips[0].style.left = e.pageX + 'px';
tooltips[0].style.top = e.pageY + 'px';
}
bars[0].onmouseenter = () => {
tooltips[0].style.display = 'flex';
}
bars[0].onmouseleave = () => {
tooltips[0].style.display = 'none';
}
.tooltip {
display: none;
position: absolute;
background-color: grey;
}
<div class="tooltip">
<p>Rect 1</p>
</div>
<svg width="500" height="500">
<rect width="200" height="30" x="0" y="0" style="fill:rgb(0,0,255)" />
</svg>

Because when the tooltip is opened, it will come over the mouse. And mouseleave for rect is triggered, and tooltip closes and mouseenter is triggered again. So it goes into an infinite loop. We can use pointer-events: none to avoid mouse events on tooltip.
const tooltips = document.querySelectorAll('.tooltip');
const bars = document.querySelectorAll('rect');
document.addEventListener('mousemove', fn);
function fn(e) {
tooltips[0].style.left = e.pageX + 'px';
tooltips[0].style.top = e.pageY + 'px';
}
bars[0].onmouseenter = () => {
tooltips[0].style.display = 'flex';
}
bars[0].onmouseleave = () => {
tooltips[0].style.display = 'none';
}
.tooltip {
display: none;
position: absolute;
background-color: grey;
pointer-events: none;
}
<div class="tooltip">
<p>Rect 1</p>
</div>
<svg width="500" height="500">
<rect width="200" height="30" x="0" y="0" style="fill:rgb(0,0,255)" />
</svg>
Another way, you can push the tooltip a little further away from the mouse. I used e.pageX + 5 and e.pageY + 5 for example.
const tooltips = document.querySelectorAll('.tooltip');
const bars = document.querySelectorAll('rect');
document.addEventListener('mousemove', fn);
function fn(e) {
tooltips[0].style.left = (e.pageX + 5) + 'px';
tooltips[0].style.top = (e.pageY + 5) + 'px';
}
bars[0].onmouseenter = () => {
tooltips[0].style.display = 'flex';
}
bars[0].onmouseleave = () => {
tooltips[0].style.display = 'none';
}
.tooltip {
display: none;
position: absolute;
background-color: grey;
}
<div class="tooltip">
<p>Rect 1</p>
</div>
<svg width="500" height="500">
<rect width="200" height="30" x="0" y="0" style="fill:rgb(0,0,255)" />
</svg>

Related

d3js pan with touchpad or mousewheel pressed

I've created zoom behaviour with d3js
const zoom = d3
.zoom<SVGSVGElement, unknown>()
.filter(function (event) {
return event.which === 2 || event.type === 'wheel';
})
.scaleExtent([-10, 10])
.on('zoom', handleZoom);
if i press the mousewheel and move the mouse then pan works, but on touchpad if i put two fingers on the touchpad and move them in the same direction i expect pan effect but zoom works instead. Is there some way to filter the event for pan using touchpad?
const { useEffect } = React;
const App = () => {
const handleZoom = (event) => {
d3.select('#group').attr('transform', event.transform);
}
useEffect(()=>{
const zoom = d3
.zoom()
.filter(function (event) {
return event.which === 2 || event.type === 'wheel';
})
.scaleExtent([-10, 10])
.on('zoom', handleZoom);
d3.select('#workflow').call(zoom)
}, [handleZoom])
return (
<svg id="workflow">
<g id="group">
<foreignObject x="100" y="50">
<div>Node 1</div>
</foreignObject>
<foreignObject x="200" y="150">
<div>Node 2</div>
</foreignObject>
</g>
</svg>
)
}
const root = ReactDOM.createRoot(
document.getElementById('app')
);
root.render(<App/>)
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #app {
width: 100%;
height: 100%;
}
#workflow {
width: 100%;
height: 100%;
}
foreignObject {
width: 1px;
height: 1px;
overflow: visible;
}
foreignObject > div {
padding: 10px;
background-color: #c0c0c0;
border-radius: 10px;
width: 100px
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js"></script>
<script src="https://unpkg.com/react#18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#18/umd/react-dom.development.js" crossorigin></script>
<div id="app"></div>
I want to set up d3 to work by scrolling the mouse wheel or zoom in/out on the touchpad, and pan to work by holding down the mouse wheel or moving two fingers on the touchpad in the same direction

Setting .className of SVG path not working

I have a chevron SVG at the bottom of my page that I want to hide once the user scrolls down. I can't figure out why my JS isn't working and the chevron is never hidden:
NOTE: the console.log statements do run and the css stylesheet does work.
chevron = document.getElementById("down-accelerate-path");
window.addEventListener("scroll", e => {
const y = window.scrollY;
if (y >= 1) {
console.log(y + " " + chevron)
chevron.className = ""
chevron.hidden = true;
} else {
chevron.className = "hide"
chevron.hidden = false;
}
});
body {
height: 300vh;
}
.hide {
display: none;
}
#down-accelerate {
position: fixed;
top: 88%;
transform: translateX(-50%);
}
<span class="center">
<svg height=88 width=88 viewBox="0 0 444.819 444.819" xmlns="http://www.w3.org/2000/svg" id = "down-accelerate">
<path id="down-accelerate-path" style="" fill="white" stroke="none"
d="M434.252,114.203l-21.409-21.416c-7.419-7.04-16.084-10.561-25.975-10.561c-10.095,0-18.657,3.521-25.7,10.561
L222.41,231.549L83.653,92.791c-7.042-7.04-15.606-10.561-25.697-10.561c-9.896,0-18.559,3.521-25.979,10.561l-21.128,21.416
C3.615,121.436,0,130.099,0,140.188c0,10.277,3.619,18.842,10.848,25.693l185.864,185.865c6.855,7.23,15.416,10.848,25.697,10.848
c10.088,0,18.75-3.617,25.977-10.848l185.865-185.865c7.043-7.044,10.567-15.608,10.567-25.693
C444.819,130.287,441.295,121.629,434.252,114.203z"/>
</svg>
</span>
Your code appears to be working. But I couldn't see the chevron. So I made it black and changed its position for testing. I also cleaned up the code a bit.
chevron = document.getElementById("chevron");
window.addEventListener("scroll", e => {
const y = window.scrollY;
if (y >= 1) {
chevron.className = 'hide'
} else {
chevron.className = ''
}
});
body {
height: 300vh;
}
.hide {
display: none;
}
#down-accelerate {
position: fixed;
right: 50%;
}
<span class="center" id="chevron">
<svg height=88 width=88 viewBox="0 0 444.819 444.819" xmlns="http://www.w3.org/2000/svg" id = "down-accelerate">
<path id="down-accelerate-path" style="" fill="black" stroke="none"
d="M434.252,114.203l-21.409-21.416c-7.419-7.04-16.084-10.561-25.975-10.561c-10.095,0-18.657,3.521-25.7,10.561
L222.41,231.549L83.653,92.791c-7.042-7.04-15.606-10.561-25.697-10.561c-9.896,0-18.559,3.521-25.979,10.561l-21.128,21.416
C3.615,121.436,0,130.099,0,140.188c0,10.277,3.619,18.842,10.848,25.693l185.864,185.865c6.855,7.23,15.416,10.848,25.697,10.848
c10.088,0,18.75-3.617,25.977-10.848l185.865-185.865c7.043-7.044,10.567-15.608,10.567-25.693
C444.819,130.287,441.295,121.629,434.252,114.203z"/>
</svg>
</span>

Problems implementing dragging by mouse

I want to implement a draggable map containing certain elements.
--> See JSFiddle: https://jsfiddle.net/7ndx7s25/7/
By use of mousedown, mousemove and mouseup I achieved the dragging.
However I am facing problems:
When pressing the mouse button down and then moving outside the window I do not get a mouseup event. Reentering the window (having released the mouse button long ago) my map still thinks the button is down and misbehaves accordingly.
When there are objects on the map, I do not get mousemove events while moving through these objects. Therefore the map hangs and jumps as I enter and leave such an object.
While over such objects I still want to have a move mouse cursor. I could change the cursor style on each object (in the Fiddle I did this for Object 1 as an example), but this doesn't seem like a good way. Is there a more elegant solution?
You need e.g. mouseout to catch when leaving the canvas, though that event will also fire when the cursor move over the other elements.
One easy fix is to simply add a class to canvas, that set pointer-events: none on those.
With that class you can control the cursor as well, and avoid setting it with the script.
Stack snippet
updateInfo = function() {
document.getElementById('info').innerHTML =
'Position = ' + JSON.stringify(position) +
'<br />dragInfo = ' + JSON.stringify(dragInfo);
};
const canvas = document.getElementsByTagName('canvas')[0];
let position = { x: 0, y : 0 };
let dragInfo = null;
updateInfo();
canvas.addEventListener('mousedown', function(e) {
dragInfo = {
startEvent: {
x: e.clientX,
y: e.clientY,
},
startPosition: position
};
canvas.classList.add('dragging');
updateInfo();
});
canvas.addEventListener('mousemove', function(e) {
if (dragInfo === null) return;
position = {
x: dragInfo.startPosition.x - (e.clientX - dragInfo.startEvent.x),
y: dragInfo.startPosition.y - (e.clientY - dragInfo.startEvent.y)
};
updateInfo();
});
canvas.addEventListener('mouseup', function(e) {
dragInfo = null;
canvas.classList.remove('dragging');
updateInfo();
});
canvas.addEventListener('mouseout', function(e) {
dragInfo = null;
canvas.classList.remove('dragging');
updateInfo();
});
* {
user-select: none;
font-family: monospace;
}
canvas {
background: black;
border: 1px solid red;
}
.dragging {
cursor: move;
}
.obj {
position: absolute;
width: 50px;
height: 50px;
background: green;
color: white;
text-align: center;
line-height: 50px;
font-weight: bold;
}
.dragging ~ .obj {
pointer-events: none;
}
<div id="myMap-ish">
<canvas width="500" height="300"></canvas>
<div class="obj" style="left: 30px; top: 35px">1</div>
<div class="obj" style="left: 175px; top: 79px">2</div>
<div class="obj" style="left: 214px; top: 145px">3</div>
<div class="obj" style="left: 314px; top: 215px">4</div>
</div>
<div id="info"></div>
Another option could be to use mouseleave, on the outer wrapper, the myMap-ish element, which could be combined with the above added class to simply cursor handling.
The main difference between mouseout and mouseleave is that the latter won't fire when hovering children, as shown in below sample, so we don't need to toggle pointer-events as we did in the first sample.
Note, to simply use mouseleave in the first sample, on canvas, will have the same issue mouseout has, since the "other element" aren't children of the canvas.
Stack snippet
updateInfo = function() {
document.getElementById('info').innerHTML =
'Position = ' + JSON.stringify(position) +
'<br />dragInfo = ' + JSON.stringify(dragInfo);
};
const canvas = document.getElementById('myMap-ish');
let position = { x: 0, y : 0 };
let dragInfo = null;
updateInfo();
canvas.addEventListener('mousedown', function(e) {
dragInfo = {
startEvent: {
x: e.clientX,
y: e.clientY,
},
startPosition: position
};
canvas.style.cursor = 'move';
document.querySelectorAll('.obj')[0].style.cursor = 'move'; // TODO for all objects
updateInfo();
});
canvas.addEventListener('mousemove', function(e) {
if (dragInfo === null) return;
position = {
x: dragInfo.startPosition.x - (e.clientX - dragInfo.startEvent.x),
y: dragInfo.startPosition.y - (e.clientY - dragInfo.startEvent.y)
};
updateInfo();
});
canvas.addEventListener('mouseup', function(e) {
dragInfo = null;
canvas.style.cursor = 'default';
document.querySelectorAll('.obj')[0].style.cursor = 'default'; // TODO for all objects
updateInfo();
});
canvas.addEventListener('mouseleave', function(e) {
dragInfo = null;
canvas.style.cursor = 'default';
document.querySelectorAll('.obj')[0].style.cursor = 'default'; // TODO for all objects
updateInfo();
});
* {
user-select: none;
font-family: monospace;
}
canvas {
background: black;
border: 1px solid red;
}
.obj {
position: absolute;
width: 50px;
height: 50px;
background: green;
color: white;
text-align: center;
line-height: 50px;
font-weight: bold;
}
<div id="myMap-ish">
<canvas width="500" height="300"></canvas>
<div class="obj" style="left: 30px; top: 35px">1</div>
<div class="obj" style="left: 175px; top: 79px">2</div>
<div class="obj" style="left: 214px; top: 145px">3</div>
<div class="obj" style="left: 314px; top: 215px">4</div>
</div>
<div id="info"></div>

SVG filter not working when imbedded with JavaScript

I have been trying to get the blur effect to work when creating the elements and imbedding them with JavaScript. I cannot figure out why the blur will not show up. I have two fiddles, one with the standard HTML & CSS and the other using JavaScript. If anyone wants to check them out and offer an explanation as to what I am doing wrong I would really appreciate it.
HTML & CSS Version (Works)
https://jsfiddle.net/aaronbalthaser/xampLtnr/
HTML:
<div id="container">
<img src="http://lorempixel.com/300/250/sports/" width="300" height="250" />
<svg width="300" height="60" viewbox="0 0 300 60">
<defs>
<filter id="blur">
<fegaussianblur in="SourceGraphic" stddeviation="5" />
</filter>
</defs>
<image filter="url(#blur)" xlink:href="http://lorempixel.com/300/250/sports/" x="0" y="-190" height="250px" width="300px" />
</svg>
</div>
CSS:
#container {
position: relative;
width: 300px;
height: 250px;
margin: 0 auto;
position: relative;
}
svg {
position: absolute;
bottom: 0;
left: 0;
}
JavaScript Version (Doesn't Work)
https://jsfiddle.net/aaronbalthaser/Lbnkfn02/
HTML:
<div id="container">
<img src="http://lorempixel.com/300/250/sports/" width="300" height="250" />
</div>
CSS:
#container {
position: relative;
width: 300px;
height: 250px;
margin: 0 auto;
position: relative;
}
svg {
position: absolute;
bottom: 0;
left: 0;
}
JS:
var width = 300;
var height = 250;
var viewbox = "0 0 300 60";
var namespace = 'http://www.w3.org/2000/svg'
var path = 'http://lorempixel.com/300/250/sports/';
var container = document.getElementById('container');
var body = document.body;
var svg = document.createElementNS(namespace,'svg');
svg.setAttribute('width', width);
svg.setAttribute('height', 60);
svg.setAttribute('viewBox', viewbox);
var defs = document.createElementNS(namespace, 'defs');
var filter = document.createElementNS(namespace, 'filter');
filter.setAttribute('id', 'blur');
var feGaussianBlur = document.createElementNS(namespace, 'feGaussianBlur');
feGaussianBlur.setAttribute('in', 'SourceGraphic');
feGaussianBlur.setAttribute('stdDeviation', '5');
var image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
image.setAttribute('filter', 'url(#blur)');
image.setAttribute('xlink:href', path);
image.setAttribute('x', + '0');
image.setAttribute('y', + '-190');
image.setAttribute('height', height + 'px');
image.setAttribute('width', width + 'px');
filter.appendChild(feGaussianBlur);
defs.appendChild(filter);
svg.appendChild(defs);
svg.appendChild(image);
container.appendChild(svg);
console.log(container);
image.setAttribute('xlink:href', path);
should be
image.setAttributeNS("http://www.w3.org/1999/xlink", 'xlink:href', path);
https://jsfiddle.net/Lbnkfn02/2/

TweenMax reuse function for all element ID's

I'm using a Tweenmax script I found to create a ripple effect on some buttons. I have HTML as follows:
<label for="slider_1" id="js-ripple-btn" class="button styl-material">
Home
<svg class="ripple-obj" id="js-ripple"><use height="100" width="100" xlink:href="#ripply-scott" class="js-ripple"></use></svg>
</label>
<label for="slider_2" id="js-ripple-btn2" class="button styl-material">
Catalogue
<svg class="ripple-obj" id="js-ripple2"><use height="100" width="100" xlink:href="#ripply-scott" class="js-ripple2"></use></svg>
</label>
...
I'm a bit lost on how to have the script listen for any of the ID's that have the js-ripple class rather than specify just one. The following code only targets the first element ID but I need it to target whichever element ID is clicked.
var ripplyScott = (function() {
var circle = document.getElementById('js-ripple'),
ripple = document.querySelectorAll('.js-ripple');
function rippleAnimation(event, timing) {
var tl = new TimelineMax();
x = event.offsetX,
y = event.offsetY,
w = event.target.offsetWidth,
h = event.target.offsetHeight,
offsetX = Math.abs( (w / 2) - x ),
offsetY = Math.abs( (h / 2) - y ),
deltaX = (w / 2) + offsetX,
deltaY = (h / 2) + offsetY,
scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
console.log('x is:' + x);
console.log('y is:' + y);
console.log('offsetX is:' + offsetX);
console.log('offsetY is:' + offsetY);
console.log('deltaX is:' + deltaX);
console.log('deltaY is:' + deltaY);
console.log('width is:' + w);
console.log('height is:' + h);
console.log('scale ratio is:' + scale_ratio);
tl.fromTo(ripple, timing, {
x: x,
y: y,
transformOrigin: '50% 50%',
scale: 0,
opacity: 1,
ease: Linear.easeIn
},{
scale: scale_ratio,
opacity: 0
});
return tl;
}
return {
init: function(target, timing) {
var button = document.getElementById(target);
button.addEventListener('click', function(event) {
rippleAnimation.call(this, event, timing);
});
}
};
})();
ripplyScott.init('js-ripple-btn', 0.75);
I can obviously duplicate the script changing the element ID to each button but that means, I'm sure, ridiculous repeating of code when there must be a way of targeting all with a slight modification. Help/guidance is appreciated thanks!
** Update **
Here is a fiddle so you can sort of see my issue. Thanks.
I have made an attempt on solving your problem based on my understanding. Take a look at this jsFiddle.
Here is the explanation:
IDs should always be unique to elements.
Your js-ripple-btn ID has been changed to class.
We needed a way to get a reference to the button that has been clicked so that we can play respective animation.
We solved that by using an index variable from buttons passing them all the way down to your animations.
Snippet:
var ripplyScott = (function() {
var circle = document.getElementById('js-ripple');
var ripple = document.querySelectorAll('.js-ripple');
function rippleAnimation(event, timing, index) {
var tl = new TimelineMax();
var x = event.offsetX;
var y = event.offsetY;
var w = event.target.offsetWidth;
var h = event.target.offsetHeight;
var offsetX = Math.abs((w / 2) - x);
var offsetY = Math.abs((h / 2) - y);
var deltaX = (w / 2) + offsetX;
var deltaY = (h / 2) + offsetY;
var scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
tl.fromTo(ripple[index], timing, {
x: x,
y: y,
transformOrigin: '50% 50%',
scale: 0,
opacity: 1,
ease: Linear.easeIn
}, {
scale: scale_ratio,
opacity: 0
});
return tl;
}
return {
init: function(target, timing) {
var buttons = document.getElementsByClassName(target);
var numButtons = buttons.length;
for (var i = 0; i < numButtons; i += 1) {
(function(index) {
buttons[index].addEventListener('click', function(event) {
rippleAnimation.call(this, event, timing, index);
});
}(i));
}
}
};
})();
ripplyScott.init('js-ripple-btn', 0.75);
label {
border-radius: 0;
display: block;
background: #ccc;
margin: 5px 0;
cursor: pointer;
color: #333;
width: 200px;
padding: 0;
font-size: 15px;
border: 1px solid #333;
text-align: center;
height: 70px;
line-height: 70px;
overflow: hidden;
}
.button {
padding: 1.5em 3em;
margin: 0;
position: relative;
display: inline-block;
overflow: hidden;
}
.button.styl-material {
transition: 200ms background cubic-bezier(0.4, 0, 0.2, 1);
}
.button.styl-material:hover,
.button.styl-material:focus {
outline: none;
}
.ripple-obj,
.ripple-obj2 {
height: 100%;
pointer-events: none;
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 100%;
fill: #AD1457;
}
.ripple-obj use,
.ripple-obj2 use {
opacity: 0;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.2/TweenMax.min.js"></script>
<label class="button styl-material js-ripple-btn" for="slider_1">Home
<svg class="ripple-obj" id="js-ripple">
<use class="js-ripple" height="100" width="100" xlink:href="#ripply-scott">
</use>
</svg>
</label>
<label class="button styl-material js-ripple-btn" for="slider_2">Catalogue
<svg class="ripple-obj" id="js-ripple2">
<use class="js-ripple" height="100" width="100" xlink:href="#ripply-scott">
</use>
</svg>
</label>
<!-- Firefox Button Fix -->
<div aria-hidden="true" style="height: 0; width: 0; position: absolute; visibility: hidden;">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialgradient id="gradient">
<stop offset="0" stop-color="#0868BB"></stop>
<stop offset="0.25" stop-color="#0075D8"></stop>
<stop offset="0.35" stop-color="#0868BB"></stop>
<stop offset="0.5" stop-color="#0075D8"></stop>
<stop offset="0.6" stop-color="#0868BB"></stop>
<stop offset="0.85" stop-color="#0075D8"></stop>
<stop offset="1" stop-color="#0868BB"></stop>
</radialgradient>
</defs>
<symbol id="ripply-scott" viewbox="0 0 100 100">
<circle cx="1" cy="1" fill="url(#gradient)" id="ripple-shape" r="1"></circle>
</symbol>
</svg>
</div>
<!-- / FF button fix -->
Hope this helps.
You can add an onClick to your buttons to grab the id of them and then run ripplyScott.init passing in the id of the clicked button to target.
<label class="button styl-material" for="slider_1" id="js-ripple-btn" onClick="myClick(this.id)">
var elementId;
function myClick(id) {
elementId = id;
ripplyScott.init(id, 0.75);
}
return {
init: function(target, timing) {
rippleAnimation.call(target, event, timing);
}
};
And declare your ripple var as so:
var ripple = document.getElementById(elementId).childNodes[1].childNodes[1];
Here's a working fiddle

Categories