Статьи

Доступное перетаскивание с несколькими элементами

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

Я собираюсь предположить, что у вас уже есть мимолетное знакомство с API перетаскивания, но если нет, взгляните на эту вводную статью (игнорируя ссылки на Modernizr, который вам не понадобится).

Основной подход к перетаскиванию нескольких элементов на самом деле довольно тривиален — нам просто нужно запомнить данные перетаскивания для более чем одного элемента. Мы могли бы сделать это, используя объект dataTransfer , или мы можем просто использовать отдельный массив (что мы и собираемся сделать). Так зачем писать целую статью об этом?

Почему — потому что, хотя данные просты, интерфейс довольно сложен.

Нам нужно будет проделать некоторую дополнительную работу для реализации механизма предварительного выбора , и нам нужно будет заставить его работать с клавиатуры (поскольку встроенное перетаскивание не поддерживает это).

Примечание. В этой статье не будут рассмотрены сенсорные события, а также не будет предоставлена ​​возможность заполнения для старых браузеров. Более того, наш подход будет работать только на одной странице, он не будет поддерживать перетаскивание между окнами. Хотя API перетаскивания поддерживает это, нет простого способа сделать его доступным с клавиатуры.

Основные Drag and Drop

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

Там есть несколько вещей, которые могут отличаться от других демонстраций, которые вы видели.

Первый способ заключается в том, что он поддерживает ссылку на элемент для перетаскиваемого элемента, а не передает ID элемента через объект dataTransfer (хотя нам нужно что-то пропустить, иначе вся операция завершится неудачей в Firefox; это может быть что угодно, поэтому пустая строка будет делать):

 var item = null; document.addEventListener('dragstart', function(e) { item = e.target; e.dataTransfer.setData('text', ''); }, false); 

Это упрощает демонстрацию, избегая необходимости иметь идентификаторы элементов, и значительно упростит расширение в приложение на стороне сервера (например, CMS ), где идентификаторы элементов не всегда легко узнать. Этот подход также будет основой множественного выбора, где ссылка на item станет массивом items .

Следующая важная вещь — это отсутствие свойств события, effectAllowed и dropEffect . Они могут принимать значения, такие как "copy" или "move" , и должны контролировать, какие действия разрешены и какой курсор будет отображать браузер. Однако реализация браузера несовместима, так что нет особого смысла включать их.

Наконец, обратите внимание, что базовый HTML не содержит draggable атрибутов:

 <ol data-draggable="target"> <li data-draggable="item">Item 0</li> <li data-draggable="item">Item 1</li> <li data-draggable="item">Item 2</li> <li data-draggable="item">Item 3</li> </ol> 

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

Этот подход также предоставляет возможность исключить любые неработающие реализации в тех случаях, когда не удается обнаружить функцию . Opera 12 или более ранняя window.opera исключена с помощью теста window.opera , потому что его реализация довольно глючная и больше не стоит тратить время на нее.

Доступный Drag and Drop

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

В ARIA Authoring Practices есть раздел, посвященный перетаскиванию , в котором описаны необходимые атрибуты, а также приведены рекомендации по работе взаимодействий. Подвести итоги:

  1. Перетаскиваемые элементы идентифицируются как aria-grabbed="false" и должны перемещаться с помощью клавиатуры.
  2. Предоставьте пользователю механизм выбора элементов, которые он хочет перетащить, и рекомендуется использовать клавишу « Пробел» . Когда элемент выбран для перетаскивания, его атрибут aria-grabbed имеет значение "true" .
  3. Предоставьте пользователю механизм для указания того, что он закончил делать выбор, и рекомендуемое нажатие клавиши — Control+M
  4. Затем целевые элементы идентифицируются с помощью aria-dropeffect со значением, которое указывает, какие действия разрешены, например, "move" или "copy" . На этом этапе процесса целевые элементы также должны перемещаться с помощью клавиатуры.
  5. Когда пользователь достигает целевого элемента, предоставьте ему механизм для выполнения действия удаления, и рекомендуемое нажатие клавиши также Control+M
  6. Пользователи могут отменить всю операцию в любое время, нажав клавишу Escape .
  7. Когда действие завершено или прервано, очистите интерфейс, установив для всех атрибутов aria-dropeffect значение "none" (или удалив их), а для всех атрибутов aria-grabbed"false" .

Теперь два атрибута ARIA , aria-grabbed и aria-dropeffect , должны использоваться в соответствии со спецификацией. Тем не менее, события и взаимодействия являются просто рекомендациями, и нам не нужно рабски следовать им. Тем не менее, мы должны (и будем) следовать этим рекомендациям как можно точнее, потому что они наиболее близки к нормативной ссылке.

И по большей части, я думаю, что это имеет смысл. Но я не согласен с обоими вариантами использования клавиши Control+M Я не убежден, что действительно необходимо иметь нажатие клавиши в конце выбора, и я не уверен, что само нажатие клавиши очень интуитивно понятно для зрячих пользователей клавиатуры.

Поэтому я думаю, что лучший подход — это дополнить эти рекомендации:

  • Мы осуществим нажатие клавиши в конце выбора, но это не потребует и не предотвратит дальнейший выбор, это просто будет ярлык для перемещения фокуса к первому целевому объекту.
  • Мы будем использовать Control+M для обоих нажатий клавиш, но мы также позволим запускать действие сброса клавишей Enter .

В руководствах также говорится о реализации множественного выбора с использованием модификаторов. Рекомендуется использовать Shift+Space для непрерывного выбора (т.е. выбор всех элементов между двумя конечными точками) и Control+Space для несмежного выбора (т.е. выбор произвольных элементов). Это, безусловно, лучшие модификаторы для использования … однако для Mac эквивалентом Control+Space будет Command+Space , но это уже связано с действием системы и не может быть подавлено JavaScript. Может быть, мы могли бы использовать Control , но пользователи Mac не ожидали этого, поскольку клавиша Control в основном используется только для нажатия правой кнопки мыши. Единственный оставшийся выбор — Shift , но он предназначен для непрерывного выбора!

Поэтому для простоты и в обход этой проблемы мы не собираемся осуществлять непрерывный отбор. Самая простая вещь, которую мы можем сделать сейчас, — это поддержать все три модификатора для несмежного выбора — Command + Space , Control + Space или Shift + Space — все обрабатываются одинаково на каждой платформе — тогда все, что может вызвать конкретный пользователь, и все, что они думают, имеет смысл, доступно.

Множественный выбор

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

Итак, начнем с обновления исходного кода, который применил draggable , чтобы он также применял значения по умолчанию для aria-grabbed и tabindex :

 for(var items = document.querySelectorAll('[data-draggable="item"]'), len = items.length, i = 0; i < len; i ++) { items[i].setAttribute('draggable', 'true'); items[i].setAttribute('aria-grabbed', 'false'); items[i].setAttribute('tabindex', '0'); } 

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

Выбор мыши

В этой демонстрации вы можете сделать одиночный выбор мышью или множественный выбор с помощью модификатора (и это все, что вы можете сделать). Но обратите внимание, что несколько выборок могут быть сделаны только в пределах одного контейнера, вы не можете сделать выбор между несколькими контейнерами:

Ограничение выборок одним контейнером «владелец» является преднамеренным и реализовано в сценариях со ссылкой на selections.owner . Цель такого поведения — упростить взаимодействие с пользователями, чтобы всегда было ясно, откуда поступили выборы и куда они могут быть перемещены (т. Е. Куда-либо еще).

Если вы посмотрите на код JavaScript, вы также увидите, как мы добавили некоторые функции выбора — addSelection , removeSelection и clearSelections — которые добавляют и удаляют aria-grabbed чтобы указать выбор элемента, и управляют данными объекта selections .

Взаимодействия с мышью, которые вызывают эти функции, используют два события мыши. Одиночный выбор или глобальный сброс (без модификатора) инициируется событием mousedown , но отмена выделения или множественный выбор (с модификатором) откладывается до события mouseup .

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

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

Невозможно реализовать оба из них, используя только dragstart mousedown и dragstart , потому что они будут прямо противоречить друг другу. Но если мы используем событие mouseup для измененного выделения и отмены выбора, то мы можем удовлетворить оба ожидания.

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

Выбор клавиатуры

Выбор клавиатуры осуществляется с использованием модели, которую мы рассматривали ранее — вы можете вкладывать элементы в элементы, а затем использовать пробел для выбора, с модификатором для нескольких вариантов выбора:

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

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

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

Перетаскивание выделения

Сделав один или несколько выборов, пользователь должен знать, куда он может перетащить элементы (то есть, какие контейнеры являются допустимыми объектами перетаскивания). Опять же, мы можем использовать один и тот же атрибут для взаимодействия мыши и клавиатуры — в этом случае aria-dropeffect , который мы сначала применяем ко всем целевым контейнерам по умолчанию:

 for(var targets = document.querySelectorAll('[data-draggable="target"]'), len = targets.length, i = 0; i < len; i ++) { targets[i].setAttribute('aria-dropeffect', 'none'); } 

Когда цели доступны, их aria-dropeffect меняется с "none" на "move" . Это позволяет программам чтения с экрана знать, что элементы теперь являются допустимыми целями, а также могут использоваться как средство визуального оформления:

 [data-draggable="target"] { border-color:#888; background:#ddd; color:#555; } [data-draggable="target"][aria-dropeffect="move"] { border-color:#68b; background:#fff; } 

(Обратите внимание, что, хотя единственными визуальными различиями являются цвета, изменение контраста достаточно, чтобы не передавать информацию только по одному цвету .)

Перетаскивание клавиатуры

Для взаимодействия с клавиатурой, состояние "move" применяется, как только будут сделаны любые выборы. Взаимодействие с клавиатурой не имеет состояния «перетаскивания» как такового — цели выбираются пользователями, когда они вкладываются в целевой контейнер, и, следовательно, целевые контейнеры должны быть доступны при наличии выбора:

Вы увидите, как в коде JavaScript появились две новые функции — addDropeffect и clearDropeffect которые управляют aria-dropeffect и tabindex в целевых контейнерах.

Но когда целевые объекты доступны, мы также удаляем tabindex из всех элементов внутри них , чтобы повысить удобство использования для пользователей клавиатуры за счет уменьшения количества элементов в порядке вкладок. Поскольку элементы вне контейнера владельца не могут быть выбраны, они вообще не должны (и, возможно, не должны ) находиться в порядке табуляции. У таких элементов также временно удален атрибут « aria-grabbed , поэтому программы чтения с экрана не продолжают объявлять их как перетаскиваемые элементы.

Эти функции вызываются теми же событиями нажатия клавиш, которые мы использовали для выбора — добавление дропэффекта всякий раз, когда выбираются какие-либо элементы, и очистка его снова, когда все они сбрасываются. Это событие также включает нажатие клавиши в конце выбора ( Control + M для ПК или Command + M для Mac), которое является просто ярлыком для установки фокуса на первый доступный целевой контейнер.

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

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

Перетаскивание мышью

Для перетаскивания мышью мы не добавляем aria-dropeffect пока перетаскивание не aria-dropeffect , и в этот момент все доступные целевые контейнеры станут подсвеченными:

Сложность взаимодействия с мышью заключается в необходимости создания состояния «зависания». Для клавиатуры было просто добавить правило :focus к целевым стилям, но мы не можем использовать :hover для взаимодействия с мышью, потому что состояние hover никогда не возникает во время перетаскивания .

Однако мы можем использовать собственные события перетаскивания для отслеживания положения мыши, а затем реализовать состояние наведения с помощью сценария. dragenter нам события называются dragenter и dragleave , которые концептуально похожи на mouseover и mouseout , но с одним важным отличием — у событий перетаскивания нет свойства relatedTarget . Поэтому, когда вы перетаскиваете мышь в целевой элемент (например), событие dragenter не может сказать вам, от какого элемента мышь отходит .

Но мы можем исправить это, поддерживая ручную ссылку — поскольку событию dragleave всегда предшествует событие dragenter , target этого dragenter должна логически быть тем же элементом, что и связанная цель dragleave . Так что, если мы запишем эту ссылку в первом, мы можем использовать его во втором:

 var related = null; document.addEventListener('dragenter', function(e) { related = e.target; }, false); document.addEventListener('dragleave', function(e) { //the related var is this event's relatedTarget element }, false); 

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

 function getContainer(element) { do { if(element.nodeType == 1 && element.getAttribute('aria-dropeffect')) { return element; } } while(element = element.parentNode); return null; } 

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

 document.addEventListener('dragleave', function(e) { var droptarget = getContainer(related); if(droptarget != selections.droptarget) { //... update target highlighting selections.droptarget = droptarget; } }, false); 

Отбрасывание выбора

Последнее взаимодействие заключается в переносе элементов в целевой контейнер. На этом этапе процесса у нас будет массив items со ссылками на все перетаскиваемые элементы, поэтому перемещение этого выделения — это просто случай добавления элементов в целевой контейнер удаления:

 for(var len = items.length, i = 0; i < len; i ++) { droptarget.appendChild(items[i]); } 

Нам вообще не нужно ссылаться на объект dataTransfer , потому что мы его не используем.

Мышиный помет

Для взаимодействия с мышью у нас уже будет ссылка droptarget созданная событием dragleave (или она будет null если текущей цели нет). Поэтому нам просто нужно событие dragend для обработки того, что происходит, когда мышь отпущена:

Здесь важно то, что здесь нет события drop .

Событие drop номинально означает, что элемент был сброшен в допустимую цель, в то время dragend событие dragend просто означает, что мышь была отпущена (т.е. была ли цель сброса действительной). Но на практике, в контексте одного документа, на самом деле нет никакой разницы между этими двумя событиями — оба события запускаются каждый раз, и «действительная цель удаления» — это не что иное, как «что бы мы ни говорили»!

Однако нам приходится иметь дело с ошибкой события браузера — Mac / Webkit не будет запускать событие удаления, если клавиша Command все еще удерживается нажатой . Следовательно, при отпускании мыши предметы останутся захваченными, а цели останутся активными, поэтому, чтобы избежать этой проблемы, мы просто используем событие dragend для всего.

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

Падение клавиатуры

Для взаимодействия с клавиатурой сброс выбора означает переход к целевому контейнеру, а затем нажатие клавиши сброса (либо Enter, либо Modifier + M ). У нас нет активной ссылки на droptarget , но тогда она нам не нужна — цель удаления — это элемент, который получает нажатие клавиши:

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

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

Вот и все!

Есть, конечно, много возможностей для дополнительных улучшений, среди них:

  • Добавление поддержки сенсорных и / или указательных событий.
  • Создание полифилла для старых браузеров.
  • Сортировка выбора, когда он упал.
  • Выбор «копировать» или «переместить» с помощью Modifier + Drop .
  • Непрерывное выделение с помощью Shift + Select и / или выделение диапазона клавиатуры с помощью Shift + Arrow .
  • Использование пользовательского перетаскивания, чтобы указать, сколько элементов перетаскивается.
  • Визуально затемнение выделения при перетаскивании элементов мышью или при нажатии клавиши «конец выделения».

Я расскажу о некоторых из этих возможностей в следующей статье.

А пока вы можете получить копию файлов из нашего репозитория GitHub: