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

  • Tutorial
Во время работы над адаптивом нам то и дело приходится изменять внешний вид объектов. Обычно нам для этого достаточно прописать несколько медиа-запросов и у нас всё получается. В некоторых случаях нам нужно изменить порядок элементов и тут на помощь приходит, например, свойство 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,0%Дублировал контент9
  • 33,3%Использовал JS6
  • 27,8%Теперь буду использовать «Динамический адаптив»5
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 11

    +1

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

      0

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


      Автор красавчик, в таком стиле и надо писать.


      А инструкций с канцелярский языком "вы можете произвольно переместить объект в пределах бла-бла-бла..." и так более, чем достаточно.


      Всегда, во всех примерах нужны не абстрактные "объекты", а конкретные Оли и Вани.

      0
      1. Непонятно, зачем нужны пляски с индексами. Гораздо лучше сделать просто 2 пустые обертки с идентификаторами или дата-аттрибутами и перекидывать блок туда-сюда. Это и в использовании намного прозрачнее и удобнее, и код сократит раза в два.

      2. Решение с matchMedia плохое. Проблему производительности решает только частично. Плюс порождает паразитные связи в коде. Нужно делать нормальный тротлинг и/или ResizeObserver.

      3. Общее качество кода так себе.
        0
        можете предложить свой вариант кода что бы сравнить удобство работы и производительность?
          0
          Я предпочту не тратить время на скрипты, а решить вопрос без них :) По возможности версткой, а если ну никак не получается — продублировать контент. Это будет проще и надёжнее. Для маленьких блочков дублирование вполне приемлемо, а для основного контента такой надобности и не возникает.
            0
            Я вас понял. К сожалению, или к счастью, мои заказы никак не предусматривают реализации дублем контента, а современный дизайн пораждает множество подобных ситуаций все чаще и чаще. Решение явно необходимо, по крайней мере в моей практике.
              +1
              Я прекрасно понимаю, почему нельзя дублировать основной/большой контент — сео, санкции за спам и вот это всё. Это понятно. Но возводить это правило в абсолют = создать себе и клиенту проблемы.

              Возьмем пример из поста — там блок буквально из десятка слов. Это точно не тянет на нарушение. Адрес и телефон часто пишут в шапке и в подвале — выходит, что такое дублирование можно? Почему же?

              Всегда выступаю против того, чтобы завязывать верстку за скрипты. Это чревато проблемами, которые неочевидны поначалу, но у кого-то да вылезут.
              Медленный интернет, скрипты долго грузятся? Сайт поломан. Юзер использует NoScript? Нарвались на старый браузер, который не прожевал что-то из ES6? Фронтэндер собирая бандл не уследил за порядком скриптов и засунул перед вашей турбомагией тяжелый социальный виджет? И так далее.

              Кстати, для фриланса это особенно важно, потому что он часто работает по принципу сделал-отдал-попрощались. В продуктовой компании-то ладно — ну всплывут проблемы, оперативно поправим. А хороший фрилансер минимизирует количество потенциальных граблей для клиента и других его разработчиков. Лучше надежность и очевидность. Нужно только уметь это объяснить.
                –1
                Я никого не заставляю это использовать) Но я, как фрилансер со стажем, применял и буду применять динамический адаптив в тех случаях когда это приемлемо. В том числе по многочисленным просьбам клиентов. Это быстро решает вопрос адаптации и превращает верстку в захватывающий процес так или иначе расставляя блоки в адаптиве.
                  0
                  Не спорю, процесс борьбы с багами всегда очень захватывающий. Особенно для тех, кто столкнется с этим чудо-кодом потом.

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

    Самое читаемое