Prevent viewport from moving when removing DOM elements - javascript

I'm trying to implement an HTML infinite scroller in which at any given time there are only a handful of div elements on list (to keep the memory footprint small).
I append a new div element to the list and at the same time I'm removing the first one, so the total count of divs remains the same.
Unfortunately the viewport doesn't stay still but instead it jumps backwards a little bit (the height of the removed div actually).
Is there a way to keep the viewport still while removing divs from the list?
I made a small self contained HTML page (well, it still needs JQuery 3.4.1) which exposes the problem: it starts by adding 5 divs and then it keeps adding a new one and removing the first one every 1 second
function getRandomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
function removeit() {
// remove first one
var tiles = $(".tile");
$(tiles[0]).remove();
}
function addit() {
// append new one
var jqueryTextElem = $('<div class="tile" style="height:100px;background-color:' + getRandomColor() + '"></div>');
$("#inner-wrap").append(jqueryTextElem);
}
function loop() {
removeit();
addit();
window.setTimeout(loop, 1000);
}
addit();
addit();
addit();
addit();
addit();
loop();
<div id="inner-wrap"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>

You can temporarily add position: fixed to the parent element:
first add position: fixed to the parent;
then remove the item;
then remove position: fixed from the parent

I have a feeling you're trying to have your cake and eat it, in that if you get the viewport to be "still", I think you're meaning you don't want a user to see the scrollbar move and then also not have any new affordance to scroll further down the page, because you would want the scrollbar thumb/grabber to still sit at the bottom of the scrollbar track?
I mean, you could just use $(window).scrollTop($(window).scrollTop() + 100); in your example to make it so the scroll position of the viewport won't visually move when removing elements, but at that point, you wouldn't be keeping the users view of the current elements the same or even allowing a user to have new content further down the page to scroll towards. You'd just be "pushing up" content through the view of the user?
If you are trying to lighten the load of what is currently parsed into document because you are doing some heavy lifting on the document object at runtime, maybe you still want to remove earlier elements, but retain their geometry with some empty sentinel element that always has the height of all previously removed elements added to it? This would allow you to both have a somewhat smaller footprint (though not layout-wise), while still having a usable scrollbar that can communicate to a user and both allow a user to scroll down, towards the content that has been added in.
All in all, I think what you currently have is how most infinite scrollers do and should work, meaning the scroll position and scrollbar should change when content is added in the direction the user is scrolling towards, this communicates to them that they can in fact keep scrolling that way. You really shouldn't want the viewports scroll position to be "still".
To see more clearly why I don't think you have an actual issue, replace your loop() definition with something like this...
function loop() {
$(window).scroll(function() {
// check for reaching bottom of scroller
if ($(window).scrollTop() == ($(document).height() - $(window).height())) {
addit();
removeit();
}
})
}

Related

How to move children of a div with no event

I am currently making a game where blocks fall and you have to avoid them. I have some jQuery code that appends blocks to a divcalled game.
I am having trouble selecting every single div spawned and making them move down with no clicking. Here is the GitHub link and here an example
Here is the jQuery code (part of it)
function spawn_block(){
$spawned_block = $("<div class='block'></div>")
$game.append($spawned_block); //add div with class block to #game div
var left=getRandomInt(0, $game.width()-$spawned_block.width()); //gets a random value from left of screen where div can appear
var top=getRandomInt(0, $game.height()-$spawned_block.height()); //not useful unless you don't want the div to appear at the top
//adds a random position and color to spawned_block
$spawned_block.css({
"left": left,
"background-color": getRandomColor()
});
//if you want a random position from top add "top" : top,
};
if($spawned_block.position.top < $game.position.top + $game.height ) {
$spawned_block.css("top", "+=25px");
}
This last piece of code is what I added at the end of the function, what am I doing wrong?
Not sure if this is what is happening to you but is only the most recently added div moving down? Perhaps it would help if you did something like:
$("#game .block").each(function(index){
if($(this).position.top < $game.position.top + $game.height ) {
$(this).css("top", "+=25px");
}
});
This goes through every single .block within the element of the id #game and runs your if statement on it.
The other thing that might be your problem (I'm afraid your question isn't clear enough for me to tell) is that you're only running the function to move everything down when an event happens (like a click or a block being added). Perhaps something like this might work for you:
function anim() {
$("#game .block").each(function(index){
if($(this).position.top < $game.position.top + $game.height ) {
$(this).css("top", "+=25px");
}
});
window.requestAnimationFrame(anim);
}
window.requestAnimationFrame(anim);
This tells the browser (through the line window.requestAnimationFrame(anim);) to, on the next frame, run the function anim() which moves the blocks down. You can read more about requestAnimationFrame() here.
Good luck!

