Как стать автором
Обновить

Ваня едет к бабушке или динамический адаптив на JavaScript

Время на прочтение9 мин
Количество просмотров8.8K
Во время работы над адаптивом нам то и дело приходится изменять внешний вид объектов. Обычно нам для этого достаточно прописать несколько медиа-запросов и у нас всё получается. В некоторых случаях нам нужно изменить порядок элементов и тут на помощь приходит, например, свойство order из технологии Flexbox или Grid. Но бывают случаи, когда нам это не поможет.

Идея динамического адаптива призвана упростить жизнь верстальщика и быстро адаптировать сложные элементы макета.


Например, посмотрим на этот случай. Здесь у нас есть следующая структура: заголовок и некоторый текст.



Но при определенном разрешении дизайнер указал – текст, который находился под заголовком, должен теперь находиться в совершенно другом месте структуры.



Обычно мастера по верстке решали эту проблему, одним из двух способов. Быстро о них напомню.

Показывать буду на примере жизненной ситуации, когда городской мальчик Ваня очень хотел приехать на лето к бабушке. Но не потому, что он соскучился по бабуле, а потому что хотел сходить на речку с девочкой Олей. И хотел он это сделать на разрешении ниже 992px.



Как же мы будем решать проблему Вани?
Мы можем поступить следующим образом: Ваню в городе скрыть на этом разрешении, а Ваню на речке вместе с Олей показать. Но для этого нам нужно сделать клон Вани.
Вот городской (оригинальный) Ваня. А вот Ваня, собственно, на речке уже с Олей.



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



У нас всё получилось – на нужном нам разрешении Ваня оказывается под Олей.



Соответственно, когда разрешение больше, чем 992px, Ваня срочным образом возвращается в город.



И всё вроде бы окей, Ваня с Олей тогда, когда это нужно. Можно расходиться…

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

Сейчас нам повезло, в нашей ситуации Ваня какой-то маленький, а бывают Вани очень большие, массивные, накачанные ребята, которые соответственно требуют очень много строк кода. И в такой ситуации, в том числе заказчики, просят выполнить перемещение объекта DOM с помощью JavaScript.

Это можно сделать примерно следующим образом. Для начала я должен в HTML убрать Ваню, который клон, и оставить Ваню оригинального. В CSS закомментировать все строки, которые у нас были изначально для другого варианта переноса. И добавить вот такой нехитрый JS-код.

//Объявляем переменные
const parent_original = document.querySelector('.content__blocks_city');
const parent = document.querySelector('.content__column_river');
const item = document.querySelector('.content__block_item');

//Слушаем изменение размера экрана
window.addEventListener('resize', move);

//Функция
function move(){
	const viewport_width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
	if (viewport_width <= 992) {
		if (!item.classList.contains('done')) {
			parent.insertBefore(item, parent.children[2]);
			item.classList.add('done');
		}
	} else {
		if (item.classList.contains('done')) {
			parent_original.insertBefore(item, parent_original.children[2]);
			item.classList.remove('done');
		}
	}
}

//Вызываем функцию
move();

Сначала мы объявляем переменные, где «дозваниваемся» до города, речки и оригинального Вани. Далее мы начинаем «слушать» ширину экрана, чтобы отлавливать тот момент, когда нам нужно Ваню переместить. После мы получаем ширину и ставим условие, в котором проверяем, что ширина меньше или равна 992px. При перемещении Вани мы ему присваиваем класс, поэтому далее мы проверяем на наличие класса, чтобы это перемещение не происходило постоянно (Ваня один раз переехал и всё, его не нужно постоянно переносить). И выполняем сам перенос внутрь родителя (к речке), здесь указываем позицию в дереве DOM, куда нам нужно Ваню переместить (под Олю, над Олей). А когда у нас разрешение больше, чем 992px, мы Ваню должны вернуть на место.

Пробуем и видим, что наш Ваня снова переместился под Олю.

И всё окей: у нас нет никакого дубля контента, всё неплохо работает, заказчик доволен, все довольны. Но у этого метода есть ряд проблем, которые связаны с самим написанием js-кода.
Мы понимаем, что для единичного случая это нормально – мы написали и хорошо. Но мне пришла идея автоматизировать этот процесс. Допустим мне нужно перебрасывать 5-6 абсолютно разных элементов на странице. Под эту задачу написание js-кода таким методом довольно трудозатратное и долгое. И второй момент, который, естественно, многих из вас напряг, – это нагрузка на ПК пользователя, т.к. мы здесь постоянно «слушаем» ширину экрана. Этот момент я тоже хотел оптимизировать. Его можно оптимизировать и этом коде, но раз уж я написал автоматизатор, который я назвал «динамический адаптив», то оптимизацию я провел там.

Сейчас я вам продемонстрирую как это всё работает.

