Flex transition: Stretch (or shrink) to fit content - javascript
I have coded a script (with the help of a user here) which allows me to expand a selected div and make the other divs behave accordingly by stretching equally to fit the remaining space (except the first one which width is fixed).
And here is a picture of what I want to achieve:
For that I use flex and transitions.
It works well, but the jQuery script specifies a "400%" stretch value (which is great for testing).
Now I would like the selected div to expand/shrink to exactly fit the content instead of the "400%" fixed value.
I have no idea how I could do that.
Is it possible ?
I tried to clone the div, fit it to the content, get its value and then use this value to transition BUT this means I have an initial width in percentages but a target value in pixels. That doesn't work.
And if I convert the pixel value in percentages, then the result doesn't exactly fit the content for whatever reason.
In all cases, this seems a bit of a complicated way to achieve what I want anyway.
Isn't there any flex property that could be transitioned in order to fit the content of a selected div?
Here is the code (edited/simplified since for a better read) :
var expanded = '';
$(document).on("click", ".div:not(:first-child)", function(e) {
var thisInd =$(this).index();
if(expanded != thisInd) {
//fit clicked fluid div to its content and reset the other fluid divs
$(this).css("width", "400%");
$('.div').not(':first').not(this).css("width", "100%");
expanded = thisInd;
} else {
//reset all fluid divs
$('.div').not(':first').css("width", "100%");
expanded = '';
}
});
.wrapper {
overflow: hidden;
width: 100%;
margin-top: 20px;
border: 1px solid black;
display: flex;
justify-content: flex-start;
}
.div {
overflow: hidden;
white-space: nowrap;
border-right: 1px solid black;
text-align:center;
}
.div:first-child {
min-width: 36px;
background: #999;
}
.div:not(:first-child) {
width: 100%;
transition: width 1s;
}
.div:not(:first-child) span {
background: #ddd;
}
.div:last-child {
border-right: 0px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Click on the div you want to fit/reset (except the first div)
<div class="wrapper">
<div class="div"><span>Fixed</span></div>
<div class="div"><span>Fluid (long long long long long text)</span></div>
<div class="div"><span>Fluid</span></div>
<div class="div"><span>Fluid</span></div>
</div>
Here is the jsfiddle:
https://jsfiddle.net/zajsLrxp/1/
EDIT: Here is my working solution with the help of you all (sizes updated on window resize + number of divs and first column's width dynamically calculated):
var tableWidth;
var expanded = '';
var fixedDivWidth = 0;
var flexPercentage = 100/($('.column').length-1);
$(document).ready(function() {
// Set width of first fixed column
$('.column:first-child .cell .fit').each(function() {
var tempFixedDivWidth = $(this)[0].getBoundingClientRect().width;
if( tempFixedDivWidth > fixedDivWidth ){fixedDivWidth = tempFixedDivWidth;}
});
$('.column:first-child' ).css('min-width',fixedDivWidth+'px')
//Reset all fluid columns
$('.column').not(':first').css('flex','1 1 '+flexPercentage+'%')
})
$(window).resize( function() {
//Reset all fluid columns
$('.column').not(':first').css('flex','1 1 '+flexPercentage+'%')
expanded = '';
})
$(document).on("click", ".column:not(:first-child)", function(e) {
var thisInd =$(this).index();
// if first click on a fluid column
if(expanded != thisInd)
{
var fitDivWidth=0;
// Set width of selected fluid column
$(this).find('.fit').each(function() {
var c = $(this)[0].getBoundingClientRect().width;
if( c > fitDivWidth ){fitDivWidth = c;}
});
tableWidth = $('.mainTable')[0].getBoundingClientRect().width;
$(this).css('flex','0 0 '+ 100/(tableWidth/fitDivWidth) +'%')
// Use remaining space equally for all other fluid column
$('.column').not(':first').not(this).css('flex','1 1 '+flexPercentage+'%')
expanded = thisInd;
}
// if second click on a fluid column
else
{
//Reset all fluid columns
$('.column').not(':first').css('flex','1 1 '+flexPercentage+'%')
expanded = '';
}
});
body{
font-family: 'Arial';
font-size: 12px;
padding: 20px;
}
.mainTable {
overflow: hidden;
width: 100%;
border: 1px solid black;
display: flex;
margin-top : 20px;
}
.cell{
height: 32px;
border-top: 1px solid black;
white-space: nowrap;
}
.cell:first-child{
background: #ccc;
border-top: none;
}
.column {
border-right: 1px solid black;
transition: flex 0.4s;
overflow: hidden;
line-height: 32px;
text-align: center;
}
.column:first-child {
background: #ccc;
}
.column:last-child {
border-right: 0px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<span class="text">Click on the header div you want to fit/reset (except the first one which is fixed)</span>
<div class="mainTable">
<div class="column">
<div class="cell"><span class="fit">Propriété</span></div>
<div class="cell"><span class="fit">Artisan 45</span></div>
<div class="cell"><span class="fit">Waterloo 528</span></div>
</div>
<div class="column">
<div class="cell"><span class="fit">Adresse</span></div>
<div class="cell"><span class="fit">Rue du puit n° 45 (E2)</span></div>
<div class="cell"><span class="fit">Chaussée de Waterloo n° 528 (E1)</span></div>
</div>
<div class="column">
<div class="cell"><span class="fit">Commune</span></div>
<div class="cell"><span class="fit">Ixelles</span></div>
<div class="cell"><span class="fit">Watermael-Boitsfort</span></div>
</div>
<div class="column">
<div class="cell"><span class="fit">Ville</span></div>
<div class="cell"><span class="fit">Marche-en-Famenne</span></div>
<div class="cell"><span class="fit">Bruxelles</span></div>
</div>
<div class="column">
<div class="cell"><span class="fit">Surface</span></div>
<div class="cell"><span class="fit">120 m<sup>2</sup></span></div>
<div class="cell"><span class="fit">350 m<sup>2</sup></span></div>
</div>
</div>
And here is a fully fledged example at work (styles + padding + more data):
https://jsfiddle.net/zrqLowx0/2/
Thank you all !
It is possible to solve it using max-width and calc().
First, replace width: 100% with flex: 1 for the divs in CSS, so they will grow, which is better in this case. In addition, use transition for max-width.
Now, we have to store some relevant values:
The amount of divs that will be animated (divsLength variable) - 3 in this case.
The total width used for the fixed div and the borders (extraSpace variable) - 39px in this case.
With those 2 variables, we can set a default max-width (defaultMaxWidth variable) to all the divs, as well as using them later. That is why they are being stored globally.
The defaultMaxWidth is calc((100% - extraSpace)/divsLength).
Now, let's enter the click function:
To expand the div, the width of the target text will be stored in a variable called textWidth and it will be applied to the div as max-width. It uses .getBoundingClientRect().width (since it return the floating-point value).
For the remaining divs, it is created a calc() for max-width that will be applied to them.
It is: calc(100% - textWidth - extraScape)/(divsLength - 1).
The calculated result is the width that each remaining div should be.
When clicking on the expanded div, that is, to return to normal, the default max-width is applied again to all .div elements.
var expanded = false,
divs = $(".div:not(:first-child)"),
divsLength = divs.length,
extraSpace = 39, //fixed width + border-right widths
defaultMaxWidth = "calc((100% - " + extraSpace + "px)/" + divsLength + ")";
divs.css("max-width", defaultMaxWidth);
$(document).on("click", ".div:not(:first-child)", function (e) {
var thisInd = $(this).index();
if (expanded !== thisInd) {
var textWidth = $(this).find('span')[0].getBoundingClientRect().width;
var restWidth = "calc((100% - " + textWidth + "px - " + extraSpace + "px)/" + (divsLength - 1) + ")";
//fit clicked fluid div to its content and reset the other fluid divs
$(this).css({ "max-width": textWidth });
$('.div').not(':first').not(this).css({ "max-width": restWidth });
expanded = thisInd;
} else {
//reset all fluid divs
$('.div').not(':first').css("max-width", defaultMaxWidth);
expanded = false;
}
});
.wrapper {
overflow: hidden;
width: 100%;
margin-top: 20px;
border: 1px solid black;
display: flex;
justify-content: flex-start;
}
.div {
overflow: hidden;
white-space: nowrap;
border-right: 1px solid black;
text-align:center;
}
.div:first-child {
min-width: 36px;
background: #999;
}
.div:not(:first-child) {
flex: 1;
transition: max-width 1s;
}
.div:not(:first-child) span {
background: #ddd;
}
.div:last-child {
border-right: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
Click on the div you want to fit/reset (except the first div)
<div class="wrapper">
<div class="div"><span>Fixed</span></div>
<div class="div"><span>Fluid (long long long long text)</span></div>
<div class="div"><span>Fluid</span></div>
<div class="div"><span>Fluid</span></div>
</div>
This approach behaves dynamically and should work on any resolution.
The only value you need to hard code is the extraSpace variable.
You need to deal with the width or calc functions. Flexbox would have a solution.
To make all divs equal (not first one) we use flex: 1 1 auto.
<div class="wrapper">
<div class="div"><span>Fixed</span></div>
<div class="div"><span>Fluid (long long long long text)</span></div>
<div class="div"><span>Fluid</span></div>
<div class="div"><span>Fluid</span></div>
</div>
Define flex rules for your normal div and selected div. transition: flex 1s; is your friend. For selected one we don't need flex grow so we use flex: 0 0 auto;
.wrapper {
width: 100%;
margin-top: 20px;
border: 1px solid black;
display: flex;
}
.div {
white-space: nowrap;
border-right: 1px solid black;
transition: flex 1s;
flex: 1 1 auto;
}
.div.selected{
flex: 0 0 auto;
}
.div:first-child {
min-width: 50px;
background: #999;
text-align: center;
}
.div:not(:first-child) {
text-align: center;
}
.div:last-child {
border-right: 0px;
}
div:not(:first-child) span {
background: #ddd;
}
Add selected class each time when the user clicks a div. You can also use toggle for the second click so you can save selected items in a map and you can show multiple selected items (not with this code example of course).
$(document).on("click", ".div:not(:first-child)", function(e) {
const expanded = $('.selected');
$(this).addClass("selected");
if (expanded) {
expanded.removeClass("selected");
}
});
https://jsfiddle.net/f3ao8xcj/
After a few trial versions, this seems to be my shortest and most straighforward solution.
All that essentially needs to be done is have Flexbox stretch the <div> elements to their limits by default, but when <span> clicked, constraint the stretch of the <div> to <span> width ...
pseudo code:
when <span> clicked and already toggled then <div> max-width = 100%, reset <span> toggle state
otherwise <div> max-width = <span> width, set <span> toggle state
I have split the CSS into a 'relevant mechanism' and 'eye-candy only' section for easy reading (and code recyling).
The code is heavily commented, so not much text here...
Quirk Somehow there is an extra delay in the transition when switching the div from max-width: 100% to max-width = span width. I've checked this behaviour in Chrome, Edge, IE11 and Firefox (all W10) and all seem to have this quirk. Either some browser internal recalc going on, or maybe the transition time is used twice ('feels like'). Vice Versa, oddly enough, there is no extra delay.
However, with a short transition time (e.g. 150ms, as I am using now) this extra delay is not/hardly noticable. (Nice one for another SO question...)
$(document).on('click', '.wrapper>:not(.caption) span', function (e) {
// Save the current 'toggle' status
var elemToggled = e.target.getAttribute('toggled');
// Set parent max-width to maximum space or constraint to current child width
e.target.parentElement.style.maxWidth =
(elemToggled=="true") ? '100%' : parseFloat(window.getComputedStyle(e.target).width) + 'px';
// (Re)set child toggle state
e.target.setAttribute('toggled', (elemToggled=="true") ? false : true);
});
/*********************/
/* Wrapper mechanism */
/*********************/
.wrapper { /* main flexible parent container */
display : flex; /* [MANDATORY] Flexbox Layout container, can't FBL without */
flex-wrap: nowrap; /* [MANDATORY] default FBL, but important. wrap to next line messes things up */
flex-grow: 1; /* [OPTIONAL] Either: if '.wrapper' is a FBL child itself, allow it to grow */
width : 100%; /* [OPTIONAL] or : full parent width */
/* (Maybe a fixed value, otherwise redundant here as 'flex-grow' = 1) */
}
/* generic rule */
.wrapper>* { /* flexed child containers, being flexible parent containers themselves */
flex-grow : 1; /* [MANDATORY] important for this mechanism to work */
overflow: hidden; /* [MANDATORY] important, otherwise output looks messy */
display: flex; /* [MANDATORY] for FBL stretching */
justify-content: center;/* [MANDATORY] as per SOQ */
max-width : 100%; /* [OPTIONAL/MANDATORY], actually needed to trigger 'transition' */
}
/* exception to the rule */
.wrapper>.fixed { /* fixed child container */
flex-grow: 0; /* [MANDATORY] as per SOQ, don't allow grow */
}
/******************/
/* Eye-candy only */
/******************/
.wrapper {
border: 1px solid black;
}
.wrapper>:not(.fixed) {
transition: max-width 150ms ease-in-out;
}
.wrapper>:not(:last-child){
border-right: 1px solid black;
}
/* generic rule */
.wrapper>*>span {
white-space: nowrap;
background-color: #ddd;
}
/* exception to the rule */
.wrapper>.fixed>span {
background-color: #999;
}
/* debug helper: show all elements with outlines (put in <body>) */
[debug="1"] * { outline: 1px dashed purple }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="wrapper">
<div class="fixed"><span>Fixed</span></div>
<div><span>Fluid (long long long long long text)</span></div>
<div><span>Fluid</span></div>
<div><span>Fluid</span></div>
</div>
UPDATE
New version that resets all other <div>. I truly hate the jumpiness, but that is due to Flexbox stretching and the transition value. Without transition no jumps visible. You need to try out what works for you.
I only added document.querySelectorAll() to the javascript code.
$(document).on('click', '.wrapper>:not(.caption) span', function (e) {
var elemToggled = e.target.getAttribute('toggled'); // Toggle status
var elemWidth = parseFloat(window.getComputedStyle(e.target).width); // Current element width
// reset ALL toggles but 'this'...
document.querySelectorAll('.wrapper>:not(.caption) span')
.forEach( function (elem,idx) {
if (elem != this){
elem.parentElement.style.maxWidth = '100%';
elem.setAttribute('toggled',false);
};
});
// Set parent max-width to maximum space or constraint to current child width
e.target.parentElement.style.maxWidth =
(elemToggled=="true") ? '100%' : parseFloat(window.getComputedStyle(e.target).width) + 'px';
// (Re)set child toggle state
e.target.setAttribute('toggled', (elemToggled=="true") ? false : true);
});
/*********************/
/* Wrapper mechanism */
/*********************/
.wrapper { /* main flexible parent container */
display : flex; /* [MANDATORY] Flexbox Layout container, can't FBL without */
flex-wrap: nowrap; /* [MANDATORY] default FBL, but important. wrap to next line messes things up */
flex-grow: 1; /* [OPTIONAL] Either: if '.wrapper' is a FBL child itself, allow it to grow */
width : 100%; /* [OPTIONAL] or : full parent width */
/* (Maybe a fixed value, otherwise redundant here as 'flex-grow' = 1) */
}
/* generic rule */
.wrapper>* { /* flexed child containers, being flexible parent containers themselves */
flex-grow : 1; /* [MANDATORY] important for this mechanism to work */
overflow: hidden; /* [MANDATORY] important, otherwise output looks messy */
display: flex; /* [MANDATORY] for FBL stretching */
justify-content: center;/* [MANDATORY] as per SOQ */
max-width : 100%; /* [OPTIONAL/MANDATORY], actually needed to trigger 'transition' */
}
/* exception to the rule */
.wrapper>.fixed { /* fixed child container */
flex-grow: 0; /* [MANDATORY] as per SOQ, don't allow grow */
}
/******************/
/* Eye-candy only */
/******************/
.wrapper {
border: 1px solid black;
}
.wrapper>:not(.fixed) {
transition: max-width 150ms ease-in-out;
}
.wrapper>:not(:last-child){
border-right: 1px solid black;
}
/* generic rule */
.wrapper>*>span {
white-space: nowrap;
background-color: #ddd;
}
/* exception to the rule */
.wrapper>.fixed>span {
background-color: #999;
}
/* show all elements with outlines (put in <body>) */
[debug="1"] * { outline: 1px dashed purple }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="wrapper">
<div class="fixed"><span>Fixed</span></div>
<div><span>Fluid (long long long long long text)</span></div>
<div><span>Fluid</span></div>
<div><span>Fluid</span></div>
</div>
If you need only one row, there is a simpler solution based on this code : https://jsfiddle.net/jpeter06/a5cu52oy/
with the css flex modified for columns instead of rows :
.container {
flex-grow: 10;
flex-shrink: 0;
flex-basis: auto;
display: flex;
flex-direction: row;
width: 100%;
}
.item { min-width:30px;
flex-basis:30px;
overflow-x:hidden;
transition: flex-basis 500ms ease-in-out;
}
.expanded {
flex-basis: 20em;
}
html, body {
width: 100%; height: 100%;
margin: 0; padding: 0; border: 0; overflow: hidden;
}
html code :
<div class="container">
<div class="item" style="background: red">a<br/>a<br/>a<br/>a<br/>a<br/>a<br/>a<br/>a<br/></div>
<div class="item" style="background: green">b<br/>b<br/>b<br/>b</div>
<div class="item" style="background: blue">c<br/>c<br/>c<br/>c</div>
</div>
JS code :
$(document).ready(function() {
$(".item").click(function() {
$(this).addClass('expanded');
$(".item").not(this).each(function() {
$(this).removeClass("expanded");
});
});
});
Related
How to line up consecutive divs left and right?
Hello I'm not too good with css/floats/blocks etc. I have this sort of setup <div class="blue">1</div> <div class="red">2</div> <div class="blue">3</div> <div class="red">4</div> <div class="blue">5</div> <div class="red">6</div> I want to style it so 1 and 2 are on the same line (floated left and right), then 3 and 4 are on the same line (floated left and right) and so on and so forth. Currently these elements are created in a loop. I have tried all sorts of methods to get the elements to line up how I want to no avail.
I think you want to make them two per row correct? You can add a flex property to the parent container. Set the flex-wrap to wrap, then add a 50% width on your child elements (taking into account any inherited margin or padding effecting the layouts width to not overflow the flex-wrap), then justify-content to space-between, this will force the elements to their respective sides or a better way to put it is; place the "left over" space the children on that row are not taking up, in the middle of the two elements. * { /* set box-sizing on all elements to border-box */ box-sizing: border-box; } body { /* remove any margin or padding from the body */ margin: 0; padding: 0; } #cont { /* add display flex, flex-wrap and justify-content to space between */ display: flex; flex-wrap: wrap; justify-content: space-between; } #cont>div { /* Set your child divs to a percentage that will only give you two per row the flex-wrap will push elements down */ width: 50%; } .blue { background-color: lightblue; border: 1px solid darkblue; } .red { background-color: pink; border: 1px solid darkred; text-align: right; /* remove if you want standard left side text-alignment on red elements */ } <div id="cont"> <div class="blue">1</div> <div class="red">2</div> <div class="blue">3</div> <div class="red">4</div> <div class="blue">5</div> <div class="red">6</div> </div> If this is not what you were looking for let me know and I can edit or remove this answer.
Responsive width and height for a group of squares
I'm creating a grid of 1:1 squares. The user can keep adding squares and I want the size of the squares to be maintained at their aspect ratio but resize accordingly. The tricky part is I want the square to always be visible on the page - that is to say that there is no scrolling and the webpage would be responsive with width and height. I have created an example that adds a square every second while testing this. However, I am unable to get it working with the height part. I have been able to get it working with the width. setInterval(() => { // console.log(document.getElementsByClassName('square-container')[0]); document.getElementsByClassName('square-container')[0].innerHTML += ("<div class='square'></div>"); }, 1000); .square-container { display: flex; flex-wrap: wrap; } .square { position: relative; flex-basis: calc(33.333% - 10px); margin: 5px; box-sizing: border-box; background: red; transition: background 1s; } .square::before { content: ""; display: block; padding-top: 100%; } <div class="square-container"> <div class="square"></div> </div> I'm not using any ui libraries like bootstrap, just vanilla html, css and javascript.
Use Float instead of wrap. set the square-container display block. <div> <div id="square-container"> <div id="square"></div> <div id="square"></div> <div id="square"></div> <div id="square"></div> <div id="square"></div> <div id="square"></div> <div id="square"></div> <div id="square"></div> </div> </div> #square-container{ display: block } #square{ float: left; height: 100px; width: 100px; background-color: orangered; margin: 1px; }
Might I suggest a pure javascript approach? Basically just set a inicial value and let javascript do all the calculating every time a square is added. // Function to resize the squares function resizeSquares(){ // Get the squares squares = document.getElementsByClassName('square'); for (i=0;i<squares.length;i++) { // Set the width of the square according to the window width squares[i].style.width = document.body.clientWidth / squarePerRow + 'px'; // Set the height of the square to its width to keep the aspect ratio squares[i].style.height = squares[i].style.width; } } // Set initial square capacity for each row squarePerRow = 3; // Inicialize the size of the squares resizeSquares(); setInterval(function(){ // Add a square document.getElementById('container').innerHTML += '<div class="square"></div>'; // Check if squares exceeds the window if(document.body.clientHeight > document.documentElement.clientHeight) { // If they do, add one square capacity per row squarePerRow = squarePerRow + 1; } // Resize the squares resizeSquares() }, 1000) #container { display: flex; flex-wrap: wrap; } .square { background: red; border: 5px solid white; box-sizing: border-box; transition: all, 0.5s; } <div id="container"> <div class="square"></div> </div>
What is the fastest way to track list of top visible divs under the fixed header?
I'm working on layout, which you may visually imagine using a link below: Layout Fixed header is a div with position: fixed. Which means, that scrolling process hides elements under it (that's why it's transparent on my image for illustration purposes). I need to track a list of vertically top elements (other divs) on a page, which are still visible. Using my layout example, I need a list of red elements. They may be positioned in any way. Expected result is JavaScript function, which returns an array of elements. Behind the scenes, the solution should definitely track window.onScroll event and somehow effectively maintain ability to fetch the result in the fastest possible way.
If all the red divs are of the same parent you can do this: $(window).scroll(function() { $('#parentDiv').children().each(function () { if($(this).position().top + $(this).outerHeight() < $("#parentDiv").position().top + $("#parentDiv").outerHeight()) { // add them to array } }); }) This will run every time the page is scrolled, the parent div's children are then iterated through (this is costly, but if you dynamically add elements to the page then this will also work with slight tweaking). Check each child's bottom position and compare that with the bottom position of the fixed header (top + outerHeight), if true then the elements bottom is less than the header's bottom and therefore underneath the header. Do as you wish after that, adding their Jquery references into an array if you wish.
I created the function getDivs which will return the jQuery object with the specific divs. You can edit the $allDivs variable inside the function to any jQuery object you wish to check Please check my snippet below: var $allDivs = $('#container > .column > div'), // Divs you want to check $header = $('#header'); function getDivs() { return $allDivs .filter(function() { var $this = $(this), top = parseInt($this.offset().top, 10), bottom = top + $this.outerHeight(), scrTop = $header.offset().top + $header.outerHeight(); if (top <= scrTop && bottom >= scrTop) return true; else return false; }); } $allDivs.removeClass('under'); getDivs() .addClass('under'); $(window).scroll(function() { $allDivs.removeClass('under'); getDivs() .addClass('under'); }) body { height: 1300px; } #header { width: 100%; padding: 50px; box-sizing: border-box; position: fixed; top: 0px; left: 0px; background: rgba(0, 0, 0, 0.3); } .column { float: left; display: flex; flex-flow: column wrap; justify-content: flex-start; align-items: flex-start; align-content: flex-start; } .column > div { height: 100px; width: 100px; border: 1px solid green; flex: 1 1 100px; } #col1 { margin: 100px 0 0; } #col3 { margin: 30px 0 0; } #col1 > div { flex: 1 1 75px; } #col2 > div { flex: 1 1 200px; } .under { background: red; } <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <div id="header"> Header </div> <div id="container"> <div id="col1" class="column"> <div></div> <div></div> <div></div> <div></div> </div> <div id="col2" class="column"> <div></div> <div></div> <div></div> <div></div> </div> <div id="col3" class="column"> <div></div> <div></div> <div></div> <div></div> </div> </div>
Animating height property :: HTML + CSS + JavaScript
I have noticed this 'issue' lately when trying some stuff. Say I want to create a drop-down menu or an accordion. This is my HTML: <div class="wrapper" onclick="toggle()"> I want to be animated! <div class="content"> Was I revealed in a timely fashion? </div> </div> Stylesheets: .wrapper { background: red; color: white; height: auto; padding: 12px; transition: 2s height; } .content { display: none; } .content.visible { display: block; } JavaScript: function toggle () { var content = document.getElementsByClassName('content')[0]; var test = content.classList.contains('visible'); test ? content.classList.remove('visible') : content.classList.add('visible'); } I am trying to achieve a nice, smooth animation when we toggle the state of the content. Obviously this does not work. Anyone can explain to me why it does not work and how to fix it? Many thanks. Link to the JSFiddle.
First things first, some CSS properties CANNOT be transitioned, display is one of them, additionally only discrete values can be transitioned, so height: auto cannot as well. In your case the problem is with height: auto, while there are a few hacks for doing this, if you are just showing and hiding stuff, why not add, and use jQuery's toggle instead? $(".content").toggle("slow"); jsFiddle --EDIT (without jQuery)-- Because it's the auto that is giving us problems, we can use javascript to replace auto with a value in pixels and then use the css transition normally, if your content doesn't have a scroll, we can easily take that value from the scrollHeight property: function toggle () { var content = document.getElementsByClassName('content')[0]; var test = content.classList.contains('visible'); console.log(test); if (test) { content.classList.remove('visible') content.style.height = "0px"; } else { content.classList.add('visible'); content.style.height = content.scrollHeight + "px"; } } Css .wrapper { background: red; color: white; height: auto; padding: 12px; transition: 2s height; } .content { height: 0px; display: block; transition: 2s height; overflow: hidden; } /* totally removed .content.visible */ jsFiddle
List rotation with limited elements
I have div container with list (cards) inside. When I hover it, cards start to moving (translateX animation). container's width is 300px, elements count in container:3, each element width:100px. So you can see 3 elements in container together overflow:hidden. What I want to make?, is that when there is no element to show translateX animation -100px = 100px blank space after third element, it start from 1 elements in the list immediately after last, with no blank space. For now, I have no idea how it could be done without duplicates and etc. Here is what I have at the moment: Fiddle (Hover cards to see translation animation) UPD 1: The code and data (cards count, container size) was taken for example, i'll try to explain better what i want: My goal is to built list of cards and after button was pressed, the list will start moving (like in example with translateX animation) for some time (for example translateX: 12491px, animation-duration: 15s;) and stops. But problem is that amount of crads in the list would be in range of 3-40 cards (each card is 100px width & height). So, when i'll set translateX: 12491px for example, it will be out of range and after the last card in the list would appear blank space. I want first and last card to be tied somehow and after the last card immediately appears first card in the list and etc.. Maybe i am searching for solution in a wrong way, but i guess you understand the main idea. UPD 2: I found that cs:go uses animation that i wanted to write on html\css\js. Here is video: youtube.com html: <div class="container"> <div class="cards"> <div class="card"> 1 </div> <div class="card"> 2 </div> <div class="card"> 3 </div> </div> </div> css: .container { width:300px; height: 100px; border: 2px solid black; overflow: hidden; } .card { float:left; height: 100px; width: 100px; background-color:blue; box-sizing: border-box; border: 2px solid red; color: white; font-size: 23px; } .cards:hover { transform: translateX(-100px); transition-duration: 3s; animation-duration: 3s; animation-fill-mode: forwards; }
start from 1 elements in the list immediately after last, with no blank space This is beyond CSS and you will need Javascript for that. Because, you have tagged the question with Javascript and not jQuery, my answer would be limited to pure Javascript only. Look ma, no JQuery ;) I have no idea how it could be done without duplicates Here is a DIY (do it yourself) idea.. The main trick is to show at least one item less than the total you have. If you have 3 cards, show only 2. If you have 4 cards, show only 3. Why, because you need to re-position a card when it goes out of view and wrap it back at the end. If you show exactly the same number of cards that you have, then you cannot break half-a-card and wrap it and you will see some blank space until the first one goes out of view. You get the idea? Do not use translate or you will end up complicating things for yourself while scripting it out. Keep things simple. Do not use a wrapper for your cards. Why? Because, we will be re-positioning the cards which have gone out of view. When we do that, the next card will take up its place and immediately go out of view making things further difficult for you. To keep things simple, arrange your cards with absolute positioning relative to its container. To start with, let all cards stack up at top:0; and left: 0;. Next wire-up Javascript to position the left property based on the width of each card and arrange them linearly. Use requestAnimationFrame to control the animation. Keep track of the left-most card and its left position. When this goes out of view (which is 0 minus width), appendChild this card to its container. This will move the card to the end of cards. Also, change the left property to it based on the last card in the list. That' all there is to it. Below is a demo. To make it easy for you to experiment, I have used a settings object to keep the configurable properties which you can easily tweak and see. Look closely at the code and you will find it simple to understand. You can set the iterations settings to 0 to make the animation infinite. Also, note that you do not need to duplicate or fake the cards. Try the demo and add as many cards you want to. The inline code comments in the snippet, will further help you understand each line of code and relate to the steps above. Snippet: var list = document.querySelector('.cardList'), // cache the container cards = document.querySelectorAll('.card'), // cache the list of cards start = document.getElementById('start'), // buttons stop = document.getElementById('stop'), reset = document.getElementById('reset'), raf, init = 0, counter = 0, lastCard, currentIteration = 0, // general purpose variables settings = { // settings object to help make things configurable 'width': 100, 'height': 100, 'speed': 2, 'iterations': 2, 'count': cards.length } ; start.addEventListener('click', startClick); // wire up click event on buttons stop.addEventListener('click', stopClick); reset.addEventListener('click', resetClick); initialize(); // initialize to arrange the cards at start function initialize() { // loop thru all cards and set the left property as per width and index position [].forEach.call(cards, function(elem, idx) { elem.style.left = (settings.width * idx) + 'px'; }); init = -(settings.width); // initialize the view cutoff lastCard = cards[settings.count - 1]; // identify the last card counter = 0; currentIteration = 0; // reset some counters settings.speed = +(document.getElementById('speed').value); settings.iterations = +(document.getElementById('iter').value); } function startClick() { initialize(); raf = window.requestAnimationFrame(keyframes); // start animating } function stopClick() { window.cancelAnimationFrame(raf); } // stop animating function resetClick() { // stop animating and re-initialize cards to start again window.cancelAnimationFrame(raf); document.getElementById('speed').value = '2'; document.getElementById('iter').value = '2'; initialize(); } // actual animation function function keyframes() { var currentCard, currentLeft = 0, newLeft = 0; // iterate all cards and decrease the left property based on speed [].forEach.call(cards, function(elem, idx) { elem.style.left = (parseInt(elem.style.left) - settings.speed) + 'px'; }); currentCard = cards[counter]; // identify left-most card currentLeft = parseInt(currentCard.style.left); // get its left position if (currentLeft <= init) { // check if it has gone out of view // calculate position of last card newLeft = parseInt(lastCard.style.left) + settings.width; list.appendChild(currentCard); // move the card to end of list currentCard.style.left = newLeft + 'px'; // change left position based on last card lastCard = currentCard; // set this as the last card for next iteration counter = (counter + 1) % settings.count; // set the next card index if ((settings.iterations > 0) && (counter >= (settings.count - 1))) { currentIteration++; // check settings for repeat iterations } } if (currentIteration >= settings.iterations) { return; } // when to stop raf = window.requestAnimationFrame(keyframes); // request another animation frame }; * { box-sizing: border-box; padding: 0; margin: 0; } .cardList { position: relative; height: 100px; width: 300px; margin: 10px; border: 2px solid #33e; overflow: hidden; white-space: nowrap; } .card { position: absolute; left: 0; top: 0; text-align: center; height: 100px; width: 100px; line-height: 100px; background-color: #99e; font-family: monospace; font-size: 2em; color: #444; border-left: 1px solid #33e; border-right: 1px solid #33e; } div.controls, button { margin: 10px; padding: 8px; font-family: monospace; } div.controls input { width: 48px; padding: 2px; text-align: center; font-family: monospace; } <div class="controls"> <label>Speed <input id="speed" type="number" min="1" max="8" value="2" />x</label> | <label>Iterations <input id="iter" type="number" min="0" max="8" value="2" /></label> </div> <div class="cardList"> <div class="card">1</div> <div class="card">2</div> <div class="card">3</div> <div class="card">4</div> </div> <button id="start">Start</button> <button id="stop">Stop</button> <button id="reset">Reset</button> Fiddle: http://jsfiddle.net/abhitalks/1hkw1v0w/ Note: I have left out a few things in the demo. Especially, although width and height of the cards is part of the settings object, but currently it left fixed. You can easily use the settings object to make the dimensions of the cards configurable as well. Edit: (as per Op's comment) If you want a greater control over distance to scroll, duration and timing-functions (easing), then you could implement those yourself using a library. A couple of such good libraries are the Robert Penner's Easing Functions and a jQuery plugin from GSGD. Although you can implement all of that with pure Javascript, it would be easier if you use a library like jQuery. Catch here is that in order to do so effectively, you must then duplicate the cards. You can do so easily by cloning the entire list a couple of times. Although you have not tagged this question with jQuery, here is a small demo (using jQuery to get it done quickly) where you can configure the speed and the distance. Snippet 2: var $cardList = $('.cardList').first(), $cards = $('.card'), $speed = $('input[name=speed]'), width = 100, randomize = true, distance = 20 * width ; for (var i = 0; i < 50; i++) { $cards.clone().appendTo($cardList); } function spin() { var newMargin = 0, newDistance = distance, speed = +($speed.filter(':checked').val()); if (randomize) { newDistance = Math.floor(Math.random() * $cards.length * 5); newDistance += $cards.length * 5; newDistance *= width; } newMargin = -(newDistance); $cards.first().animate({ marginLeft: newMargin }, speed); } $('#spin').click(function() { $cards.first().css('margin-left', 0); spin(); return false; }); * { box-sizing: border-box; padding: 0; margin: 0; } .cardList { height: 100px; width: 302px; position: relative; margin: 10px; border: 1px solid #33e; overflow: hidden; white-space: nowrap; } .card { display: inline-block; text-align: center; height: 100px; width: 100px; line-height: 100px; background-color: #99e; font-family: monospace; font-size: 2em; color: #444; border-left: 1px solid #33e; border-right: 1px solid #33e; } .cardList::before, .cardList::after { content: ''; display: block; z-index: 100; width: 0px; height: 0px; transform: translateX(-50%); border-left: 8px solid transparent; border-right: 8px solid transparent; } .cardList::before { position: absolute; top: 0px; left: 50%; border-top: 12px solid #33e; } .cardList::after { position: absolute; bottom: 0px; left: 50%; border-bottom: 12px solid #33e; } div.controls, button { margin: 10px; padding: 8px; font-family: monospace; } div.controls input { width: 48px; padding: 2px; text-align: center; font-family: monospace; } <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <div class="controls"> <label>Speed: </label> | <label><input name="speed" type="radio" value='6000' />Slow</label> <label><input name="speed" type="radio" value='5000' checked />Medium</label> <label><input name="speed" type="radio" value='3000' />Fast</label> </div> <div class="cardList"><!-- --><div class="card">1</div><!-- --><div class="card">2</div><!-- --><div class="card">3</div><!-- --><div class="card">4</div><!-- --></div> <button id="spin">Spin</button> Fiddle 2: http://jsfiddle.net/abhitalks/c50upco5/
If you don't want to modify the dom elements you could take advantage of flex-item's order property; to do this you'd still need a little JS to add this property after animation has ended; I also changed to animation instead of transition so it automatically resets the transform property at the end of animation. $('.cards').mouseenter(function() { setTimeout(function() { $('.card').first().css("order", "2"); }, 3000); }); $('.cards').mouseleave(function() { $('.card').first().css("order", "-1"); }); .container { width: 300px; height: 100px; border: 2px solid black; overflow: hidden; } .card { float: left; /* height: 100px; width: 100px;*/ background-color: blue; box-sizing: border-box; border: 2px solid red; color: white; font-size: 23px; flex: 0 0 25%; } .cards:hover { animation: trans 3s; } /**/ .cards { width: 400px; height: 100%; display: flex; transition: transform 3s; } #keyframes trans { 0% { transform: translateX(0) } 100% { transform: translateX(-100px) } } <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script> <div class="container"> <div class="cards"> <div class="card">1</div> <div class="card">2</div> <div class="card">3</div> </div> </div> fiddle But if you're OK to use JS I suggest you manipulate the order of DOM elements directly,taking the first child element of .cards and appending it to the end of list at the end of each animation; try this: var anim; $('.cards').mouseenter(function(){ anim = setInterval(function(){ $('.cards').append($('.card').first()) },3000) }); $('.cards').mouseleave(function(){ clearInterval(anim) }); .container{ width:300px; height: 100px; border: 2px solid black; overflow: hidden; } .card{ float:left; /* height: 100px; width: 100px;*/ background-color:blue; box-sizing: border-box; border: 2px solid red; color: white; font-size: 23px; /**/ flex:0 0 25%; } .cards:hover{ animation: trans 3s infinite; } /**/ .cards{ width:400px; height:100%; display:flex; } #keyframes trans { 0% { transform: translateX(0) } 100% { transform: translateX(-100px) } } <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script> <div class="container"> <div class="cards"> <div class="card"> 1 </div> <div class="card"> 2 </div> <div class="card"> 3 </div> </div> </div> in case you want one card to be present at same time both at the beginning and at the end of card-list you'll need to make a deep-copy / clone of the element; here's an example;
Update 2: I wrote a jquery plugin that may act the way you want: you can add as many cards as you want, right now the "translateX" is random (the script will choose randomly the final card) link to the demo Update: I know, I used duplicates, but now my code works on three cards: I added three "fake" cards Each "real" card has it's own animation the "fake" cards will be overlapped by the real ones once their cycle is finished ("when there is no element to show" as you asked) check the snippet: .container { width: 300px; height: 100px; border: 2px solid black; overflow: hidden; } .card { float: left; height: 100px; width: 100px; background-color: blue; box-sizing: border-box; border: 2px solid red; color: white; font-size: 23px; } .cards { width: 600px; } .container:hover .card1{ animation: 1600ms slide1 infinite linear; } .container:hover .card2{ animation: 1600ms slide2 infinite linear; } .container:hover .card3{ animation: 1600ms slide3 infinite linear; } .fakecard{z-index:-1000;} .container:hover .fakecard{ animation: 1600ms fakeslide infinite linear; } #keyframes slide1 { 0% { transform: translateX(0px); } 33% { transform: translateX(-100px); } 33.1% { transform: translateX(+200px); } 100% { transform: translateX(0px); } } #keyframes slide2 { 0% { transform: translateX(0px); } 66% { transform: translateX(-200px); } 66.1% { transform: translateX(100px); } 100% { transform: translateX(0px); } } #keyframes slide3 { 0% { transform: translateX(0px); } 99% { transform: translateX(-300px); } 99.1% { transform: translateX(+300px); } 100% { transform: translateX(0px); } } #keyframes fakeslide { 0% { transform: translateX(0px); } 99% { transform: translateX(-300px); } 99.1% { transform: translateX(+300px); } 100% { transform: translateX(0px); } } <div class="container"> <div class="cards"> <div class="card card1"> 1 </div> <div class="card card2"> 2 </div> <div class="card card3"> 3 </div> <div class="card fakecard"> 1 (fake) </div> <div class="card fakecard"> 2 (fake) </div> <div class="card fakecard"> 3 (fake) </div> </div> </div> Previous answer: Is this what you are trying to achieve? I don't think you can do it without duplicates... If not, can you explain better what you are trying to achieve here? [snipped code removed]
Here is the same effect that you mentioned, with a little tweak on your CSS and a helpful hand from jQuery. CSS Change your selector for the translateX animation to apply on each of the .card boxes when their immediate parent is hovered, and not the .cards (which is the immediate parent of the .cards). This is because you'd want the cards to move to the left, and not the window through which they appear while making the movement. That is, .cards:hover .card { transform: translateX(-100px); transition-duration: 1.5s; animation-duration: 1.5s; animation-fill-mode: forwards; } jQuery var $container = $('.container'); var cardWidth = 100; $container.on('mouseenter', function (e) { e.preventDefault(); var $card0Clone = $('.card').eq(0).clone(); // clone of the first .card element $('.cards').append($card0Clone); updateWidth(); }); $container.on('mouseleave', function (e) { e.preventDefault(); var $cards = $('.card'); $cards.eq(0).remove(); // remove the last .card element }); function updateWidth() { $('.cards').width(($('.card').length) * cardWidth); // no of cards in the queue times the width of each card would result in a container fit enough for all of them } Code Explained As you move in the mouse pointer, a clone of the first card is created, and appended to the end of the cards collection. Further, as you move the mouse out of the hover area, the original .card (which was cloned earlier) will be removed from the head of the queue - hence, producing a cyclic effect. The real trick though is with the updateWidth function. Every time the mouse enters the .container the width of the .cards' immediate parent (i.e. .cards div) is updated, so that .cards div is wide enough to fit in all the .cards, and therefore, making sure that each of the cards push against each other and stay in one line at the time the translation animation is being done.
Here is a simple technique that manipulates the Dom to create your desired effect Javascript: document.querySelector('.cards').addEventListener('mousedown', function(e) { if (e.clientX < (this.offsetWidth >> 1)) { this.appendChild(this.removeChild(this.firstElementChild)); } else { this.insertBefore(this.lastElementChild, this.firstElementChild); }}); then in you css use the nth-of-type selector to position elements as required. Here is your fiddle If you are using mouseover you might need to wait for transitionend event before firing again.
Check out this demo Here I used JQuery, you can configure your animation using two variables var translateX = 1000; //adjust the whole distance to translate var stepSpeed = 100; //adjust the speed of each step transition in milliseconds After setting your variables, on the click event of the cards do the following:- Get the number of the steps required based on translateX Loop for the number of steps Inside each loop (each step) move the cards 1 step to the left, then put the first card to the end of the cards to form the connected loop, then return back the cards to it's initial position Here is the code: var stepsNumber = translateX/100; for(var i=0; i< stepsNumber; i++) { $('.cards').animate({'left' : -100}, stepSpeed,function(){ $('.cards div:last').after($('.cards div:first')); $('.cards').css({'left' : '0px'}); }); }