Статьи

Создание пользовательского контекстного меню правой кнопкой мыши с помощью JavaScript

Веб-приложения, в отличие от просто информационных веб-сайтов, все шире используются, поскольку сеть продолжает развиваться. Два передовых и ведущих примера веб-приложений — это Gmail и Dropbox. По мере того, как функциональность, доступность и полезность веб-приложений продолжают расти, потребность в их эффективности также возрастает. Появляющаяся (и очень полезная) концепция, которую используют два ранее упомянутых приложения, представляет собой настраиваемое контекстное меню. В этом уроке мы рассмотрим несколько вещей:

  1. Определите, что такое контекстное меню, и поймите его назначение и положение в архитектуре веб-приложения.
  2. Используйте интерфейсный код для создания нашего собственного пользовательского контекстного меню, от стилизации с помощью CSS до запуска с помощью JavaScript
  3. Завершите все это небольшим обсуждением практических возможностей настраиваемых контекстных меню, а некоторые делают и не делают, когда дело доходит до реализаций уровня производства.

Давайте погрузимся в!

Что такое контекстное меню?

Я заимствую и адаптирую определение из Википедии , так как оно хорошо покрывает это:

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

На вашем компьютере щелчок правой кнопкой мыши на рабочем столе запускает собственное контекстное меню вашей ОС. Отсюда вы, вероятно, можете создать новую папку, получить некоторую информацию, а также другие действия. При использовании веб-браузера щелчок правой кнопкой мыши на странице вызовет контекстное меню этого браузера по умолчанию. Вы сможете получить информацию о странице или просмотреть источник. Щелчок правой кнопкой мыши по изображению также предоставляет свой собственный набор параметров — вы можете сохранить изображение, открыть его в новой вкладке, скопировать его, среди прочего. Это все поведение по умолчанию, но они когда-то создавались создателями приложений. Интересно отметить, что доступные действия в меню меняются в зависимости от ситуации или контекста, в котором вы запускаете меню.

Веб-приложения начинают развертывать свои собственные настраиваемые контекстные меню, чтобы предоставить пользователям соответствующие действия. Dropbox и Gmail являются прекрасными примерами этого, позволяя вам выполнять такие действия, как архивирование, удаление, загрузка, просмотр и т. Д. Но как это сделать? В веб-браузере, когда выполняется действие по щелчку правой кнопкой мыши, событие запускается. Это событие является событием contextmenu . Чтобы развернуть настраиваемое контекстное меню, нам нужно предотвратить поведение по умолчанию, а затем настроить, запустить и расположить наше собственное меню. Это немного работы, но мы собираемся сделать это шаг за шагом. Давайте начнем с создания базовой структуры приложения, чтобы у нас было что-то реальное, с чем можно поиграть.

Рисование картинки — пример приложения списка задач

Представьте, что мы создаем хорошее старое приложение со списком задач. Да, это распространенный пример, и вам, вероятно, надоело слышать о приложениях со списком задач, но давайте все равно будем использовать его, потому что он всегда дает четкую картину. На целевой странице нашего приложения у нас, скорее всего, есть список задач, которые еще предстоит выполнить. Мы можем нажать на задачу для получения дополнительной информации, мы можем добавить новые задачи, отредактировать задачу и удалить задачу — основное CRUD-приложение, если хотите. В последнее время, когда к веб-приложениям получают доступ различные устройства, мы реализуем множество жестов и действий, чтобы придать им более естественное ощущение. Например, проведя пальцем влево, можно попросить пользователя удалить или заархивировать задачу.

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

В конце этого руководства вы увидите рабочий пример CodePen. Впрочем, до тех пор давайте работать вместе и строить его сами, шаг за шагом, чтобы вы могли видеть прогресс. Я буду использовать современный CSS для быстрой структурной разработки и создам фиктивный список задач с некоторыми атрибутами data-* . Я также включу сброс CSS Эрика Мейера и box-sizing свойство box-sizing на border-box для всех свойств, например так:

 *, *::before, *::after { box-sizing: border-box; } 

