- Собственное контекстное меню с использованием JavaScript
- Что есть контекстное меню?
- Список задач
- Создание базовой структуры
- Набросаем наше контекстное меню — разметка
- Приводим наше меню в порядок — CSS
- Разворачиваем наше контекстное меню — JavaScript
- Рефакторинг нашего кода
- Позиционирование нашего контекстного меню
- Цепляем события к пунктам контекстного меню
- Некоторые замечания
- Большой вопрос
- Совместимость с браузерами
- Заключение и демо
- От переводчика
- Как реализовать контекстные меню (Context Menu) в iOS 13
- Рекомендации при использовании контекстных меню
- Проектируйте правильно
- Включайте в меню только необходимое
- Используйте вложенные меню
- Используйте не более 1 уровня вложенности
- Располагайте наиболее часто используемые пункты в верхней части
- Используйте группировку
- Избегайте одновременного использования контекстного меню и меню редактирования на одном элементе
- Не добавляйте отдельную кнопку “Открыть” в меню
- Простейшее контекстное меню для UIView
- Вы можете скачать пример полностью отсюда.
- Добавляем вложенное меню
- Добавляем контекстное меню в UICollectionView
- Добавляем контекстное меню в UITableView
- Заключение
Собственное контекстное меню с использованием JavaScript
Что есть контекстное меню?
Если верить Википедии, контекстное меню — меню, появляющееся при взаимодействии пользователя с графическим интерфейсом (при нажатии правой кнопки мыши). Контекстное меню содержит ограниченный набор возможных действий, который обычно связаны с выбранным объектом.
На вашем компьютере клик правой кнопкой мыши на рабочем столе вызовет контекстное меню операционной системы. Отсюда вы, вероятно, можете создать новую папку, получить какую-то информацию и сделать что-нибудь еще. Контекстное меню в браузере позволяет, например, получить информацию о странице, посмотреть ее исходники, сохранить изображение, открыть ссылку в новой вкладке, поработать с буфером обмена и всякое такое. Причем набор доступных действий зависит от того, куда именно вы кликнули, то есть от контекста. Это стандартное поведение, закладываемое разработчиками браузера [И расширений к нему].
Веб-приложения постепенно начинают заменять стандартные контекстные меню своими собственными. Отличными примерами являются все те же Gmail и Dropbox. Вопрос лишь в том, как сделать свое контекстное меню? В браузере при клике правой кнопкой мыши срабатывает событие contextmenu. Нам придется отменить поведение по умолчанию и сделать так, чтобы вместо стандартного меню выводилось наше собственное. Это не так уж сложно, но разбираться будем пошагово, так что выйдет довольно объемно. Для начала создадим базовую структуру приложения, чтоб разрабатываемый пример не был совсем уж оторван от реальности.
Список задач
Представим, что мы создаем приложение, позволяющее вести список задач. Я понимаю, вы уже наверняка неимоверно устали от всех этих списков задач, но пусть будет. Страница приложения содержит список незавершенных задач. Для каждой задачи доступен типичный набор действий CRUD: получить информацию о задаче, добавить новую, редактировать, удалить.
Пример результата есть на CodePen. Можете заглянуть туда сразу, если лень читать или хотите убедиться, что действительно заинтересованы в дальнейшем чтении. Ну а пока приступим к пошаговой разработке задуманного. Я буду использовать некоторые современные фишки CSS и создам простейший список задач на data-атрибутах. Также воспользуюсь сбросом стилей от Эрика Мейера и сброшу свойство box-sizing для всех элементов в border-box:
Я не буду использовать префиксы в CSS, но в демо на CodePen включен автопрефиксер.
Создание базовой структуры
Откроем наш HTML-документ, накидаем шапку, контентную часть с некоторым списком задач и подвал. Я также подтяну Font Awesome и шрифт Roboto, чтобы сделать оформление немного лучше. Каждая задача должна содержать атрибут data-id, который в реальности брался бы из БД. Также каждая задача будет содержать список действий. Вот важные части разметки:
Если вы используете CodePen, можете в настройках включить автопрефиксер и подключение CSS-сброса. В противном случае придется делать все руками, если еще не автоматизировали этот процесс. Не забывайте, что наша цель — создание контекстного меню, так что обработка действий не будет реализована. А теперь давайте добавим еще немного CSS:
Полный набор стилей (да и всего остального) представлен на CodePen. А здесь будут самые важные части кода, разметки и оформления. Но давайте уже наконец приблизимся к нашему контекстному меню.
Набросаем наше контекстное меню — разметка
Основа нашего меню такая же, как и у любого другого меню — неупорядоченный список, вложенный в тег nav. Каждое действие будет представлено в виде элемента списка со ссылкой. Каждая ссылка отвечает за определенное действие. Как я упоминал ранее, нам нужно три действия в контекстном меню:
- Просмотр задачи.
- Редактирование задачи.
- Удаление задачи.
Накидаем разметку:
Если не представляете, куда поместить эту разметку, помещайте перед закрывающим тегом body. Прежде чем перейти к CSS, уточним пару моментов:
- Мы хотим, чтобы контекстное меню появлялось там, где был выполнен правый клик, то есть ему нужно абсолютное позиционирование. Поэтому не стоит его разметку помещать в контейнер с относительным позиционированием.
- Нам потребуются какие-нибудь переменные или атрибуты, чтобы можно было определить, к какой задаче относится выбранное действие.
А теперь стили.
Приводим наше меню в порядок — CSS
Уже была упомянута необходимость абсолютного позиционирования разрабатываемого меню. Кроме того, установим свойство z-index в 10. Не забывайте, что в вашем приложении может потребоваться другое значение. Это не все возможные стили, в демке наведены прочие красивости, но они уже зависят от ваших потребностей и не являются обязательными. Прежде чем переходить к JS, сделаем меню невидимым по умолчанию и добавим дополнительный класс для его отображения.
Разворачиваем наше контекстное меню — JavaScript
Начнем с того, что посмотрим, как зарегистрировать событие contextmenu. Откроем самовыполняющуюся функцию и отловим событие на всем документе. Также будем логировать событие в консоль, чтобы получить какую-то информацию:
Если откроете консоль и кликнете где-нибудь правой кнопкой мыши, то увидите, что событие действительно там отображается. Тут много разной информации, которую мы можем использовать. Особенно нас интересуют координаты. Прежде чем отменять поведение по умолчанию, давайте учитывать, что делать это надо не на всем документе, а лишь для элементов списка задач. С учетом этого, потребуется выполнить следующие шаги:
- Потребуется цикл по всем элементам списка задач и добавление обработчика события contextmenu к каждому из них.
- Для каждого обработчика отменим стандартное поведение.
- Будем логировать в консоли событие и элемент, к которому оно относится.
В общем, делаем что-то такое:
Если заглянуть в консоль, можно увидеть, что уникальное событие срабатывает при каждом нажатии на элемент из списка задач. Теперь помимо отмены поведения по умолчанию реализуем отображение контекстного меню, добавляя к нему вспомогательный класс.
Но для начала давайте добавим к меню ID, чтобы его проще было получать посредством JS. Также добавим переменную состояния меню menuState и и переменную с активным классом. Получились три переменных:
Едем дальше. Пересмотрим функцию contextMenuListener и добавим toggleMenuOn, отображающую меню:
На данный момент правой кнопкой мыши уже можно вызвать наше контекстное меню. Но нельзя сказать, что оно работает правильно. Во-первых, оно находится совсем не там, где хотелось бы. Для устранения проблемы понадобится немного математики. Во-вторых, закрыть это меню пока невозможно. С учетом того, как работают обычные контекстные меню, хотелось бы, чтоб наша его реализация закрывалась при клике не меню и при нажатии Escape. Помимо этого при правом клике вне нашего меню оно должно закрыться, а вместо него требуется открытие меню по умолчанию. Давайте попробуем все это решить.
Рефакторинг нашего кода
Теперь мы не перебираем элементы списка. Вместо этого будем обрабатывать событие contextmenu по всему документу, проверяя, относится ли оно к одной из задач. Поэтому введена переменная taskItemClassName. Делать это мы будем, используя вспомогательную функцию clickInsideElement, принимающую два параметра:
- Само проверяемое событие.
- Имя класса для сравнения. Если событие произошло внутри элемента, имеющего указанный класс, или родитель этого элемента имеет такой класс, то нужно вернуть этот элемент.
Вот и первая вспомогательная функция:
Вернемся и отредактируем contextListener:
Имея вспомогательную функцию, делающую за нас часть грязной работы, и отлавливая событие contextmenu на всем документе, мы можем теперь закрывать меню при клике вне его. Для этого добавим функцию toggleMenuOff и отредактируем contextListener:
Теперь нажмите правой кнопкой по элементу списка. А после — где-нибудь в другом месте документа. Вуаля! Наше меню закрылось и открылось стандартное. Затем сделаем нечто похожее самое для события click, чтобы не от одной правой кнопки оно закрывалось:
Этот кусок кода несколько отличается от предыдущего, потому что Firefox. После того, как правая кнопка мыши отжата, в Firefox срабатывает событие click, так что здесь нам приходится дополнительно убеждаться, что произошел действительно клик левой кнопкой. Теперь меню не моргает при правом клике. Давайте добавим похожий обработчик и на нажатие клавиши ESC:
Мы получили меню, которое открывается и закрывается как задумано, взаимодействующее с пользователем естественным образом. Давайте наконец отпозиционируем меню и попробуем обрабатывать события внутри него.
Позиционирование нашего контекстного меню
С учетом текущих HTML и CSS, наше меню отображается в нижней части экрана. Но нам-то хотелось бы, чтоб оно появлялось там, где произошел клик. Давайте исправим сие досадное упущение. Во-первых, добавим еще одну вспомогательную функцию, получающую точные координаты клика. Назовем ее getPosition и попробуем заставить обрабатывать разные причуды браузеров:
Наш первый шаг в позиционировании меню — подготовка трех переменных. Добавим их в соответствующий блок кода:
Создадим функцию positionMenu, принимающую единственный аргумент — событие. Пока что пусть она выводит координаты меню в консоль:
Отредактируем contextListener, чтобы начать процесс позиционирования:
Снова повтыкайте в контекстное меню и загляните в консоль. Убедитесь, что позиция действительно доступна и логируется. Мы можем использовать встроенные стили для задания свойств top и left посредством JS. Вот и новая версия positionMenu:
Покликайте теперь везде. Меню появляется везде! Это потрясающе, но есть пара моментов, которые надо бы решить:
- Что будет, если пользователь кликнет близко к правому краю окна? Контекстное меню выйдет за его пределы.
- Что делать, если пользователя меняет размеры окна при открытом контекстном меню? Возникает та же проблема. Dropbox решает эту проблему скрытием переполнения по оси x (x-overflow: hidden).
Давайте решать первую проблему. Воспользуемся JS, чтобы определить ширину и высоту нашего меню, и проверим, что меню полностью влезает. В противном случае немного сместим его. Это потребует небольшого количества математики и размышлений, но мы сделаем это просто и по шагам. Для начала проверим ширину и высоту окна. Затем найдем ширину и высоту меню. А после убедимся, что разница между координатами клика и шириной окна с отступом больше, чем ширина меню. И то же самое сделаем для высоты. Если меню не умещается на экране, скорректируем его координаты. Начнем с добавления двух переменных:
Как вы помните, по умолчанию наше меню скрыто, так что нельзя просто взять и посчитать его размеры. В нашем случае меню статическое, но при реальном применении его содержимое может меняться в зависимости от контекста, так что рассчитывать ширину и высоту лучше в момент открытия. Получим нужные величины внутри функции positionMenu:
Введем еще две переменных, но на этот раз для размеров окна:
Посчитаем их значения похожим образом:
В конечном счете давайте предположим, что хотим показывать меню не ближе, чем в 4 пикселях от края окна. Можно сравнить значения, как я говорил выше, и скорректировать позицию как-то так:
Сейчас наше меню ведет себя совсем хорошо. Осталось что-то сделать с ресайзом окна. Я уже говорил, как поступает Dropbox, но вместо этого мы будем закрывать контекстное меню. [Такое поведение куда ближе к стандартному] Добавим в функцию init следующую строку:
И напишем саму функцию:
Цепляем события к пунктам контекстного меню
Если ваше приложение сложнее данного примера, и у вас планируется динамическое содержимое контекстного меню, придется подробнее вникнуть в происходящее далее, чтобы самому додумать недостающие детали. В нашем приложении все проще, и есть всего одно меню с постоянным набором действий. Таким образом, можно быстро проверить, какой именно элемент был выбран, и обработать этот выбор. В нашем примере просто сохраним выбранный элемент в переменную и запишем в консоль его data-id и выбранное действие. Для этого отредактируем разметку меню:
Далее давайте закэшируем все нужные объекты:
Появилась переменная taskItemInContext, которой присваивается значение при правом клике по элементу списка. Она нам потребуется для логирования ID элементов. Также появились новые имена классов. Теперь пройдемся по функциональности.
Функция инициализации остается такой же. Первое изменение затрагивает contextListener, ведь мы хотим сохранять в taskItemInContext элемент, на который кликнул пользователь, а функция clickInsideElement как раз его и возвращает:
Мы сбрасываем ее в null, если правый клик произошел не по элементу списка. Ну и возьмемся за clickListener. Как я упоминал ранее, для простоты мы просто будем выводить информацию в консоль. Сейчас при отлавливании события click производится несколько проверок и меню закрывается. Внесем коррективы и начнем обрабатывать клик внутри контекстного меню, выполняя некоторое действие и лишь после этого закрывая меню:
Вы могли заметить, что вызывается функция menuItemListener. Ее мы определим чуть позже. Функции keyupListener, resizeListener и positionMenu оставляем без изменений. Функции toggleMenuOn и toggleMenuOff отредактируем незначительно, изменив имена переменных для лучшей читаемости кода:
Наконец реализуем menuItemListener:
На этом разработка функциональности заканчивается.
Некоторые замечания
Большой вопрос
Я выделил эту проблему в отдельный пункт, потому что это действительно важно после всего, что мы проделали. Задайтесь вопросом: действительно ли вам нужно собственное контекстное меню? Такие штуки — это круто, но прежде чем их использовать, вы должны убедиться, что это действительно полезно в вашем случае. Обычно пользователи ожидают привычного поведения приложения. Например, после правого клика по фотографии они ожидают получения возможности сохранить ее, скопировать ссылку и т. д. Отсутствие нужных пунктов в кастомном меню может их огорчить.
Совместимость с браузерами
Заключение и демо
Если вы тщательно все обдумали и уверены, что такая функциональность нужна вашему приложению, вы можете использовать разработанное меню. Конечно, оно может потребовать каких-либо изменений, но данное руководство подробно описывает процесс разработки, так что реализовывать свои поправки должно быть не так сложно.
Кодовая база этого руководства залита на GitHub и будет поддерживаться и, возможно, обновляться в течение долгого времени. А полное рабочее демо можно потыкать на CodePen.
От переводчика
Перевод местами достаточно вольный, но не в ущерб смыслу или содержанию. Все, что не относится напрямую к оригиналу, вынесено в примечания.
С предложениями, пожеланиями и замечаниями, как обычно, в ЛС.
Источник
Как реализовать контекстные меню (Context Menu) в iOS 13
Всем привет, меня зовут Денис, мы разрабатываем сервис по аналитике подписок iOS-приложений – Apphud.
На WWDC 2019 Apple представила новый способ взаимодействия с интерфейсом вашего приложения: контекстные меню. Они выглядят так:
В этой статье мы рассмотрим некоторые тонкости их использования и научимся их делать.
Контекстные меню являются логичным продолжением технологии “Peek and Pop”, когда пользователь мог открыть предпросмотр элемента, сильно нажав на него. Но между ними есть и несколько существенных отличий.
Контекстные меню работают на любых устройствах под управлением iOS 13. Поддержка 3D touch от устройства не требуется. Поэтому, в частности, их можно применять на всех iPad.
Кнопки, позволяющие взаимодействовать с элементом, появляются сразу и не требуют свайпа вверх.
Чтобы открыть контекстное меню, пользователю достаточно удержать палец на нужном элементе или сильно на него нажать (если устройство поддерживает 3D Touch).
Рекомендации при использовании контекстных меню
Apple в Human Interface Guidelines рекомендует придерживаться следующих правил при проектировании контекстных меню.
Проектируйте правильно
Будет нехорошо, если вы добавите меню для некоторых элементов в одних местах и не добавите его для похожих элементов в других. Тогда пользователю будет казаться, что приложение работает неправильно.
Включайте в меню только необходимое
Контекстное меню – отличное место для наиболее часто использующихся команд. “Наиболее часто” – ключевая фраза. Не добавляйте туда все подряд.
Используйте вложенные меню
Используйте вложенные меню, чтобы пользователю было проще сориентироваться. Дайте пунктам меню простые и понятные названия.
Используйте не более 1 уровня вложенности
Несмотря на то, что вложенные меню могут сделать навигацию проще, они ее могут и запросто усложнить. Apple не рекомендует использовать более 1 уровня вложенности.
Располагайте наиболее часто используемые пункты в верхней части
Люди в первую очередь фокусируются на верхней части меню, поэтому так им немного проще будет сориентироваться в вашем приложении.
Используйте группировку
Группируйте похожие пункты меню
Избегайте одновременного использования контекстного меню и меню редактирования на одном элементе
Они могут конфликтовать друг с другом, потому что оба вызываются долгим тапом.
Не добавляйте отдельную кнопку “Открыть” в меню
Пользователи могут открыть элемент, просто тапнув по нему. Дополнительная кнопка “Открыть” будет лишней.
Простейшее контекстное меню для UIView
Теперь, когда мы усвоили основные правила использования контекстных меню, перейдем к практике. Разумеется, меню работают только на iOS 13 и выше и для тестирования вам понадобится Xcode 11. Beta-версию Xcode 11 вы можете скачать здесь.
Вы можете скачать пример полностью отсюда.
Давайте добавим контекстное меню, например, на UIImageView , как в анимации выше.
Для этого достаточно добавить объект UIImageView на контроллер и написать несколько строк кода, например в методе viewDidLoad :
В начале создается объект класса UIContextMenuInteraction . Конструктор требует указать делегат, который будет отвечать за меню. Вернемся к этому чуть позднее. А методом addInteraction мы добавляем наше меню к картинке.
Теперь осталось реализовать протокол UIContextMenuInteractionDelegate . В нем только один обязательный метод, который отвечает за создание меню:
Если в этом методе вернуть nil , то контекстное меню не будет вызвано. Внутри самого метода мы создаем объект класса UIContextMenuConfiguration . При создании мы передаем эти параметры:
identifier – идентификатор меню.
previewProvider – кастомный контроллер, который опционально может быть отображен вместо текущего элемента в меню. Мы рассмотрим это чуть позднее.
в actionProvider мы передаем элементы контекстного меню.
Сами элементы создаются проще некуда: указывается название, опциональная иконка и обработчик нажатия на пункт меню. Вот и все!
Добавляем вложенное меню
Давайте немного усложним. Добавим к нашей картинке меню с двумя пунктами: “Save” и “Edit…”. По нажатии на “Edit…” откроется подменю с пунктами “Rotate” и “Delete”. Это должно выглядеть так:
Для этого надо переписать метод протокола UIContextMenuInteractionDelegate следующим образом:
Здесь мы создаем последовательно кнопки “Save”, “Rotate” и “Delete”, добавляем последние две в подменю “Edit…” и оборачиваем все в главное контекстное меню.
Добавляем контекстное меню в UICollectionView
Давайте добавим контекстное меню в UICollectionView . При долгом нажатии на ячейку пользователю будет показано меню с пунктом “Archive”, вот так:
Добавление контекстного меню в UICollectionView проще простого: достаточно реализовать опциональный метод func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? протокола UICollectionViewDelegate . Вот, что у нас вышло:
Тут, как и прежде, создается элемент и само меню. Теперь при долгом (сильном) нажатии на ячейку пользователь увидит контекстное меню.
Добавляем контекстное меню в UITableView
Здесь все аналогично UICollectionView . Нужно имплементировать метод contextMenuConfigurationForRowAt протокола UITableViewDelegate так:
Но что, если мы хотим использовать кастомный экран в контекстном меню? Например, такой:
Для этого при создании UIContextMenuConfiguration следует передать нужный UIViewController в previewProvider . Вот пример кода, реализующего это:
В примере PreviewViewController инициализируется из сториборда и отображается в контекстном меню.
Осталось добавить обработку нажатия на этот ViewController. Для этого нужно имплементировать метод willCommitMenuWithAnimator протокола UITableViewDelegate . Сам обработчик поместим внутрь animator.addCompletion :
Заключение
Контекстные меню — это новый мощный инструмент взаимодействия пользователя с вашим приложением. И, как видите, их реализация довольно проста. Но не следует забывать, что методы могут измениться, пока не выйдет релизная версия iOS 13.
Хотите внедрить подписки в iOS-приложение за 10 минут? Интегрируйте Apphud и:
- оформляйте покупки с помощью лишь одного метода;
- автоматически отслеживайте состояние подписки каждого пользователя;
- легко интегрируйте Subscription Offers;
- отправляйте события о подписках в Amplitude, Mixpanel, Slack и Telegram с учетом локальной валюты пользователя;
- уменьшайте Churn rate в приложениях и возвращайте отписавшихся пользователей.
Источник