Доброго времени суток.

Возникла задача: в twitter bootstrap есть две колонки (div) с классами «col-md-6» и «col-md-6». В первой колонке текст, во второй видео с ютюб, при чем первая колонка в два раза выше второй. Нужно центрировать видео по вертикали относительно первой колонки. И таких участков на сайте несколько. В некоторых местах нужно у двух колонок сделать равные высоты.

Можно было бы руками править каждый раз, но решил попробовать всё ускорить, написать скрипт, который при помощи добавления data- сделает рутинную работу за меня.

Обозначим атрибуты, которые нам нужны:

  • data-compareheight='NAME' — Указываем параметр (имя) div, по которому будем отслеживать div для сравнения между собой.
  • data-compareheightdo='WHAT' — Указываем, что мы хотим сделать с нашими колонками.

Варианта выбрал три возможных действия:

  1. height — Выравниваем колонки по высоте.
  2. margin — Выравниваем дочерние элементы div по середине экрана (при помощи margin-top на первом дочернем элементе).
  3. margin height — Выравниваем дочерние элементы по середине (при помощи margin-top на первом дочернем элементе), а также опускаем нижнюю границу меньшей колонки до значения большей колонки (при помощи margin-bottom на последнем дочернем элементе).

Определились, какие нам нужны действия. Затем добавим условия:

  1. При изменении ширины экрана пользователя пересчитывать размеры в скрипте (если пользователь не будет играться с шириной браузера, то перерасчет будет выполнен однократно, при загрузке страницы)
  2. Есть ширина браузера, при снижении ширины окна браузера менее, чем это значение, будут колонки расположены друг под другом. Здесь нам центрирование или расчет высоты не нужен.

Определимся с переменными:

var allowedWidthOfTheWindow = 992;     // Устанавливаем значение, при котором не будет производиться перерасчет ширины
var elemsToCompare = [],               // DOM элементы в массиве [[a,b,c],[a,b,c]]
    dataForCompareHeight = {};         // Объект с элементами (для расчета localArrayForStaticElems)    
var localArrayForStaticElems = [];     // Массив с номерами элементов по порядку (промежуточное)
var correctingHeights = false;         // Если значения меньше allowedWidthOfTheWindow, то не будем центрировать и/или равнять колонки
var userScreenWidth, userScreenHeight; // Размеры изображения экрана пользователя

Получим массив со всеми div, использующими атрибут data-compareheight:

var elemsWithOurDataset = document.querySelectorAll("div[data-compareheight]");

Из полученного массива DOM элементами нужно извлечь элементы с одинаковыми атрибутами data-compareheight. А также отбросить элементы, которые без пары.

// Составляем цифровые массивы из элементов
var i = 0;
while(true){
	if (elemsWithOurDataset.length <= 1) break; // Если у нас найдено суммарно меньше 2х элементов, то пар точно не будет	
	if(!dataForCompareHeight[elemsWithOurDataset[i].dataset.compareheight]) {
		dataForCompareHeight[elemsWithOurDataset[i].dataset.compareheight] = "";
		localArrayForStaticElems.push(elemsWithOurDataset[i].dataset.compareheight);};		
	dataForCompareHeight[elemsWithOurDataset[i].dataset.compareheight] += i + " ";	
	i++;
	if (i == elemsWithOurDataset.length) break; // Достижение конца массива с DOM элементами
}

После кода выше получим массив с массивами, состоящими из номеров элементов с общим data-compareheight. Другими словами, на данный момент имеем:
[ [ a, b, c ], [ d, e ] ] (где a, b, c, d, e — номера элементов, при чем у элементов a, b, c и у элементов d, e одинаковые data-compareheight).

Нам нужно преобразовать полученный массив из номеров элементов в массив из DOM элементов, с которым будем работать:

// Составляем массивы из DOM элементов
for (var i = 0; i < localArrayForStaticElems.length; i++){
	var numberOfElems = dataForCompareHeight[localArrayForStaticElems[i]].split(" ");
	numberOfElems.pop();
	if (numberOfElems.length < 2) continue;
	var localArr = [];
	for (var j = 0; j < numberOfElems.length; j++){
		localArr.push(elemsWithOurDataset[numberOfElems[j]]);
		}
	elemsToCompare.push(localArr);
}

Теперь у нас есть заполненный массив с DOM элементами (а не номерами элементов). Постоянная часть (при условии отсутствия добавления элементов и не перезагрузки страницы) получена. Дальше будем работать с размерами и стилями.

Также стоит рассчитать ширину полосы прокрутки (если есть), т.к. она уменьшает видимую ширину окна клиента и перенос колонок происходит при меньшем значении, чем ожидается.

// Проверяем ширину полосы прокутки и выставляем значение (меньше) при котором будет отменяться центрирование
function setBreakePointWidthMinusScrollWidth(){
allowedWidthOfTheWindow = allowedWidthOfTheWindow - window.innerWidth + document.body.clientWidth;
}

Напишем функцию для определения высоты и ширины экрана пользователя:

function checkUserScreenSize(){
	userScreenWidth = document.documentElement.clientWidth;
	userScreenHeight = document.documentElement.clientHeight;
}

Теперь напишем функцию для нахождения максимального значения в массиве. Это можно сделать простой сортировкой (но нужно учесть, что обычно сортировка .sort() идёт как для строк, напишем доп. функцию для сортировки по типу numbers):

// Проверим наибольший элемент обычной сортировкой, больший - первый
// При расчетах будем с ним сравнивать
function checkTheLargestElement(){		
	for (var i = 0; i < elemsToCompare.length; i++){			
			elemsToCompare[i].sort(compareNumericArray);		
		}	
}

// Функция для сортировки элемента по numberic типу
function compareNumericArray(a, b) {
  return b.clientHeight + getComputedStyle(b).marginTop + getComputedStyle(b).marginBottom - a.clientHeight - getComputedStyle(a).marginTop - getComputedStyle(a).marginBottom;
}

Напишем функцию для определения, превысила ли ширина браузера пользователя значение, при котором колонки выстраиваются друг под другом.

// Проверяем размер экрана и от этого разрешаем или нет что-то делать
function checkAllowedMovingOrNot(){
	if (userScreenWidth < allowedWidthOfTheWindow){		
		if (!correctingHeights) return;
		correctingHeights = false;		
	}
	else{
		correctingHeights = true;
	}
}

В функции выше в зависимости от размера экрана (а точнее, ширины) получаем true/false, которое можно будет использовать при расчете отступов.

Пришло время написать функцию, которая определяет, что делать в зависимости от второго атрибута data-compareheightdo:

function checkParams(elem){
	if (elem.getAttribute("data-compareheightdo")){
		var a = elem.getAttribute("data-compareheightdo").toLowerCase();
		if (a == "margin"){
			return 1;
		}
		if (a == "height"){
			return 2;
		}
		
		if ((a == "margin height") || (a == "height margin")){
			return 3;
		}		
	}
	return 0;
}

Если функция возвращает 0, то или не указан атрибут data-compareheightdo, или он указан неверно.

Перейдем к размерам. Рассчитаем высоту всех дочерних элементов внутри DIV (не углубляясь в дочерние элементы дочерних элементов, то есть, ограничимся parent.children[i] ). Для первого дочернего элемента не будем учитывать margin-top. Это значение будет использоваться для центрирования по вертикали и автоматически выставляться в зависимости от других параметров.

// Расчет высоты дочерних элементов без учета margin-top первого элемента
function getTotalHeightOfChildren(parent){
	var heightNow = 0;
	for (var i = 0; i < parent.children.length; i++){
		if(i == 0){
			heightNow += parent.children[i].offsetHeight + parseInt(getComputedStyle(parent.children[i]).marginBottom);
			continue;
		}
		if(i == (parent.children.length - 1)){
			heightNow += parent.children[i].offsetHeight + parseInt(getComputedStyle(parent.children[i]).marginTop);
			continue;
		}		
			heightNow += parent.children[i].offsetHeight + parseInt(getComputedStyle(parent.children[i]).marginTop + getComputedStyle(parent.children[i]).marginBottom);
				
	}
	return heightNow;
} 

