I am trying to execute a transition for an image gallery. When the gallery transitions to the next slide, I will put it just to the left of the current slide, and slide it over the current slide by transitioning on its left style.
My current markup:
<section id="nd-gallery">
<div class="nd-slide nd-slide-current">
slide 1
</div>
<div class="nd-slide nd-slide-next">
slide 2
</div>
</section>
Relevant CSS:
#nd-gallery{
position:relative;
height:100px;
width:100px;
}
#nd-gallery > .nd-slide{
position:absolute;
height:100px;
height:100px;
outline:1px solid red;
left:-40px;
}
#nd-gallery > .nd-slide.nd-slide-current{
z-index:5;
left:0;
}
So, by default, I offset slides by moving them left by 40px (obviously I will hide them better outside of this demo with a larger offset)
Now, with some script, I want to:
Apply a new left value to the next slide to offset it (for the purpose of this demo, 50px which offset the next slide over the current slide by half)
Assign the appropriate transition properties
Assign a new left value so that the next slide will slide over the current slide
SCRIPT:
var next = document.querySelector('.nd-slide-next');
next.style.left = '-50px';
next.style.transitionProperty = 'left';
next.style.transitionDuration = '5s';
next.style.left = '0';
However, when I run this, the next slide appears on top of the current slide without a transition:
http://jsfiddle.net/ACdc7/4/
Note: Apparently Firefox performs the transition properly. In Chrome v.32 on Windows 7, the transition is not performed. Can anyone point out if I've done something incorrect, or perhaps I'm dealing with a bug in Chrome?
Like it happens with many UI frameworks that execute all UI operations in only one thread (Windows Forms, WPF ...), most layout operations are blocked while user code is executing. Why? Well, while Javascript code is executing, the browser is tipically unresponsive, because its UI thread is occupied processing your code. So, if you change the value of the same property multiple times before relinquishing control to the browser, only the last value remains. It would be a waste of processing power to go through a layout process when you change, let's say, the left value of an element, when maybe you can change it later in the same event handler. So, the browser just creates a queue of pending layout operations an performs them when it has nothing else to do.
Applied to your case, the browser adds to the queue the operation {next.style, 'left', -50px}. When you update left again, the browser doesn't add a new operation. As there is one pending that does the same, it just updates it. So, to the browser, you aren't changing the left property from -50px to 0px, you are just setting it to 0px.
You need to relinquish control to the browser after setting left to -50px. Then, you set it back to 0px. You can do it this way:
var next = document.querySelector('.nd-slide-next');
next.style.left = '-50px';
setTimeout(function() {
next.style.transitionProperty = 'left';
next.style.transitionDuration = '5s';
next.style.left = '0';
}, 10);
It also could work if you force a synchronous layout (which is often undesirable). You can achieve this by accessing some properties of a DOM node, like the offset- or client- properties. These properties always return the most updated values, so, if there are layout operations pending, the browser stops the execution of javascript code, performs the layout, and continues executing:
var next = document.querySelector('.nd-slide-next');
next.style.left = '-50px';
var offset = next.offsetLeft; //force layout. The broswer know acknowledges that next.style.left is -50px.
next.style.transitionProperty = 'left';
next.style.transitionDuration = '5s';
next.style.left = '0';
Related
I'm trying to use the vanilla JS to scroll to the bottom of a div (inside my scrollable webpage) upon receiving a message (WebSocket, not important). The message adds a new element el into the div, and I want the div to scroll to the el:
div.append(el); // or ...array
// clear div if too many messages here,
// using while (childNodes.length > ...) firstChild.remove();
div.scroll...(el);
Using div.scrollTo(0, div.scrollHeight) (edit: or div.scrollTop = div.scrollHeight) would be fine, if it was not that unbelievably slow - I receive a lot of messages quite often, and it basically freezes the browser whenever I have to force reflow with scrollHeight.
Using el.scrollIntoView() (edit: or its variants) is actually much faster (accumulated from brief uneducated profiling sessions, 20s of scrollTo and 0.6s of scrollIntoView, given relatively same load), however, my div happens to be inside a scrollable page, and scrollIntoView causes the whole page to scroll to make the el appear in the view. Basically, I want it to scroll to bottom of the div even off-screen (or achieve a similar effect).
Are there any other solutions to this problem, or is it possible to fix one of the top two variants?
UPDATE:
Using div.scrollTo(0, Number.MAX_SAFE_INTEGER); and similar works only on Chrome, not Firefox. Runs fast though (comparable to the 2nd solution). Similar variant div.scrollTop = Number.MAX_SAFE_INTEGER actually performs quite poorly for some reason.
Try div.scrollTop = div.scrollHeight:
var contents = document.querySelector('.contents');
const before = new Date().getTime();
contents.scrollTop = contents.scrollHeight;
const after = new Date().getTime();
const diff = after - before;
console.log(`Took ${diff} milliseconds`);
.contents{
height:200px;
overflow-y:auto;
}
.section{
height:197px;
border:1px solid;
}
<div class="contents">
<div class="section">1</div>
<div class="section">2</div>
<div class="section">3</div>
</div>
div.scrollIntoView(false);
This will take you to the bottom of the div.
I would like to move my image down the screen from the top left to the bottom left. I call two functions when the body loads:
window.onload = function() {
MoveRight();
MoveDown();
};
I then retreive the width and height of the clients browser window (to ensure the animation stops when it reaches the sides of the window):
document.body.style.height = height;
document.body.style.width = width;
The function "MoveDown()" is this:
function MoveDown(){
for(var i = 0; i < ; i++)
{
document.getElementById("Amanda").style.top=+i;
}
}
For some reason when I load the webpage, the image just sits in the top left. I had hoped the for loop would increment the "top" value by 1px every time, until such time that it was touching the bottom of the window when it would stop.
If it helps, the image position is set to relative with left and top both set to 0px.
If anyone could help it would be great.
*I collect the width as I want the image to move diagonally but figured that if I got moving down figured out I could easily change the code to make it go sideways at the same time.
The reason it's not moving is most likely (depending on browser) because you're not setting the units. Try
document.getElementById("Amanda").style.top=i+"px";
However, you'll find that it jumps straight down rather than animating. The reason is your loop executes all in one go without giving the browser a chance to redraw. There are a number of ways of getting around this, but one simple one would be like this
function MoveDown() {
var i=0;
function step() {
document.getElementById("Amanda").style.top=i+"px";
i++;
if (i<=100) setTimeout(step,10);
}
step();
}
Do you have position: absolute or position: relative (or position: fixed) as styling for your image?
Asking this because top applies only to positioned elements (and by default elements have position: static which is they are not explicitly positioned).
See https://developer.mozilla.org/en-US/docs/Web/CSS/top and https://developer.mozilla.org/en-US/docs/Web/CSS/position
On rereading your question, this loop of yours looks like an endless loop. Consider adding a stop rule for it, or as suggested in the comments - if you do not need some kind of sliding animation, just put css rule for bottom: 0
You'll want to use setTimeout or setInterval (I can never remember) with some interval and a function that increments the top value every time it runs. Then cancel the timeout/interval when the image reaches it's destination!
My problem
I am making a vertical website for a client who wishes to have the window "snap" to the nearest page when most of the element is visible in the viewport. So, if the page is 85% visible, it should scroll to be 100% visible.
My problem is that occasionally when scrolling all the way to the top or bottom of the viewport, the viewport will "stick" to the first or last element, preventing a few scroll events and causing a highly noticeable flicker.
A working fiddle is here: http://jsfiddle.net/RTzu8/1/
To reproduce the error, use the scrollbar to scroll to the bottom of the page. Then, scroll up with your mousewheel. You should see the flicker. Sometimes it takes a few refreshes or attempts, but the issue is highly reproducible.
I'm at a loss as to what could be causing this issue. See below for a run-down of my code and what I have tried to prevent it so far.
My code
To accomplish my snapping, I needed to detect whether an element was a certain percentage visible. So, I added a jQuery function, isNearScreen, below. I have thoroughly tested this function, and as far as I can tell it returns accurate results.
//Modification of http://upshots.org/javascript/jquery-test-if-element-is-in-viewport-visible-on-screen
//Returns "true" if element is percent visible within the viewport
$.fn.isNearScreen = function(percent){
var offset = 1 - percent;
var win = $(window);
var viewport = {
top : win.scrollTop()
};
viewport.bottom = viewport.top + win.height();
var bounds = this.offset();
bounds.bottom = bounds.top + this.outerHeight();
bounds.top = bounds.top;
//If the element is visible
if(!(viewport.bottom < bounds.top || viewport.top > bounds.bottom)){
//Get the percentage of the element that's visible
var percentage = (viewport.bottom - bounds.top) / this.height();
//If it's greater than percent, but less than 1 + (1 - percent), return true;
return (percentage > (1 - offset) && percentage < (1 + offset));
}
return false;
};
I then created a snap function, which makes use of Underscore.js's _.debounce function, to only fire on the trailing end of continuous scroll events. It fires after a 500ms timeout, and I am fairly (though not 100%) convinced that it is firing correctly. I have not been able to reproduce console logs that would indicate multiple concurrent firings.
//Snaps the screen to a page after scroll input has stopped arriving.
var snap = _.debounce(function(event){
//Check each page view
$.each($('.page-contents'), function(index, element){
//If the page view is 70% of the screen and we are allowed to snap, snap into view
if($(element).isNearScreen(0.7)){
$('html,body').animate({
scrollTop: $(element).offset().top
}, 300);
}
});
}, 500);
Finally, I bind to the window's scroll event
$(window).on('scroll', snap});
The (extremely simplified) HTML:
<div class="page">
<div class="page-contents"></div>
</div>
<div class="page">
<div class="page-contents"></div>
</div>
<div class="page">
<div class="page-contents"></div>
</div>
<div class="page">
<div class="page-contents"></div>
</div>
and CSS:
.page{
height: 750px;
width: 100%;
margin: 10px 0;
background: gray;
}
.page-contents{
height: 100%;
width: 100%;
}
What I've tried
I have tried the following, with no success:
Setting a boolean, 'preventSnap', on the window, checking its state, and only firing the animate portion of snap if it is set to false. After animation, set it to true, then set it to false after 500ms (which should in theory prevent double firings).
Calling .stop() on the element before running the snap animation.
Calling event.preventDefault() on the scroll event before running the animation.
Reducing and increasing my _.debounce delay. Interestingly, a lower _.debounce delay (200-300ms) seems to aggravate the problem and a higher _.debounce delay (1000ms) seems to fix it. This is not an acceptable solution, however, as it feels "long" waiting 1sec for the page to "snap".
Changing the heights of the elements
If there is any other information I can provide, please let me know. I'm at a loss!
I think this is a combination of events and how _.debounce works. I noticed in the fiddle (in Chrome) that the elements were 'jitterring' long after the snap finished. If you put a console log in the snap event handler you can see it's constantly being called after a snap even with no scroll inputs.
This must be the scroll animation itself setting off the snap, I tried to set a flag to prevent dual snapping and clearing the flag after the animation was finished -- however that didn't work I think because _.debounce is queuing the event to happen later (after the animation finishes and clears the flag).
So what does work is to add this as the start of the snap handler:
var nosnap = false;
var snap = _.debounce(function(event){
// Don't snap if already animating ...
if (nosnap) { nosnap = false; return; }
nosnap = true;
Fiddle
That prevents the animation directly firing the next snap event -- however that's going to cause issues if you scroll again during the animation.
So, that's a bit of a hack. Ideally you want to be able to tell what's causing the scroll event and react accordingly but there's no easy way to do that.
I absolutely think you need to stop the animation when handling a second scroll event as well.
I'm currently building a somewhat basic video timeline. On this timeline are various media assets of variable duration and the width of the DIVs representing the assets reflect the duration, with 1 second being measured as 35px. So, for example, a 5 second asset would take up 175px in width on the timeline.
Because the timeline needs to be longer than the width of my usable space on the page, it needs to scroll horizontally. Instead of using an ugly standard scrollbar, I'm using a jQuery plugin scrollbar, which requires that the full-width DIV of the timeline sits inside another DIV that is the width of the usable area of the page and acts as a frame, with the inner DIV being absolutely positioned. When you move the scrollbar left or right it changes the "left" value of the inner DIV.
Having given that context, I now come to my problem. The assets on the timeline need be horizontally resizable to adjust their duration. I have that working using jQuery UI, but I need to make it so that when I drag the right edge of an asset near to the right edge of the outer DIV framing the timeline, the inner DIV of the timeline moves (basically scrolls) left and the width of the asset increases by 1 second (35px).
Even this last bit I have working to a certain degree, but not well enough. What I need is that when I drag far enough to the right, so that I'm within 35-70 pixels of the right edge of the framing DIV, the inner DIV timeline will move to the left, the width of the asset will increase, and this will keep happening until I move my mouse back towards the left.
The best example I can think of is like when you're selecting text in your browser and you drag past the bottom of the screen, the screen starts scrolling down and it keeps doing that till you move your mouse up.
Currently I'm trying to to this by drawing on the "resize" event of the jQuery UI resizable element, but the problem is that I can't get that continuous effect I was just talking about, I have to keep dragging my mouse further to the right rather than just keeping it still. And when I reach the right edge of the window I have to release the mouse button, move back over to the resizing handle and start dragging again.
Here's the function I was trying to write (FYI, a .mediaInstance is an asset on the timeline):
//Scroll Timeline when resized handle comes close to right edge
function timelineScroll() {
//console.log('running');
var mediaElement = $('#mediaTrack .mediaInstance.resizing');
var track = $('#horiz_container_inner');
//Determine location of right edge of the timeline viewport
var timeline = $('#horiz_container_outer');
var timelineOffset = timeline.offset();
var timelineLeft = timelineOffset.left;
var timelineRight = timelineLeft + $('#horiz_container_outer').width();
//Find right edge of current .mediaInstance
var instanceOffset = mediaElement.offset();
var instanceLeft = instanceOffset.left;
var instanceRight = instanceLeft + mediaElement.width();
if ( (timelineRight-instanceRight) < 35 ) {
var timelineCurrentLeft = Number(track.css('left').replace('px',''));
var timelineNewLeft = timelineCurrentLeft - 70;
track.css('left',timelineNewLeft);
mediaCurrentWidth = mediaElement.width();
mediaElement.width(mediaCurrentWidth+35);
if (currentMousePos.x > timelineRight) {
while (currentMousePos.x > timelineRight) {
var timelineCurrentLeft = Number(track.css('left').replace('px',''));
var timelineNewLeft = timelineCurrentLeft - 35;
track.css('left',timelineNewLeft);
mediaCurrentWidth = mediaElement.width();
mediaElement.width(mediaCurrentWidth+35);
}
}
}
}
You'll notice I even tried a loop at the end there based on the mouse position being farther right than the right edge of the framing DIV, but I didn't think it would work, and it didn't ... just seemed to put me in an infinite loop.
In any case, I'd really appreciate any help anyone can offer on this. I'm working on a project with a really short turnaround time and I've never really done any of this particular stuff before.
It turns out I solved my own problem. I just needed to use a setTimeout within the 'resize' function checking if the mouse was beyond the right edge of the framing DIV every 250 milliseconds and, if so, move the inner DIV left and increase the width of the asset. Here's what I used...
EDIT: It turns out my solution didn't work as I'd hoped, so I could use some help after all.
Here's the HTML:
<div id="horiz_container_outer">
<div id="horiz_container_inner" style="position: absolute; left: 0px; top: 0px; visibility: visible;">
<div id="horiz_container" style="width: 10500px;">
<div id="transitionTrack"></div>
<div id="mediaTrack" class="ui-sortable ui-droppable">
<div class="transitionBoxContainer first ui-droppable"></div>
<div class="mediaInstance" assetid="001" assettype="video" thumb="video1_thumb.jpg" style="display: block; width: 419px;" type="asset" duration="12">
<div class="mediaThumbnail" style="background-image: url(./images/assets/thumbnails/video1_thumb.jpg);"></div>
<div class="mediaInfo">
<div class="mediaFilename">video1.avi</div>
<div class="mediaDuration">12s</div>
<div class="mediaHandle"></div>
</div>
<div class="transitionBoxContainer ui-droppable"></div>
<div class="deleteButton"></div>
</div>
</div><!-- End of #mediaTrack -->
</div><!-- End of #horiz_container -->
</div><!-- End of #horiz_container_inner -->
</div><!-- End of #horiz_container_outer -->
And here's my code to make the mediaInstance on the timeline resizable, snapping to 35px increments:
//Watch timeline assets for click on resize handle
$("#mediaTrack").on('mousedown','.mediaInstance .mediaHandle', function(e) {
e.stopPropagation();
e.preventDefault();
//Find offset location of right edge of the timeline viewport
var timelineViewport = $('#horiz_container_outer');
var timelineViewportLeft = timelineViewport.offset().left;
var timelineViewportRight = timelineViewportLeft + timelineViewport.width();
//Assign track object to variable
var track = $('#horiz_container_inner');
var thisInstance = $(this).parents('.mediaInstance');
//Store the mouse position before we start moving
var startMousePos = e.pageX;
$(document).mousemove(function(e){
var currentInstanceWidth = thisInstance.width();
//Find right edge offset of current .mediaInstance
var instanceLeft = thisInstance.offset().left;
var instanceRight = instanceLeft + currentInstanceWidth;
if ( (e.pageX < (startMousePos-35)) || (e.pageX > (startMousePos+35)) ) {
if ( e.pageX < (startMousePos-35) ) {
thisInstance.width(currentInstanceWidth-35);
} else {
thisInstance.width(currentInstanceWidth+35);
}
startMousePos = e.pageX;
recalcDuration(thisInstance);
calcTotalDuration();
}
});
});
$(document).mouseup(function(e){
$(document).unbind('mousemove');
});
That works great for the actual resizing, but the problem I'm having is that when I move the mouse past the right edge of #horiz_container_outer, which acts as a frame for the timeline, I want the #horiz_container_inner DIV to start moving its left position to the left by increments of 35px while also continuing to resize the .mediaInstance div to make it 35px wider ... and I want both those things to happen every 0.25 seconds ... HOWEVER, I don't want the "scrolling" of #horiz_container_inner to continuously fire with the mousemoves. Once the mouse passes the right edge of #horiz_container_outer, I want some function to take over and start scrolling and resizing the .mediaInstance DIV at a set interval until the mouse once again moves left, past the right edge of #horiz_container_outer, at which point the original resizing shown above takes over again.
The problem is that I have no idea how to achieve this. I tried using a flag variable and conditional to tell me when my mouse is "in the zone", with inTheZone = false to begin with, running a conditional to run my initial code only when inTheZone == false, then setting it to true once the mouse enters the right area and having a setTimeout takeover to loop the scrolling and resizing. This worked to a certain degree, but the mouse position suddenly became unavailable so I couldn't tell when I moved outside the zone and the div just kept scrolling indefinitely.
Any ideas?
I have encountered an issue with CSS transitions and before I try something else, I want to understand what the problem is.
There are 3 boxes in a container and a "next" button. The goal is for the next box top appear on top and to fade in when the "next" button is pressed. The box is positioned on top by appending it to the container, so that it is added as the last element and thus visible on top, and should fade in through css transition.
The problem is that the css transition does not seem to work after the box was appended.
The css transition works well if tested on a box element that is not appended.
Fiddle here, code below:
HTML:
<div class="container">
<div class="box red"></div>
<div class="box blue"></div>
<div class="box green"></div>
</div>
<div id="next">Next</div>
JS:
var container = $(".container");
// Save the current list order
var list = container.children(".box");
// The current index
var current = 0;
// Put the first on top
container.append(list[0]);
function next() {
// Figure out what is the index of the next box
if (current + 1 < list.length) current++;
else current = 0;
// Save it in a variable
var target = $(list[current]);
// Put it on top
container.append(target);
// Hide it and then fade it in
target.css("opacity", 0).css("transition", "opacity 1000ms ease-out").css("opacity", 1);
// The fading in is not working
}
$("#next").click(next);
Update:
A basic solution to this problem was to call offset() on the target after setting the opacity to 0 and before setting the transition css:
target.css("opacity", 0);
target.offset();
target.css("transition", "opacity 1000ms ease-out").css("opacity", 1);
Updated version of the above fiddle here
The "list" variable is a jQuery object, but the elements you pull out of it as "target" are not jQuery objects - they're the DOM nodes. Thus your calls to ".css()" are failing (which is reported in the error console for me).
Once you've fixed that, then the next thing is the issue of how the browser deals with a sequence of CSS updates. It's not clear to me what exactly I'm seeing (from Firefox 18 on Linux), but I think the basic issue is that because no layout reflow is done between the changes, the net effect is that the styles are "collapsed" so that there's no net change.
In this update to the fiddle I took a different approach. I put the transition rules in the "box" class, and then added a "prefade" class:
.prefade {
transition-duration: 0;
opacity: 0;
}
Then, instead of messing with the element style, I add "prefade" before appending, and then trigger a layout update by asking for the element's offset. Then I can remove the "prefade" class, and the box fades in.
target.addClass('prefade');
// Put it on top
container.append(target);
var p = target.offset();
target.removeClass('prefade');
I don't know whether that's the "right" way to do things. edit — to make it work in Chrome, the "transition" properties need to be repeated with the -webkit- prefix.
Is there any particular reason to use CSS transitions, when you could use jQuery fades instead and have your code work on browsers that don't support CSS transitions?
var $container = $(".container");
function next() {
var $next = $container.children().first();
$next.hide().appendTo($container).fadeIn();
}
$("#next").click(next);
Note that you can avoid a lot of the state you're maintaining merely by taking the first element from the container and moving it to the back - the DOM maintains your state for you!
See http://jsfiddle.net/alnitak/w6y3B/