No bot Ru lang root

Равномерное размещение блоков разных размеров

Как-то на одном проекте понадобилось красиво равномерно разместить небольшие блоки-виджеты в контейнере на странице. Сложность в том, что эти блоки различаются, как по высоте, так и по ширине. При чём нужно учесть адаптивность вёрстки и динамическое изменение содержимого, как контейнера, так и самих элементов - виджетов. Собственно мои изыскания по этой теме и вылились в разработку собственного решения и эту статью, которые, я надеюсь, будут полезны читателям.   


Эта статья на Хабре


Опишем подробно условия/пожелания задачи:


  1.  как можно меньшие пустоты между элементами и ими и границами контейнера;

  2.  равномерное расположение пустот;

  3.  по возможности, вообще обойтись без JavaScript кода, либо сделать его участие минимальным;

  4.  адаптивность под разные размеры экранов и влияние других нод на странице вокруг контейнера;

  5.  поддержка динамической адаптивности под изменения (размеры, содержимое) страницы, контейнера и элементов, а так же создания/добавления/удаления таких контейнеров/элементов что называется "на лету".

 

Интерактивную демонстрацию всех, приведённых в статье примеров, можно опробовать здесь.


"Дедовский" метод "float: left"


float: left

css
1
2
3
4
5
6
7
8
9
.container_float_left{
    overflow: auto;
    > *{
        float: left;
    }
    &::after{
        clear: both;
    }
}


    

Как видим, элементы распределяются по строкам, отсюда вытекают особенности. Вертикальные пустоты между элементом в строке определяется разницей между им и самым высоким блоком в той же строке. Так же возможна выделяющаяся по размеру горизонтальная пустота между последними элементом и правой границей контейнера.


По условиям/пожеланиям получаем:


  1. величина пустот: если разница между блоками не большая, то /- подходит, иначе - нет;

  2. равномерность пустот: нет, равномерности тут не наблюдаем;  

  3. без JS: подходит;

  4. адаптивность: подходит;

  5. динамическая адаптивность: подходит.




Метод "flex-flow: row wrap"


flex-flow: row wrap

css
1
2
3
4
5
.container_flex_row_wrap{
    display: flex;
    flex-flow: row wrap;
    justify-content: space-evenly;
}


Блоки распределяются так же по строкам, как и в предыдущем методе, но, благодаря "justify-content: space-evenly" пустые расстояния между соседними элементами в строке равные.  


По условиям/пожеланиям получаем:


  1. величина пустот: если разница между блоками не большая, то /- подходит, иначе - нет;

  2.  равномерность пустот: частично, соблюдается только между соседними элементами в строке;  

  3. без JS: подходит;

  4. адаптивность: подходит; 

  5. динамическая адаптивность: подходит.




Метод "column-count: 4"


columns-count: 4


css
1
2
3
4
5
6
7
.container_columns{
    column-count: 4;
    column-gap: 0;
    > *{
        display: inline-block;
    }
}


На первый взгляд, смотрится, как то, что нужно. Минимальные пустоты, так как распределение блоков идёт по столбцам, а не по строкам. Правда, равномерности в промежутках между столбцами уже нет. В минусы можно отнести то, что количество столбцов нужно прописывать вручную, так что адаптивность можно обеспечить разве что правилами @media для каждого случая размера экрана. О поддержке динамических изменений говорить не приходится.


Стоит отметить, что если не делать внутренние блоки строковыми, происходит их разрезание на части, которые разносятся на разные столбцы.


По условиям/пожеланиям получаем:


  1. величина пустот: подходит;

  2. равномерность пустот: частично, соблюдается только между блоками в столбцах;  

  3. без JS: подходит;

  4. адаптивность: подходит с условием использования правил @media для каждого случая размера экрана;

  5. динамическая адаптивность: нет.




Мой метод "flex-flow: column wrap" js-скрипт


flex-flow: column wrap js-скрипт


В основе лежит режим разметки "flex" со свойством "flex-flow: column wrap". Дело в том, что без ограничения контейнера по высоте мы получаем просто один столбец со всеми вложенными элементами. Однако, установив точную высоту, мы получаем как-раз то, что нужно, что наблюдаем на иллюстрационном изображении. Как не трудно догадаться, скрипт занимается как раз определением этой высоты блока, а так же следит за изменениями страницы, запуская это переопределение по необходимости.


Суть в том, чтобы установив примерную (заведомо меньшую реальной) высоту контейнера, постепенно в цикле по небольшим шагам увеличивать оную (высоту) до тех пока не будет устранено переполнение. По итогу это и будет искомая оптимальная величина. Наличие переполнения по высоте определяется так: .scrollHeight > .scrollHeight. По ширине: .scrollWidth > .offsetWidth.