Теперь у нас есть все вспомогательные функции для написания функции формирования отступов.

// Выставляем первому дочернему элементу margin-top (всем, кроме наибольшему)
// Также выставляем height
function makeElementsEqualHeight(){	
	if(correctingHeights){ // Если ширина экрана больше значения перемещения колонок друг под друга
	for(var i = 0; i < elemsToCompare.length; i++){			
			for (var j = 1; j < elemsToCompare[i].length; j++){
				if (checkParams(elemsToCompare[i][j]) == 0) continue;
								
				if(checkParams(elemsToCompare[i][j]) == 1){
elemsToCompare[i][j].children[0].style.marginTop = parseInt((elemsToCompare[i][0].offsetHeight - getTotalHeightOfChildren(elemsToCompare[i][j]))/2) + "px";
				}
				
				if(checkParams(elemsToCompare[i][j]) == 2){
					if(!elemsToCompare[i][j].querySelector("div[class='notchange']")){ // Проверяем наличие последнего дочернего элемента с классом notchange 
var div = document.createElement("div"); // Если нет данного значения, то создадим
div.classList.add("notchange"); // Добавим класс для удобного нахождения данного DOM элемента
elemsToCompare[i][j].appendChild(div); // И разместим в конец дочерних элементов
						}
elemsToCompare[i][j].lastChild.style.marginBottom = parseInt((elemsToCompare[i][0].offsetHeight - getTotalHeightOfChildren(elemsToCompare[i][j])) - 1) + "px";
				}
				
				if(checkParams(elemsToCompare[i][j]) == 3){
					if(!elemsToCompare[i][j].querySelector("div[class='notchange']")){
						var div = document.createElement("div");
						div.classList.add("notchange");
						elemsToCompare[i][j].appendChild(div);
						}
elemsToCompare[i][j].children[0].style.marginTop = parseInt((elemsToCompare[i][0].offsetHeight - getTotalHeightOfChildren(elemsToCompare[i][j]))/2) + "px";
elemsToCompare[i][j].lastChild.style.marginBottom = parseInt((elemsToCompare[i][0].offsetHeight - getTotalHeightOfChildren(elemsToCompare[i][j]))/2 - 1) + "px";					
				}
			}			
		}
	}	
	else{ // Если ширина экрана меньше значения перемещения колонок друг под друга
		for(var i = 0; i < elemsToCompare.length; i++){			
			for (var j = 1; j < elemsToCompare[i].length; j++){
				if (checkParams == 0) continue;
				elemsToCompare[i][j].children[0].style.marginTop = "";
				elemsToCompare[i][j].lastChild.style.marginBottom = "";
			}			
		}
	}
}

Обращу внимание на момент с добавлением дочернего div элемента. Это сделано с целью легкого доступа к последнему элементу и этот элемент можно легко редактировать с помощью javascript, не мешая основному коду.

Также обращу внимание, что мы отнимаем 1px чтобы была разница между наибольшим и дочерними элементами.

Есть участки кода, где используется не elemsToCompare[i][j], а elemsToCompare[i][0]. Мы ранее написали функцию для сортировки элементов массива. Значит, первое значение (с индексом 0) имеет наибольшие размеры.

В зависимости от атрибута data-compareheightdo имеем значения 0/1/2/3, в зависимости от этого игнорируем / только отступ сверху (центрирование) / Только выравнивание нижнего края колонки по нижнему краю наибольшего значения / Центрирование и выравнивание нижнего края.

Напишем функцию, которая объединяет все функции воедино:

// Создадим общую функцию для перерасчета элементов
function recountElems(){
	checkUserScreenSize();     // Проверяем размер экрана пользователя
	checkTheLargestElement();  // Проверяем наибольший элемент из массива элементов (с одним data-compareheight="NAME")
	checkAllowedMovingOrNot(); // Проверяем, можно ли выравнивать элемент или нет
	makeElementsEqualHeight(); // Выставляем margin-top и margin-bottom у элементов	
};

