В прошлый раз мы научились менять стандартное контекстное меню браузера на своё. Но наше меню получилось слишком простым и некрасивым, а ещё им сложно было управлять из скрипта. Сегодня мы исправим оба недостатка и сделаем красиво:
Готовим страницу
В предыдущем проекте мы на страницу добавили только пустой блок, а всё остальное делали через скрипт. Это не очень удобно — если нужно добавить или убрать какие-то элементы, надо идти в скрипт и править всё там. На этот раз сделаем иначе — сверстаем контекстное меню как часть страницы, а потом в скрипте скроем его.
Чтобы меню получилось красивым, сразу подключим новый шрифт и нормализатор — он уберёт всё лишнее оформление в браузерах и позволит нам настраивать внешний вид по своему усмотрению.
<!DOCTYPE html>
<html lang="ru" >
<head>
<meta charset="UTF-8">
<title>Стильное контекстное меню</title>
<!-- подключаем новый шрифт -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<!-- подключаем нормализатор CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
<!-- подключаем свои стили -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- всё меню будет в этом блоке -->
<div class="menu">
</div>
</body>
</html>
Наполним наше меню и используем для этого список, как и прошлый раз. Но если тогда мы делали единое меню, то сейчас добавим разделы — они будут отделяться друг от друга и группировать по смыслу разные пункты меню. Для этого добавим к каждому такому списку класс “menu-list”
. Внутрь этого списка мы сложим все элементы нашего меню, причём у каждого элемента списка будет класс “menu-item”
— так мы сможем настраивать внешний вид всех элементов.
А теперь сложная часть: каждый пункт меню сделаем кнопкой — так мы сможем легко обрабатывать нажатия и группировать визуальные элементы в меню. Сначала в кнопку мы положим иконку c помощью тега <i>
— он устанавливает курсивное начертание текста, но текста в нём не будет, только иконка. После этого мы пишем название кнопки и закрываем тег кнопки.
Чтобы было понятнее, разберём первый элемент максимально подробно:
<!-- всё меню будет в этом блоке -->
<div class="menu">
<!-- первый раздел в меню -->
<ul class="menu-list">
<!-- список с элементами меню -->
<li class="menu-item">
<!-- каждый пункт меню — это кнопка -->
<button class="menu-button">
<!-- иконка -->
<i data-feather="corner-up-right"></i>
<!-- название пункта меню -->
Поделиться
</button>
</li>
<!-- начался новый пункт меню -->
<li class="menu-item"><button class="menu-button"><i data-feather="edit-2"></i>Переименовать</button></li>
</ul>
</div>
На странице появились две кнопки со стандартным оформлением — это нормально, потому что мы ещё не настраивали стили. Но на кнопках нет иконок, которые мы прописывали в теге <i>, — а всё потому, что браузер ничего не знает про эти иконки и откуда их брать. Чтобы это исправить, используем js-библиотеку иконок Feather, которая работает так:
- Мы на странице указываем название иконки, которую хотим получить, с помощью свойства data-feather, например data-feather=”edit-2″
- Потом подключаем библиотеку на странице:
<script src=’https://unpkg.com/feather-icons@4.29.0/dist/feather.min.js’></script> - А затем подключаем свой скрипт script.js и в нём пишем команду:
feather.replace(); - Эта команда находит на странице все ссылки на иконки и меняет их на SVG-картинки.
Сделаем всё это и посмотрим на результат:
Точно так же, по разделам, сделаем всё остальное меню. Единственное, что нам понадобится дополнительно — это новый список с классом “menu-sub-list” для вложенного меню. Читайте комментарии в коде, чтобы было проще понять, что происходит на странице:
<!-- всё меню будет в этом блоке -->
<div class="menu">
<!-- первый раздел в меню -->
<ul class="menu-list">
<!-- список с элементами меню -->
<li class="menu-item">
<!-- каждый пункт меню — это кнопка -->
<button class="menu-button">
<!-- иконка -->
<i data-feather="corner-up-right"></i>
<!-- название пункта меню -->
Поделиться
</button>
</li>
<!-- начался новый пункт меню -->
<li class="menu-item"><button class="menu-button"><i data-feather="edit-2"></i>Переименовать</button></li>
</ul>
<!-- второй раздел -->
<ul class="menu-list">
<li class="menu-item"><button class="menu-button menu-button--black"><i data-feather="circle"></i>Статус<i data-feather="chevron-right"></i></button>
<!-- делаем вложенное меню -->
<ul class="menu-sub-list">
<li class="menu-item"><button class="menu-button menu-button--orange"><i data-feather="square"></i>В работе</button></li>
<li class="menu-item"><button class="menu-button menu-button--purple"><i data-feather="octagon"></i>На паузе</button></li>
<li class="menu-item"><button class="menu-button menu-button--green"><i data-feather="triangle"></i>Утверждено</button></li>
<li class="menu-item"><button class="menu-button menu-button--black menu-button--checked"><i data-feather="circle"></i>Без статуса<i data-feather="check"></i></button></li>
</ul>
<!-- закончилось вложенное меню -->
</li>
<!-- обычные пункты меню -->
<li class="menu-item"><button class="menu-button"><i data-feather="link"></i>Копировать ссылку</button></li>
<li class="menu-item"><button class="menu-button"><i data-feather="folder-plus"></i>Переместить</button></li>
<li class="menu-item"><button class="menu-button"><i data-feather="copy"></i>Скопировать</button></li>
<li class="menu-item"><button class="menu-button"><i data-feather="lock"></i>Защитить</button></li>
<li class="menu-item"><button class="menu-button"><i data-feather="download"></i>Загрузить</button></li>
</ul>
<!-- третий раздел -->
<ul class="menu-list">
<li class="menu-item"><button class="menu-button menu-button--delete"><i data-feather="trash-2"></i>Удалить</button></li>
</ul>
</div>
<!-- подключаем jQuery -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
<!-- подключаем иконки-->
<script src='https://unpkg.com/feather-icons@4.29.0/dist/feather.min.js'></script>
<!-- и подключаем наш скрипт -->
<script src="script.js"></script>
Настраиваем стили
Мы не будем задавать жёсткие размеры для меню — они будут зависеть от текста внутри каждого пункта и подстраиваться под него. Для этого скажем всем элементам, чтобы они смотрели на контент и свои размеры считали относительно содержимого.
Также сразу добавим блок c CSS-переменными — так мы сможем настраивать все цвета на странице в одном месте — и подключим новый шрифт:
/* для всех элементов и псевдоэлементов делаем так, */
/* чтобы у них задавались размеры не их контента, а блока, в котором этот контент */
*,
*:after,
*:before {
box-sizing: border-box;
}
/* блок переменных с цветами для всей страницы */
:root {
/* фон */
--color-bg-primary: #d0d6df;
/* фон выбранного пункта меню */
--color-bg-primary-offset: #f1f3f7;
/* цвет фона меню */
--color-bg-secondary: #fff;
/* текст */
--color-text-primary: #3a3c42;
/* цвет иконок по умолчанию */
--color-text-primary-offset: #898c94;
/* цвета для статусов во вложенном меню */
--color-orange: #dc9960;
--color-green: #1eb8b1;
--color-purple: #657cc4;
/* чёрный цвет совпадает с цветом текста */
--color-black: var(--color-text-primary);
/* красный — нужен для пункта «Удалить» */
--color-red: #d92027;
}
/* настройки для всей страницы */
body {
/* используем новый шрифр */
font-family: "Inter", sans-serif;
/* устанавливаем цвета на странице */
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
}
Настраиваем меню и разделы
Сделаем из нашего простого списка кнопок что-то похожее на меню: добавим подложку со скруглёнными краями, тени и разделители между разделами.
С разделителями поступим хитро: мы нарисуем верхнюю границу только в том разделе, перед которым тоже есть свой раздел. Так мы сможем добавлять сколько угодно разделов, а разделители нарисуются автоматически.
/* общие настройки для всего меню */
.menu {
/* включаем абсолютное позиционирование */
position: absolute;
/* говорим, что содержимое этого блока будет в контейнерах */
display: flex;
flex-direction: column;
/* устанавливаем цвет фона */
background-color: var(--color-bg-secondary);
/* добавляем скругление */
border-radius: 10px;
/* и отбрасываем тень */
box-shadow: 0 10px 20px rgba(64, 64, 64, 0.15);
}
/* настройки для каждого раздела меню */
.menu-list {
/* убираем внешние отступы */
margin: 0;
/* каждый элемент будет находиться в своём блоке */
display: block;
/* ширина будет зависеть от текста внутри блока */
width: 100%;
/* добавляем внутренние отступы */
padding: 8px;
}
/* настройки для линии-разделителя в меню */
.menu-list + .menu-list {
/* рисуем верхнюю границу на стыке двух разделов */
border-top: 1px solid #ddd;
}
Скрываем вложенное меню
Сейчас у нашего контекстного меню есть большой минус — вложенное меню видно сразу, хотя оно должно появляться только при наведении мышки на соответствующий пункт. Исправим это, добавив два новых блока в стили:
/* настройки вложенного меню */
.menu-sub-list {
/* скрываем его на старте */
display: none;
/* добавляем внутренние отступы */
padding: 8px;
/* устанавливаем цвет фона */
background-color: var(--color-bg-secondary);
/* добавляем скругление и тень */
border-radius: 10px;
box-shadow: 0 10px 20px rgba(64, 64, 64, 0.15);
/* включаем абсолютное позиционирование */
position: absolute;
/* сдвигаем вправо на всю ширину родительского меню */
left: 100%;
/* пусть верх вложенного меню совпадает с верхом выбранного пункта */
top: 0;
/* виртуально выносим вложенное меню вперёд, чтобы оно накладывалось сверху на основное */
z-index: 100;
/* ширина меню будет зависеть от текста внутри */
width: 100%;
/* блоки с содержимым будут располагаться друг под другом */
flex-direction: column;
}
/* при наведении мышки на элемент с вложенным меню — показываем вложенное меню */
.menu-button:hover + .menu-sub-list {
/* до этого у нас было display:none; */
display: flex;
}
Настраиваем внешний вид кнопок
Сейчас кнопки в меню выглядят как кнопки, а так быть не должно. Добавим в стили настройки кнопок: уберём границы, сделаем тот же фон, что у подложки, и настроим отступы:
/* настройки для каждого элемента в меню */
.menu-item {
/* включаем абсолютное позиционирование */
position: relative;
}
/* настройки кнопок */
.menu-button {
/* наследуем шрифт из страницы */
font: inherit;
/* убираем границы кнопки */
border: 0;
/* добавляем внутренние отступы снизу и сверху */
padding: 8px 8px;
/* делаем отступ справа */
padding-right: 36px;
/* ширина кнопки будет зависеть от текста внутри */
width: 100%;
/* добавляем скругление */
border-radius: 8px;
/* выравниваем текст по левому краю */
text-align: left;
/* оборачиваем всё в контейнеры */
display: flex;
align-items: center;
position: relative;
/* цвет фона */
background-color: var(--color-bg-secondary);
}
Вроде хорошо, но иконки всё ещё слишком близко к тексту. А ещё они того же цвета, что и текст, а так быть не должно — сделаем их серыми:
/* настройки иконок */
.menu-button svg {
/* делаем иконки нужного размера */
flex-shrink: 0;
width: 20px;
height: 20px;
/* добавляем отступ справа */
margin-right: 10px;
/* рисуем иконку бледно-серым цветом */
stroke: var(--color-text-primary-offset);
}
/* те иконки, которые стоят после текста (вторые в списке элементов), сдвигаем к правому краю */
.menu-button svg:nth-of-type(2) {
margin-right: 0;
position: absolute;
right: 8px;
}
Добавляем выделение элемента при наведении мышки
Чтобы явно выделить нужный элемент меню при выборе, добавим ему свой фон и сделаем иконку более заметной — раскрасим её в цвет текста. Так будет сразу понятно, с каким пунктом мы работаем. Заодно добавим красный цвет на пункт «Удалить» при наведении:
/* что будет при наведении мышки на кнопку */
.menu-button:hover {
/* меняем цвет фона */
background-color: var(--color-bg-primary-offset);
}
/* что будет при наведении мышки на кнопку во вложенном меню */
.menu-sub-list:hover {
/* меняем стиль отображения, чтобы изменился цвет фона */
display: flex;
}
/* как отреагирует иконка при наведении мышки */
.menu-button:hover svg {
/* делаем её того же цвета, что и основной текст */
stroke: var(--color-text-primary);
}
/* подсвечиваем красным пункт «Удалить» при наведении мышки */
.menu-button--delete:hover {
color: var(--color-red);
}
/* и то же самое делаем с иконкой */
.menu-button--delete:hover svg:first-of-type {
stroke: var(--color-red);
}
Раскрашиваем иконки во вложенном меню
Последнее, что нам осталось сделать в стилях, — раскрасить иконки во вложенном меню. Используем для этого цвета из глобальных переменных, которые мы описали в самом начале:
/* раскрашиваем иконки во вложенном меню */
.menu-button--orange svg:first-of-type {
stroke: var(--color-orange);
}
.menu-button--green svg:first-of-type {
stroke: var(--color-green);
}
.menu-button--purple svg:first-of-type {
stroke: var(--color-purple);
}
.menu-button--black svg:first-of-type {
stroke: var(--color-black);
}
/* и раскрашиваем галочку во вложенном меню */
.menu-button--checked svg:nth-of-type(2) {
stroke: var(--color-purple);
}
Пишем скрипт
Сейчас меню выглядит красиво, но его сразу видно на странице и оно не работает как контекстное меню. Настоящее контекстное меню пока никуда не исчезло:
Чтобы это исправить, добавим в наш скрипт два обработчика: первый уберёт родное контекстное меню браузера и покажет наше вместо него, а второй — скроет наше меню, если мы кликнем в любом другом месте страницы.
Обратите внимание — мы передаём координаты top и left в стили, чтобы контекстное меню появилось именно там, где мы щёлкнули мышкой, а не просто в левом верхнем углу.
// скрываем контекстное меню
$("div.menu").hide();
// прикрепляем обработчик контекстного меню к элементу с классом "menu"
$(document).bind("contextmenu", function(event) {
// отменяем действие браузера по умолчанию
event.preventDefault();
// показываем наше меню
$("div.menu")
.show()
// привязываем координаты левого верхнего угла к координатам мыши
.css({top: event.pageY + 15, left: event.pageX + 10});
});
// обработчик клика на странице
// если кликнуть мимо меню — оно исчезнет
$(document).click(function() {
// смотрим, курсор сейчас находится на меню или нет
// если на меню — он точно над каким-то элементом и этот элемент сейчас имеет псевдокласс hover
isHovered = $("div.menu").is(":hover");
// если курсор ни над одним элементом меню
if (isHovered == false){
// скрываем меню
$("div.menu").fadeOut("fast");
}
});
Посмотреть работу меню на странице проекта.
<!DOCTYPE html>
<html lang="ru" >
<head>
<meta charset="UTF-8">
<title>Стильное контекстное меню</title>
<!-- подключаем новый шрифт -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<!-- подключаем нормализатор CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
<!-- подключаем свои стили -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- всё меню будет в этом блоке -->
<div class="menu">
<!-- первый раздел в меню -->
<ul class="menu-list">
<!-- список с элементами меню -->
<li class="menu-item">
<!-- каждый пункт меню — это кнопка -->
<button class="menu-button">
<!-- иконка -->
<i data-feather="corner-up-right"></i>
<!-- название пункта меню -->
Поделиться
</button>
</li>
<!-- начался новый пункт меню -->
<li class="menu-item"><button class="menu-button"><i data-feather="edit-2"></i>Переименовать</button></li>
</ul>
<!-- второй раздел -->
<ul class="menu-list">
<li class="menu-item"><button class="menu-button menu-button--black"><i data-feather="circle"></i>Статус<i data-feather="chevron-right"></i></button>
<!-- делаем вложенное меню -->
<ul class="menu-sub-list">
<li class="menu-item"><button class="menu-button menu-button--orange"><i data-feather="square"></i>В работе</button></li>
<li class="menu-item"><button class="menu-button menu-button--purple"><i data-feather="octagon"></i>На паузе</button></li>
<li class="menu-item"><button class="menu-button menu-button--green"><i data-feather="triangle"></i>Утверждено</button></li>
<li class="menu-item"><button class="menu-button menu-button--black menu-button--checked"><i data-feather="circle"></i>Без статуса<i data-feather="check"></i></button></li>
</ul>
<!-- закончилось вложенное меню -->
</li>
<!-- обычные пункты меню -->
<li class="menu-item"><button class="menu-button"><i data-feather="link"></i>Копировать ссылку</button></li>
<li class="menu-item"><button class="menu-button"><i data-feather="folder-plus"></i>Переместить</button></li>
<li class="menu-item"><button class="menu-button"><i data-feather="copy"></i>Скопировать</button></li>
<li class="menu-item"><button class="menu-button"><i data-feather="lock"></i>Защитить</button></li>
<li class="menu-item"><button class="menu-button"><i data-feather="download"></i>Загрузить</button></li>
</ul>
<!-- третий раздел -->
<ul class="menu-list">
<li class="menu-item"><button class="menu-button menu-button--delete"><i data-feather="trash-2"></i>Удалить</button></li>
</ul>
</div>
<!-- подключаем jQuery -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
<!-- подключаем иконки-->
<script src='https://unpkg.com/feather-icons@4.29.0/dist/feather.min.js'></script>
<!-- и подключаем наш скрипт -->
<script src="script.js"></script>
</body>
</html>
/* для всех элементов и псевдоэлементов делаем так, */
/* чтобы у них задавались размеры не их контента, а блока, в котором этот контент */
*,
*:after,
*:before {
box-sizing: border-box;
}
/* блок переменных с цветами для всей страницы */
:root {
/* фон */
--color-bg-primary: #d0d6df;
/* фон выбранного пункта меню */
--color-bg-primary-offset: #f1f3f7;
/* цвет фона меню */
--color-bg-secondary: #fff;
/* текст */
--color-text-primary: #3a3c42;
/* цвет иконок по умолчанию */
--color-text-primary-offset: #898c94;
/* цвета для статусов во вложенном меню */
--color-orange: #dc9960;
--color-green: #1eb8b1;
--color-purple: #657cc4;
/* чёрный цвет совпадает с цветом текста */
--color-black: var(--color-text-primary);
/* красный — нужен для пункта «Удалить» */
--color-red: #d92027;
}
/* настройки для всей страницы */
body {
/* используем новый шрифр */
font-family: "Inter", sans-serif;
/* устанавливаем цвета на странице */
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
}
/* общие настройки для всего меню */
.menu {
/* включаем абсолютное позиционирование */
position: absolute;
/* говорим, что содержимое этого блока будет в контейнерах */
display: flex;
flex-direction: column;
/* устанавливаем цвет фона */
background-color: var(--color-bg-secondary);
/* добавляем скругление */
border-radius: 10px;
/* и отбрасываем тень */
box-shadow: 0 10px 20px rgba(64, 64, 64, 0.15);
}
/* настройки для каждого раздела меню */
.menu-list {
/* убираем внешние отступы */
margin: 0;
/* каждый элемент будет находиться в своём блоке */
display: block;
/* ширина будет зависеть от текста внутри блока */
width: 100%;
/* добавляем внутренние отступы */
padding: 8px;
}
/* настройки для линии-разделителя в меню */
.menu-list + .menu-list {
/* рисуем верхнюю границу на стыке двух разделов */
border-top: 1px solid #ddd;
}
/* настройки вложенного меню */
.menu-sub-list {
/* скрываем его на старте */
display: none;
/* добавляем внутренние отступы */
padding: 8px;
/* устанавливаем цвет фона */
background-color: var(--color-bg-secondary);
/* добавляем скругление и тень */
border-radius: 10px;
box-shadow: 0 10px 20px rgba(64, 64, 64, 0.15);
/* включаем абсолютное позиционирование */
position: absolute;
/* сдвигаем вправо на всю ширину родительского меню */
left: 100%;
/* пусть верх вложенного меню совпадает с верхом выбранного пункта */
top: 0;
/* виртуально выносим вложенное меню вперёд, чтобы оно накладывалось сверху на основное */
z-index: 100;
/* ширина меню будет зависеть от текста внутри */
width: 100%;
/* блоки с содержимым будут располагаться друг под другом */
flex-direction: column;
}
/* при наведении мышки на элемент с вложенным меню — показываем вложенное меню */
.menu-button:hover + .menu-sub-list {
/* до этого у нас было display:none; */
display: flex;
}
/* настройки для каждого элемента в меню */
.menu-item {
/* включаем абсолютное позиционирование */
position: relative;
}
/* настройки кнопок */
.menu-button {
/* наследуем шрифт из страницы */
font: inherit;
/* убираем границы кнопки */
border: 0;
/* добавляем внутренние отступы снизу и сверху */
padding: 8px 8px;
/* делаем отступ справа */
padding-right: 36px;
/* ширина кнопки будет зависеть от текста внутри */
width: 100%;
/* добавляем скругление */
border-radius: 8px;
/* выравниваем текст по левому краю */
text-align: left;
/* оборачиваем всё в контейнеры */
display: flex;
align-items: center;
position: relative;
/* цвет фона */
background-color: var(--color-bg-secondary);
}
/* настройки иконок */
.menu-button svg {
/* делаем иконки нужного размера */
flex-shrink: 0;
width: 20px;
height: 20px;
/* добавляем отступ справа */
margin-right: 10px;
/* рисуем иконку бледно-серым цветом */
stroke: var(--color-text-primary-offset);
}
/* те иконки, которые стоят после текста (вторые в списке элементов), сдвигаем к правому краю */
.menu-button svg:nth-of-type(2) {
margin-right: 0;
position: absolute;
right: 8px;
}
/* что будет при наведении мышки на кнопку */
.menu-button:hover {
/* меняем цвет фона */
background-color: var(--color-bg-primary-offset);
}
/* что будет при наведении мышки на кнопку во вложенном меню */
.menu-sub-list:hover {
/* меняем стиль отображения, чтобы изменился цвет фона */
display: flex;
}
/* как отреагирует иконка при наведении мышки */
.menu-button:hover svg {
/* делаем её того же цвета, что и основной текст */
stroke: var(--color-text-primary);
}
/* подсвечиваем красным пункт «Удалить» при наведении мышки */
.menu-button--delete:hover {
color: var(--color-red);
}
/* и то же самое делаем с иконкой */
.menu-button--delete:hover svg:first-of-type {
stroke: var(--color-red);
}
/* раскрашиваем иконки во вложенном меню */
.menu-button--orange svg:first-of-type {
stroke: var(--color-orange);
}
.menu-button--green svg:first-of-type {
stroke: var(--color-green);
}
.menu-button--purple svg:first-of-type {
stroke: var(--color-purple);
}
.menu-button--black svg:first-of-type {
stroke: var(--color-black);
}
/* и раскрашиваем галочку во вложенном меню */
.menu-button--checked svg:nth-of-type(2) {
stroke: var(--color-purple);
}
// рисуем стильные иконки
feather.replace();
// скрываем контекстное меню
$("div.menu").hide();
// прикрепляем обработчик контекстного меню к элементу с классом "menu"
$(document).bind("contextmenu", function(event) {
// отменяем действие браузера по умолчанию
event.preventDefault();
// показываем наше меню
$("div.menu")
.show()
// привязываем координаты левого верхнего угла к координатам мыши
.css({top: event.pageY + 15, left: event.pageX + 10});
});
// обработчик клика на странице
// если кликнуть мимо меню — оно исчезнет
$(document).click(function() {
// смотрим, курсор сейчас находится на меню или нет
// если на меню — он точно над каким-то элементом и этот элемент сейчас имеет псевдокласс hover
isHovered = $("div.menu").is(":hover");
// если курсор ни над одним элементом меню
if (isHovered == false){
// скрываем меню
$("div.menu").fadeOut("fast");
}
});