I need a function that can calculate the visible area of an element that is currently visible on the screen without the part hidden by overflow: scroll, position: absolute etc.
That is, the result of this function getVisiblePart(el) will be Visible Rect is: {x: 10, y: 20, height: 50, width: 700}
Background of the question:
The need for such a function is due to a feature of the W3C specification for working with webdriver: https://w3c.github.io/webdriver/webdriver-spec.html#element-interactability
An element’s in-view centre point is the centre point of the area of the first DOM client rectangle that is inside the viewport.
Frameworks like selenoid/selenide for e2e tests use the w3c principle to calculate the center of the visible element to move the cursor to it, while allowing you to specify an offset. The main problem is to find out the actual size of the visible area of the element at the moment, in order to calculate the correct offsets, for example, to calculate the upper left border.
The solution to this problem for selenoid/selenide would be:
Selenide.actions().moveToElement(el, getVisiblePart(el).width / -2, getVisiblePart(el).height / -2)
I have read a lot of similar topics, for example:
How can I tell if a DOM element is visible in the current viewport? (it only answers the question whether the element has been seen or not)
How to check if element is visible after scrolling? (same)
Is it possible to programmatically determine whether W3C action commands are used? (close but no working answer)
All answers either give a boolean whether the element is visible, or fail to take into account that the element may be partially hidden overflow: scroll
Real Example with scrolls (I need to find visible blue rect position with any scroll position):
.a {
height: 250px;
overflow: scroll;
padding-top: 100px;
background: yellow;
}
.b {
height: 500px;
overflow: scroll;
}
.c {
height: 1000px;
background: blue;
}
#target {
border: 2px dashed red;
position: absolute;
pointer-events: none;
transform: translate(-1px,-1px); /*because of border*/
}
<div class="a">
<div class="b">
<div class="c" />
</div>
</div>
<div id="target" />
In answer to this question, I have already partially solved this problem, achieved the result I needed using Intersection Observer API, but I do not consider this solution to be good, at least because it is not synchronous with when the function is called and on the issue of cross-browser compatibility.
My workaround based on Intersection Observer API.
I do not consider this solution to be good, at least because it is not synchronous with when calling the function and on the issue of cross-browser compatibility.
This example perfectly demonstrates the final version that I want to get.
var options = {
// root: document.querySelector('#scrollArea'),
// rootMargin: '0px',
threshold: 1
}
var callback = function(entries, observer) {
const r = entries[0].intersectionRect;
document.getElementById('target').setAttribute('style', `top: ${r.top}px;left: ${r.left}px;height: ${r.height}px;width: ${r.width}px;`);
observer.unobserve(c);
};
const c = document.querySelector('.c');
setInterval(() => {
var observer = new IntersectionObserver(callback, options);
observer.observe(c);
}, 200);
.a {
height: 250px;
overflow: scroll;
padding-top: 100px;
background: yellow;
}
.b {
height: 500px;
overflow: scroll;
}
.c {
height: 1000px;
background: blue;
}
#target {
border: 2px dashed red;
position: absolute;
pointer-events: none;
transform: translate(-1px,-1px); /*because of border*/
}
<div class="a">
<div class="b">
<div class="c" />
</div>
</div>
<div id="target" />
Explanation: every 200ms we calculate the visible area of the blue element and highlight it.
An example of how I use this for Selenide:
val containerSelector = "div.testEl"
val rect = executeAsyncJavaScript<String>(
"var callback = arguments[arguments.length - 1];" +
"var el = document.querySelector('" + containerSelector + "');" +
"new IntersectionObserver(" +
" (entries, observer) => {" +
" observer.unobserve(el);" +
" var r = entries[0].intersectionRect;" +
" callback([r.x, r.y, r.height, r.width].map(v => Math.ceil(v)).join(','));" +
" }," +
" { threshold: 1 }" +
").observe(el);"
)!!.split(',').toTypedArray();
LEFT_TOP_X = rect[3].toInt() / -2 + 1 // +1 for Float tolerance
LEFT_TOP_Y = rect[2].toInt() / -2 + 1 // +1 for Float tolerance
// Move cursor to TOP LEFT
Selenide.actions().moveToElement(el, LEFT_TOP_X, LEFT_TOP_Y)
v2 may help but the asynch still stinks
https://github.com/szager-chromium/IntersectionObserver/blob/v2/explainer.md
https://w3c.github.io/IntersectionObserver/v2/
https://chromestatus.com/feature/5878481493688320
https://szager-chromium.github.io/IntersectionObserver/demo/cashbomb/hidden/
Related
An intersection observer is set up on an element. When the element is scrolled past a certain point, the intersection observer handler is fired as expected. However, if a button is clicked to scroll the element past that same point, the handler is not fired.
Why is that? Is there a way to force the handler to be fired when using scrollTo/scrollIntoView?
const container = document.getElementById("container");
const hello = document.getElementById("hello");
const button = document.getElementById("button");
const options = {
rootMargin: "-100px 0px 0px 0px",
threshold: 1
}
const handleIntersect = entries => {
entries.forEach((entry) => {
console.log("handleIntersect")
});
};
const observer = new IntersectionObserver(handleIntersect, options);
observer.observe(hello);
button.addEventListener("click", () => {
container.scrollTo({
top: 120
});
})
body {
margin: 0;
}
#container {
background-color: #ddd;
height: 400px;
overflow-y: auto;
}
.inner-container {
height: 100px;
border-bottom: 1px solid #bbb;
position: absolute;
top: 0;
left: 0;
right: 0;
text-align: right;
}
#button {
margin: 40px;
font-size: 20px;
}
#hello {
display: inline-block;
padding: 20px 40px;
background-color: blue;
color: white;
margin-top: 150px;
margin-bottom: 500px;
}
<div id="container">
<div class="inner-container">
<button id="button">Scroll</button>
</div>
<div id="hello">Hello</div>
</div>
remove rootMargin from options object and it will intersect, also you can decide percentage of visibility, if callback should be fired if even 50% is visible, you can provide inside options object
{ threshold: 0.5}
and so all...
I don't know if this solves your problem, But What I think is when we have scrollIntoView linked to a button we specify a value to the position of scrollbar if the button is clicked, for example when we click a button which has a scrollTo() function we expect the scrollbar to be at a specific place but that doesn't mean the scrollbar is sliding to the place which looks similar to the action that happens when we scroll the mouse.
In other words the intersection API fires an even when you cross a particular position or a point, however it does not fire an even if you just skip crossing the point and jump directly the desired position which happens when you use scrollIntoView,
In case you wonder that when you use scrollTo() to smooth scroll the webpage, you can visually see the scroll bar sliding to the particular point as if it passes the threshold point, however it is not the case, behind the scene the scrollbar just skip all the content and moves directly the specified position.
One way to counter the problem (not efficient) is to use looping, try looping from the current page offset value to your target value instead of hardcoding the value to the scrollIntoView() , it does gives you the desired output but the scrolling animation will be poor and it loses it's objective.
I've just revamped my tooltip code due to issues with the position altering depending on the size of it's parent (mostly due to using offsetX/Y instead of pageX/Y, but page was being weird, too). So I decided to just have one tooltip for each of my site's pages, parented to the main div, and just feed it different text depending on what the mouse is hovering over (I'll be dealing with the visibility part later).
And it's worked quite well so far, but the only issue is that, the smaller I make my window, the farther the tooltip is from my mouse, until it's not even in view anymore.
Here's the JavaScript coding I've done for it.
var body = document.getElementsByClassName("test");
var tooltip = document.getElementById("tooltip");
body[0].addEventListener("mousemove", tooltipMove)
function tooltipMove(event) {
var x = event.pageX;
var y = event.pageY;
tooltip.style.top = (y + -900) + "px";
tooltip.style.left = (x + -875) + "px";
}
The CSS coding for the tooltip:
.tooltip {
visibility: hidden;
width: 170px;
background-color: white;
background-image: url("images/tooltipbackground.png");
color: black;
text-align: center;
border-style: groove;
border-color: #f4bb4c #ffd966 #ffd966 #f4bb4c;
border-radius: 2px;
padding: 5px 5px;
position: absolute;
z-index: 1;
}
.notfound:hover .tooltip {
visibility: visible;
}
And the HTML:
<div class="test" style="top: 70px; position: relative; height: 100%; width: 100%;">
<h1>TEST</h1>
<img src="images/pagenotfound.png">
</div>
<div style="width: 1px; height: 1px; position: relative;">
<span class="tooltip" id="tooltip">testing</span>
</div>
I should mention the body's (which has the "notfound" class) height is 900px, and it's width 600px, in case that's one of the problems.
The 1 pixel div is just what I'm using to "host" the tooltip, not sure if it's causing any problems as well. I inspected the page in order to see it, and it never seemed to slide around with the window size.
Any sort of help would be greatly appreciated. I've tried to switch it from pageX/Y to clientX/Y, but it's the same issue. And using offset causes it's position to shift depending on what I'm hovering over, which is the reason I'm revamping the code in the first place.
I've also tried to change the tooltip's position from absolute to, well, anything else (after resizing it's parent so it doesn't get squashed), but that hasn't helped.
Another thing I should mention is that, for some reason, the shifting doesn't seem to happen in the Y axis, it's only when I squish the window horizontally that the tooltip shifts, at least from what I've noticed.
I had thought changing the tooltip's position to fixed had made it disappear, but I just couldn't see it due to the massive repositioning I had done to it. Once I deleted that it was visible and fine, and better yet, it stays in it's proper position no matter the screen size!
Also note: I had to change pageX/Y to clientX/Y, as using page made the tooltip shift vertically when squished.
<div style="height: 1px; width: 1px; position: relative;">
<span class="tooltip" id="tooltip" style="position: fixed;">Placeholder</span>
</div>
for (i = 0; i < tip.length; i++) {
tip[i].addEventListener("mousemove", tooltipMove)
tip[i].addEventListener("mouseleave", defaultVis)
}
function tooltipMove(event) {
var x = event.clientX;
var y = event.clientY;
tooltip.style.visibility = "visible";
tooltip.style.top = (y + -50) + "px";
tooltip.style.left = (x + -200) + "px";
}
function defaultVis() {
tooltip.style.visibility = "hidden";
}
I am trying to create a tooltip element that has a min width of 50px and a max width of 200px. I place the tooltip element inside another element so that I can easily control when the tooltip appears or disappears when there is a hover event on the parent.
The problem that I have is that the tooltip element's width appears to be controlled by the parent's width even though I specified that the child(tooltip) has an absolute position.
let p = document.getElementById( 'parent' );
let b = true;
setInterval( ()=> {
b = !b;
let w = 10;
if( b ) {
w = 300;
}
p.style.width = `${w}px`
}, 5000 );
#parent {
background-color: cyan;
width: 100px;
height: 25px;
position: relative;
transition: width 2s;
}
#tooltip {
position: absolute;
top: calc( 100% + 5px );
left: 5px;
min-width: 50px;
max-width: 200px;
background-color: yellow;
padding: 5px;
border: 1px solid black;
}
<div id="parent">
<div id="tooltip">
My long tooltip text that wraps to multiple lines as needed.
</div>
</div>
I would like the tooltip (yellow div) to keep it's size at 200px in this example, but we can see that when the parent changes width, the tooltip width also changes. Why?
Is there a way to fix this problem?
Clarification: In this example: https://codepen.io/anon/pen/ePPWER we see that the tooltip text looks nice on one line. I don't want the tooltip's div to change its width when the parent changes width, because it forces the tooltip text to wrap onto 2 lines which is undesirable.
If we check the specification related to the width of absolutely positioned element we can read this:
'width' and 'right' are 'auto' and 'left' is not 'auto', then the width is shrink-to-fit . Then solve for 'right'
So in your case the width of your element is shrink to fit:
Calculation of the shrink-to-fit width is similar to calculating the
width of a table cell using the automatic table layout algorithm.
Roughly: calculate the preferred width by formatting the content
without breaking lines other than where explicit line breaks occur,
and also calculate the preferred minimum width, e.g., by trying all
possible line breaks. CSS 2.1 does not define the exact algorithm.
Thirdly, calculate the available width: this is found by solving for
'width' after setting 'left' (in case 1) or 'right' (in case 3) to 0.
Then the shrink-to-fit width is: min(max(preferred minimum width,
available width), preferred width).
To make it easy, and without considering the min/max-width, the width of your element will try to fit the content without exceding the width of its parent container (containing block). By adding min/max-width you simply add more constraint.
One idea of fix it to remove positon:relative from the parent element so that it's no more the containing block of the position:absolute element (it will be the initial containing block which is wide enough to avoid the available width constraint).
Then use margin instead of top/left to control the position:
let p = document.getElementById( 'parent' );
let b = true;
setInterval( ()=> {
b = !b;
let w = 10;
if( b ) {
w = 300;
}
p.style.width = `${w}px`
}, 5000 );
#parent {
background-color: cyan;
width: 100px;
height: 25px;
transition: width 2s;
}
#tooltip {
position: absolute;
margin-top: 30px;
min-width: 50px;
max-width: 200px;
background-color: yellow;
padding: 5px;
border: 1px solid black;
}
<div id="parent">
<div id="tooltip">
My long tooltip text that wraps to multiple lines as needed.
</div>
</div>
ID Tooltip is being used under Parent. When parent's width changes, it also suggest that tooltip's total width is changed. Since you have used mix-width and max-width it will expand till it reaches max-width. If you want it to be fixed then simple use width.
It is because the .parent has a position: relative. This will keep all children (position: absolute included) as confined by the parent div.
Not sure if this will work for you because it is pulling the tooltip out of the parent and making it's own with span wrapping the text. Alternatively, you'll need to change the parent from being relative otherwise it'll continually affect the child.
let p = document.getElementById('parent');
let b = true;
setInterval(() => {
b = !b;
let w = 10;
if (b) {
w = 300;
}
p.style.width = `${w}px`
}, 5000);
#parent {
background-color: cyan;
width: 100px;
height: 25px;
transition: width 2s;
position: relative;
}
#root {
position: relative;
}
#tooltip {
width: 100%;
}
#tooltip span {
position: absolute;
top: calc( 100% + 5px);
left: 5px;
min-width: 50px;
max-width: 200px;
background-color: yellow;
padding: 5px;
border: 1px solid black;
}
<div id="root">
<div id="parent"></div>
<div id="tooltip">
<span>My long tooltip text that wraps to multiple lines as needed.</span>
</div>
</div>
I have been asked to write a prototype application where a user lassos important locations on a background image inside a div frame. My approach is Photoshop-like, drawing divs of dynamic size and position into the DOM within the frame. But, my next step is where I need help.
I need to allow the user to select groups of lasso divs. These will be used by another function. The "tailingdiv" is a plain 1px border div that will be mouse drawn to enclose some of its sibling divs. The challenge is detecting which divs are positioned inside the "tailingdiv"! In other words, I may need to compare the xy coordinates of these divs and determine which ones are visibly inside the tailingdiv in spite of it being sibling in the DOM tree. As a bonus, I would like to work in some fudge factor for cases where a div is 75% inside the tailingdiv.
<div class="frame" id="lassoFrame" style="display: block; height: 333px; width: 500px; background-image: url("dump-1459285968.png"); background-size: 500px 333px;">
<div class="lasso ui-draggable" style="position: absolute; left: 96px; top: 263px; width: 320px; height: 35px;" coords="{"x1":96,"x2":416,"y1":263,"y2":298}"></div>
<div class="lasso ui-draggable" style="position: absolute; left: 62px; top: 8px; width: 89px; height: 46px;" coords="{"x1":62,"x2":151,"y1":8,"y2":54}"></div>
<div class="nudgeControl lasso ui-draggable" style="position: absolute; left: 161px; top: 14px; width: 88px; height: 40px;" coords="{"x1":161,"x2":249,"y1":14,"y2":54}"></div>
<div class="tailingdiv" style="position: absolute; left: 51px; top: 4px; width: 388px; height: 71px;" coords="{"x1":162,"x2":249,"y1":13,"y2":56}"></div>
</div>
It may look something like this. We have a background where someone has mouse drawn a set of lassos and they want to draw the tailingdiv to enclose the top two lasso divs. Upon drawing the tailingdiv (mouseup event), I need to examine the coordinates of these objects and determine which divs are visually positioned inside the tailingdiv.
Here is an answer that assumes your want to select all divs with class lasso that touch or collide with the div with class tailingdiv:
function getBox(elem) {
elem=$(elem);
var ofs=elem.offset();
return {
x:ofs.left,
y:ofs.top,
width:elem.width(),
height:elem.height()
};
}
function collide(elem1, elem2) {
var a=getBox(elem1);
var b=getBox(elem2);
return (Math.abs(a.x - b.x) * 2 < (a.width + b.width)) &&
(Math.abs(a.y - b.y) * 2 < (a.height + b.height));
}
$(function() {
var tail=$('.tailingdiv');
var divs=$('div.lasso').filter(function() {
return collide(tail,this);
});
console.log(divs);
});
Since you need the entire box, you might consider using getBoundingClientRect() instead of the classic jQuery functions.
Here is the code for using getBoundingClientRect and also specifying a minimal overlap percentage (value between 0 and 1. If omitted it will default to 0.7. Set it to 0 to use it as in the previous example - strict collision):
function getBox(elem) {
var e=$(elem);
if (e.length==0||e.length>1) throw 'getBox accepts only one element as argument';
var rect=e[0].getBoundingClientRect();
return {
x:rect.left,
y:rect.top,
width:rect.width,
height:rect.height
};
}
function collide(elem1, elem2, overlap) {
if (typeof(overlap)=='undefined') overlap=0.7;
var a=getBox(elem1);
var b=getBox(elem2);
var area=Math.max(0, Math.min(a.x+a.width, b.x+b.width) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y+a.height, b.y+b.height) - Math.max(a.y, b.y))
return area/(b.width*b.height)>overlap;
}
And here is an example of jQuery integration and use:
$.fn.overlappedBy=function(elem,overlap) {
return this.filter(function() {
return collide(elem,this,overlap);
});
}
$(function() {
var tail=$('.tailingdiv');
var divs=$('div.lasso').overlappedBy('.tailingdiv',0.3);
console.log(divs);
});
I'm trying to achieve the following in HTML/Javascript:
have a coloured circle with a piece of text perfectly centred (both horizontally and vertically) within it;
dynamically from JavaScript, be able to alter the size of the circle, maintaining the text centred within it at all times.
The following achieves the first of these:
Create the circle using a DIV element whose style has appropriate background and border-radius;
Inside the DIV, put a P element whose style has "text-aligbn: center" and "line-height: ".
For example:
p.circlecaption {
text-align: center;
line-height: 128px;
}
...
<div style="background: #a0a0a0; margin: 0px; width: 128px;
height: 128px; border-radius: 64px;" id="theCircleDiv">
<p class="circlecaption" id="theText">TEST!</p>
</div>
This works fine for the initial, static, case. The problem comes when, from JavaScript, I attempt to set the line-height property in order to keep the text vertically centred as I change the size of the div. I expected something like the following to work:
var obj = document.getElementById('theCircleDiv');
var sz = '' + (rad*2) + 'px';
obj.style.width = sz;
obj.style.height = sz;
obj.style.margin = '' + (64 - rad) + 'px';
obj = document.getElementById('theText');
obj.style['line-height'] = sz;
However, while this code re-sizes and re-centres the circle perfectly, it doesn't vertically re-centre the text-- i.e. the attempt to dynamically set line-height appears to be ignored.
Can anybody offer any help on either how to set line-height dynamically, or else a way to achieve my desired goal of keeping the text centred within the circle? From my reading around, I've seen various other suggestions such as calling the property "lineHeight" or playing around with "vertical-align: middle", but none seems to work.
(I am currently testing in Safari on Mac OS which is likely to be most used among the site's target audience, but am also looking for a solution that is reasonably cross-browser compatible.)
You can achieve that with pure css
#theCircleDiv {
display: table-cell;
text-align: center;
vertical-align: middle;
}
#theText {
vertical-align: middle;
}
Working Example: http://jsfiddle.net/bZj52/
Here is an example. I'm using jQuery UI to wire up some dynamic setting of the size but the re-size code should work in a pure JavaScript environment.
First, I cleaned up the HTML and put it's style in CSS
HTML:
<div id="theCircleDiv">
<p class="circlecaption" id="theText">TEST!</p>
</div>
CSS:
#theText {
text-align: center;
line-height: 128px;
}
#theCircleDiv {
background: #a0a0a0;
margin: 0px;
width: 128px;
height: 128px;
border-radius: 64px;
}
JavaScript:
function resize(size) {
var circle = document.getElementById('theCircleDiv'),
text = document.getElementById('theText');
circle.style.width = size + 'px';
circle.style.height = size + 'px';
circle.style.borderRadius = (size / 2) + 'px';
text.style.lineHeight = size + 'px';
}
And this is much easier with flex box.
div {
background-color: #ccc;
height: 50px;
width: 100%;
/* Important Part */
display: flex;
justify-content: center;
align-items: center;
}
<div>
<span>Center Me Please</span>
</div>