So far, this is what I have concluded: it is impossible to not cause the other div
s to move while overlaying an element with an initially not absolutely positioned element. As a result, I've decided to create a copy element that is absolutely positioned that has exactly the same width of the original element. This is what I changed in your JS:
'use strict';
const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ac auctor augue mauris augue neque gravida in fermentum et. Accumsan in nisl nisi scelerisque. Sed pulvinar proin gravida hendrerit lectus.';
const items = Array(30).fill(0)
.map(() => Math.ceil(lorem.length * Math.random()))
.map(idx => lorem.substring(0, idx))
.map(txt => `<div class="lorem-card"><div class="lorem-card--real">${txt}</div><div class="lorem-card--substitute">${txt}</div></div>`)
.join('');
const html = `<div class="cards-container">${items}</div>`;
document.body.innerHTML += html;
And your CSS:
.lorem-card {
position: relative;
flex-grow: 1;
max-width: 150px;
max-height: 200px;
margin: 15px;
overflow: hidden;
}
.lorem-card--real {
max-width: 150px;
max-height: 200px;
padding: 15px;
border: 3px solid #008CBA;
background: #FFF;
text-overflow: ellipsis;
}
.lorem-card--substitute {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 15px;
background: #FFF;
border: 3px solid #008CBA;
}
.lorem-card:hover {
overflow: visible;
}
.lorem-card:hover .lorem-card--real {
opacity: 0;
}
.lorem-card:hover .lorem-card--substitute{
height: auto;
overflow: visible;
border: 3px solid red;
z-index: 100;
}
The idea is to create a container for two Lorem cards
. One that has a set width and height (200px and 150px at maximum) and one that can expand dynamically and overlay other elements without causing other divs
to move (by using position: absolute
). As can be seen, the effect is that the absolutely positioned element only expands vertically downwards. Furthermore, as height
is set to auto
, CSS transition does not work, which leads to the following suggestion.
My current idea, if you want it to expand both horizontally both ways and vertically downwards, is to compute the total width of the overall string (arranged in one horizontal line) and the height the largest character. Then, use some math to constraint the div's width and height ratio to 4:3 (while handling edge cases, e.g. specifying min-width and min-height) until you find an area that fits the whole text. This will allow you to have the exact width and height needed and as you can specify the exact width and height, you can also use CSS transition to smoothly expand the div.
Here is the new code: here
EDIT
After fiddling some more, here's the final code. In summary, what I added:
- A dummy function that you can configure to decide how much the
div
should expand
- Detection of whether an
div
needs to expand based on its TextNode
's bounding rectangle
- A smooth transition (the timeout logic has not been tested with edge cases)
Here's the full-working example:
'use strict';
const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ac auctor augue mauris augue neque gravida in fermentum et. Accumsan in nisl nisi scelerisque. Sed pulvinar proin gravida hendrerit lectus.';
const items = Array(30).fill(0)
.map(() => Math.ceil(lorem.length * Math.random()))
.map(idx => lorem.substring(0, idx))
.map(txt => `<div class="lorem-card"><div class="lorem-card--real">${txt}</div><div class="lorem-card--substitute">${txt}</div></div>`)
.join('');
const html = `<div class="cards-container">${items}</div>`;
document.body.innerHTML += html;
function getTextWidthAndHeight() {
return [210, 280]
}
let cards = document.querySelectorAll('.lorem-card')
let resetCardOverflow
for (let card of cards) {
card.addEventListener('mouseover', e => {
clearTimeout(resetCardOverflow)
card.style.overflow = 'visible'
// Configure "getTextWidthAndHeight" to fit the new rectangle size needs
let size = getTextWidthAndHeight()
let targetReal = card.querySelector('.lorem-card--real')
let targetSubstitute = card.querySelector('.lorem-card--substitute')
let textNode
for (let child of targetReal.childNodes) {
if (child.nodeName == '#text') {
textNode = child
break
}
}
// Get "height" of textNode's bounding box
let textHeight = 0;
let range = document.createRange();
range.selectNodeContents(textNode);
if (range.getBoundingClientRect) {
var rect = range.getBoundingClientRect();
if (rect) {
textHeight = rect.bottom - rect.top;
}
}
// If text exceeds box height (padding considered)
if (textHeight > 200 - 30) {
targetSubstitute.style.width = `${size[0]}px`
targetSubstitute.style.height = `${size[1]}px`
targetSubstitute.style.transform = `translateX(-30px)`
}
})
card.addEventListener('mouseleave', e => {
let targetSubstitute = card.querySelector('.lorem-card--substitute')
targetSubstitute.style.width = ''
targetSubstitute.style.height = ''
targetSubstitute.style.transform = ''
resetCardOverflow = setTimeout(() => {
card.style.overflow = ''
}, 200)
})
}
* {
box-sizing: border-box;
}
.cards-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-items: flex-start;
align-content: space-between;
}
.lorem-card {
position: relative;
flex-grow: 1;
max-width: 150px;
max-height: 200px;
margin: 15px;
overflow: hidden;
}
.lorem-card--real {
max-width: 150px;
max-height: 200px;
padding: 15px;
border: 3px solid #008CBA;
background: #FFF;
overflow: hidden;
}
.lorem-card--substitute {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 15px;
background: #FFF;
border: 3px solid #008CBA;
transition: all .2s ease;
overflow: hidden;
}
.lorem-card:hover .lorem-card--real {
opacity: 0;
}
.lorem-card:hover .lorem-card--substitute{
border: 3px solid red;
z-index: 1000;
}
<html>
<head>
<meta charset="utf-8">
<title>Cards demo</title>
<link rel="stylesheet" type="text/css" href="./cards.css">
</head>
<body>
</body>
<script src="./gen.js"></script>
</html>
In case you need the JS fiddle code: here