Подключив свой динамический адаптив, мне достаточно работать только с HTML файлом и нет необходимости обращаться к JavaScript. Для этого мне нужно написать атрибут data-da (da – сокращение от dynamic adaptive) и в кавычках указать три параметра, разделенные запятой.
Первый параметр – куда. Я беру уникальный класс объекта, куда я хочу Ваню перемещать.
Второй параметр – какой (по счёту). В моём объекте два элемента: нулевой и первый (мы знаем, что в JavaScript всё начинается с нуля). Мне нужно вставить Ваню после «1» объекта, поэтому я пишу параметр «2».

Третий параметр – когда. В третьем параметре я пишу «992», это разрешение, ниже которого Ваня очень хочет попасть к Оле.

Второй и третий параметр не обязательны. По умолчанию объект будет перемещатся в конец блока при разрешении ниже 768px.



Проверяем. Работает всё также, как в предыдущих версиях, но уже без дубля контента и всё автоматизировано.

Рассмотрим удобства.

Теперь для того, чтобы переместить Ваню над Олей, мне достаточно просто во втором параметре атрибута поменять порядковый номер. И Ваня уже окажется над Олей.



Удобно, правда? Тоже самое я могу сделать с третьим параметром-брейкпоинтом. Я могу указать, например, 1280px. (data-da=«content__column_river,1,1280»). Так Ваня поедет к Оле немного раньше, именно при достижении этого значения. И что самое прикольное – Ваня вернётся на своё место абсолютно автоматически.

Для дополнительного комфорта, вместо порядкового номера во втором параметре атрибута, можно указывать одно из двух значений – first или last. Когда я напишу first, Ваня переедет над заголовком «Я речка». Он расположится самым первым внутри объекта, куда мы его отправляем. Соответственно, если указать last, то наш Ваня окажется в самом низу объекта.



Рассмотрим ещё пару примеров. Допустим, Колю мы можем отправить в огород к Лоцы, а Аню тоже отправить на речку к Ване и Оле, будет веселее. Пишем для Коли атрибут с параметрами
data-da=«content__column_garden,2,992». А для Ани пишем data-da=«content__column_river,1,1280»

Смотрим: Аня, Оля и Ваня на речке. Коля едет к Лоцы немного позже. При возврате все ребята оказываются на своих местах. Всё замечательно.

Поиграться в CodePen: codepen.io/FreelancerLifeStyle/project/full/ZmWOPm

Конечно у вас может возникнуть вопрос: а как же событие, которое могли повесить на перемещаемые объекты? Например, изначально при клике на Аню у нас появляется окошко с надписью: «Всё ок». Посмотрим что будет, когда она переедет с ребятами на речку.
Перемещаем, делаем клик – всё отлично работает, окошко также отображается. Событие остаётся вместе с объектом при его переносе.



И второй волнующий вопрос – это нагрузка, которую я постарался минимизировать. Сейчас я это покажу, немного рассказав о коде, который скрыт под капотом динамического адаптива.

Первым делом я объявляю некоторые переменные, в том числе создаю массивы, которые ниже заполняю. Я получаю атрибут, разделяю его, превращая в массив, чтобы получить отдельные элементы. Далее я заполняю оригинальные позиции объектов для того, чтобы потом их вернуть на место к родителям (в город). После я заполняю массив самих элементов, чтобы его потом отсортировать для верной работы.

let originalPositions = [];
let daElements = document.querySelectorAll('[data-da]');
let daElementsArray = [];
let daMatchMedia = [];
//Заполняем массивы
if (daElements.length > 0) {
	let number = 0;
	for (let index = 0; index < daElements.length; index++) {
		const daElement = daElements[index];
		const daMove = daElement.getAttribute('data-da');
		if (daMove != '') {
			const daArray = daMove.split(',');
			const daPlace = daArray[1] ? daArray[1].trim() : 'last';
			const daBreakpoint = daArray[2] ? daArray[2].trim() : '767';
			const daDestination = document.querySelector('.' + daArray[0].trim())
			if (daArray.length > 0 && daDestination) {
				daElement.setAttribute('data-da-index', number);
				//Заполняем массив первоначальных позиций
				originalPositions[number] = {
					"parent": daElement.parentNode,
					"index": indexInParent(daElement)
				};
				//Заполняем массив элементов 
				daElementsArray[number] = {
					"element": daElement,
					"destination": document.querySelector('.' + daArray[0].trim()),
					"place": daPlace,
					"breakpoint": daBreakpoint
				}
				number++;
			}
		}
	}
	dynamicAdaptSort(daElementsArray);

	//Создаем события в точке брейкпоинта
	for (let index = 0; index < daElementsArray.length; index++) {
		const el = daElementsArray[index];
		const daBreakpoint = el.breakpoint;
		const daType = "max"; //Для MobileFirst поменять на min

		daMatchMedia.push(window.matchMedia("(" + daType + "-width: " + daBreakpoint + "px)"));
		daMatchMedia[index].addListener(dynamicAdapt);
	}
}

И в конце осуществляется та оптимизация, о которой я говорил: вместо window resize (слушать каждое изменение окна), мы будем слушать только изменения в точке брейкпоинта с помощью window matchMedia. Таким образом мы получим реакцию браузера только в некоторых точках: в тех, которые мы указали в атрибутах наших объектов.

Далее я собираю массив этих брейкпоинтов и на каждый брейкпоинт вешаю событие. В основной функции dynamic_adapt я всё это разворачиваю внутри массива, который мы собрали и отсортировали, проверяю наличие того или иного брейкпоинта, перемещаю элементы или же возвращаю их на место.

//Основная функция
function dynamicAdapt(e) {
	for (let index = 0; index < daElementsArray.length; index++) {
		const el = daElementsArray[index];
		const daElement = el.element;
		const daDestination = el.destination;
		const daPlace = el.place;
		const daBreakpoint = el.breakpoint;
		const daClassname = "_dynamic_adapt_" + daBreakpoint;

		if (daMatchMedia[index].matches) {
			//Перебрасываем элементы
			if (!daElement.classList.contains(daClassname)) {
				let actualIndex = indexOfElements(daDestination)[daPlace];
				if (daPlace === 'first') {
					actualIndex = indexOfElements(daDestination)[0];
				} else if (daPlace === 'last') {
					actualIndex = indexOfElements(daDestination)[indexOfElements(daDestination).length];
				}
				daDestination.insertBefore(daElement, daDestination.children[actualIndex]);
				daElement.classList.add(daClassname);
			}
		} else {
			//Возвращаем на место
			if (daElement.classList.contains(daClassname)) {
				dynamicAdaptBack(daElement);
				daElement.classList.remove(daClassname);
			}
		}
	}
	customAdapt();
}

Функция возврата на изначальную позицию:

//Функция возврата на место
function dynamicAdaptBack(el) {
	const daIndex = el.getAttribute('data-da-index');
	const originalPlace = originalPositions[daIndex];
	const parentPlace = originalPlace['parent'];
	const indexPlace = originalPlace['index'];
	const actualIndex = indexOfElements(parentPlace, true)[indexPlace];
	parentPlace.insertBefore(el, parentPlace.children[actualIndex]);
}

Естественно, у многих появился вопрос по поводу Mobile First. Нет никаких проблем – нам в коде нужно изменить только некоторые переменные (вынос этой настройки в HTML уже в работе):

  1. Когда я собираю массив из брейкпоинтов, нам нужно здесь «max» поменять на «min». И будет «слушаться» событие не по max-width, а по min-width.
  2. Также нужно изменить сортировку, чтобы у нас объекты в массиве выстраивались уже в другом порядке, когда мы сортируем их по брейкпоинту.

	//Сортировка объекта
	function dynamicAdaptSort(arr) {
		arr.sort(function (a, b) {
			if (a.breakpoint > b.breakpoint) { return -1 } else { return 1 } //Для MobileFirst поменять
		});
		arr.sort(function (a, b) {
			if (a.place > b.place) { return 1 } else { return -1 }
		});
	}

Также в динамическом адаптиве я использую определённый метод получения индекса при перебросе объекта. И здесь есть первые недочёты – мне приходится исключать перенесённый объект. Т.е. когда я переношу объекты в одно и тоже место, чтобы у нас было всё адекватно и объекты выстраивались в том порядке, в котором мы хотим, мне нужно при переносе исключить уже перенесенный ранее объект в ту же точку.

//Функция получения массива индексов элементов внутри родителя 
function indexOfElements(parent, back) {
	const children = parent.children;
	const childrenArray = [];
	for (let i = 0; i < children.length; i++) {
		const childrenElement = children[i];
		if (back) {
			childrenArray.push(i);
		} else {
			//Исключая перенесенный элемент
			if (childrenElement.getAttribute('data-da') == null) {
				childrenArray.push(i);
			}
		}
	}
	return childrenArray;
}

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

Одним из путей развития этого динамического адаптива могут быть, например, создание нескольких брейкпоинтов. Пример, нам нужно Ваню переместить к Оле на 1280px, а на 992px Ваню переместить в огород, либо вернуть на место. Этого функционала сейчас нет, так что есть ещё над чем работать. В целом, для покрытия 95% обыденных задач при адаптиве, мне вполне достаточно того функционала, который есть сейчас. Чтобы решить задачу с переносом элементов в другое место, мне нужно потратить теперь несколько секунд, написав атрибут. И я получу нужный мне результат.

Естественно, я призываю абсолютно всех, кому понравился динамический адаптив, принять участие в его тестировании, разработке и улучшении как функционально, так и оптимизации самого кода. И всё для того, чтобы наши Коли, Ани, Вани и Оли находились вместе тогда, когда они этого хотят.

Репозиторий на GitHubgithub.com/FreelancerLifeStyle/dynamic_adapt

На основе видео "Динамический адаптив // Быстрая адаптация сложных объектов на JavaScript" на youtube канале "Фрилансер по жизни"
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как вы решали подобные задачи при адаптиве?
50% Дублировал контент16
31.25% Использовал JS10
40.63% Теперь буду использовать «Динамический адаптив»13
Проголосовали 32 пользователя. Воздержались 9 пользователей.
Теги:
Хабы:
Всего голосов 6: ↑3 и ↓30
Комментарии11

Публикации

Истории

Работа

Ближайшие события