Wiem, że to trzydziesta odpowiedź na to pytanie, ale myślę, że warto, więc proszę. Jest to rozwiązanie tylko do obsługi CSS o następujących właściwościach:
- Na początku nie ma opóźnienia, a przejście nie kończy się wcześnie. W obu kierunkach (rozwijanie i zwijanie), jeśli określisz czas trwania przejścia w CSS na 300 ms, przejście zajmie 300 ms.
- Przenosi rzeczywistą wysokość (w przeciwieństwie do
transform: scaleY(0)
), więc robi to dobrze, jeśli po składanym elemencie jest treść.
- While (podobnie jak w innych rozwiązaniach) tam są numery Magic (typu „pick długość, która jest większa niż twoja skrzynka jest kiedykolwiek będzie”), to nie jest śmiertelne, jeśli kończy się na tym, założenie błędne. W takim przypadku przejście może nie wyglądać niesamowicie, ale przed przejściem i po nim nie stanowi to problemu: w stanie rozwiniętym (
height: auto
) cała zawartość zawsze ma odpowiednią wysokość (w przeciwieństwie do np. Wybrania takiego, max-height
który okazuje się być za nisko). A w stanie złożonym wysokość wynosi zero, tak jak powinna.
Próbny
Oto demo z trzema składanymi elementami, o różnych wysokościach, które wykorzystują ten sam CSS. Możesz kliknąć „całą stronę” po kliknięciu „uruchom snippet”. Zauważ, że JavaScript przełącza tylko collapsed
klasę CSS, nie wymaga żadnego pomiaru. (Możesz zrobić dokładnie to demo bez JavaScript, używając pola wyboru lub :target
). Zauważ też, że część CSS odpowiedzialna za przejście jest dość krótka, a HTML wymaga tylko jednego dodatkowego elementu opakowania.
$(function () {
$(".toggler").click(function () {
$(this).next().toggleClass("collapsed");
$(this).toggleClass("toggled"); // this just rotates the expander arrow
});
});
.collapsible-wrapper {
display: flex;
overflow: hidden;
}
.collapsible-wrapper:after {
content: '';
height: 50px;
transition: height 0.3s linear, max-height 0s 0.3s linear;
max-height: 0px;
}
.collapsible {
transition: margin-bottom 0.3s cubic-bezier(0, 0, 0, 1);
margin-bottom: 0;
max-height: 1000000px;
}
.collapsible-wrapper.collapsed > .collapsible {
margin-bottom: -2000px;
transition: margin-bottom 0.3s cubic-bezier(1, 0, 1, 1),
visibility 0s 0.3s, max-height 0s 0.3s;
visibility: hidden;
max-height: 0;
}
.collapsible-wrapper.collapsed:after
{
height: 0;
transition: height 0.3s linear;
max-height: 50px;
}
/* END of the collapsible implementation; the stuff below
is just styling for this demo */
#container {
display: flex;
align-items: flex-start;
max-width: 1000px;
margin: 0 auto;
}
.menu {
border: 1px solid #ccc;
box-shadow: 0 1px 3px rgba(0,0,0,0.5);
margin: 20px;
}
.menu-item {
display: block;
background: linear-gradient(to bottom, #fff 0%,#eee 100%);
margin: 0;
padding: 1em;
line-height: 1.3;
}
.collapsible .menu-item {
border-left: 2px solid #888;
border-right: 2px solid #888;
background: linear-gradient(to bottom, #eee 0%,#ddd 100%);
}
.menu-item.toggler {
background: linear-gradient(to bottom, #aaa 0%,#888 100%);
color: white;
cursor: pointer;
}
.menu-item.toggler:before {
content: '';
display: block;
border-left: 8px solid white;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
width: 0;
height: 0;
float: right;
transition: transform 0.3s ease-out;
}
.menu-item.toggler.toggled:before {
transform: rotate(90deg);
}
body { font-family: sans-serif; font-size: 14px; }
*, *:after {
box-sizing: border-box;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="container">
<div class="menu">
<div class="menu-item">Something involving a holodeck</div>
<div class="menu-item">Send an away team</div>
<div class="menu-item toggler">Advanced solutions</div>
<div class="collapsible-wrapper collapsed">
<div class="collapsible">
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
<div class="menu-item">Ask Worf</div>
<div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
<div class="menu-item">Ask Q for help</div>
</div>
</div>
<div class="menu-item">Sweet-talk the alien aggressor</div>
<div class="menu-item">Re-route power from auxiliary systems</div>
</div>
<div class="menu">
<div class="menu-item">Something involving a holodeck</div>
<div class="menu-item">Send an away team</div>
<div class="menu-item toggler">Advanced solutions</div>
<div class="collapsible-wrapper collapsed">
<div class="collapsible">
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
</div>
</div>
<div class="menu-item">Sweet-talk the alien aggressor</div>
<div class="menu-item">Re-route power from auxiliary systems</div>
</div>
<div class="menu">
<div class="menu-item">Something involving a holodeck</div>
<div class="menu-item">Send an away team</div>
<div class="menu-item toggler">Advanced solutions</div>
<div class="collapsible-wrapper collapsed">
<div class="collapsible">
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
<div class="menu-item">Ask Worf</div>
<div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
<div class="menu-item">Ask Q for help</div>
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
<div class="menu-item">Ask Worf</div>
<div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
<div class="menu-item">Ask Q for help</div>
</div>
</div>
<div class="menu-item">Sweet-talk the alien aggressor</div>
<div class="menu-item">Re-route power from auxiliary systems</div>
</div>
</div>
Jak to działa?
W rzeczywistości dwie zmiany są zaangażowane w realizację tego. Jedna z nich zmienia stan margin-bottom
z 0px (w stanie rozwiniętym) na -2000px
stan zwinięty (podobny do tej odpowiedzi ). 2000 tutaj jest pierwszą magiczną liczbą, opiera się na założeniu, że twoje pudełko nie będzie wyższe niż to (2000 pikseli wydaje się rozsądnym wyborem).
margin-bottom
Samo użycie przejścia ma dwa problemy:
- Jeśli faktycznie masz pudełko większe niż 2000 pikseli, to
margin-bottom: -2000px
nie ukryje wszystkiego - będą widoczne rzeczy nawet w zwiniętej obudowie. To drobna poprawka, którą zrobimy później.
- Jeśli rzeczywiste pole ma, powiedzmy, 1000 pikseli wysokości, a twoje przejście ma 300 ms długości, wówczas widoczne przejście jest już zakończone po około 150 ms (lub, w przeciwnym kierunku, zaczyna się 150 ms późno).
Naprawienie drugiego problemu polega na tym, że pojawia się drugie przejście, a to przejście koncepcyjnie jest ukierunkowane na minimalną wysokość opakowania („koncepcyjnie”, ponieważ tak naprawdę nie używamy min-height
do tego właściwości; więcej na ten temat później).
Oto animacja pokazująca, jak połączenie przejścia dolnego marginesu z przejściem minimalnej wysokości, oba o jednakowym czasie trwania, daje nam połączone przejście z pełnej wysokości do zera wysokości, które ma taki sam czas trwania.
Lewy pasek pokazuje, w jaki sposób ujemny dolny margines popycha dno w górę, zmniejszając widoczną wysokość. Środkowy pasek pokazuje, w jaki sposób minimalna wysokość zapewnia, że w przypadku zwijania przejście nie kończy się wcześnie, aw przypadku rozszerzania przejście nie zaczyna się późno. Prawy pasek pokazuje, w jaki sposób połączenie tych dwóch powoduje przejście skrzynki z pełnej wysokości do zera wysokości w odpowiednim czasie.
W przypadku mojej wersji demonstracyjnej zdecydowałem się na 50 pikseli jako górną minimalną wartość wysokości. To druga magiczna liczba, która powinna być niższa niż kiedykolwiek wysokość pudełka. 50 pikseli również wydaje się rozsądne; wydaje się mało prawdopodobne, że bardzo często chciałbyś, aby element składany nie miał nawet 50 pikseli wysokości.
Jak widać na animacji, powstałe przejście jest ciągłe, ale nie można go rozróżnić - w momencie, gdy minimalna wysokość jest równa pełnej wysokości skorygowanej o dolny margines, następuje nagła zmiana prędkości. Jest to bardzo widoczne w animacji, ponieważ wykorzystuje liniową funkcję synchronizacji dla obu przejść i ponieważ całe przejście jest bardzo wolne. W rzeczywistym przypadku (moje demo u góry) przejście zajmuje tylko 300 ms, a przejście dolnego marginesu nie jest liniowe. Grałem z wieloma różnymi funkcjami pomiaru czasu dla obu przejść, a te, w których skończyłem, wydawały się działać najlepiej w najróżniejszych przypadkach.
Do rozwiązania pozostają dwa problemy:
- punkt z góry, w którym pola o wysokości większej niż 2000 pikseli nie są całkowicie ukryte w stanie zwiniętym,
- i odwrotny problem, gdzie w przypadku nie ukrytym, pola o wysokości mniejszej niż 50 pikseli są zbyt wysokie, nawet gdy przejście nie jest uruchomione, ponieważ minimalna wysokość utrzymuje je na 50 pikseli.
Pierwszy problem rozwiązujemy, nadając elementowi kontenerowi max-height: 0
zwiniętą skrzynkę z 0s 0.3s
przejściem. Oznacza to, że tak naprawdę nie jest to przejście, ale max-height
jest stosowane z opóźnieniem; ma zastosowanie tylko po zakończeniu przejścia. Aby to działało poprawnie, musimy również wybrać wartość liczbową max-height
dla przeciwnego, nie zwiniętego stanu. Ale w przeciwieństwie do przypadku 2000px, w którym wybranie zbyt dużej liczby wpływa na jakość przejścia, w tym przypadku tak naprawdę nie ma znaczenia. Możemy więc wybrać liczbę, która jest tak wysoka, że wiemy, że żadna wysokość nigdy nie zbliży się do tego. Wybrałem milion pikseli. Jeśli uważasz, że możesz potrzebować obsługiwać zawartość o wysokości większej niż milion pikseli, to 1) Przepraszam i 2) po prostu dodaj kilka zer.
Drugi problem jest powodem, dla którego tak naprawdę nie używamy min-height
do przejścia minimalnej wysokości. Zamiast tego ::after
w kontenerze znajduje się pseudoelement, height
który zmienia się z 50px na zero. Ma to taki sam efekt jak min-height
: Nie pozwoli, aby pojemnik zmniejszył się poniżej jakiejkolwiek wysokości, jaką aktualnie ma pseudoelement. Ale ponieważ używamy height
, nie min-height
, możemy teraz użyć max-height
(ponownie zastosowane z opóźnieniem), aby ustawić rzeczywistą wysokość pseudoelementu na zero po zakończeniu przejścia, zapewniając, że przynajmniej poza przejściem nawet małe elementy mają prawidłowa wysokość. Ponieważ min-height
jest silniejszy niż max-height
, nie działałoby to, gdybyśmy użyli kontenera min-height
zamiast pseudoelementuheight
. Podobnie jak max-height
w poprzednim akapicie, max-height
wymaga to również wartości dla przeciwnego końca przejścia. Ale w tym przypadku możemy po prostu wybrać 50 pikseli.
Testowane w Chrome (Win, Mac, Android, iOS), Firefox (Win, Mac, Android), Edge, IE11 (z wyjątkiem problemu z układem Flexbox w mojej wersji demonstracyjnej, że nie zawracałem sobie głowy debugowaniem) i Safari (Mac, iOS ). Mówiąc o Flexboksie, powinno być możliwe, aby działało to bez użycia Flexboksa; w rzeczywistości myślę, że możesz sprawić, by prawie wszystko działało w IE7 - z wyjątkiem faktu, że nie będziesz mieć przejść CSS, co czyni je raczej bezcelowym ćwiczeniem.
height:auto/max-height
rozwiązanie będzie działać tylko wtedy, gdy obszar powiększania jest większy niżheight
chcesz ograniczyć. Jeśli maszmax-height
od300px
, ale rozwijana pola kombi, które mogą wrócić50px
, tomax-height
nie pomoże,50px
jest zmienna w zależności od liczby elementów, można dotrzeć do niemożliwej sytuacji, gdy nie można ustalić go, ponieważheight
nie jest naprawiono,height:auto
było rozwiązaniem, ale nie mogę z tym korzystać z przejść.