Using Jquery to toggle on and off items based distance to top

I am creating a site in which there are a number of fixed background images that you scroll past. Associated with each fixed background is an image slider (or text) that is hidden until the title is clicked on. These items are all fixed positioned.
I was able to make this work by using z-index to place items in order top to bottom/first to last and then have each disappear in turn using:
$(document).scroll(function() {
$('#porttitle').toggle($(this).scrollTop() < 225);
});
However, I am unable to use this because the length pixel distance down on the page changes based on the screen size. I am pretty new to Jquery but wanted to try to use .offset .top to have the item disappear not based on the pixel length to the top of the page but instead when an element appears on the screen. This is what I have so far but it isn't seeming to work.
$(document).scroll(function() {
$('#porttitle').toggle($(this).scrollTop() < $(‘article.post-100’).offset().top);
});
Here is the link to the site: http://s416809079.onlinehome.us (not final location - just developing)
Any thoughts?
Thanks!
I think this may work for you, read the comments on the code for a line by line explanation.
Working Example
$(window).scroll(function () { // When the user scrolls
$('div').each(function () { // check each div
if ($(window).scrollTop() < $(this).offset().top) { // if the window has been scrolled beyond the top of the div
$(this).css('opacity', '1'); //change the opacity to 1
} else { // if not
$(this).css('opacity', '0'); // change the opacity to 0
}
});
});
I'm conditionally changing the opacity rather than using toggle because:
...jQuery does not support getting the offset coordinates of hidden
elements or accounting for borders, margins, or padding set on the
body element.
While it is possible to get the coordinates of elements with
visibility:hidden set, display:none is excluded from the rendering
tree and thus has a position that is undefined.
Related documentation:
.offset()
.each()
.scroll()
.scrollTop()

jQuery Menu how to contain submenus within Div

I am trying to create an 'application' contained in a div on a web page. This can't be any larger than certain dimensions (lets say: 550px by 280px). I have a menu with at least 1-3 sub menus for each item. The problem is, while I know the submenu is no larger than 280px high, the submenus often extend beyond the parent div's bounds (except for the last one which always grows upward not down).
Is there any way to make the menus grow up or down depending on whether it will extend beyond the div's bounds?
Here is a JSFiddle: http://jsfiddle.net/3FqcG/
Notice how the "Salzburg" submenu grows down beyond the bounds of the black DIV? I want that to grow up if it is too long and down if there is enough room.
Currently, I am just using the basic initialization: $( "#menu" ).menu();
Thanks!
I don't believe you can do this in CSS.
This leaves us with javascript. The basic idea is to:
calculate the baseline of the menu
if this lies outside the boundary
move the menu upwards to correct the position
live almost happily ever after
But, we have one major issue:
Though we capture the focus of an element, we don't know when its submenu is displayed & positioned. So although your problem is technically solved, it is by far not a desirable solution.
UPDATE
The best workaround I could come up with was to:
Turn off the animation (to avoid ugly glitches)
Add a watcher that would constantly monitor the element that is about to be opened
If opened, apply the position correction
Anyway, if you consider coming this far, you might as well override the default positioning of the jquery ui component, with the note that you will not be able to easily update the library. Update: or try Rudy Garcia's version if it works
Demo
Code of the demo:
var BASE_OFFSET = $('#menuContainer').offset().top;
var MAX_OFFSET = $('#menuContainer').height(); // get the offset of the container
var whenVisible = function($el, callback){ //executes callback when $el is visible
if($el.is(':visible')){ // if visible
callback(); // execute callback
}else{ // otherwise
setTimeout(function(){whenVisible($el, callback);},10); // do the same check in 10 ms
}
};
var fixPosition = function($menu){ // correct the position of the menu in respect to #menuContainer
var h = $menu.outerHeight(true); // take the height of the menu
var menuBottom = $menu.offset().top + h - BASE_OFFSET; // the baseline of the menu (taking into consideration the BASE_OFFSET)
if(menuBottom > MAX_OFFSET){ // if this is outside the MAX height
var diff = MAX_OFFSET - menuBottom; // calculate the difference
var newTop = $menu.position().top + diff; // modify current positioning with the calculated diff value
$menu.css('top', newTop + 'px'); // apply it to top (which is also used by jquery to position submenus
}
$.fx.off = false; // switch animations back on
};
$( "#menu" ).menu().on('menufocus', function(ev, ui){ // on the event menufocus
var $ui = $(ui.item); //take the focused element
var $menu = $ui.children('ul'); // take its related submenu
if($menu.length === 0){ // if there is none
return; // just terminate
}
$.fx.off = true; // switch off jQuery effects (otherwise you'll have glitches)
whenVisible($menu, function(){fixPosition($menu);}); // execute fixPosition when $menu is visible
});
You could also look at the API for this widget:
http://api.jqueryui.com/menu/
You can use the position option to position the elements how you want.
This will change the position so that they are within the box, however you will want to dynamically access the last to give it the position you want as the code below will change all menu items to move up 50.
$( "#menu" ).menu({ position: { my: "left top", at: "right+5 top-50" } });
A complete list of positioning options are also found here: http://api.jqueryui.com/position/
Apparently jquery UI has accounted for this and has given the option "within" to make sure your element stays within another element of your choice.
Therefore Your solution should be this:
$( "#menu" ).menu({ position: {within: '#menuContainer' } });

So DOM scrollIntoView aligns top/bottom, but what about left/right?

I would like to scroll horizontally the given element. The only way I find is using ScrollIntoView DOM method, which allows either align the element's bottom with the view bottom or the top - with the view top.
But what if the element is OK with respect to the Y axis, and I only want to scroll it horizontally? How can I align its left with the view left or its right with the view right?
EDIT
Here is more context. I have a YUI table with a horizontal scrollbar. I wish to scroll it programmatically to a certain TD node. I do not think window.scrollTo is of any help to me, since the scrollbar is on a div element, not on the whole page.
EDIT2
Turns out there is a duplicate SO question with the right answer - How can I scroll programmatically a div with its own scrollbars?
Voting to close mine.
I've recently had a problem with a table header that had inputs as filters for each column. Tabbing through the filters would move the focus, but if one of the inputs wasn't visible or if it was partly visible, it would only JUST bring the input into view, and I was asked to bring the full input and column into view. And then I was asked to do the same if tabbing backwards to the left.
This link helped to get me started: http://www.webdeveloper.com/forum/showthread.php?197612-scrollIntoView-horizontal
The short answer is that you want to use:
document.getElementById('myElement').scrollLeft = 50;
or:
$('#myElement')[0].scrollLeft = 50;
Here's my solution (which may be overkill for this question, but maybe it'll help someone):
// I used $.on() because the table was re-created every time the data was refreshed
// #tableWrapper is the div that limits the size of the viewable table
// don't ask me why I had to move the head head AND the body, they were in 2 different tables & divs, I didn't make the page
$('#someParentDiv').on('focus', '#tableWrapper input', function () {
var tableWidth = $('#tableWrapper')[0].offsetWidth;
var cellOffset = $(this).parent()[0].offsetLeft;
var cellWidth = $(this).parent()[0].offsetWidth;
var cellTotalOffset = cellOffset + cellWidth;
// if cell is cut off on the right
if (cellTotalOffset > tableWidth) {
var difference = cellTotalOffset - tableWidth;
$('#tableWrapper').find('.dataTables_scrollHead')[0].scrollLeft = difference;
$('#tableWrapper').find('.dataTables_scrollBody')[0].scrollLeft = difference;
}
// if cell is cut off on the left
else if ($('#tableWrapper').find('.dataTables_scrollHead')[0].scrollLeft > cellOffset) {
$('#tableWrapper').find('.dataTables_scrollHead')[0].scrollLeft = cellOffset;
$('#tableWrapper').find('.dataTables_scrollBody')[0].scrollLeft = cellOffset;
}
});
This is something I used a while back. There might be better and more efficient way of doing it but this gets the work done:
$(".item").click(function(event){
event.preventDefault();
// I broke it down into variable to make it easier to read
var tgt_full = this.href,
parts = tgt_full.split("#"),
tgt_clean = parts[1],
tgt_offset = $("#"+tgt_clean).offset(),
tgt_left = tgt_offset.left;
$('html, body').animate({scrollLeft:tgt_left}, 1000, "easeInOutExpo");
})
You just need to make sure that the item is an a tag with an href that corresponds to the target id element:
HTML:
go to section 1
...
<section id="one"> Section 1</section>
Hope this helps!

Scrolling into view an element hidden beneath a persistent header

I'm currently working under some tight restrictions with regard to what I can do with JavaScript (no frameworks such as jQuery for example, and nothing post Firefox 2.0).
Here's my problem; I have a persistent header floating at the top of the page. I have input elements scattered throughout (we're replicating a paper form exactly, including the background image). There is a field nearing the bottom of the page that gets tabbed out (using keyboard tab button) and puts the focus on a field at the top of the page. Firefox will automatically scroll the field "into view". However, while the browser believes the field is in view, it's actually hidden beneath the persistent header.
http://imageshack.us/a/img546/5561/scrollintoviewproblem.png
The blue field above is accessed by hitting "tab" from another location on the page. The browser believes the field has been scrolled into view, but it's in fact hidden beneath the floating persistent header.
What I'm looking for is ideas as to how I can detect that the field is beneath this header and scroll the entire page accordingly.
I've tried a few variations of margin & padding (see other considerations at http://code.stephenmorley.org/javascript/detachable-navigation/#considerations) without luck. I've also tried calling the JavaScript function "scrollIntoView(element)" each time we focus on a field, but given the amount of fields on the form (and the fact that we're aligning them to match the background image of a paper form exactly), this was causing some pretty severe "jumping" behavior when tabbing through fields close to each other that were at slightly different heights.
I can change how the persistent header is done, so long as it doesn't require too much effort. Unfortunately, frames are out of the question because we need to interact with the page content from the persistent header.
Ideally the solution would be in CSS, but I'm open to JavaScript if it solves my problem.
Another note, we require that the input elements have a background color, which means that adding padding to them would make the background color stretch, which hides parts of the background image. BUT, the input elements are in a div, so we might be able to use this to our advantage.
So after doing some more searching (thanks to #Kraz for leading on this route with the scrollTo() suggestion) I've found a solution that works for me.
I've added an onFocus call to each element dynamically, so they always call the scrollScreenArea(element) function, which determines if they're hidden beneath the top header or too close to the footer area (this solves another problem entirely, using the same solution).
/* This function finds an element's position relative to the top of the viewable area */
function findPosFromTop(node) {
var curtop = 0;
var curtopscroll = 0;
if (node.offsetParent) {
do {
curtop += node.offsetTop;
curtopscroll = window.scrollY;
} while (node = node.offsetParent);
}
return (curtop - curtopscroll);
}
/* This function scrolls the page to ensure that elements are
always in the viewable area */
function scrollScreenArea(el)
{
var topOffset = 200; //reserved space (in px) for header
var bottomOffset = 30; //amount of space to leave at bottom
var position = findPosFromTop(el);
//if hidden beneath header, scroll into view
if (position < topOffset)
{
// to scroll up use a negative number
scrollTo(el.offsetLeft,position-topOffset);
}
//if too close to bottom of view screen, scroll into view
else if ((position + bottomOffset) > window.innerHeight)
{
scrollTo(0,window.scrollY+bottomOffset);
}
}
Let me know if you have any questions. Thanks #Kraz for sending me onto this solution.
As well, I'd like to reference Can I detect the user viewable area on the browser? since I took some code from there and that partially described my problem (with a neat diagram to boot).
The easiest method for doing this will be listening for each element's focus event and then seeing if it is on the page. In pure JS, it is something like:
var input = Array.prototype.slice.call(document.getElementsByTagName('input'));
for ( var i in input )
input[i].addEventListener( 'focus', function (e) {
var diff = 150 /* Header height */ - e.target.getBoundingClientRect().top;
if ( diff > 0 ) {
document.body.scrollTop += diff;
document.documentElement && document.documentElement.scrollTop += diff;
}
}, false );
I didn't include the IE addEvent method, but it should be pretty easy to make that on your own given this base.

Categories

Resources