Книга: JavaScript. Подробное руководство, 6-е издание

17.7. События механизма буксировки (drag-and-drop)

17.7. События механизма буксировки (drag-and-drop)

В примере 17.2 было показано, как реализовать операцию буксировки элементов мышью. Она позволяет перетаскивать и оставлять элементы в пределах веб-страницы, но истинная буксировка - это нечто иное. Буксировка (drag-and-drop, или DnD)- это интерфейс взаимодействия с пользователем, позволяющий перемещать данные между «источником» и «приемником», которые могут находиться в одном или в разных приложениях. Буксировка (DnD) - это сложный механизм организации взаимодействий между человеком и машиной, и прикладные интерфейсы, реализующие поддержку буксировки, всегда отличались высокой сложностью:

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

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

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

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

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

Прикладной интерфейс механизма буксировки в IE довольно сложен в использовании, а различия между реализациями в текущих броузерах не позволяют использовать наиболее сложные части API переносимым способом. Тем не менее он дает возможность веб-приложениям участвовать в операциях буксировки подобно обычным приложениям. Броузеры всегда позволяли выполнять простейшие операции буксировки. Если выделить текст в веб-броузере, его легко можно отбуксировать в текстовый процессор. А если выделить URL-адрес в текстовом процессоре, его можно отбуксировать в броузер, чтобы открыть страницу с этим адресом. В этом разделе будет показано, как создавать собственные источники, которые позволят перемещать данные, не являющиеся текстом, и собственные приемники, откликающиеся на попытки оставить в них данные некоторым способом, помимо простого их отображения.

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

События, возбуждаемые в источнике, относительно просты, поэтому начнем с них. Источником для механизма буксировки является любой элемент документа, имеющий HTML-атрибут draggable. Когда пользователь начинает перемещать указатель мыши с нажатой кнопкой над элементом-источником, броузер не выделяет содержимое элемента, а возбуждает в нем событие «dragstart». Обработчик этого события должен вызвать метод dataTransfer.setData(), чтобы определить данные (и тип этих данных), доступные в источнике. (Когда будет реализован новый HTML5 API, вместо этого метода нужно будет вызывать метод dataTransfer.items.add().) Обработчику также может потребоваться установить свойство dataTransfer.effectAllowed, чтобы определить тип операции - «перемещение», «копирование» или «создание ссылки» - поддерживаемой источником, и, возможно, необходимо будет вызвать метод dataTransfer.setDragImage() или dataTransfer.addElement() (в броузерах, поддерживающих эти методы), чтобы определить изображение или элемент документа, который будет использоваться для визуального представления перемещаемых данных.

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

Когда выполняется сброс, возбуждается событие «dragend». Если источник поддерживает операцию «перемещения», он должен проверить свойство dataTransfer.dropEffect, чтобы убедиться, действительно ли была выполнена операция перемещения. Если это так, данные, перемещенные в другое место, следует удалить из источника.

Событие «dragstart» является единственным, обработку которого необходимо реализовать в простейшем источнике данных. Реализация такого источника представлена в примере 17.4. Он отображает текущее время в формате «hh:mm» в элементе <span> и обновляет время раз в минуту. Если бы это было все, что реализует пример, пользователь мог бы просто выделить отображаемый текст и от; буксировать его. Но этот пример превращает часы в источник данных для механизма буксировки, устанавливая свойство draggable элемента часов в значение true и определяя функцию-обработчик ondragstart. Обработчик вызывает метод dataTransfer.setData(), чтобы определить перемещаемые данные - строку с полной информацией о текущем времени (включая дату, секунды и информацию о часовом поясе). Он также вызывает dataTransfer.setDragIcon(), чтобы определить изображение (ярлык с изображением часов), которое будет перемещаться в процессе буксировки.

Пример 17.4. Источник данных для механизма буксировки