Фрагмент кода с непосредственными вычислением и установкой высоты контейнера:


js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* node - блок-контейнер */
const step = 10; // шаг подбора высоты

let 
	square = 0, // общая площадь всех внутренних блоков
	max_height = 0; // максимальна высота блока

/* обход всех дочерних елементов с заполнением переменных, объявленных выше */
[...node.childNodes].forEach(child => {
	if(child.nodeName === '#text') return;
	square  = child.offsetHeight * child.offsetWidth;
	if(child.offsetHeight > max_height) max_height = child.offsetHeight;
});

/* вычисляем стартовую высоту */
let start_height = square / node.offsetWidth;
if(start_height < max_height) start_height = max_height;

/* устанавливаем стартовую высоту */
if(!isNaN(start_height)) node.style.height = start_height   'px';

/* цикл увеличения высоты контейнера "node" проводит итерации до тех пор
, пока переполнение по высоте (.scrollHeight > .scrollHeight) и ширине (.scrollWidth > .offsetWidth) не будет устранено */
let savety = 1000;
while((node.scrollHeight > node.offsetHeight || node.scrollWidth > node.offsetWidth) && savety){
	node.style.height = parseInt(node.style.height)   step   'px';
	savety--;
}


Примечание. В принципе можно и не заморачиваться и начинать высоту с ноля. Дело в облегчении работы браузера, хотя на сколько именно в среднем оно благотворно сказывается на производительности трудно сказать, и уж точно конечный пользователь не заметит разницы.


Остальной код не буду здесь приводить, ибо банальность с обходом DOM дерева с поиском целевых нод и отслеживание за изменениями страницы и участвующих узлов с помощью "обзёрверов", без каких-то подводных камней. Хотя, нужно заметить не очевидную особенность: при наличие прокрутки страницы и динамическом изменении целевой ноды может произойти резкий неприятный сдвиг прокрученной области (как-будто без причины). Фиксится просто: 

  1.  находим родительский HTML-элемент с прокруткой; 

  2.  запоминаем текущее положение скрола; 

  3.  проводим непосредственную установку новой высоты для целевого блока;

  4.  возвращаем положение текущей прокрутки с помощью ранее сохранённой координаты. 


Буквально 4 строки кода. 


js
1
2
3
4
5
6
7
8
const 
	get_scroll_parent = node => node.parentNode?.scrollTop ? node.parentNode : node.parentNode ? get_scroll_parent(node.parentNode) : null, 
	scroll_parent = get_scroll_parent(node), // находим родителя с прокруткой, если он есть
	current_scroll = scroll_parent ? scroll_parent.scrollTop : null; // запоминаем текущее положение скролла
...
/* Подгоняем высоту блока  */
...
if(current_scroll && current_scroll !== scroll_parent.scrollTop) scroll_parent.scrollTo({top: current_scroll, behavior: 'instant'}); // исправляем прокрутку, если требуется


Так как вмешательство скрипта минимально у нас имеется арсенал нативных CSS-свойств для кастомизации. 

Однако, отсутствует возможность пользоваться только следующими свойствами:

  • display;

  • flex-flow;

  • flex-direction;

  • flex-wrap;

  • flex-shrink;

  • flex-grow. 


Как пользоваться:

  1. Скачиваем скрипт и подключаем к своей странице либо копируем весь код из файла и размещаем в теге "script" внутри блока "head" либо "body" нашего html

  2. Активируем целевые блоки-контейнеры с помощью атрибута "data-flex-size-fix".


Подключение скрипта (на всякий случай):


html
1
<script language="JavaScript" src="./flex_size_fix.js"></script> 


Инициализация целевых блоков-контейнеров с помощью атрибута "data-flex-size-fix":


html
1
2
3
4
5
...
<div data-flex-size-fix>
...
</div>
...



По условиям/пожеланиям получаем:


  1. величина пустот: подходит;

  2. равномерность пустот: подходит;  

  3. без JS: нет, но минимальное вмешательство;

  4. адаптивность: подходит;

  5. динамическая адаптивность: подходит.


Примечание. Ломал голову о звучном названии скрипта. Пришёл к выводу, что тот набор слов через тире (Flex-Size-Fix) в принципе отражает всю суть. 




P.S. Стоит так же упомянуть о известной подключаемой библиотеке, решающей как раз данную задачу, Masonry. К сожалению, говорить о легковесности и динамической адаптивности не приходится.


P.P.S. На случай, если кому-то важно, статья и код писались без использования ИИ.