Я не буду добавлять префиксы к каким-либо стилям CSS, но демонстрационная программа CodePen будет использовать автоматический префикс. Давайте строить!

Построение нашей базовой структуры

Мы откроем наш HTML-документ как обычно, добавим заголовок, область основного содержимого, фиктивный список задач и нижний колонтитул. Я добавлю Font Awesome для небольшого дополнительного визуального удовольствия и немного подумаю. Я позабочусь, чтобы каждая задача содержала атрибут data-id , который в реальном мире генерировался бы из какой-либо базы данных. Каждое задание также будет иметь раздел «действия», потому что мы хотим, чтобы действия были максимально доступны. Помните, что контекстное меню предоставляет один уникальный способ представить пользователю действия. Вот важные части разметки:

 <body> <ul class="tasks"> <li class="task" data-id="3"> <div class="task__content"> Go To Grocery </div> <div class="task__actions"> <i class="fa fa-eye"></i> <i class="fa fa-edit"></i> <i class="fa fa-times"></i> </div> </li> <!-- more task items here... --> </ul> <script src="main.js"></script> </body> 

Помните, что вы можете проверить демонстрацию CodePen, чтобы следовать всей структуре документа. Давайте теперь посмотрим на CSS. Если вы работаете вместе в CodePen, вы можете легко включить автоматический префикс и сброс CSS в панели настроек. Если нет, вам придется включить их вручную или настроить исполнителей задач в соответствии со своей средой разработки. Я также использовал семейство шрифтов Roboto и, как упоминалось выше, я включил Font Awesome. Помните, что в центре внимания этой демонстрации находится создание контекстных меню, поэтому индикаторы действия значков и кнопка добавления задачи не будут работать. Вот соответствующие части CSS на данный момент:

 /* tasks */ .tasks { list-style: none; margin: 0; padding: 0; } .task { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: solid 1px #dfdfdf; } .task:last-child { border-bottom: none; } 

Опять же, вы можете получить полный набор стилей из демонстрации CodePen. Мы просто сосредоточены на важных частях здесь. Хорошо, теперь у нас есть простая маленькая игровая площадка для работы внутри. Давайте начнем рассматривать фактическое контекстное меню!

Начало нашего пользовательского контекстного меню — разметка

Наше меню начнется, как и большинство меню в наши дни, — неупорядоченный список, вложенный в тег nav . Каждое действие в нашем меню будет элементом списка со ссылкой. Каждая ссылка будет отвечать за конкретное действие. Как я упоминал ранее, мы хотим, чтобы наше контекстное меню отвечало за три действия:

  1. Просмотр задачи
  2. Редактирование задачи
  3. Удаление задачи

Давайте разметим наше контекстное меню следующим образом:

 <nav class="context-menu"> <ul class="context-menu__items"> <li class="context-menu__item"> <a href="#" class="context-menu__link"> <i class="fa fa-eye"></i> View Task </a> </li> <li class="context-menu__item"> <a href="#" class="context-menu__link"> <i class="fa fa-edit"></i> Edit Task </a> </li> <li class="context-menu__item"> <a href="#" class="context-menu__link"> <i class="fa fa-times"></i> Delete Task </a> </li> </ul> </nav> 

Если вам интересно, куда поместить эту разметку, давайте оставим ее прямо перед закрывающим тегом body . Для этого есть причина, и прежде чем мы перейдем к нашему CSS, давайте рассмотрим несколько вещей:

  1. В конечном итоге мы хотим, чтобы наше контекстное меню отображалось там, где мы щелкаем правой кнопкой мыши, а это означает, что оно должно быть абсолютно позиционировано и вне потока остальной части документа. Мы не хотим, чтобы он отображался внутри какого-либо относительного контейнера, который будет портить координаты размещения, поэтому я оставил его в нижней части документа.
  2. В конечном итоге мы хотим, чтобы определенные переменные или атрибуты были связаны с контекстным меню. Например, когда мы нажимаем «удалить задачу», мы хотим знать, какую задачу нужно удалить, и мы узнаем об этом, только выбрав, какая задача была нажата правой кнопкой мыши в первую очередь.

Давайте продолжим с CSS.

Стилизация нашего пользовательского меню — CSS

Мы знаем, что хотим, чтобы наше меню было абсолютно позиционировано. Помимо этого, давайте добавим немного дополнительного стиля, чтобы он выглядел презентабельно. Я также установлю для z-index значение 10, но помните, что в вашем приложении вы хотите, чтобы меню располагалось поверх всего содержимого, поэтому установите z-index соответствующим образом. В демо и исходном коде вы найдете много дополнительных стилей для эстетического удовольствия, но здесь важно отметить расположение и z-indexing контекстного меню:

 .context-menu { position: absolute; z-index: 10; } 

Прежде чем перейти к части JavaScript, давайте вспомним, что по умолчанию контекстное меню должно быть скрыто. Мы установим display на none и создадим вспомогательный «активный» класс для того времени, когда мы хотим его показать. И если вам интересно позиционирование активного контекстного меню, мы займемся этим позже. Вот мой дополнительный CSS:

 .context-menu { display: none; position: absolute; z-index: 10; } .context-menu--active { display: block; } 

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

Развертывание нашего контекстного меню — JavaScript

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

 (function() { "use strict"; document.addEventListener( "contextmenu", function(e) { console.log(e); }); })(); 

Если вы откроете консоль и щелкните правой кнопкой мыши в любом месте документа, вы увидите, что событие действительно регистрируется. Там очень много интересной информации, которая нам пригодится, в частности, позиционные координаты. Прежде чем мы предотвратим поведение по умолчанию, давайте учтем, что в большинстве частей документа нам на самом деле не нужно настраивать поведение. Мы только хотим развернуть пользовательское контекстное меню на наших элементах задачи. Имея это в виду, давайте создадим немного в соответствии со следующими шагами:

  1. Нам нужно contextmenu каждый из наших элементов задачи и добавить к contextmenu прослушиватель событий contextmenu .
  2. Для каждого прослушивателя событий для каждого элемента задачи мы будем предотвращать поведение по умолчанию.
  3. А пока давайте просто зарегистрируем событие и рассматриваемый элемент на консоли.

Вот что мы имеем до сих пор:

 (function() { "use strict"; var taskItems = document.querySelectorAll(".task"); for ( var i = 0, len = taskItems.length; i < len; i++ ) { var taskItem = taskItems[i]; contextMenuListener(taskItem); } function contextMenuListener(el) { el.addEventListener( "contextmenu", function(e) { console.log(e, el); }); } })(); 

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

  1. Мы предотвратим поведение по умолчанию, когда щелкнем правой кнопкой мыши на элементе задачи.
  2. Мы отобразим пользовательское контекстное меню, добавив в него наш вспомогательный класс.

Прежде чем мы сделаем это, давайте добавим в наше меню идентификатор context-menu чтобы упростить выборку, а также создадим новую переменную под названием menuState в нашем JavaScript. По умолчанию это 0 , и предполагается, что 0 означает выключение, а 1 означает включение. Наконец, давайте кешируем переменную для нашего активного класса и назовем ее active . Вот мои три дополнительные переменные:

 var menu = document.querySelector("#context-menu"); var menuState = 0; var active = "context-menu--active"; 

Теперь давайте пересмотрим нашу функцию contextMenuListener , а также добавим toggleMenuOn для запуска меню:

 function contextMenuListener(el) { el.addEventListener( "contextmenu", function(e) { e.preventDefault(); toggleMenuOn(); }); } function toggleMenuOn() { if ( menuState !== 1 ) { menuState = 1; menu.classList.add(active); } } 

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

  1. Если щелкнуть за пределами меню, меню вернется в неактивное состояние.
  2. Нажатие клавиши выхода (или keycode 27) также приводит к тому, что меню возвращается в неактивное состояние.

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

Рефакторинг нашего Кодекса

Сейчас очевидно, что три основных события будут ответственны за инициирование действий:

  1. contextmenu — Проверяет состояния и развертывает контекстные меню.
  2. click — скрывает меню, когда это применимо.
  3. keyup — отвечает за действия клавиатуры. Мы сосредоточимся только на ключе ESC для этого урока.

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

 (function() { "use strict"; /////////////////////////////////////// /////////////////////////////////////// // // HELPERFUNCTIONS // /////////////////////////////////////// /////////////////////////////////////// /** * Some helper functions here. */ /////////////////////////////////////// /////////////////////////////////////// // // COREFUNCTIONS // /////////////////////////////////////// /////////////////////////////////////// /** * Variables. */ var taskItemClassName = 'task'; var menu = document.querySelector("#context-menu"); var menuState = 0; var activeClassName = "context-menu--active"; /** * Initialise our application's code. */ function init() { contextListener(); clickListener(); keyupListener(); } /** * Listens for contextmenu events. */ function contextListener() { } /** * Listens for click events. */ function clickListener() { } /** * Listens for keyup events. */ function keyupListener() { } /** * Turns the custom context menu on. */ function toggleMenuOn() { if ( menuState !== 1 ) { menuState = 1; menu.classList.add(activeClassName); } } /** * Run the app. */ init(); })(); 

На этот раз мы не будем перебирать наши задачи. Вместо этого мы прослушаем событие contextmenu для всего документа и проверим, произошло ли это событие в нашем элементе задачи. Вот почему у нас теперь есть переменная taskItemClassName . Для этого нам понадобится наша первая вспомогательная функция, clickInsideElement . Это займет два параметра:

  1. Само событие, из которого мы можем проверить целевой или исходный элемент события.
  2. Имя класса для сравнения. Если событие произошло внутри элемента, у которого либо есть имя класса, либо для которого у родителя есть имя класса, мы хотим вернуть этот элемент.

Вот эта первая вспомогательная функция:

 function clickInsideElement( e, className ) { var el = e.srcElement || e.target; if ( el.classList.contains(className) ) { return el; } else { while ( el = el.parentNode ) { if ( el.classList && el.classList.contains(className) ) { return el; } } } return false; } 

Теперь мы можем вернуться туда, где мы были раньше, отредактировав нашу функцию contextListener следующим образом:

 function contextListener() { document.addEventListener( "contextmenu", function(e) { if ( clickInsideElement( e, taskItemClassName ) ) { e.preventDefault(); toggleMenuOn(); } }); } 

Теперь, когда наша вспомогательная функция выполняет за нас какую-то грязную работу, а событие contextmenu прослушивается для всего документа, мы можем теперь отключить меню, когда этот щелчок происходит за пределами области контекста и когда открыто пользовательское меню. Давайте добавим функцию toggleMenuOff и тоже отредактируем нашу функцию contextListener :

 function contextListener() { document.addEventListener( "contextmenu", function(e) { if ( clickInsideElement( e, taskItemClassName ) ) { e.preventDefault(); toggleMenuOn(); } else { toggleMenuOff(); } }); } function toggleMenuOff() { if ( menuState !== 0 ) { menuState = 0; menu.classList.remove(activeClassName); } } 

Идите сейчас и щелкните правой кнопкой мыши на элемент задачи. Теперь щелкните правой кнопкой мыши где-нибудь еще на документе. Вуаля! Пользовательское меню исчезает, а по умолчанию отображается как обычное. Теперь мы можем сделать что-то похожее для наших обычных событий click :

 function clickListener() { document.addEventListener( "click", function(e) { var button = e.which || e.button; if ( button === 1 ) { toggleMenuOff(); } }); } 

Вышеприведенный тест немного отличается от предыдущего, и это из-за небольшой причуды в Firefox. После того, как кнопка щелчка правой кнопкой мыши отпущена, Firefox также инициирует событие click , но это не код щелчка левой кнопкой мыши, поэтому мы должны убедиться, что щелчок является щелчком левой кнопкой мыши. Таким образом, меню не мигает, когда мы щелкаем правой кнопкой мыши по элементу. Давайте теперь сделаем нечто подобное снова для события keyup :

 function keyupListener() { window.onkeyup = function(e) { if ( e.keyCode === 27 ) { toggleMenuOff(); } } } 

Теперь мы успешно показываем и скрываем меню по мере необходимости и делаем взаимодействие с пользователем максимально естественным. Давайте перейдем к позиционированию меню и, наконец, рассмотрим возможный вариант обработки событий внутри меню.

Позиционирование нашего контекстного меню

Из-за наших текущих настроек HTML и CSS наше меню только появляется в нижней части экрана. Естественно, мы предпочитаем, чтобы он отображался рядом с тем местом, где мы нажали. Давайте сделаем это. Во-первых, давайте воспользуемся другой вспомогательной функцией, которая получает точные координаты того места, где мы щелкнули, в зависимости от события. Я назову его getPosition , и он обрабатывает некоторые кросс-браузерные особенности, чтобы повысить его точность:

 function getPosition(e) { var posx = 0; var posy = 0; if (!e) var e = window.event; if (e.pageX || e.pageY) { posx = e.pageX; posy = e.pageY; } else if (e.clientX || e.clientY) { posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; } return { x: posx, y: posy } } 

Нашим первым шагом в позиционировании нашего меню является подготовка трех переменных. Давайте добавим их в наш блок переменных:

 var menuPosition; var menuPositionX; var menuPositionY; 

Теперь давайте создадим функцию с именем positionMenu которая принимает один аргумент — событие. А пока давайте запишем результат позиции в консоль:

 function positionMenu(e) { menuPosition = getPosition(e); console.log(menuPosition); } 

Теперь мы можем отредактировать нашу функцию contextListener чтобы начать процесс позиционирования:

 function contextListener() { document.addEventListener( "contextmenu", function(e) { if ( clickInsideElement( e, taskItemClassName ) ) { e.preventDefault(); toggleMenuOn(); positionMenu(e); } else { toggleMenuOff(); } }); } 

Снова переключите контекстное меню и проверьте консоль. Вы заметите, что позиция зарегистрирована и доступна для нас. Мы можем использовать это для вставки top и left стилей в наше меню через JavaScript. Вот обновленная функция positionMenu :

 function positionMenu(e) { menuPosition = getPosition(e); menuPositionX = menuPosition.x + "px"; menuPositionY = menuPosition.y + "px"; menu.style.left = menuPositionX; menu.style.top = menuPositionY; } 

Нажмите сейчас и запустите контекстное меню. Он появляется везде, где мы нажимаем! Это круто, но у нас есть пара моментов, на которые стоит обратить внимание:

  1. Что происходит, если экран пользователя немного сужен, и пользователь щелкает далеко вправо от экрана? Контекстное меню появится за пределами экрана, а тело переполнится.
  2. Что если пользователь изменит размер окна, пока открыто контекстное меню? Возникнет та же проблема переполнения. Dropbox обходит это, скрывая x-overflow.

Давайте займемся первым вопросом. Мы будем использовать JavaScript, чтобы определить ширину и высоту нашего меню, и проверим, чтобы позиция меню не переполнялась. Если это произойдет, мы сместим его на несколько пикселей, аналогично тому, как работает меню по умолчанию. Это немного математики и требует некоторых размышлений, но мы можем пройти через это шаг за шагом. Сначала нам нужно проверить ширину и высоту окна. Затем нам нужно узнать ширину и высоту меню. Наконец, нам нужно проверить, больше ли разница позиции нажатия и ширины окна со смещением, чем ширина меню, и одинакова для высоты. Если оно больше, мы вручную устанавливаем позицию, а если нет, мы продолжаем как обычно. Начнем с кэширования двух переменных для ширины и высоты:

 var menuWidth; var menuHeight; 

Помните, что по умолчанию наше меню скрыто, поэтому мы пока не сможем рассчитать ширину и высоту. В любом случае, мы статически генерируем меню, и в реальном приложении, таком как Dropbox, содержимое меню может меняться в зависимости от контекста. Рекомендуется рассчитать ширину и высоту при развертывании меню. Внутри нашей функции positionMenu мы можем получить ширину и высоту следующим образом:

 menuWidth = menu.offsetWidth; menuHeight = menu.offsetHeight; 

Теперь давайте подготовим еще две переменные для внутренней ширины и высоты окна:

 var windowWidth; var windowHeight; 

Точно так же мы можем вычислить их, когда мы развернем наше меню следующим образом:

 windowWidth = window.innerWidth; windowHeight = window.innerHeight; 

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

 var clickCoords; var clickCoordsX; var clickCoordsY; // updated positionMenu function function positionMenu(e) { clickCoords = getPosition(e); clickCoordsX = clickCoords.x; clickCoordsY = clickCoords.y; menuWidth = menu.offsetWidth + 4; menuHeight = menu.offsetHeight + 4; windowWidth = window.innerWidth; windowHeight = window.innerHeight; if ( (windowWidth - clickCoordsX) < menuWidth ) { menu.style.left = windowWidth - menuWidth + "px"; } else { menu.style.left = clickCoordsX + "px"; } if ( (windowHeight - clickCoordsY) < menuHeight ) { menu.style.top = windowHeight - menuHeight + "px"; } else { menu.style.top = clickCoordsY + "px"; } } 

Теперь наше меню должно работать так, как ожидается, даже если мы запускаем его на узких окнах! Как уже упоминалось, при изменении размера окна все еще существует вероятность его перетекания. Ранее я упоминал, как Dropbox справляется с этим, но для нас давайте добавим конечный прослушиватель событий, чтобы отключить меню при изменении размера окна. В нашей функции init мы можем добавить это:

 resizeListener(); 

И функция будет выглядеть так:

 function resizeListener() { window.onresize = function(e) { toggleMenuOff(); }; } 

Потрясающие.

Присоединение событий к элементам контекстного меню

Если ваше приложение более сложное, чем это, и вам необходимо динамически генерировать содержимое контекстного меню, то вам необходимо это учесть. Для нашего приложения у нас есть одно меню с одинаковыми параметрами. Таким образом, мы можем сделать быструю проверку, чтобы увидеть, по какому предмету щелкнули, и запустить какое-то действие. Используя Dropbox снова в качестве примера, если вы щелкнете правой кнопкой мыши по элементу и нажмете «Удалить», появится модальное подтверждение. Для нашего приложения давайте просто сохраним текущий элемент задачи в переменной и зарегистрируем в консоли атрибут data-id который мы установили в начале. Мы также запишем соответствующее действие, поэтому давайте отредактируем разметку нашего контекстного меню, чтобы включить некоторые атрибуты данных:

 <nav id="context-menu" class="context-menu"> <ul class="context-menu__items"> <li class="context-menu__item"> <a href="#" class="context-menu__link" data-action="View"> <i class="fa fa-eye"></i> View Task </a> </li> <li class="context-menu__item"> <a href="#" class="context-menu__link" data-action="Edit"> <i class="fa fa-edit"></i> Edit Task </a> </li> <li class="context-menu__item"> <a href="#" class="context-menu__link" data-action="Delete"> <i class="fa fa-times"></i> Delete Task </a> </li> </ul> </nav> 

У нас также есть довольно много функций и объектов для изменения и кэширования, поэтому давайте рассмотрим их все шаг за шагом. Прежде всего, давайте кешируем все нужные нам объекты:

 var contextMenuClassName = "context-menu"; var contextMenuItemClassName = "context-menu__item"; var contextMenuLinkClassName = "context-menu__link"; var contextMenuActive = "context-menu--active"; var taskItemClassName = "task"; var taskItemInContext; var clickCoords; var clickCoordsX; var clickCoordsY; var menu = document.querySelector("#context-menu"); var menuItems = menu.querySelectorAll(".context-menu__item"); var menuState = 0; var menuWidth; var menuHeight; var menuPosition; var menuPositionX; var menuPositionY; var windowWidth; var windowHeight; 

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

Наша функция инициализатора остается прежней, но наше первое изменение заключается в функции contextListener . Помните, что мы хотим сохранить переменную taskItemInContext если щелкнуть правой кнопкой мыши элемент задачи. Наш помощник clickInsideElement самом деле возвращает элемент, если у нас есть совпадение, поэтому вот где мы можем его установить:

 function contextListener() { document.addEventListener( "contextmenu", function(e) { taskItemInContext = clickInsideElement( e, taskItemClassName ); if ( taskItemInContext ) { e.preventDefault(); toggleMenuOn(); positionMenu(e); } else { taskItemInContext = null; toggleMenuOff(); } }); } 

Мы сбрасываем его на null если щелкнуть правой кнопкой мыши не на элементе задачи. Давайте перейдем к функции clickListener . Как я упоминал ранее, мы просто хотим записать некоторую информацию в консоль для простоты. В настоящее время, когда регистрируется событие клика, мы запускаем пару проверок и закрываем меню. Тем не менее, давайте сделаем небольшую разбивку этой функции и проверим, был ли этот щелчок элементом в нашем контекстном меню. Если это так, то мы выполним наше действие и закроем меню после:

 function clickListener() { document.addEventListener( "click", function(e) { var clickeElIsLink = clickInsideElement( e, contextMenuLinkClassName ); if ( clickeElIsLink ) { e.preventDefault(); menuItemListener( clickeElIsLink ); } else { var button = e.which || e.button; if ( button === 1 ) { toggleMenuOff(); } } }); } 

Вы заметите, что функция menuItemListener когда мы menuItemListener элемент контекстного меню, поэтому мы оценим это через минуту. Функции keyupListener и resizeListener остаются без изменений. То же самое в основном toggleMenuOff функций toggleMenuOn и toggleMenuOff , единственное отличие заключается в изменении имени переменной для активного класса, что обеспечивает лучшую читаемость кода:

 function toggleMenuOn() { if ( menuState !== 1 ) { menuState = 1; menu.classList.add( contextMenuActive ); } } function toggleMenuOff() { if ( menuState !== 0 ) { menuState = 0; menu.classList.remove( contextMenuActive ); } } 

Функция positionMenu остается неизменной, и, наконец, вот новая функция menuItemListener которая принимает один аргумент:

 function menuItemListener( link ) { console.log( "Task ID - " + taskItemInContext.getAttribute("data-id") + ", Task action - " + link.getAttribute("data-action")); toggleMenuOff(); } 

Это подводит нас к концу нашей функциональности … тьфу!

Некоторые замечания и соображения

Прежде чем закончить, давайте учтем пару вещей:

  1. На протяжении всей статьи я упоминал «щелчок правой кнопкой мыши» как событие, с помощью которого запускается контекстное меню. Не каждый прав, и не у всех одинаковые настройки мыши. Независимо от этого, событие contextmenu действует в соответствии с настройкой мыши пользователя.
  2. Еще один важный момент, на который стоит обратить внимание, это то, что мы рассматривали только полноценные настольные приложения. Пользователи могут получать доступ к вашему приложению или веб-сайту с помощью клавиатуры или мобильных технологий. Вы должны убедиться, что, если вы решите изменить поведение по умолчанию, вы сделаете его удобным и удобным для всех пользователей.

Имея это в виду, вы должны хорошо продвинуться в этом шаге дальше.

Большой вопрос

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

Совместимость браузера

В этом руководстве использовались некоторые современные CSS и JavaScript, а именно flex для макетов в CSS и classList для переключения классов в JavaScript. Если вам нужна обратная совместимость со старыми браузерами, я предлагаю вам отредактировать соответственно. Этот учебник был протестирован в следующих браузерах:

  • Chrome 39
  • Safari 7
  • Firefox 33

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

Wrap Up and Demo

Если вы тщательно все продумали и на 100% уверены, что ваше приложение может использовать этот тип функциональности, то сделайте это. Сегодня мы многое рассмотрели в этом уроке, и теперь у вас должно быть четкое представление о том, что такое контекстное меню, как его реализовать в вашем приложении, и о некоторых важных соображениях, которые необходимо учитывать. Надеюсь, вам понравилось чтение, и я жду ваших комментариев!

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