2
votes

There are N cards of the same class, each with a different amount of text inside. All cards are in the same flex container (row, wrap). In some cards part of the text is hidden because it exceeds the dimension limits for the class. I want such cards to expand and show the entire content on mouse hover. So far I can make it either to overlay the text only (as in the image) or to expand while moving the neighbors. Instead, such card should overlay above neighbor cards without moving them. enter image description here

Ideally, the hovered card should expand both horizontally (if possible, symmetrically, otherwise to the left or to the right only), and vertically downwards.

Here is a very simple example created for this question. Each card contains a random length substring of lorem ipsum.

JavaScript, generates cards with random amount of text:

'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. Tortor id aliquet lectus proin nibh nisl condimentum. Erat pellentesque adipiscing commodo elit at imperdiet dui accumsan sit.';

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">${txt}</div>`)
  .join('');

const html = `<div class="cards-container">${items}</div>`;
document.body.innerHTML += html;

html is a skeleton only because the content is generated by JavaScript

<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>

And here is current CSS:

.cards-container {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  align-content: space-between;
}

.lorem-card {
  flex-grow: 1;
  background-color: white;
  border: 3px solid #008CBA;
  padding: 15px;
  margin: 15px;
  max-width: 150px;
  max-height: 200px;
  overflow: hidden;
  opacity: 1;
}

.lorem-card:hover {
  flex-grow: 10;
  height: auto;
  width: auto;
  overflow: visible;
  border: 3px solid red;
  z-index: 100;
}

https://jsfiddle.net/fpmuc5Lz/3/

1
Did you tried max-height: 100% on hover?Ahed Kabalan
I really wouldn't do this on hover unless you're going to put up an overlay through JS, or clone the node and place it on top outright. Otherwise you're going to flicker constantly in every scenario.zfrisch
a side question: what it the reason for the votes to close this question?Serg
Does my answer answers your question?Richard

1 Answers

1
votes

So far, this is what I have concluded: it is impossible to not cause the other divs 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