Given two overlapping divs a and b: Changing the "pointer-events" property of a (top div) to "none" will not allow mouse event passthrough to div b (below). The change only takes effect whenever the curser is moved a bit.
How can one make this change be instant, without the user having to move the mouse?
Steps for problem replication using a hover effect:
Open the Codepen
Reload the page
Immediatly place cursor on the red box and do not move the mouse
After 3 seconds the changes take place and the red box will turn purple. But until the mouse moves a bit there will be no update on the css hover of the blue box below.
HTML:
<div class="a"></div>
<div class="b"></div>
CSS:
.a {
top: 0;
position: absolute;
height: 30px;
width: 30px;
background: red;
z-index: 1;
}
.b {
top: 0;
position: absolute;
height: 50px;
width: 50px;
background: blue;
}
.b:hover {
background: green;
}
JS:
delay(3000).then(() => {
let a = document.getElementsByClassName("a")[0];
a.style["pointer-events"] = "none"; // < this updating is the problem
a.style["background"] = "rgba(255,0,0,.5)"; // purely visual
});
// delay function
function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
To achieve this you want the browser to rerender the scene. There's plenty of ways of doing this – adding or removing elements, for example.
An example is bellow – I've added a div "c" which does nothing, but after you change the pointer-events on "a", I call a setTimeout (without milliseconds, so it's the very next frame) when the background is set, and the display for "c" is set to none. This makes the whole scene redraw and you get the hover value on "b" that you want.
delay(3000).then(() => {
let a = document.getElementsByClassName("a")[0];
a.style["pointer-events"] = "none"; // < this updating is the problem
// this is just to show that it could be any div that you do do this to
let c = document.getElementsByClassName("c")[0];
setTimeout(() => {
a.style["background"] = "rgba(255,0,0,.5)"; // purely visual
c.style.display = "none";
});
});
// delay function
function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
.a {
top: 0;
position: absolute;
height: 30px;
width: 30px;
background: red;
z-index: 1;
}
.b {
top: 0;
position: absolute;
height: 50px;
width: 50px;
background: blue;
}
.b:hover {
background: green;
}
<div class="a"></div>
<div class="b"></div>
<div class="c"></div>
Related
I have created a very simple example of my problem.
Fiddle Link
In the fiddle, I have created a div named parent containing 2 imgs (i take divs in the example for simplicity but in my project, these are images) and a controller div. I place the images on the top of each other by positioning 2nd image as absolute.
I want to clip the 2nd image using clip-path property whenever, I click and then drag the controller" over the parent div.
But the controller div is causing issue with parent mousemove event whenever cursor goes on controller div, mouseout event is fired on parent div causing glitch in animation.
Adding pointer-events: none property to controller div fix the glitch but it also takes away every kind of mouse interaction from the element and I want click and drag effect.
I want to create similar effect used in this website.
The problem seems to be that the positioning of the controller sometimes (not always) 'interferes' with the reading of offsetX on the parent. And the offset goes down (to 0 or up to about 10 in the given fiddle). Hence you get the flickering as the controller moves back and then up along again.
I cannot at the moment totally explain this, particularly since the controller is an absolutely positioned element.
However, one solution is to move the controller out of the parent.
UPDATE It is though possible to leave the controller in the parent if one ignores any mousemove within the controller (so we don't get readings of 0 to 10 for the offset when the mousemove is within the controller - ignore them and we'll get the event bubbling through to the parent and can then take a reading of offset).
_
<head>
<style>
* {
margin: 0;
padding: 0;
}
#parent {
width: 100%;
position: relative;
}
#img1, #img2 {
display: block;
width: 100%;
height: 200px;
pointer-events: none;
}
#img1 {
background: red;
}
#img2 {
background: green;
position: absolute;
top: 0;
left: 0;
}
#controller {
position: absolute;
top: 0;
left: 10px;
width: 10px;
height: 100%;
height: 200px;
background: black;
z-index: 1;
cursor: ew-resize;
/* pointer-events: none; */
}
</style>
</head>
<body>
<div id="parent">
<div id="img1"></div>
<div id="img2"></div>
<div id="controller"></div>
</div>
<h4>
Click and Drag the controller to clip the front image
</h4>
<!-- img1, img2 are images in my case so i named them as imgs -->
<script>
const parent = document.getElementById('parent'),
img2 = document.getElementById('img2'),
controller = document.getElementById('controller');
let pressed = false;
console.log(pressed)
parent.addEventListener('mousemove', (e) => {
if(!pressed) return;
if (e.target != parent) return;
img2.style.clipPath = `inset(0px 0px 0px ${e.offsetX}px)`;
controller.style.left = `${e.offsetX}px`;
});
// for testing purpose
/* parent.addEventListener('mouseout', (e) => {
console.log('mouse out is called');
}); */
controller.addEventListener('mousedown', (e) => {
pressed = true;
});
controller.addEventListener('mouseup', (e) => {
pressed = false;
});
</script>
</body>
const parent = document.getElementById('parent'),
img2 = document.getElementById('img2'),
controller = document.getElementById('controller');
let pressed = false;
parent.addEventListener('mousemove', (e) => {
if (pressed) {
img2.style.clipPath = `inset(0px 0px 0px ${e.clientX - parent.offsetLeft}px)`;
controller.style.left = `${e.clientX - parent.offsetLeft}px`;
}
});
controller.addEventListener('mousedown', (e) => {
pressed = true;
});
controller.addEventListener('mouseup', (e) => {
pressed = false;
});
#parent {
width: 100%;
position: relative;
}
#img1,
#img2 {
display: block;
width: 100%;
height: 200px;
pointer-events: none;
}
#img1 {
background: red;
}
#img2 {
background: green;
position: absolute;
top: 0;
left: 0;
}
#controller {
position: absolute;
top: 0;
left: 10px;
width: 10px;
height: 100%;
background: black;
z-index: 1;
cursor: ew-resize;
/* pointer-events: none; */
}
<div id="parent">
<div id="img1"></div>
<div id="img2"></div>
<div id="controller"></div>
</div>
<h4>
Click and Drag the controller to clip the front image
</h4>
The problem is, you used offsetX which defines the distance between the top left edge of your controller element. This means the distance is about 5px, your controller jumps to 5px from left, the distance is bigger now, the controller jumps back and so on.
The offsetX read-only property of the MouseEvent interface provides
the offset in the X coordinate of the mouse pointer between that event
and the padding edge of the target node.
So therefore you can use the difference between the mouse x-position and the x-position of parent for positioning your controller:
Instead use clientX which gets the mouse position relative to the window.
img2.style.clipPath = `inset(0px 0px 0px ${e.clientX - parent.offsetLeft}px)`;
controller.style.left = `${e.clientX - parent.offsetLeft}px`;
Top expression has following meaning:
<mouse x-position> - <distance between left screen edge and parent>
It's pretty easy to tell with an intersection observer when the entirety of an element in within the viewport. The code is roughly like so:
// get an element
const thingIWantEntirelyInView = $("#Thing")[0];
const checkInView = new IntersectionObserver((event) => {
console.log("It's in view");
}, {
root: null, // viewport
threshold: 1.0,
});
checkInView.observe(thingIWantEntirelyInView);
What I can't figure out, though, is how to flip it around, so it's less like contain and more like cover. I want to know when my element (which is larger than the viewport) is totally covering the screen.
I tried switching the places of null and thingIWantEntirelyInView above, but that didn't work.
I also tried adding a position fixed height: 100vh; width: 100vw element to the page and checking the intersection with that, but it appears intersection observers don't work with two elements that don't have a parent child relationship? Maybe I did the code wrong, but here's an example:
const thingIWantFillingView = document.getElementById("BigScrolly");
const viewPort = document.getElementById("ViewPort");
// what I tried
const checkInView = new IntersectionObserver((event) => {
console.log("This never seems to happen never happen");
}, {
root: thingIWantFillingView,
threshold: 0.5,
});
// when the viewport is at least half inside BigScrolly
checkInView.observe(viewPort);
// example of the thing I don't want
const checkTouchView = new IntersectionObserver((event) => {
console.log("It has touched the viewport");
}, {
root: null,
threshold: 0,
});
// when BigScrolly has touched the viewport
checkTouchView.observe(thingIWantFillingView);
#ViewPort {
position: fixed;
height: 100vh;
width: 100vw;
top: 0px;
left: 0px;
pointer-events: none;
border: 2px solid red;
box-sizing: border-box;
}
#SomePreviousThing {
height: 100vh;
}
#BigScrolly {
height: 300vh;
background: linear-gradient(#FFEEEA, #222);
}
<div id="ViewPort"></div>
<div id="SomePreviousThing">
Ignore this section, scroll down.
</div>
<div id="BigScrolly"></div>
How can I tell when one of my elements is totally covering the screen using Intersection Observers?
Came up with a solution: I have a sticky element inside BigScrolly that's 100vh tall (make sure to wrap in an absolute positioned element to negate it's place in flow). As long as I'm fully scrolled into that section, it'll be taking up the entire view.
const coverChecker = document.querySelector(".CoverChecker");
// what I tried
const checkInView = new IntersectionObserver((event) => {
const coverage = event[0].intersectionRatio;
if (coverage >= 1) {
console.log("BigScrolly covers the viewport");
} else {
console.log("Coverage", event[0].intersectionRatio);
}
}, {
root: null,
threshold: [0, 0.25, 0.5, 0.75, 1.0],
});
// when the viewport is at least half inside BigScrolly
checkInView.observe(coverChecker);
.Positioner {
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
pointer-events: none;
}
.CoverChecker {
position: sticky;
height: 95vh;
width: 95vw;
border: 2px solid red;
box-sizing: border-box;
top: 2px;
}
.OtherSection {
height: 100vh;
}
#BigScrolly {
position: relative;
height: 300vh;
padding: 20px;
background: linear-gradient(#FFEEEA, #222);
}
<div class="OtherSection">
Ignore this section, scroll down.
</div>
<div id="BigScrolly">
<div class="Positioner">
<div class="CoverChecker"></div>
</div>
This is some text
</div>
<div class="OtherSection">
Ignore this section, scroll tup.
</div>
You can achieve this by simply checking if intersectionRect.top is zero (and also check if there's any intersection at all):
new IntersectionObserver((entries) => {
entries.forEach( entry => {
if (entry.intersectionRatio == 0) {
console.log('not visible at all')
return
}
if (entry.intersectionRect.top == 0) {
console.log('at the top of the viewport')
return
}
console.log('partially visible — not at the top of the viewport')
})
})
Here's a full example - where I use this to invert a floating page header while a dark section of the page is under the header: https://jsfiddle.net/okvtm7gc/
trying to make a button like this: https://gyazo.com/9afbd559c15bb707a2d1b24ac790cf7a. The problem with the code right now is that it works as it is supposed to on the first time; but after that, instead of going from left to right as intented, it goes from right to left to right.
HTML
<div class="btn-slide block relative mx-auto" style="overflow: hidden; width: 12rem;">
<span class="z-10">View Pricing</span>
<span class="slide-bg block absolute transition" style="background-color: rgba(0,0,0,.1); z-index: -1; top: 0; left:-10rem; width: 10rem; height: 3rem;"></span>
</div>
Javascript
const btns = document.querySelectorAll(".btn-slide");
const slide = document.getElementsByClassName('slide-bg');
btns.forEach(function(btn) {
btn.addEventListener('mouseout', function () {
slide[0].style.transform = 'translateX(230%)';
slide[0].style.transform = 'none';
})
btn.addEventListener('mouseover', function() {
slide[0].style.transform = 'translateX(80%)';
}, true)
})
Unless you have to compute a value in JavaScript (like the height of an element).
Use CSS classes as modifiers (is-hidden, is-folded, is-collapsed, ...).
Using JavaScript, only add/remove/toggle the class
yourElement.addEventListener(
"mouseenter",
function (event)
{
yourElement.classList.remove("is-collapsed");
}
);
yourElement.addEventListener(
"mouseleave",
function (event)
{
yourElement.classList.add("is-collapsed");
}
);
is-collapsed is only an exemple, name it according to your class naming standard.
You're probably going to need a bit more code than what you're showing, as you have two mutually exclusive CSS things you want to do: transition that background across the "button" on mouseenter/mouseout, which is animated, and then reset the background to its start position, which should absolutely not be animated. So you need to not just toggle the background, you also need to toggle whether or not to animation those changes.
function setupAnimation(container) {
const fg = container.querySelector('.label');
const bg = container.querySelector('.slide-bg');
const stop = evt => evt.stopPropagation();
// step one: make label text inert. This is critical.
fg.addEventListener('mouseenter', stop);
fg.addEventListener('mouseout', stop);
// mouse enter: start the slide in animation
container.addEventListener('mouseenter', evt => {
bg.classList.add('animate');
bg.classList.add('slide-in');
});
// mouse out: start the slide-out animation
container.addEventListener('mouseout', evt => {
bg.classList.remove('slide-in');
bg.classList.add('slide-out');
});
// when the slide-out transition is done,
// reset the CSS with animations _turned off_
bg.addEventListener('transitionend', evt => {
if (bg.classList.contains('slide-out')) {
bg.classList.remove('animate');
bg.classList.remove('slide-out');
}
});
}
setupAnimation(document.querySelector('.slide'));
.slide {
position: relative;
overflow: hidden;
width: 12rem;
height: 1.25rem;
cursor: pointer;
border: 1px solid black;
text-align: center;
}
.slide span {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.slide-bg {
background-color: rgba(0,0,0,.1);
transform: translate(-100%, 0);
transition: none;
z-index: 0;
}
.slide-bg.animate {
transition: transform 0.5s ease-in-out;
}
.slide-bg.slide-in {
transform: translate(0%, 0);
}
.slide-bg.slide-out {
transform: translate(100%, 0);
}
<div class="slide">
<span class="label">View Pricing</span>
<span class="slide-bg"></span>
</div>
And thanks to browsers being finicky with rapid succession mouseenter/mouseout events, depending on how fast you move the cursor this may not even be enough: you might very well still need a "step" tracker so that your JS knows which part of your total animation is currently active, and not trigger the mouseout code if, by the time the slide-in transition ends, the cursor is in fact (still) over the top container (or, again).
I advice you use the .on event listener
$('').on("mouseentre","elem",function(){$('').toggleclass('.classname')})
$('').on("mouseleave","elem",function(){$('').toggleclass('.classname')})
Then you can toggle css classes to your element in the function
toggle class adds the css of a class to your jquery selection, you can do it multiple times and have keyframes for animation in the css class
Keyframes are great way to implement animation and are supported on every browers
This question is about SVG.js
I use two move commands on animations of two SVG objects to swap their positions. I then use a callback from afterAll to move one of these objects further. I find that I need to specify my new position relative the the previous changes, i.e. from the position of the object right at the start. This is a headache, for keeping track of old coordinate changes (deltas).
So:
do I need to "commit" (or finish) my animation to change the object position permanently, before moving on? How?
am I accidentally re-using the FX object by calling animation() again on the same object at a different part in the code?
Thanks for any help...
If you just want to swap SVG images maybe you don't need a library (A and B options)
A) To do so you can rely on basic CSS transition.
div {
position: absolute;
width: 100px;
height: 100px;
}
#one {
background-color: #39464e;
left: 0;
}
#two {
background-color: #ff4f68;
right: 0;
}
body.swap #one {
left: calc(100% - 100px);
transition: 5s;
}
body.swap #two {
right: calc(100% - 100px);
transition: 5s;
}
Example: https://jsfiddle.net/gengns/et9dbpur/
You can use a svg tag instead of a div or set your SVG in a div background-image.
B) If you don't want to animate, just simple swap them you can do it in a declarative way with CSS Grid.
body {
display: grid;
grid-template-areas: 'one two';
}
body.swap {
grid-template-areas: 'two one';
}
div {
width: 100px;
height: 100px;
}
#one {
background-color: #39464e;
grid-area: one;
}
#two {
background-color: #ff4f68;
grid-area: two;
}
Example: https://jsfiddle.net/gengns/bsacypd8/
C) Using svg.js 2.7.1
SVG.on(document, 'DOMContentLoaded', function() {
const draw = SVG('drawing')
// Rectangles
const rect_one = draw.rect(100, 100).attr({fill: '#39464e'})
const rect_two = draw.rect(100, 100).attr({fill: '#ff4f68'}).move(500, 0)
// Swap
rect_one.animate().move(500, 0)
rect_two.animate().move(0, 0).after(() => {
// Swap again
rect_one.animate().move(0, 0)
rect_two.animate().move(500, 0)
})
})
Example: https://jsfiddle.net/gengns/497j1z0a/
Hope this help :)
I'm trying to make a menu that contains 5 items/icons with the selected one being in the center. Clicking to the left or right of this centered icon, rotates the menu left or right, wrapping round the edges and moving whichever item was closest to the edge back in through the opposite one. Clicking on the centered item takes you to its linked URL.
The menu should also magnify in a way similar to the OS X dock except the magnification levels are set based on position not mouseover.
I've made a diagram which is easier to understand than my ramblings.
(source: yfrog.com)
I've managed to cobble together a simple jQuery version, where the items swap positions as needed, but can't figure out how to animate this movement, especially the wrap around the edges part, and change size based on position.
I'm guessing my code is probably not the best either :)
The HTML is as follows:
<div id="nav">
<div id="leftnav"></div>
<div id="rightnav"></div>
<div id="navblock1" class="navblock">
one
</div>
<div id="navblock2" class="navblock">
two
</div>
<div id="navblock3" class="navblock">
three
</div>
<div id="navblock4" class="navblock">
four
</div>
<div id="navblock5" class="navblock">
five
</div>
And the JS:
function rotateNav(direction) {
var change = (direction=='left')?(-1):(+1);
$('div.navblock').each(function() {
oldPos = parseInt($(this).attr('id').substr(9));
newPos = oldPos+change;
if (newPos == 0)
newPos = 5;
else if (newPos == 6)
newPos = 1;
$(this).attr('id','navblock'+newPos);
});
}
$(document).ready(function(){
$("#leftnav").click(function() {
rotateNav('right');
});
$("#rightnav").click(function() {
rotateNav('left');
});
});
All the .navblock elements are absolutely positionned. The #leftnav and #rightnav elements also and they have a higher z-index so float above the items/icons.
I've looked at various jQuery plugins but none seem close to what I need.
Instead of changing id attributes (which you really shouldn't do in the first place) you can change CSS classes and use jQuery UI's switchClass() method to animate the rotation.
You would also have to do a bit of clone()ing to make it look like the edge navblocks have rotated around to the other side of the widget and some queue()/dequeue()ing to handle multiple clicks.
Working Demo:
http://jsbin.com/ovemu (editable via http://jsbin.com/ovemu/edit)
Full Source:
JavaScript
function rotateNav(direction) {
if (direction === 'left') {
var change = 1;
$('.navblock5').clone()
.removeClass('navblock5')
.addClass('navblock0')
.appendTo('#nav');
}
else {
var change = -1;
$('.navblock1').clone()
.removeClass('navblock1')
.addClass('navblock6')
.appendTo('#nav');
}
$('div.navblock').each(function() {
var oldClassName = this.className.split(' ')[1],
oldPos = parseInt(oldClassName.substr(8)),
newPos = oldPos + change;
$(this).switchClass(
oldClassName,
'navblock'+newPos,
'fast',
function () {
var animated = $('.navblock:animated').length;
if (newPos === 6 || newPos === 0) {
$(this).remove();
}
if (animated === 1) {
$('#nav').dequeue();
}
}
);
});
}
$(document).ready(function(){
$("#leftnav").click(function() {
$('#nav').queue(function(){rotateNav('right');});
});
$("#rightnav").click(function() {
$('#nav').queue(function(){rotateNav('left');});
});
});
CSS
#nav {
width: 580px; height: 120px;
position: relative; left: 150px;
overflow: hidden;
}
.navblock {
height: 100px; width: 100px;
position: absolute; top: 10px; z-index: 50;
background-color: grey;
}
.navblock0 { left: -110px; }
.navblock1 { left: 10px; }
.navblock2 { left: 120px; }
.navblock3 { left: 230px; width: 120px; height: 120px; top: 0;}
.navblock4 { left: 360px; }
.navblock5 { left: 470px; }
.navblock6 { left: 590px; }
#leftnav, #rightnav {
position: absolute; z-index: 100; height: 120px; width: 228px;
}
#leftnav { left: 0; }
#rightnav { right: 0; }
/*Uncomment the following to help debug or see the inner workings */
/*
#nav { border: 1px solid green; overflow: visible; }
#leftnav, #rightnav { border: 1px solid blue; }
*/
HTML
<div id="nav">
<div id="leftnav"></div>
<div id="rightnav"></div>
<div class="navblock navblock1">one</div>
<div class="navblock navblock2">two</div>
<div class="navblock navblock3">three</div>
<div class="navblock navblock4">four</div>
<div class="navblock navblock5">five</div>
Instead of doing this yourself and wasting time on getting this to work properly I suggest you use existing solutions ones. Here a few pointers (I guess many more can be found by using google
jQuery: Mac-like Dock
Mac-like icon dock (v2)
MAC CSS Dock Menu
jQuery mimicking the OS X dock
Simple OSX-like dock with jQuery
iconDock jQuery Plugin
You seem to be on the right track. One issue is that this line
oldPos = parseInt($(this).attr('id').substr(9));
Should use 8 in the substr:
oldPos = parseInt($(this).attr('id').substr(8));