Рассчитаем значения при загрузке страницы. Если человек не будет играться с окном браузера, то эти значения не будут меняться:

setBreakePointWidthMinusScrollWidth(); // Вычисляем один раз ширину полосы прокрутки и вычетаем её
recountElems(); // Первый раз пересчитаем элементы, чтобы страница корректно отображалась

Добавим обработчик, который срабатывает при изменении размеров экрана пользователя и считает новые значения:

// При изменении размеров экрана пользователя перерасчитывать все значения
window.onresize = function(e){
	recountElems();
}

Всё, мы закончили разрабатывать скрипт, он готов.

Код целиком:

// Определилил глобальные переменные
var allowedWidthOfTheWindow = 992;     // Устанавливаем значение, при котором не будет производиться перерасчет ширины

var elemsToCompare = [],               // DOM элементы в массиве [[a,b,c],[a,b,c]]
    dataForCompareHeight = {};         // Объект с элементами (для расчета localArrayForStaticElems)
    
var localArrayForStaticElems = [];     // Массив с номерами элементов по порядку (промежуточное)

var correctingHeights = false;         // Если значения меньше allowedWidthOfTheWindow, то false

var userScreenWidth, userScreenHeight; // Размеры ихображения экрана пользователя

var elemsWithOurDataset = document.querySelectorAll("div[data-compareheight]");

// Составляем цифровые массивы из элементов
var i = 0;
while(true){
	if (elemsWithOurDataset.length <= 1) break;
	
	if(!dataForCompareHeight[elemsWithOurDataset[i].dataset.compareheight]) {
		dataForCompareHeight[elemsWithOurDataset[i].dataset.compareheight] = "";
		localArrayForStaticElems.push(elemsWithOurDataset[i].dataset.compareheight);};		
	dataForCompareHeight[elemsWithOurDataset[i].dataset.compareheight] += i + " ";	
	i++;
	if (i == elemsWithOurDataset.length) break;
}

// Создавляем массивы из DOM элементов
for (var i = 0; i < localArrayForStaticElems.length; i++){
	var numberOfElems = dataForCompareHeight[localArrayForStaticElems[i]].split(" ");
	numberOfElems.pop();
	if (numberOfElems.length < 2) continue;
	var localArr = [];
	for (var j = 0; j < numberOfElems.length; j++){
		localArr.push(elemsWithOurDataset[numberOfElems[j]]);
		}
	elemsToCompare.push(localArr);
}

// Начинается динамическая часть

// Проверяем ширину скролла и выставляем значение (меньше) при котором будет отменяться центрирование
function setBreakePointWidthMinusScrollWidth(){
	allowedWidthOfTheWindow = allowedWidthOfTheWindow - window.innerWidth + document.body.clientWidth;
}

function checkUserScreenSize(){
	userScreenWidth = document.documentElement.clientWidth;
	userScreenHeight = document.documentElement.clientHeight;
}

// Проверим наибольший элемент обычной сортировкой, больший - первый
// При расчетах будем с ним сравнивать
function checkTheLargestElement(){		
	for (var i = 0; i < elemsToCompare.length; i++){			
			elemsToCompare[i].sort(compareNumericArray);		
		}	
}

// Функция для сортировки элемента по numberic типу
function compareNumericArray(a, b) {
  return b.clientHeight + getComputedStyle(b).marginTop + getComputedStyle(b).marginBottom - a.clientHeight - getComputedStyle(a).marginTop - getComputedStyle(a).marginBottom;
}

// Расчет высоты дочерних элементов без учета margin-top первого элемента
function getTotalHeightOfChildren(parent){
	var heightNow = 0;
	for (var i = 0; i < parent.children.length; i++){
		if(i == 0){
			heightNow += parent.children[i].offsetHeight + parseInt(getComputedStyle(parent.children[i]).marginBottom);
			continue;
		}
		if(i == (parent.children.length - 1)){
			heightNow += parent.children[i].offsetHeight + parseInt(getComputedStyle(parent.children[i]).marginTop);
			continue;
		}		
			heightNow += parent.children[i].offsetHeight + parseInt(getComputedStyle(parent.children[i]).marginTop + getComputedStyle(parent.children[i]).marginBottom);
				
	}
	return heightNow;
}

// Выставляем первому дочернему элементу margin-top (всем, кроме наибольшему)
// Также выставляем height
function makeElementsEqualHeight(){	
	if(correctingHeights){ // Если ширина экрана больше значения перемещения колонок друг под друга
	for(var i = 0; i < elemsToCompare.length; i++){			
			for (var j = 1; j < elemsToCompare[i].length; j++){
				if (checkParams(elemsToCompare[i][j]) == 0) continue;
								
				if(checkParams(elemsToCompare[i][j]) == 1){
				elemsToCompare[i][j].children[0].style.marginTop = parseInt((elemsToCompare[i][0].offsetHeight - getTotalHeightOfChildren(elemsToCompare[i][j]))/2) + "px";
				}
				
				if(checkParams(elemsToCompare[i][j]) == 2){
					if(!elemsToCompare[i][j].querySelector("div[class='notchange']")){ // Проверяем наличие последнего дочернего элемента с классом notchange 
						var div = document.createElement("div"); // Если нет данного значения, то создадим
						div.classList.add("notchange"); // Добавим класс для удобного нахождения данного DOM элемента
						elemsToCompare[i][j].appendChild(div); // И разместим в конец дочерних элементов
						}
						elemsToCompare[i][j].lastChild.style.marginBottom = parseInt((elemsToCompare[i][0].offsetHeight - getTotalHeightOfChildren(elemsToCompare[i][j])) - 1) + "px";
				}
				
				if(checkParams(elemsToCompare[i][j]) == 3){
					if(!elemsToCompare[i][j].querySelector("div[class='notchange']")){
						var div = document.createElement("div");
						div.classList.add("notchange");
						elemsToCompare[i][j].appendChild(div);
						}
					elemsToCompare[i][j].children[0].style.marginTop = parseInt((elemsToCompare[i][0].offsetHeight - getTotalHeightOfChildren(elemsToCompare[i][j]))/2) + "px";
					elemsToCompare[i][j].lastChild.style.marginBottom = parseInt((elemsToCompare[i][0].offsetHeight - getTotalHeightOfChildren(elemsToCompare[i][j]))/2 - 1) + "px";					
				}
			}			
		}
	}	
	else{ // Если ширина экрана меньше значения перемещения колонок друг под друга
		for(var i = 0; i < elemsToCompare.length; i++){			
			for (var j = 1; j < elemsToCompare[i].length; j++){
				if (checkParams == 0) continue;
				elemsToCompare[i][j].children[0].style.marginTop = "";
				elemsToCompare[i][j].lastChild.style.marginBottom = "";
			}			
		}
	}
}

// Проверяем размер экрана и от этого разрешаем или нет что-то делать
function checkAllowedMovingOrNot(){
	if (userScreenWidth < allowedWidthOfTheWindow){		
		if (!correctingHeights) return;
		correctingHeights = false;		
	}
	else{
		correctingHeights = true;
	}
}

// Проверяем, что делать с полученными элементами при изменении высоты
// То есть, добавляем функционал:
// height
// margin

function checkParams(elem){
	if (elem.getAttribute("data-compareheightdo")){
		var a = elem.getAttribute("data-compareheightdo").toLowerCase();
		if (a == "margin"){
			return 1;
		}
		if (a == "height"){
			return 2;
		}
		
		if ((a == "margin height") || (a == "height margin")){
			return 3;
		}		
	}
	return 0;
}


// Создадим общую функцию для перерасчета элементов
function recountElems(){
	checkUserScreenSize();     // Проверяем размер экрана пользователя
	checkTheLargestElement();  // Проверяем наибольший элемент из массива элементов (с одним data-compareheight="NAME")
	checkAllowedMovingOrNot(); // Проверяем, можно ли выравнивать элемент или нет
	makeElementsEqualHeight(); // Выставляем margin-top у элементов	
};

setBreakePointWidthMinusScrollWidth(); // Вычисляем один раз ширину полосы прокрутки и вычетаем её
recountElems(); // Первый раз пересчитаем элементы, чтобы страница корректно отображалась

// При изменении размеров экрана пользователя перерасчитывать все значения
window.onresize = function(e){
	recountElems();
}

Проверим данный код на простом примере. Используем только центрирование по вертикали margin. Создадим две колонки через div, со стилем float:left. Рассчитаем размеры, какие получаем.

<head>
	<meta charset="UTF-8">
	<style>
		.elem1{
			width: 35%;
			border: 1px dashed #ccc;
			float: left;
			margin-left: 20px;
		}		
		p{
			padding: 0px;
			margin: 0px;
		}
		body{
			padding: 0px;
			margin: 0px;
		}
	</style>
</head>
<body>
	<div class="parent">
	<div class="elem1" data-compareheight="hello" data-compareheightdo="margin"><p>What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum</p></div>
	<div class="elem1" data-compareheight="hello" data-compareheightdo="margin"><p>What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's</p><p style="margin-top: 15px;">What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's</p></div>
	</div>
	<div style="clear: both"></div>
<script src="compareHeight.js"></script> <!-- Подключили наш скрипт -->
</body>

Ширина экрана 1100px. Высота большей колонки 198px. Высота дочерних элементов меньшей колонки 54px (высота строк) + 54px (высота строк) + 15px (margin-top у второго P ). В сумме получаем высоту дочерних элементов 123px.

Чтобы центрировать по вертикали нужно разницу поделить на два и добавить сверху в виде margin-top. (198px — 123px) / 2 = (75px) / 2 = 37.5px (требуемая величина margin-top). На практике имеем 38px. Всё верно.

Проверим также высоту, заменив data-compareheightdo="margin" на data-compareheightdo="height".

Разница в 1px между наибольшим и наименьшим блоком. (этот 1px взялся ранее, когда мы вычитали его в функции makeElementsEqualHeight.

Проверим ещё вариант, когда и центрирование по вертикали, и выравнивание высот колонок. Заменим: data-compareheightdo="margin" на data-compareheightdo="margin height"

Получаем:

  • 200px к 200px (высоты блоков равны)
  • margin-top: 38px и margin-bottom: 37px, объект центрирован по вертикали

P.S. эти значения я получил в Chrome в инструментах разработчика (F12 клавиша).

P.S.S. Данные методы работают только с DIV элементами (селектор по div установил).

Спасибо за внимание!
Поделиться с друзьями
-->

Комментарии (7)


  1. Fess_blaga
    29.03.2017 12:38
    +5

    Т.е. ты написал тонну js, чтобы вертикально выровнять элементы?

    С учетом поддержки flexbox на сегодняшний день и наличия как минимум 3-х альтернативных css способов, по меньшей мере странно))


    1. a84277755
      29.03.2017 15:54

      Да, уже понял, что немного погорячился с кодом, его размерами, когда можно было бы решить через CSS в пару строк. Но, может кому-то будет интересен ход мысли или поддержка IE9 (в коде вроде бы все элементы работают на IE9+))


      1. Aingis
        29.03.2017 22:11

        Через таблицы хоть IE4+ можно сделать. См. например ya.ru. А есть ещё методы через display: inline-block с распоркой (IE6+), позиционирование с трансформацией (IE9+, но желательно избегать), просто позиционирование с margin: auto, если заданы размеры (IE7+).


        Недаром есть правило, что в любой работе надо начинать с анализа существующих решений.


  1. Glomberg
    29.03.2017 12:38
    +1

    Дак а flex-box?
    CSS:

    .flex {
         display:flex;
         align-items:center;
    }
    

    HTML:
    <div class="row flex">
         <div class="col-md-6">текст</div>
         <div class="col-md-6">видео</div>
    </div>
    

    И просто добавляйте класс «flex» к нужным рядам.


    1. a84277755
      29.03.2017 15:52

      Спасибо, не уделял достаточно внимания CSS, обязательно изучу.



  1. DiphenylOxalate
    29.03.2017 19:06

    В тему — суровое выравнивание элементов на джаваскрипте: демо на fiddle