Динамическое меню c поддержкой touch move и mouse move на RevolveR -3




Наверняка многие из вас хотели бы научиться создавать красивые и подвижные меню в духе Android Java и Kotlin приложений. Скорее всего даже многие из вас ради этого уходили в области программирования отдельных приложений и были вынуждены осваивать инородный стек.

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

И так: Simple Dynamic Menu by RevolveR Labs.

image

Начинается все с верстки. Она должна быть семантической, легкой и современной.



<nav class="dynamic-menu">

	<ul>
		<li><a href="https://revolvercmf.ru">RevolveR Labs</a></li>
		<li><a href="#">Ultra newest solutions</a></li>
		<li><a href="#">The way of incredible</a></li>
		<li><a href="#">In search of the best</a></li>
		<li><a href="#">Progressive RevolveR frontends</a></li>
		<li><a href="#">Developing of new era</a></li>
	</ul>
</nav>

Мы используем стандартный маркированный список и HTML 5 в качестве элемента враппера, а чтобы сделать меню плавающим сразу пропишем CSS стили вытягивающие меню на за пределы экрана на всю ширину списка элементов и скроем все лишнее до области видимости:



.dynamic-menu {

	display: inline-block;
	text-align: center;
	overflow: hidden;
	margin: 0 auto;
	height: 3vw;
	width: 80%;

}

	.dynamic-menu ul {

		transition: all 2.5s ease-in-out;
		position: relative;
		list-style: none;
		width: 900vw;
		padding: 0;
		margin: 0;
		left: 0vw;

	}

		.dynamic-menu ul li {

			box-shadow: 0 0 0.1vw #333;
			border: .1vw dashed #fff;
			background: #a2a2a2;
			margin-bottom: 1vw;
			display: inline-block;
			border-radius: .2vw;
			margin-right: .5vw;
			padding: .2vw 1vw;
			background: #888;
			float: left;

		}

			.dynamic-menu ul li a {

				text-shadow: 0 0 0.2vw #fff;
				font: normal 2vw Helvetica;
				text-decoration: none;
				color: #006400;

			}

			.dynamic-menu ul li a:hover {

				text-decoration: underline;
				color: #674c2be0;

			}


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



Handler для desktop версии


Для работы хэндлера нам понадобится инициализировать RevolveR инстанс и использовать некоторое встроенное API работы с событиями:



let launch = RR.browser;

RR.menuMove = null;

if( !RR.isM ) {

	RR.event('.dynamic-menu ul', 'mousedown', (e) => {

		e.preventDefault();

		if( !RR.menuMove ) {

			RR.menuLeft = RR.curxy[0];

			RR.MenuMoveObserver = RR.event('body', 'mousemove', (e) => {

				e.preventDefault();

				RR.styleApply('.dynamic-menu ul', ['transition: all 0s ease']);

				RR.menuMove = true;

				RR.menuPosition = ( RR.menuLeft - RR.curxy[0] ) *-1;

				RR.styleApply('.dynamic-menu ul', ['left:'+ RR.menuPosition +'px']);

				RR.event('body', 'mouseup', (e) => {

					e.preventDefault();

					if( e.target.tagName === 'A' && !RR.touchFreeze ) {

						//R.loadURI(target.href, target.title);

						console.log(e.target.href);

						RR.touchFreeze = true;

						RR.menuMove = null;

					}

					void setTimeout(() => { 

						RR.menuMove = null;

					}, 50);

					void setTimeout(() => {

						if( !RR.menuMove ) {

							RR.styleApply('.dynamic-menu ul', ['left: 0px', 'transition: all 2.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)']);

						}

					}, 2500);

				});

			});

		}

	});

}

Большинство необходимых event уже работают после запуска гетера RR.browser(). Это например отслеживание событий изменения размера окна и постоянно обновление положения указателя мыши RR.curxy.



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



RR.MenuMoveObserver является собой event стеком, который хранит MD5 hash события для того, чтобы можно было выключить часть хэндлера отвечающего за смену положения по оси X. Мы выключаем обсерверы каждый раз когда событие клик завершилось в пользу mouseup.



Готово. При нажатии на левую клавишу мыши, если держать кнопку утопленной будет происходить отслеживание положения курсора мыши по оси X, а обсервер обеспечит своевременное обновление положения left контейнера списка меню внутри враппера области видимости и лента меню начнет двигаться открывая не поместившиеся элементы списка.



Мобильный handler меню


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



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



if( RR.isM ) {

	RR.event('.dynamic-menu ul', 'touchstart', (e) => {

		e.preventDefault();

		RR.menuMove = null;

		RR.event('body', 'touchend', (e) => {

			e.preventDefault();

			if( !RR.menuMove ) {

				RR.touchFreeze = null;

				let target = e.changedTouches[0].target;

				if( RR.isO(RR.MenuMoveObserver) ) {

					for( i of RR.MenuMoveObserver ) {

						RR.detachEvent( i[ 2 ] );

					}

				}

				if( target.tagName === 'A' && !RR.touchFreeze ) {

					//R.loadURI(target.href, target.title);

					console.log(e.target.href);

					RR.touchFreeze = true;

					RR.menuMove = null;

				}

				void setTimeout(() => {

					if( !RR.menuMove ) {

						RR.styleApply('.dynamic-menu ul', ['left: 0px', 'transition: all 2.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)']);
						//RR.animate('.dynamic-menu ul', ['left:0px:1000:wobble']);

					}

				}, 2500);

			}

		});

		if( !RR.menuMove ) {

			RR.menuLeft = e.changedTouches[0].screenX;

			RR.MenuMoveObserver = RR.event('body', 'touchmove', (e) => {

				e.preventDefault();

				RR.styleApply('.dynamic-menu ul', ['transition: all 0s ease']);

				RR.menuMove = true;

				RR.menuPosition = ( RR.menuLeft - e.changedTouches[0].screenX ) *-1; 

				RR.styleApply('.dynamic-menu ul', ['left:'+ RR.menuPosition +'px']);

					RR.event('body', 'touchend', (e) => {

						RR.menuMove = null;

					});

			});

		}

	});

}

В коде вы увидите небольшую разницу. Во первых event.target теперь не работает и нужно следить за сериями touch. Я добавил анимацию возвращения меню с эффектом easing и теперь меню само плавно возвращается в начальное положение спустя некоторое время бездействия с меню:



void setTimeout(() => {

	if( !RR.menuMove ) {

		RR.styleApply('.dynamic-menu ul', ['left: 0px', 'transition: all 2.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)']);

	}

}, 2500);

Demo


Чтобы посмотреть как работает Dynamic Menu на базе библиотеки RevolveR вы можете пройти по ссылке.



Итог


Многие из вас используют готовые плагины или встраивают блоки верстки с меню гамбургера, однако динамические линейные меню ни чуть не хуже и позволяют экономить место в интерфейсах. Многим из вас наверняка показалось бы сложным реализовать такие меню из-за разницы в слушателях событий и неумения использовать флаги. Этот пример был направлен показать, что средствами обычного JavaScript можно не обладая какой-то запредельной квалификацией реализовывать такие же вещи, как и в удобных Android приложениях, а мастерская работа с анимациями позволяет создавать потрясные эффекты переходов и разные хуки событий на какие-либо срабатывания.

Теги:




Комментарии (32):

  1. devlev
    /#22196262

    Да тут просто ничего не работает, от слова совсем!


    Сразу вспомнилась история с перезапуском кинопоиска

    • Full-R
      /#22198480

      Что именно не работает. Подробнее.

      • devlev
        /#22198824

        Ну на словах сложно объяснить. Тут видео нужно записывать экрана. Под рукой такого софта нет.


        Попробую объяснить на словах: сдвигаю меню влево примерно на 100-200px, далее бросаю курсор и снова двигаю меню на столько же, когда я третий раз пытаюсь передвинуть меню, после бросания курсора оно сразу уезжает на начальную позицию. Предполагается что меню не должно езжать пока курсор висит над меню. А если пунктов будет не 5 а 50?


        Но я по стилю кода вижу, что баги будут 100%. Например, если вы создаете setTimeout но не как не используете clearTimeout. Если вы рассчитываете, что проверка if( !RR.menuMove ) спасет от повторного вызова, то вы глубоко ошибаетесь!


        Хороший тон для каждого setTimeout писать где то рядышком clearTimeout чтобы если вдруг нужно отменить действие, это можно было сделать легко быстро!


        Пример из мира React

        • Full-R
          /#22198922

          Спасибо. Как то не учел лихорадочных и очень длинные списки. Будет время — добавлю ещё один флаг блокировки возврата. Если void стоит таймер типа setTimeout уничтожается после первого и единственного тика. Если void добавить к setIntetval, то таймер потеряет id и его невозможно будет остановить. Флагом просто проще сделать, если блокировку отката на hover повешать.

          • devlev
            /#22201696

            Вы ничего не поняли! Вот такой код:
            void setTimeout(() => {
            это плохо! Нужно писать:
            const timer = setTimeout(() => {
            и где-то должен быть возможен вызов
            clearTimeout(timer)


            Иначе у вас будет бесконечное число сайд эффектов, и вы только и дальше будите их плодить.

            • Full-R
              /#22202326

              А вы хорошо уверены, что хороший тон константой nullable делать? Оно ведь не сбросится скорее всего.

              • devlev
                /#22202438

                Слово const используется чтобы самому себе не выстрелить в ногу: не перезаписать переменную и не забыть что надо удалить таймер в случае отмены эффекта.

                • Full-R
                  /#22202576

                  В циклах for тоже можно const на итератор поставить и оно даже работает пока strict не включить. А я вот так никогда не сделаю.

        • Full-R
          /#22199952

          Вот чуть доработанный код. Теперь у рыпнутых нет возврата меню когда они рыпают мышью, пальцами и в общем-то можно применять к длинным спискам. Если подскажете что можно улучшить — обновлю в публикацию.

          let launch = RR.browser;
          
          RR.menuMove = null;
          
          let turnBack = () => {
          
          	return void setTimeout(() => {
          
          		if( !RR.menuMove && RR.turnBack && RR.allowReturn ) {
          
          			RR.styleApply('.dynamic-menu ul', ['left: 0px', 'transition: all 2.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)']);
          
          		}
          
          	}, 2500);
          
          }
          
          if( !RR.isM ) {
          
          	RR.event('.dynamic-menu ul', 'click', (e) => {
          
          		e.preventDefault();
          
          	});
          
          	RR.event('.dynamic-menu ul', 'mousedown', (e) => {
          
          		e.preventDefault();
          
          		if( !RR.menuMove ) {
          
          			RR.menuLeft = RR.curxy[0];
          
          			RR.touchFreeze = null;
          
          			RR.MenuMoveObserver = RR.event('body', 'mousemove', (e) => {
          
          				e.preventDefault();
          
          				RR.styleApply('.dynamic-menu ul', ['transition: all 0s ease']);
          
          				RR.menuMove = true;
          
          				RR.menuPosition = ( RR.menuLeft - RR.curxy[0] ) *-1;
          
          				RR.styleApply('.dynamic-menu ul', ['left:'+ RR.menuPosition +'px']);
          
          				RR.event('body', 'mouseup', (e) => {
          
          					e.preventDefault();
          
          					for( i of RR.MenuMoveObserver ) {
          
          						RR.detachEvent(i[ 2 ]);
          
          					}
          
          					if( e.target.tagName === 'A' && !RR.touchFreeze ) {
          
          						//R.loadURI(target.href, target.title);
          
          						console.log(e.target.href);
          
          						RR.touchFreeze = true;
          
          						RR.menuMove = null;
          
          					}
          
          					void setTimeout(() => { 
          
          						RR.menuMove = null;
          
          					}, 50);
          
          					RR.event('.dynamic-menu ul', 'mouseenter', () => {
          
          						RR.turnBack = null;
          
          						RR.event('.dynamic-menu ul', 'mouseleave', () => {
          
          							RR.turnBack = true;
          
          							RR.allowReturn = true;
          
          							turnBack();
          
          						});
          
          					});
          
          				});
          
          			});
          
          		}
          
          	});
          
          }
          
          if( RR.isM ) {
          
          	RR.event('.dynamic-menu ul', 'touchstart', (e) => {
          
          		e.preventDefault();
          
          		RR.menuMove = null;
          
          		RR.turnBack = null;
          
          		RR.allowReturn = true;
          
          		RR.event('body', 'touchend', (e) => {
          
          			e.preventDefault();
          
          			if( !RR.menuMove ) {
          
          				RR.touchFreeze = null;
          
          				let target = e.changedTouches[0].target;
          
          				if( RR.isO(RR.MenuMoveObserver) ) {
          
          					for( i of RR.MenuMoveObserver ) {
          
          						RR.detachEvent( i[ 2 ] );
          
          					}
          
          				}
          
          				if( target.tagName === 'A' && !RR.touchFreeze ) {
          
          					//R.loadURI(target.href, target.title);
          
          					console.log(e.target.href);
          
          					RR.touchFreeze = true;
          
          					RR.menuMove = null;
          
          				}
          
          			}
          
          		});
          
          		if( !RR.menuMove ) {
          
          			RR.menuLeft = e.changedTouches[0].screenX;
          
          			RR.MenuMoveObserver = RR.event('body', 'touchmove', (e) => {
          
          				void setInterval(() => {
          
          					if(RR.menuMove) {
          
          						RR.allowReturn = null;
          
          					} 
          					else {
          
          						RR.allowReturn = true;
          
          					}
          					
          
          				}, 300);
          
          				e.preventDefault();
          
          				RR.turnBack = null;
          
          				RR.event('.dynamic-menu ul, body', 'touchend', () => {
          
          					RR.turnBack = true;
          
          					setTimeout(() => {
          
          						RR.allowReturn = true;
          
          					}, 300);
          
          					turnBack();
          
          				});
          
          				RR.styleApply('.dynamic-menu ul', ['transition: all 0s ease']);
          
          				RR.menuMove = true;
          
          				RR.menuPosition = ( RR.menuLeft - e.changedTouches[0].screenX ) *-1; 
          
          				RR.styleApply('.dynamic-menu ul', ['left:'+ RR.menuPosition +'px']);
          
          					RR.event('body', 'touchend', (e) => {
          
          						RR.menuMove = null;
          
          					});
          
          			});
          
          		}
          
          	});
          
          }
          

          • devlev
            /#22201718

            Это называется спагетти код! У меня было пару программистов, которые писали подобные вещи. Они не хотели учиться. Уволены. Мой вам совет, выкиньте на помойку свои знания по Javascript и начните учиться современным методам программирования. То что вы пишите, писали в начале 2000 годов. Сейчас уже есть куча всего готового и удобного. А перед тем как изобретать свой велосипед нужно сначала изучить все остальные велосипеды: Typescript, React, Vue, Angular — список можно продолжать бесконечно.

            • Full-R
              /#22207022

              А че вы Laravel сразу не пиарите? Ненавижу шестерок инфраструктуры, в которой они даже не разработчики. Захотелось разбить вам лицо.

              • owwyye
                /#22211404

                Милейший, вам таблеточки пить надо. Или в Красноуфимске все айтишники друг другу лицо бьют во время дискуссий?

                • Full-R
                  /#22211416

                  Таблеточки пить надо после того как лицо разобьют. Для вас не милейший.

                  • owwyye
                    /#22213346

                    Знаешь, я из научного интереса посмотрел твой вк. Ты настолько цельный персонаж что прямо хоть в палату мер и весов. Скажи, ты правда считаешь что в этих очках неотразим, мачо ты красноуфимский?

                    И про хабраэффект смешно ты написал.

                    • Full-R
                      /#22213418

                      Давайте от темы не отвлекаться. Я с этого и начинал. Отвечайте мне вот там же где это и было, если хотите. Нафиг ты мне срешь?

                      p.s.: можно ли зачистить offtop или как-то в спойлер свернуть? надоели.

  2. vmkazakoff
    /#22198086

    Это такая реклама вашей штуки под названием РевольвеР? Вы сами же пробовали открыть свои демо? И сайт cmf дальше? С телефона тоже?


    На всякий подскажу: полезно бывает дать ссылку человеку который не знает ничего ещё и посмотреть на его реакцию. Вот просто взять и в глаза заглянуть.


    В данном случае там будет немой вопрос "за что?"… Не все разделяют вашу любовь к странным штриховке на прелоадере, думаю. А когда 3 прелоадера вместе вдруг появляются, это блин вообще жесть что творится.


    По существу: вы сделали меню, которое можно было сделать одним css (overflow-x: scroll и если надо то можно ещё скроллбар убрать потом). А ещё ваше творение выглядит как поделка школьника, уж простите (

    • Full-R
      /#22198512

      Какой у вас телефон? Я смартфон использую с Edge. У меня все работает. Недавно перестал обращать внимание на косяки FireFox из-за частых багов самого браузера и ориентируюсь в основном на Chromium браузеры. Preloader к статье не относится. Он просто версткой сделан и CSS и любой желающий может его убрать или переделать.

      • vmkazakoff
        /#22200596

        Меню получилось очень плохим. Совсем. Совершенно не пригодно ни для чего. Идеальный антипаттерн по всем параметрам.

        — я не могу листать его боковым скроллом на тачпаде ноутбука
        — листать горизонтально меню на десктопе зажав кнопку это жесть с точки зрения интерфейса и удобства
        — поведение на мобиле это не свайп (который совсем другую физику имеет), а именно таскание, что ну абсолютно не удобно — я не могу быстро провести пальцем и прокрутить
        — на десктопе легче сделать стрелки или любой другой аналог слайдера выбрать (да даже просто нативный скролл лучше в 100500 раз)
        — вы наоверинжинирили длиннющий JS когда вообще можно было обойтись несколькими строками CSS (и то если думать про IE где надо скроллбар спрятать, а на хромиум так вообще одним стилем)
        — при этом оверинжиниринге вы не предусмотрели 100500 в 100500 степени условий, за который юзеры будут вас не любить (попробуйте нажать правой кнопкой в любом месте вашего меню, а потом в любое другое место — не могу проверить с мобилы, но я уверен что вы дальше будете водить мышкой во все стороны и меню будет крутиться, хотя кнопку вы уже не жмете)
        — да только за код написанный через строчку я бы джуна как минимум попросил так больше не делать, но вообще настройки вашего линтера я реально не хочу знать — мусье явно знает толк в извращениях
        — страница с демо — блок меню с фоном #888, с темно зеленым шрифтом, и блок и шрифт с тенями, да еще у блока белая обводка dashed?! Не, на вкус и цвет, само собой, не хочу быть занудой и придираться к вкусовщине… Но блин!!!
        — раз затронул стили — border толщина заданная в vh? Это зачем вообще?
        — посмотрите пример с табами (https://materializecss.com/tabs.html) — второй блок как раз скроллится как и ваш, но выглядит по людски и управлять им легче. Я молчу уж что читсый CSS без костылей и забытых вами на каждом шагу листнеров (уже выше написали)

        Ну и оффтом (статья правда не про прелоадеры была) — вот как выглядит ваша страница cmf в хроме на мобиле: image

        • Full-R
          /#22200638

          Спасибо за ценное замечание про touchpad. У меня было ориентировано на touch экраны и полноценную мышь. Хотя я вообще то на touchpad всю Ghotic 3 прошел с двумя мечами и луком.


          Я бы свами согласился, что на CSS лучше, если бы вы показали годный рабочий пример.


          Что до дизайна сайта моей компании — это к статье не относится. В chrome под Android у меня все прекрасно загружается и прелоадер исчезает. Дальше при промотке страницы срабатывает Lazy Load для изображений, как и задумано.


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

  3. Sorvs71
    /#22198474

    Полезно однако)

  4. owwyye
    /#22198476

    Какой у вас прекрасный удивительный сайт! Три разных лоадера!

    • Full-R
      /#22198558

      Где вы увидели три разных лоадера? Один и тот же эффект и код используется для DOMContentLoaded и fetch запросов. И тот же самый SVG для lazy load картинок.

      • owwyye
        /#22200540

        Как бы вам объяснить… У вас плохо все, абсолютно все.

        • Full-R
          /#22200610

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

        • vmkazakoff
          /#22200620

          Я вот расписал выше подробнее, но ощущение, что если человек сам этого не видит, то ваш ответ был более логичный… )))

          • Full-R
            /#22200668

            Господин мазафаков, ой, вмказакоффЪ. Какой никнейм сложновоспринимаемый. Я вам ответил выше. Предлагаю схлопнуться в одну веточку, а то полезное осязаемое пользователями пространство транжириться на 80% не относящимся к теме вопросам.

            • vmkazakoff
              /#22200732

              Сударь, вы неизлечимы :)

              Раз медицина тут бессильна, то и я не стану мешать эволюции — авось сама разберется.

            • dopusteam
              /#22201850

              Вам нормальный фидбек выше дали, вы от него как то мастерски уклонились и зачем то просите ещё

              • owwyye
                /#22205796

                Просто человек любит обмазываться и дро… учить.

          • owwyye
            /#22203294

            Да, это тот самый случай. Удивительно вот что: человек вроде живет не в вакууме, пользуется (хочется верить) современными сайтами, но как у многих других айтишников напрочь лишен способности анализировать увиденное (см. дизайн программиста). Обычно это еще накладывается на брызжущее во все стороны эго и исключительную упёртость выражающиеся в репликах «индустрия свернула не туда» и тотальное отрицание типичных интерфейсных практик и паттернов.

            И это все не про спагетти-код, с этим пациентом в принципе никакой диалог невозможен.

            • Full-R
              /#22203690

              Вас бы поняли может быть, но раньше говорили проще: «Портфолио покажи». Не надо за всех отвечать и про мою неполноценность намекать. Это скорее вам надо пару курсов хорошего тона и долгую переквалификацию. Я уже в современном мире, а вы где-то как раз в заднем вакууме со своим недовольством.

              • owwyye
                /#22205794

                Бхахаха, а поциэнт реально упоротый.