Tôi biết đây là câu trả lời thứ ba mươi cho câu hỏi này, nhưng tôi nghĩ nó đáng giá, vì vậy hãy đi. Đây là một giải pháp chỉ dành cho CSS với các thuộc tính sau:
- Không có sự chậm trễ khi bắt đầu và quá trình chuyển đổi không dừng lại sớm. Theo cả hai hướng (mở rộng và thu gọn), nếu bạn chỉ định thời lượng chuyển đổi là 300ms trong CSS của mình, thì quá trình chuyển đổi sẽ mất 300ms, theo chu kỳ.
- Đó là chuyển đổi chiều cao thực tế (không giống như
transform: scaleY(0)
), do đó, nó thực hiện đúng nếu có nội dung sau phần tử đóng mở.
- Mặc dù (như trong các giải pháp khác) có các số ma thuật (như "chọn độ dài cao hơn hộp của bạn"), nhưng sẽ không gây tử vong nếu giả định của bạn kết thúc là sai. Quá trình chuyển đổi có thể trông không tuyệt vời trong trường hợp đó, nhưng trước và sau quá trình chuyển đổi, đây không phải là vấn đề: Ở
height: auto
trạng thái mở rộng ( ), toàn bộ nội dung luôn có chiều cao chính xác (không giống như nếu bạn chọn một max-height
biến hóa ra quá thấp). Và trong trạng thái sụp đổ, chiều cao bằng không.
Bản giới thiệu
Đây là bản demo với ba yếu tố có thể thu gọn, tất cả các độ cao khác nhau, tất cả đều sử dụng cùng một CSS. Bạn có thể muốn nhấp vào "toàn trang" sau khi nhấp vào "chạy đoạn trích". Lưu ý rằng JavaScript chỉ bật tắt collapsed
lớp CSS, không có phép đo liên quan. (Bạn có thể thực hiện bản demo chính xác này mà không cần bất kỳ JavaScript nào bằng cách sử dụng hộp kiểm hoặc :target
). Cũng lưu ý rằng phần CSS chịu trách nhiệm cho quá trình chuyển đổi khá ngắn và HTML chỉ yêu cầu một yếu tố trình bao bọc bổ sung duy nhất.
$(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>
Làm thế nào nó hoạt động?
Thực tế có hai sự chuyển đổi liên quan đến việc thực hiện điều này. Một trong số chúng chuyển margin-bottom
từ 0px (ở trạng thái mở rộng) sang -2000px
trạng thái thu gọn (tương tự câu trả lời này ). 2000 ở đây là con số ma thuật đầu tiên, nó dựa trên giả định rằng hộp của bạn sẽ không cao hơn số này (2000 pixel có vẻ như là một lựa chọn hợp lý).
Bản thân việc sử dụng margin-bottom
quá trình chuyển đổi có hai vấn đề:
- Nếu bạn thực sự có một hộp cao hơn 2000 pixel, thì
margin-bottom: -2000px
sẽ không ẩn mọi thứ - sẽ có những thứ có thể nhìn thấy ngay cả trong trường hợp bị sập. Đây là một sửa chữa nhỏ mà chúng ta sẽ làm sau.
- Nếu hộp thực tế cao 1000 pixel và quá trình chuyển đổi của bạn dài 300ms thì quá trình chuyển đổi có thể nhìn thấy đã kết thúc sau khoảng 150ms (hoặc, theo hướng ngược lại, bắt đầu trễ 150ms).
Khắc phục sự cố thứ hai này là nơi chuyển tiếp thứ hai đến và quá trình chuyển đổi này nhắm mục tiêu theo chiều cao tối thiểu của trình bao bọc ("về mặt khái niệm" vì chúng tôi không thực sự sử dụng thuộc min-height
tính cho việc này; nhiều hơn về sau).
Đây là một hình ảnh động cho thấy cách kết hợp chuyển đổi lề dưới với chuyển đổi chiều cao tối thiểu, cả hai thời lượng bằng nhau, cho chúng ta một chuyển đổi kết hợp từ chiều cao đầy đủ sang chiều cao bằng không có cùng thời lượng.
Thanh bên trái cho thấy cách lề dưới âm đẩy đáy lên trên, làm giảm chiều cao nhìn thấy được. Thanh giữa cho thấy chiều cao tối thiểu đảm bảo rằng trong trường hợp sụp đổ, quá trình chuyển đổi không kết thúc sớm và trong trường hợp mở rộng, quá trình chuyển đổi không bắt đầu muộn. Thanh bên phải cho thấy cách kết hợp cả hai làm cho hộp chuyển từ chiều cao đầy đủ sang chiều cao bằng không trong khoảng thời gian chính xác.
Đối với bản demo của tôi, tôi đã giải quyết trên 50px là giá trị chiều cao tối thiểu trên. Đây là con số ma thuật thứ hai và nó phải thấp hơn chiều cao của chiếc hộp. 50px có vẻ hợp lý là tốt; Có vẻ như bạn không thường xuyên muốn làm cho một yếu tố có thể thu gọn được thậm chí không cao 50 pixel ở vị trí đầu tiên.
Như bạn có thể thấy trong hình ảnh động, quá trình chuyển đổi kết quả là liên tục, nhưng nó không khác biệt - tại thời điểm khi chiều cao tối thiểu bằng với chiều cao đầy đủ được điều chỉnh bởi lề dưới, tốc độ thay đổi đột ngột. Điều này rất đáng chú ý trong hoạt hình vì nó sử dụng chức năng định thời tuyến tính cho cả hai lần chuyển đổi và vì toàn bộ quá trình chuyển đổi rất chậm. Trong trường hợp thực tế (bản demo của tôi ở trên cùng), quá trình chuyển đổi chỉ mất 300ms và chuyển đổi lề dưới không phải là tuyến tính. Tôi đã chơi xung quanh với rất nhiều chức năng thời gian khác nhau cho cả hai lần chuyển đổi và những chức năng tôi kết thúc với cảm giác như chúng hoạt động tốt nhất cho nhiều trường hợp khác nhau.
Vẫn còn hai vấn đề cần khắc phục:
- điểm từ trên cao, nơi các hộp có chiều cao hơn 2000 pixel không bị ẩn hoàn toàn trong trạng thái thu gọn,
- và vấn đề ngược lại, trong trường hợp không ẩn, các hộp có chiều cao dưới 50 pixel quá cao ngay cả khi quá trình chuyển đổi không chạy, vì chiều cao tối thiểu giữ chúng ở mức 50 pixel.
Chúng tôi giải quyết vấn đề đầu tiên bằng cách cung cấp cho phần tử container max-height: 0
trong trường hợp bị sập, với một 0s 0.3s
chuyển đổi. Điều này có nghĩa là nó không thực sự là một quá trình chuyển đổi, nhưng nó max-height
được áp dụng với độ trễ; nó chỉ áp dụng khi quá trình chuyển đổi kết thúc. Để điều này hoạt động chính xác, chúng ta cũng cần chọn một số max-height
cho trạng thái ngược lại, không sụp đổ. Nhưng không giống như trong trường hợp 2000px, khi việc chọn một số quá lớn ảnh hưởng đến chất lượng của quá trình chuyển đổi, trong trường hợp này, điều đó thực sự không quan trọng. Vì vậy, chúng tôi chỉ có thể chọn một số cao đến mức chúng tôi biết rằng sẽ không có chiều cao nào đến gần với điều này. Tôi đã chọn một triệu pixel. Nếu bạn cảm thấy có thể cần hỗ trợ nội dung có chiều cao hơn một triệu pixel, thì 1) Tôi xin lỗi và 2) chỉ cần thêm một vài số không.
Vấn đề thứ hai là lý do tại sao chúng ta không thực sự sử dụng min-height
cho việc chuyển đổi chiều cao tối thiểu. Thay vào đó, có một ::after
phần tử giả trong vùng chứa có phần height
chuyển từ 50px sang 0. Điều này có tác dụng tương tự như min-height
: Nó sẽ không cho phép container co lại dưới bất kỳ chiều cao nào mà phần tử giả hiện có. Nhưng vì chúng tôi đang sử dụng height
chứ không phải min-height
bây giờ chúng tôi có thể sử dụng max-height
(một lần nữa áp dụng độ trễ) để đặt chiều cao thực tế của phần tử giả thành 0 khi quá trình chuyển đổi kết thúc, đảm bảo rằng ít nhất là bên ngoài quá trình chuyển đổi, ngay cả các phần tử nhỏ cũng có chiều cao chính xác. Bởi vì min-height
là mạnh hơn max-height
, điều này sẽ không hoạt động nếu chúng tôi sử dụng các containermin-height
thay vì của pseudo-yếu tốheight
. Cũng giống như max-height
trong đoạn trước, điều này max-height
cũng cần một giá trị cho phần cuối đối diện của quá trình chuyển đổi. Nhưng trong trường hợp này, chúng ta chỉ có thể chọn 50px.
Đã thử nghiệm trong Chrome (Win, Mac, Android, iOS), Firefox (Win, Mac, Android), Edge, IE11 (ngoại trừ vấn đề bố cục flexbox với bản demo của tôi mà tôi không bận tâm gỡ lỗi) và Safari (Mac, iOS ). Nói về flexbox, có thể làm cho công việc này hoạt động mà không cần sử dụng bất kỳ flexbox nào; thực tế tôi nghĩ rằng bạn có thể làm cho hầu hết mọi thứ hoạt động trong IE7 - ngoại trừ thực tế là bạn sẽ không có chuyển đổi CSS, làm cho nó trở thành một bài tập khá vô nghĩa.
height:auto/max-height
giải pháp sẽ chỉ hoạt động nếu diện tích mở rộng củaheight
bạn lớn hơn mức bạn muốn hạn chế. Nếu bạn có mộtmax-height
số300px
, nhưng một thả xuống combo box, có thể quay trở lại50px
, sau đómax-height
sẽ không giúp bạn,50px
có thể thay đổi tùy thuộc vào số lượng các yếu tố, bạn có thể đến với một tình huống không thể nơi tôi không thể sửa chữa nó vìheight
không phải là đã sửa,height:auto
là giải pháp, nhưng tôi không thể sử dụng hiệu ứng chuyển tiếp với cái này.