<script src="whenReady.js"></script>
<script>
  whenReady(function() {
    var clock = document.getElementById("clock"); // Элемент часов
    var icon = new Image(); // Буксируемое изображение
    icon.src = "clock-icon.png"; // URL-адрес изображения
    // Отображает время раз в минуту
    function displayTime() {
      var now = new Date(); // Получить текущее время
      var hrs = now.getHours(), mins = now.getMinutes();
      if (mins < 10) mins = "0" + mins;
      clock.innerHTML = hrs + ":" + mins; // Отобразить текущее время
      setTimeout(displayTime, 60000); // Запустить через 1 минуту
    }
    displayTime();
    // Сделать часы доступными для буксировки.
    // То же самое можно сделать с помощью HTML-атрибута:
    // <span draggable="true">... clock.draggable = true;
    // Обработчики событий
    clock.ondragstart = function(event) {
      var event = event || window.event; // Для совместимости c IE
      // Свойство dataTransfer является ключом к drag-and-drop API
      var dt = event.dataTransfer;
      // Сообщить броузеру, какие данные будут буксироваться.
      // Если конструктор Date() вызывается как функция, он возвращает
      // строку с полной информацией о текущем времени
      dt.setData("Text", Date() + ”n”);
      // Определить ярлык, который будет служить визуальным представлением перемещаемой
      // строки, в броузерах, поддерживающих эту возможность. Без этого для визуального
      // представления броузер мог бы использовать изображение текста в часах,
      if (dt.setDraglmage) dt.setDragImage(icon, 0, 0);
    };
  }):
</script>
<style>
  flclock { /* Придать часам привлекательный внешний вид */
    font: bold 24pt sans;
    background: #ddf;
    padding: 10px;
    border: solid black 2px;
    border-radius: 10px;
}
</style>
<h1>Drag timestamps from the clock</h1>
<span></span> <!-- Здесь отображается время -->
<textarea cols=60 rows=20></textarea> <!-- Сюда можно сбросить строку -->

Приемники буксируемых данных сложнее в реализации, чем источники. Приемником может быть любой элемент документа: чтобы создать приемник, не требуется устанавливать HTML-атрибуты, как при создании источников, - достаточно просто определить соответствующие обработчики событий. (Однако с реализацией нового прикладного интерфейса буксировки, определяемого стандартом HTML5, вместо некоторых обработчиков событий, описываемых ниже, в элементе-приемнике необходимо будет установить атрибут dropzone.) В приемнике возбуждается четыре события. Когда буксируемый объект оказывается над элементом документа, броузер возбуждает в этом элементе событие «dragenter». Определить, содержит ли буксируемый объект данные в формате, понятном приемнику, можно с помощью свойства dataTransfer.types. (При этом может также потребоваться проверить свойство dataTransfer.effectAllowed, чтобы убедиться, что источник и приемник поддерживают выполняемую операцию: перемещение, копирование или создание ссылки.) В случае успешного прохождения этих проверок элемент-приемник должен дать знать пользователю и броузеру, что он может принять буксируемый объект. Обратную связь с пользователем можно реализовать в виде изменения цвета рамки или фона. Самое интересное: приемник сообщает броузеру, что он готов принять буксируемый объект, отменяя это событие.

Если элемент не отменит событие «dragenter», переданное ему броузером, броузер не будет считать этот элемент приемником для данной операции буксировки и не будет передавать ему дополнительные события. Но если приемник отменит событие «dragenter», броузер будет посылать ему события «dragover», пока пользователь будет буксировать объект над приемником. Интересно (снова) отметить, что приемник должен обрабатывать и отменять все эти события, чтобы сообщить, что он по-прежнему готов принять буксируемый объект. Если приемнику потребуется сообщить, что он поддерживает только операцию перемещение, копирования или создания ссылки, он должен устанавливать свойство dataTransfer.dropEffect в обработчике события «dragover».

Если пользователь переместит буксируемый объект за границы приемника, который сообщал о своей готовности принять объект отменой событий, в приемнике будет возбуждено событие «dragleave». Обработчик этого события должен восстановить прежний цвет рамки или фона элемента или отменить любые другие визуальные изменения, выполненные в ответ на событие «dragenter». К сожалению, оба события, «dragenter» и «dragleave», всплывают; и если приемник имеет вложенные элементы, будет сложно отличить, говорит ли событие «dragleave» о том, что указатель мыши с буксируемым объектом вышел за границы элемента-приемника или что он вышел за границы вложенного элемента внутри приемника.

Наконец, если пользователь сбросит буксируемый объект над приемником, в приемнике будет возбуждено событие «drop». Обработчик этого события должен получить перемещаемые данные с помощью dataTransfer.getData() и выполнить над ними соответствующие операции. Если пользователь сбросит над приемником один или более файлов, свойство dataTransfer.files будет содержать объект, подобный массиву, с объектами File. (Работа с этим свойством демонстрируется в примере 18.11.) После реализации нового HTML5 API обработчики события «drop» должны будут выполнить обход элементов массива dataTransfer.items[], чтобы обработать данные, которые могут быть или не быть файлами.

Пример 17.5 демонстрирует, как превратить в приемники элементы <ul> и как превратить вложенные в них элементы <li> в источники. Этот пример является демонстрацией концепции ненавязчивого JavaScript. Он отыскивает элементы списка <ul>, атрибут class которых включает класс «dnd», и регистрирует в них обработчики событий буксировки. Обработчики событий превращают список в приемник: любой текст, сброшенный над таким списком, будет преобразован в новый элемент списка и добавлен в конец. Обработчики событий также обслуживают ситуации перемещения элементов внутри списка и делают текст каждого элемента списка доступным для буксировки. Обработчики событий источников поддерживают операции копирования и перемещения и удаляют элементы списка, которые были сброшены в ходе выполнения операции перемещения. (Обратите внимание, что не все броузеры обеспечивают переносимую поддержку операции перемещения.)

Пример 17.5. Список как приемник и источник

/* Прикладной программный интерфейс механизма буксировки весьма сложен, его реализации
* в разных броузерах не являются полностью совместимыми. В своей основе этот пример
* реализован правильно, но все броузеры немного отличаются друг от друга,
* и каждый из них имеет свои уникальные особенности. В данном примере не делается
* попыток реализовать обходные решения, характерные для отдельных броузеров.
*/
whenReady(function() { // Вызовет эту функцию, когда документ будет загружен
  // Отыскать все элементы <ul и вызвать функцию dnd() для них
  var lists = document.getElementsByTagName("ul");
  var regexp = /bdndb/;
  for(var і = 0; і < lists.length; i++)
    if (regexp.test(lists[i].className)) dnd(lists[i]);
  // Добавляет обработчики событий буксировки в элемент списка
  function dnd(list) {
    var original_class = list.className; // Сохранить начальный CSS-class
    var entered =0; // Вход и выход за границы
    // Этот обработчик вызывается, когда буксируемый объект оказывается над списком.
    // Он проверяет, содержит ли буксируемый объект данные в поддерживаемом формате.
    // и, если это так, отменяет событие, чтобы сообщить, что список готов
    // принять объект. В этом случае он также подсвечивает элемент-приемник,
    // чтобы показать пользователю, что готов к приему данных,
    list.ondragenter = function(e) {
      е = е И window.event; // Объект события, стандартный или IE
      var from = е.relatedTarget;
      // События dragenter и dragleave всплывают, из-за чего сложнее определить,
      // когда следует подсвечивать элемент, а когда снимать подсветку в случаях,
      // подобных этому, где элемент <ui> содержит дочерние элементы <li>.
      // В броузерах, поддерживающих свойство relatedTarget, эту проблему можно решить.
      // В других броузерах приходится считать пары событий входа/выхода.
      // Если указатель мыши оказался над списком, переместившись из-за его пределов,
      // или он оказался над списком впервые, необходимо выполнить некоторые операции
      entered++;
      if ((from && !ischild(from, list)) || entered == 1) {
        // Вся информация о буксируемом объекте находится в объекте dataTransfer
        var dt = е.dataTransfer;
        // Объект dt.types содержит список типов, или форматов, в которых доступны
        // буксируемые данные. Спецификация HTML5 требует, чтобы свойство types имело
        // метод contains(). В некоторых броузерах это свойство является массивом
        // с методом indexOf. В IE версии 8 и ниже оно просто отсутствует,
        var types = dt.types; // В каких форматах доступны данные
        // Если информация о типах отсутствует или данные доступны в простом
        // текстовом формате, подсветить список, чтобы показать пользователю, что он
        // готов принять данные, и вернуть false, чтобы известить о том же и броузер,
        if (!types ||                                            //IE
             (types.contains && types.contains("text/plain")) || //HTML5
             (types.indexOf && types.indexOf("text/plain")!=-1)) //Webkit
        {
          list.className = original_class + " droppable";
          return false;
        }
        // Если тип данных не поддерживается, мы не сможем принять их return;
        // без отмены
      }
      return false; // Если это не первое вхождение, мы по-прежнему готовы
    };
    // Этот обработчик вызывается в ходе буксировки объекта над списком.
    // Этот обработчик должен быть определен, и он должен возвращать false,
    // иначе сброс объектов будет невозможен,
    list.ondragover = function(e) { return false; };
    // Этот обработчик вызывается, когда буксируемый объект выходит за границы списка
    // или за границы одного из его дочерних элементов. Если объект действительно
    // покидает границы списка (а не границы одного из его элементов),
    // то нужно снять подсветку списка,
    list.ondragleave = function(e) {
      е = е || window.event;
      var to = e.relatedTarget;
      // Если буксируемый объект покидает границы списка или если количество выходов
      // за границы совпадает с количеством входов, следует снять подсветку списка entered--;
      if ((to && !ischild(to,list)) || entered <= 0) {
        list.className = original_class;
        entered = 0;
      }
      return false;
    }
    // Этот обработчик вызывается, когда происходит сброс объекта.
    // Он извлекает сброшенный текст и превращает его в новый элемент <li>
    list.ondrop = function(e) {
      е = е И window.event; // Получить объект события
      // Получить сброшенные данные в текстовом формате.
      // "Text" - это псевдоним для "text/plain".
      // IE не поддерживает "text/plain”, поэтому здесь используется "Text".
      var dt = e.dataTransfer; // объект dataTransfer
      var text = dt.getData("Text"); // Получить данные в текстовом формате.
      // Если был получен некоторый текст, превратить его в новый элемент
      // списка и добавить в конец,
      if (text) {
        var item = document.createElement("li"); // Создать новый <li>
        item.draggable = true; // Сделать буксируемым
        item.appendChild(document.createTextNode(text)); // Добавить текст
        list.appendChild(item); // Добавить в список
        // Восстановить первоначальный стиль списка и сбросить счетчик entered
        list.className = original_class;
        entered = 0;
        return false;
      }
    };
    // Сделать все элементы списка буксируемыми
    var items = list.getElementsByTagName("li");
    for(var і = 0; і < items.length; i++) items[i].draggable = true;
    // И зарегистрировать обработчики для поддержки буксировки элементов списка.
    // Обратите внимание, что мы поместили эти обработчики в список и ожидаем,
    // что события будут всплывать вверх от элементов списка.
    // Этот обработчик вызывается, когда буксировка начинается внутри списка,
    list.ondragstart = function(e) {
      var е = е || window.event;
      var target = e.target || e.srcElement;
      // Если всплыло событие от элемента, отличного от <li>, игнорировать его
      if (target.tagName !== "li") return false;
      // Получить важный объект dataTransfer
      var dt = e.dataTransfer;
      // Сохранить данные и указать информацию об их формате
      dt.setData("Text", target.innerText || target.textContent);
      // Сообщить, что поддерживаются операции копирования и перемещения
      dt.effectAllowed = "copyMove";
    };
    // Этот обработчик вызывается после успешного сброса
    list.ondragend = function(e) {
      е = е || window.event;
      var target = e.target || e.srcElement;
      // Если выполнялась операция перемещения, удалить элемент списка.
      // В IE8 это свойство будет иметь значение "none", если явно
      // не установить его в значение "move" в обработчике ondrop выше.
      // Но принудительная установка в значение "move" для IE будет
      // препятствовать другим броузерам дать пользователю возможность
      // выбирать между операцияим перемещения и копирования,
      if (e.dataTransfer.dropEffect === "move") target.parentNode.removeChild(target);
    }
    // Вспомогательная функция, используемая в обработчиках ondragenter и ondragleave.
    // Возвращает true, если элемент а является дочерним по отношению к элементу b.
    function ischild(a,b) {
      for(; а; а = a.parentNode)
        if (а === b) return true;
      return false;
    }
  }
});

Оглавление книги


Генерация: 0.621. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз