I have got a method to check whether an element is in view or not from How to tell if a DOM element is visible in the current viewport?. And trying to run the test to check whether elements are in view or not
var visibleY = function (el) {
var top = el.getBoundingClientRect().top, rect, el = el.parentNode;
do {
rect = el.getBoundingClientRect();
if (top <= rect.bottom === false) return false;
el = el.parentNode;
} while (el != document.body);
// Check its within the document viewport
return top <= document.documentElement.clientHeight;
};
But it returns true for all the elements who are below parent element's client height value. What changes are required to make this work. Fiddle
The following answer from that question works if you remove the jQuery cruft (which throws an error if you don't have a global variable called jQuery):
[deleted code]
Edit
Based on various answers from the link in the OP, following seems to work, only lightly tested but it works in the OP's fiddle. It checks if an element is inside its parents and the viewport. Hopefully the comments are sufficient:
// Return true if an element overlaps its parents and the viewport
function isVisible(element) {
return isInParents(element) && isInViewport(element);
}
// Return true if an element overlaps its parents
function isInParents(el) {
var rect = el.getBoundingClientRect(),
rectP,
visible = true;
while (el && el.parentNode && el.parentNode.getBoundingClientRect && visible) {
el = el.parentNode;
rectP = el.getBoundingClientRect();
visible = rectInRect(rectP, rect);
}
return visible;
}
// Return true if element overlaps the viewport
function isInViewport (element) {
var rect = element.getBoundingClientRect();
return rectInRect({top:0, left:0,
bottom: window.innerHeight || document.documentElement.clientHeight,
right: window.innerWidth || document.documentElement.clientWidth
}, rect);
}
// Return true if r1 overlaps r0
function rectInRect(r0, r1) {
return r1.top < r0.bottom &&
r1.bottom > r0.top &&
r1.left < r0.right &&
r1.right > r0.left;
}
As to whether the element is visible or not depends on other factors, such as whether overlapping elements are hidden or not, or whether some other non–ancestor element is positioned on top, etc. Those conditions can be checked for but it becomes less and less efficient the more you have to check.
If thoroughness and performance matter, create a binary tree index of the spatial location of all elements on the page and update it as you go. Creating the index is slow, but checking position will be hugely faster.
Related
I am trying to create a simple project, using TypeScript and React, to be able to generate a new <div> that has random width and height (min/max 50/300px), and is randomly placed inside a wrapper (which for this example is 1920x1080).
Goal is to check if newly generated div rectangle is overlapping or not existing divs.
If not, create the element, else generate new position and size and check again, goal being to fill up the wrapper as much as possible since min/max size of div is set and have no overlapping ones.
So far I managed to write code for generating random positions and sizes, but I am stuck at checking collisions and sending message when empty space isn't left.
const [count,setCount]=useState(0)
const wrapper = document.getElementById('wrapper');
var posX:number,posY:number,divSizeH:number,divSizeW:number;
var willOverlap:boolean=false;
function createRandomRectangle(){
divSizeW = Math.round(((Math.random()*250) + 50));
divSizeH = Math.round(((Math.random()*250) + 50));
if (wrapper!=null) {
const width = wrapper.offsetWidth , height = wrapper.offsetHeight;
posX = Math.round( (Math.random() * ( width - divSizeW )) );
posY = Math.round( (Math.random() * ( height - divSizeH )) );
//checking collision
document.querySelectorAll('.Rectangle').forEach(element=>{
var r2 = element.getBoundingClientRect();
//my attempt //if(((posX>=r2.x&&posX<=r2.right)&&(posY>=r2.top&&posY<=r2.bottom))||((posX+divSizeW>=r2.x&&posX+divSizeW<=r2.right)&&(posY>=r2.top&&posY<=r2.bottom))||((posX>=r2.x&&posX<=r2.right)&&(posY+divSizeH>=r2.top//&&posY+divSizeH<=r2.bottom))||((posX+divSizeW>=r2.x&&posX+divSizeW<=r2.right)&&(posY+divSizeH>=r2.top&&posY+divSizeH<=r2.bottom))){
//copied from someone elses code for checking collisions
if((posX <= r2.x && r2.x <= posX+divSizeW) && (posY <= r2.y && r2.y <= posY+divSizeH) ||
(posX <= r2.x && r2.x <= posX+divSizeW) && (posY <= r2.bottom && r2.bottom <= posY+divSizeH) ||
(posX <= r2.x+r2.height && r2.x+r2.height <= posX+divSizeW) && (posY <= r2.y+r2.width && r2.y+r2.width <= posY+divSizeW) ||
(posX <= r2.x+r2.height && r2.x+r2.height <= posX+divSizeW) && (posY <= r2.y && r2.y <= posY+divSizeW)){
willOverlap=true;
while(willOverlap){
posX = Math.round((Math.random() * ( width- divSizeW)));
posY = Math.round((Math.random() * ( height- divSizeH)));
divSizeW = Math.round(((Math.random()*250) + 50));
divSizeH = Math.round(((Math.random()*250) + 50));
if(!(((posX>=r2.x&&posX<=r2.right)&&(posY>=r2.top&&posY<=r2.bottom))||((posX+divSizeW>=r2.x&&posX+divSizeW<=r2.right)&&(posY>=r2.top&&posY<=r2.bottom))||((posX>=r2.x&&posX<=r2.right)&&(posY+divSizeH>=r2.top&&posY+divSizeH<=r2.bottom))||((posX+divSizeW>=r2.x&&posX+divSizeW<=r2.right)&&(posY+divSizeH>=r2.top&&posY+divSizeH<=r2.bottom)))){
willOverlap=false;
}
}
}
})
}
}
//if there is no more place send message and dont create....
const newDiv = document.createElement('div');
newDiv.classList.add('Rectangle');
newDiv.style.width=divSizeW+"px";
newDiv.style.height=divSizeH+"px";
newDiv.style.left=posX+"px";
newDiv.style.top=posY+"px";
boxxy?.appendChild(newDiv);
setCount(count+1);
}
As you started to guess, you need to repeat your code (here new position & size + check collision) in case you find a collision.
But since you implement the repetition within the forEach loop of your list of already existing Rectangles, the newly generated position & size is not re-checked against the previous Rectangles (beginning of the list). Hence you may end up with still colliding elements.
Another issue, much more difficult, is to detect when your wrapper is "full", or rather when the empty space can no longer take any new of your Rectangles, given their minimum size. Because this step is so much more complex, let's leave it aside for now, and use a much more simplistic approach, by using a maximum number of repetitions.
With this simplification, your algorithm could look like:
const existingRectangles = document.querySelectorAll('.Rectangle');
let repCount = 0; // Number of repetitions
do {
var overlapping = false;
var newPositionAndSize = generateRandomPositionAndSize();
// Check for collision with any existing Rectangle.
// Use a classic for loop to be able to break it
// on the first collision (no need to check further)
for (let i = 0: i < existingRectangles.length; i += 1) {
if (checkCollision(existingRectangles[i], newPositionAndSize)) {
overlapping = true;
repCount += 1;
break; // No need to check the rest of the list
}
}
} while (overlapping && repCount < 1000);
if (overlapping) {
// If still overlapping, it means we could not find
// a new empty spot (here because of max repetitions reached)
showMessageWrapperIsFull();
} else {
// If we checked through the entire list without raising
// the overlapping flag, it means we have found
// a suitable empty spot
createNewRectangleAndInsertIt(newPositionAndSize);
}
So I am implementing a little circle around you cursor that expands when your mouse touches a button, an a, and an image. I have a lot of code to make this work, and I am not going to make you go through all of it. The gist is, i have a function: checkIfTouching. Here is the code (messy but idc):
function findTopLeft(element) {
var rec = element.getBoundingClientRect();
return {top: rec.top + window.scrollY, left: rec.left + window.scrollX};
} //call it like findTopLeft('#header');
function getElementBBox(element) {
var rec = element.getBoundingClientRect();
return {width: rec.width, height: rec.height};
}
function checkIfTouching(element, x, y) {
// check if the cursor is in the elementm (from top left to bottom right)
const elementBBox = getElementBBox(element);
const elementTopLeft = findTopLeft(element);
const elementBottomRight = {top: elementTopLeft.top + elementBBox.height, left: elementTopLeft.left + elementBBox.width};
// check if it is near the cursor
const cursorDistances = {
top: Math.abs(elementTopLeft.top - y),
left: Math.abs(elementTopLeft.left - x),
bottom: Math.abs(elementBottomRight.top - y),
right: Math.abs(elementBottomRight.left - x)
};
// if the cursor is within 100px of the element, return true
if (cursorDistances.top < 100 && cursorDistances.left < 100 && cursorDistances.bottom < 100 && cursorDistances.right < 100) {
if (x >= elementTopLeft.left && x <= elementBottomRight.left && y >= elementTopLeft.top && y <= elementBottomRight.top) {
return true;
} else {
return false;
}
} else {
return false;
}
}
It works perfectly. The issue I'm having is I get all elements on the page, filer them out, and then I loop through each one using a for loop. This stops the circle from expanding fully because I am hovered over one object, and it has to switch between both. My question is: Is there a way to run a function on all items in a list, or make the for loop run faster. You can see my entire cursor.js file here: https://gist.github.com/BlueFalconHD/eb8a246035e317bfc5d3b0a210b71b6c.
I really do not understand this enough, and have no idea half of what this code does honestly. It was written by copilot. Anyway, thanks for the help! :)
I am using cytoscape.js.
I have allowed the user to drag the nodes, but right now it is also possible to drag the node outside of the 'canvas'.
I have looked at the options at http://js.cytoscape.org/#core/initialisation but I don't know how to make sure the nodes cannot leave the visible area <div id="cy"></div>.
Try the automove extension: https://github.com/cytoscape/cytoscape.js-automove
It lets you set rules for node position, like constraining within the current viewport or a specified region.
I know this is long time ago, but today I have the same problem and this is my solution, just move the node back in.
cy.on('mouseup', function (e) {
let tg = e.target;
if (tg.group != undefined && tg.group() == 'nodes') {
let w = cy.width();
let h = cy.height();
if (tg.position().x > w) tg.position().x = w;
if (tg.position().x < 0) tg.position().x = 0;
if (tg.position().y > h) tg.position().y = h;
if (tg.position().y < 0) tg.position().y = 0;
}
})
I have an element who's overflow is hidden until a function is called. I am trying to determine if the mouse is hovering over this overflow material after I set overflow: visible;, but instead, it tells me that my mouse is hovering on the overflow content even when it's still invisible.
Is there a way to check visible height in jQuery? Here is what I was trying:
off = $(curSub).offset();
xSubStart = parseInt(off.left);
ySubStart = parseInt(off.top);
xSubEnd = xSubStart + parseInt($(curSub).width());
ySubEnd = ySubStart + parseInt($(curSub).height());
if ( (x >= xStart && x <= xEnd && y >= yStart && y <= yEnd) ||
(x >= xSubStart && x <= xSubEnd && y >= ySubStart && y <= ySubEnd) ) {
// display menu
$(cur).css('overflow', 'visible');
match = true;
}
The xStart, xEnd, yStart, and yEnd variables are defined above that code and work just fine. I believe the problem is that the jQuery function width(), height(), outerWidth(), and outerHeight() don't test to see if the element is visible.
Is there anyway to achieve this? I thought about just moving it from hidden to visible physically with top and left specifications, but I think this way would be cleaner if it's possible.
Hope someone knows the answer.
The problem is that JavaScript did not complete the action before executing the next line of your code.
You could use a callback function:
var setHoverAfterOverFlowVisible = function(cur, callback) {
$(cur).css('overflow', 'visible');
match = true;
//other stuff
callback();
}
setHoverAfterOverFlowVisible(cur, /*hoverFunction*/);
More info about callbacks: http://www.impressivewebs.com/callback-functions-javascript/
I'm looking for a very fast solution to a div scrolling problem.
I have a set of divs, like forum posts, that are laid out one on top of the other. As the page scrolls down or up, I'd like to know when one of those divs hit's an arbitrary point on the page.
One way I tried was adding an onScroll event to each item, but as the number of items grow the page really starts to lag.
Anyone know a more efficient way to do this? Thanks /w
Well, I'm new to all this, so may be someone should correct me :)
I propose to
cache posts position
caсhe current
use binary search
Demo: http://jsfiddle.net/zYe8M/
<div class="post"></div>
<div class="post"></div>
<div class="post"></div>
...
var posts = $(".post"), // our elements
postsPos = [], // caсhe for positions
postsCur = -1, // cache for current
targetOffset = 50; // position from top of window where you want to make post current
// filling postsPos with positions
posts.each(function(){
postsPos.push($(this).offset().top);
});
// on window scroll
$(window).bind("scroll", function(){
// get target post number
var targ = postsPos.binarySearch($(window).scrollTop() + targetOffset);
// only if we scrolled to another post
if (targ != postsCur) {
// set new cur
postsCur = targ;
// moving cur class
posts.removeClass("cur").eq(targ).addClass("cur");
}
});
// binary search with little tuning on return to get nearest from bottom
Array.prototype.binarySearch = function(find) {
var low = 0, high = this.length - 1,
i, comparison;
while (low <= high) {
i = Math.floor((low + high) / 2);
if (this[i] < find) { low = i + 1; continue; };
if (this[i] > find) { high = i - 1; continue; };
return i;
}
return this[i] > find ? i-1 : i;
};
You shouldn't bind scroll event to all the divs but only to window instead. Then, you should check whether one of the divs overlap with the target point by making a simple calculation of the element offset values.
$(window).scroll(function(event)
{
var isCaptured = capture();
console.log(isCaptured);
});
function capture()
{
var c = $('.box'); //this is the divs
var t = $('#target'); //this is the target element
var cPos = c.offset(); var tPos = t.offset();
var overlapY = (cPos.top <= tPos.top + t.height() && cPos.top + c.height() >= tPos.top);
var overlapX = (cPos.left <= tPos.left + t.width() && cPos.left + c.width() >= tPos.left);
return overlapY && overlapX;
}
Instead of the $('#target') element, you can pass top and left (X, Y) offset values directly to the function.
Well, here is a dirty demonstration.