getBoundingClientRect within svg incorrect only on Chrome - javascript

When calling getBoundingClientRect of a div within an svg in order to position other elements outside of the svg accordingly, the top and left values are too high only on Chrome (78.0.3904.108) and Windows 10.
Here is a codepen to demonstrate the problem. The red border around the green box is positioned using the getBoundingClientRect coordinates of the element within the svg. On Windows Chrome, you'll see the result of the top and left values being inflated somehow (first screenshot below). In other browsers it behaves as expected (second screenshot). Is there a better way to achieve this, or is there a reason for why this issue only appears in Windows Chrome?
Update: Adding code snippet.
const svg = document.querySelector('.svg');
const ref = document.querySelector('.ref');
const outer = document.querySelector('.outer');
const refRect = ref.getBoundingClientRect();
console.log('.svg BoundingClientRect', svg.getBoundingClientRect());
console.log('.ref BoundingClientRect', refRect);
$(outer).css('top', refRect.top - window.scrollY)
$(outer).css('left', refRect.left - window.scrollX)
svg {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 200px;
}
.ref {
background: #ccffcc;
width: 100%;
height: 100%;
}
.outer {
border: 1px solid red;
width: 100px;
height: 100px;
position: fixed;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<svg class="svg" version="1.1" xmlns="http://www.w3.org/2000/svg">
<foreignObject x="18%" y="14%" width="100" height="100">
<div class="ref">This should be perfectly surrounded by a red border</div>
</foreignObject>
</svg>
<div class="outer"></div>

Related

Retrieving and setting dynamically loaded image sizes via js

Consider this javascript code:
const profileImageURL = 'https://api.images.com/image/' + id +'/profile';
let profileImage = [];
async function getProfileImage() {
const response = await fetch(profileImageURL);
profileImage = await response.json();
};
and this svelte template code:
<div class="thumbImg">
{#await getProfileImage()}
<div class='spinnerHolder'
in:fade={{duration: 500}}
out:fade={{duration: 500}}
on:introstart={() => doneLoading = false}
on:outroend={() => doneLoading = true}
>
<svg class="spinner" viewBox="0 0 50 50">
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
</svg>
</div>
{:then value}
{#if doneLoading}
<img transition:fade alt='profile-image' src={profileImage.url}/>
{/if}
{/await}
</div>
and css:
.thumbImg {
width: 64px;
height: 64px;
display:inline-block;
background-color: var(--border-color);
border: none;
border-radius: var(--large-radius);
overflow: hidden;
img {
min-width: 64px;
border-radius: var(--large-radius);
border-image-width: 0;
}
}
The above loaded images arrive at different widths and heights. Some portrait and some landscape. They need to all be displayed in a square 1:1 box that is 64px x 64px without white space at the side or the bottom. How can I maintain the aspect ratio of these dynamically loaded images so that they either fill the width or height depending on which is needed ? Ideally they would also be offset so they are centered in the 1:1 box.
Visual Examples of the problem:
Updated answer building on Rich's comment below
You can use the object-fit and object-position CSS properties together to fine tune the way the image is displayed inside its content box:
.thumbImg {
width: 64px;
height: 64px;
display:inline-block;
background-color: var(--border-color);
border: none;
border-radius: var(--large-radius);
overflow: hidden;
img {
height: 64px;
width: 64px;
object-fit: cover; // this will make the image cover the content box, clipping horizontally if the original image is landscape orientation, or vertically if it is portrait orientation
object-position: 50% 50%; // this will center the image in the content box (i.e. clip on the left & right if source is landscape and clip top & bottom if source is portrait)
}
}
With this, you do not need to modify your code any further.
Original answer, using CSS background images
You can achieve the desired positioning by using background image CSS properties rather than an img tag, for example:
.avatar {
height: 64px;
width: 64px;
background-size: cover; // this will make the image cover the div, clipping horizontally if the original image is landscape orientation, or vertically if it is portrait orientation
background-position: 50% 50%; // this will center the image (i.e. clip on the left & right if source is landscape and clip top & bottom if source is portrait)
}
With this, all you have to do is replace your <img> tag with a <div class="avatar"> tag and set the background image source dynamically:
<div class="thumbImg">
{#await getProfileImage()}
<div class='spinnerHolder'
in:fade={{duration: 500}}
out:fade={{duration: 500}}
on:introstart={() => doneLoading = false}
on:outroend={() => doneLoading = true}
>
<svg class="spinner" viewBox="0 0 50 50">
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
</svg>
</div>
{:then value}
{#if doneLoading}
<div
transition:fade
class="avatar"
alt="Profile image"
style="background-image: url('{profileImage.url}')"
/>
{/if}
{/await}
</div>
Of course the downside is that you're not using a native image tag anymore, and maybe this is important to you. On the upside, it works without having to use javascript-based DOM manipulation.
You can either make the images width and height relative to the parent div by using percentage values or just set the images to the required max-width and max-height
.thumbImg {
max-width: 64px;
max-height: 64px;
display:inline-block;
background-color: var(--border-color);
border: none;
border-radius: var(--large-radius);
overflow: hidden;
img {
max-width: 100%; //relative to the parent DIV
border-radius: var(--large-radius);
border-image-width: 0;
}
}

How to get coordinates of element from top of element to container?

I am trying to get the coordinates of the box on the image. The coordinates should be based on the image itself and not on the screen size. I am currently using getBoundingClientRect(). How do I retrieve the coordinates based on the image, the box is on, rather than the window size?
CODE that I've tried:
var rect = div[index].getBoundingClientRect();
I found this post on SO : How to get xy coordinates of child element from parent element in jquery? but it was 7 years ago...
You would use the difference between the elements position and its containers position. I've used getBoundingClientRect and returned a new DomRect like you were trying but be cautions as there is no support for Internet Expolorer, Edge or Safari at the moment.
const getBoundingClientRect_RelativeToParent = element => {
const domRect = element.getBoundingClientRect(),
parentDomRect = element.parentElement.getBoundingClientRect()
return new DOMRect(domRect.left - parentDomRect.left, domRect.top - parentDomRect.top, domRect.width, domRect.height)
}
console.log(getBoundingClientRect_RelativeToParent(document.querySelector(".child")))
.parent {
width: 500px;
height: 300px;
margin-left: 200px;
margin-top: 200px;
padding-left: 30px;
padding-top: 30px;
background: green;
}
.child {
width: 100px;
height: 50px;
background: red
}
body {
background: blue
}
<div class="parent">
<div class="child">
</div>
</div>

How to place tree or more canvases on top of one another?

I want three or more canvases overlay properly. I found similar topics (How to place two canvases on top of one another?), just can not do it for overlaying all 3 or 4 canvases together. After the 2nd canvas all other canvases renders below the second canvas.
What I want is a canvas for the base layer. A canvas for the color map and a canvas for the information, which i can click on the 3rd canvas and request the object
Code i'm using:
<style>
#wrapper{ position: relative; }
.canvas { position: absolute; top: 0; left: 0; }
</style>
<div id="wrapper">
<canvas class="canvas" id="canvast" style="z-index: -2;"></canvas>
<canvas class="canvas" id="canvast1" style="z-index: 1;"></canvas>
<canvas class="canvas" id="canvast2" style="z-index: -2;"> </canvas>
</div>
Note: fabric.js library canvas code to fill the canvases
canvas = new fabric.Canvas("canvast"); //(canvast1 etc.)
canvas.renderOnAddRemove = false; // faster loading of canvas
canvas.add(RectangleObject); //adding object
canvas.renderAll(); //rendering canvas
I don't know fabric.js, so I don't know how helpful my answer can be.
I made this jsfiddle : https://jsfiddle.net/xn78jzg3/4/
You can see layering three is not a Problem, even with most of your code (only thing I changed was the z-index on canvast2 from -2 to 2 and adding some Styling:
.canvas {
position: absolute;
top: 0;
left: 0;
border: #333 solid 2px;
width: 200px;
height: 200px;
}
).
So whatever goes wrong with the rendering of your 3rd Canvas, the error might come from fabric.js.

HTML content after (under) canvas element

I am using this code http://cssdeck.com/labs/pa0yqlki that displays a canvas covering the size of the browser's window.
I am able to display content on top of the canvas (by using absolute positioning and z-index: -1)
What I am not able to do is add content AFTER the canvas.
Once the canvas ends, and so does the window, I want to have an <h1> lets say. So a scroll bar should appear when the page is loaded I should be able to scroll a bit more so that I see the <h1>.
Any ideas?
Okay, thanks to markE's reply I was able to achieve what I wanted.
[...] <canvas> </canvas>
<h1 id="myText"> Text </h1> [...]
this is the part of my HTML. The "myText" will be displayed under the canvas based on the size of the window.
To achieve that I added the following code in the CSS.
#myText
{
padding-top: 100vh;
}
You can achieve this with playing with CSS positions;
Try something like this:
<div>
<canvas width="300" height="200" class="custom-canvas" />
<div class="text">
<div class="main">20 %</div>
<div class="head">Completed</div>
</div>
</div>
SCSS:
.custom-canvas{
position: relative;
clear: both;
width: 450px;
height: 200px;
}
.custom-canvas {
.text {
bottom: 13px;
position: absolute;
left: 6px;
right: 0;
text-align: center;
}
.head {
margin-top: 14px;
}
}
Play with left,right,top and bottom to achieve the exact position.

How to add a tooltip to an svg graphic?

I have a series of svg rectangles (using D3.js) and I want to display a message on mouseover, the message should be surrounded by a box that acts as background. They should both be perfectly aligned to each other and to the rectangle (on top and centered). What is the best way to do this?
I tried adding an svg text using the "x", "y", "width" and "height" attributes, and then prepending an svg rect. The problem is that the reference point for the text is in the middle (since I want it centered aligned I used text-anchor: middle), but for the rectangle it's the top left coordinate, plus I wanted a bit of margin around the text which makes it kind of a pain.
The other option was using an html div, which would be nice, because I can add the text and padding directly but I don't know how to get the absolute coordinates for each rectangle. Is there a way to do this?
Can you use simply the SVG <title> element and the default browser rendering it conveys? (Note: this is not the same as the title attribute you can use on div/img/spans in html, it needs to be a child element named title)
rect {
width: 100%;
height: 100%;
fill: #69c;
stroke: #069;
stroke-width: 5px;
opacity: 0.5
}
<p>Mouseover the rect to see the tooltip on supporting browsers.</p>
<svg xmlns="http://www.w3.org/2000/svg">
<rect>
<title>Hello, World!</title>
</rect>
</svg>
Alternatively, if you really want to show HTML in your SVG, you can embed HTML directly:
rect {
width: 100%;
height: 100%;
fill: #69c;
stroke: #069;
stroke-width: 5px;
opacity: 0.5
}
foreignObject {
width: 100%;
}
svg div {
text-align: center;
line-height: 150px;
}
<svg xmlns="http://www.w3.org/2000/svg">
<rect/>
<foreignObject>
<body xmlns="http://www.w3.org/1999/xhtml">
<div>
Hello, <b>World</b>!
</div>
</body>
</foreignObject>
</svg>
…but then you'd need JS to turn the display on and off. As shown above, one way to make the label appear at the right spot is to wrap the rect and HTML in the same <g> that positions them both together.
To use JS to find where an SVG element is on screen, you can use getBoundingClientRect(), e.g. http://phrogz.net/svg/html_location_in_svg_in_html.xhtml
The only good way I found was to use Javascript to move a tooltip <div> around. Obviously this only works if you have SVG inside an HTML document - not standalone. And it requires Javascript.
function showTooltip(evt, text) {
let tooltip = document.getElementById("tooltip");
tooltip.innerHTML = text;
tooltip.style.display = "block";
tooltip.style.left = evt.pageX + 10 + 'px';
tooltip.style.top = evt.pageY + 10 + 'px';
}
function hideTooltip() {
var tooltip = document.getElementById("tooltip");
tooltip.style.display = "none";
}
#tooltip {
background: cornsilk;
border: 1px solid black;
border-radius: 5px;
padding: 5px;
}
<div id="tooltip" display="none" style="position: absolute; display: none;"></div>
<svg>
<rect width="100" height="50" style="fill: blue;" onmousemove="showTooltip(evt, 'This is blue');" onmouseout="hideTooltip();" >
</rect>
</svg>
You can use the title element as Phrogz indicated. There are also some good tooltips like jQuery's Tipsy http://onehackoranother.com/projects/jquery/tipsy/ (which can be used to replace all title elements), Bob Monteverde's nvd3 or even the Twitter's tooltip from their Bootstrap http://twitter.github.com/bootstrap/
On svg, the right way to write the title
<svg>
<title id="unique-id">Checkout</title>
</svg>
check here for more details https://css-tricks.com/svg-title-vs-html-title-attribute/
I came up with something using HTML + CSS only. Hope it works for you
.mzhrttltp {
position: relative;
display: inline-block;
}
.mzhrttltp .hrttltptxt {
visibility: hidden;
width: 120px;
background-color: #040505;
font-size:13px;color:#fff;font-family:IranYekanWeb;
text-align: center;
border-radius: 3px;
padding: 4px 0;
position: absolute;
z-index: 1;
top: 105%;
left: 50%;
margin-left: -60px;
}
.mzhrttltp .hrttltptxt::after {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent #040505 transparent;
}
.mzhrttltp:hover .hrttltptxt {
visibility: visible;
}
<div class="mzhrttltp"><svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="#e2062c" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg><div class="hrttltptxt">علاقه‌مندی‌ها</div></div>
I always go with the generic css title with my setup. I'm just building analytics for my blog admin page. I don't need anything fancy. Here's some code...
let comps = g.selectAll('.myClass')
.data(data)
.enter()
.append('rect')
...styling...
...transitions...
...whatever...
g.selectAll('.myClass')
.append('svg:title')
.text((d, i) => d.name + '-' + i);
And a screenshot of chrome...
I use heroicons for the project I am working on. (This is JSX format) I will handle the tooltip issue with this code.
<svg className="h-6 w-6">
<title>{reasons.join(" ")}</title>
<QuestionMarkCircleIcon className={style} />
</svg>

Categories