Javascript: Drag and Drop кроссбраузерно

Javascript: Drag and Drop кроссбраузерно

В данной заметке речь пойдёт о кроссбраузерной реализации функционала Drag and Drop - перетаскивание мышкой. В очередном порыве души решил я написать свою функцию, не использующую jQuery, которая:

  • содержала по возможности минимум кода
  • была кроссбраузерна
  • не шибко б жрала память
  • работала без глюков
  • не засоряла глобальное пространство имён
  • по возможности была б совместима с новым стандартом ECMA 5

Пример плавает где-то здесь рядом, а ниже я опишу алгоритм работы функции Drag and Drop, и поясню некоторые моменты, что бы вам не показались не понятными некоторые вещи в коде. Например в нашей функции, реализующей функционал Drag and Drop введены некие переменные deltaX и deltaY, служебная функция pageOffset. Зачем они нужны станет понятно, когда вы прочитаете описание алгоритма.

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

Кстати:
позиционирование, перетаскиваемого объекта в нашем случае, должно быть абсолютным, т.е. css свойство position должно иметь значение absolute, иначе ничего не получится. Я считаю, что назначение этого свойства нужно реализовать внутри нашей фукции, что бы она была самодостаточной.

Ещё кстати:
объект события, во всех браузерах, кроме IE - это самостоятельный объект, и передается как аргумент в функцию - обработчик, а в IE нет, но он доступен, как свойство объекта window: window.event. Кроссбраузерно, в функции обработчике события, объект события можно получить следующим образом, например возьмём совершенно "левый" обработчик события click:


  // Назначаем обработчик события:
  // в аргумент "е" в нормальных браузерах
  // поступит объект события Event.
  element.onclick = function(e) {

    // Кроссбраузерно получаем объект события:
    // В IE объект события доступен нам как window.event:
    var e = window.event || event;

    // ... какой то код ...
  }

Здесь также стоит отметить, как используется оператор || - логическое "или". Это можно понять на примере:


  var test = true||false;
  var test = false||true;
  // Выводим test на консоль:
  console.log(test); // test будет иметь значение true

Я надеюсь, понятно, что в переменную test всегда попадёт значение, которое можно привести к логическому типу true. Этот приём часто используется при достижении кроссбраузерных "фишек". Если какого тосвойства или метода не существует, то в переменную занесётся, то которое есть. Свойства должны быть перечислены через знак логическго "или", в общем, ниже вы увидите ещё кучу примеров :)

Алгоритм работы Drag and Drop

Алгоритм мне представляется следующим: на объекте висит обработчик события: mousedown, в котором запускается некая функция, которая вешает на объект document обработчик события: mousemove, который срабатывает каждый раз, как только курсор переместился хотя бы на один пиксель.

Мы вешаем обработчик события mousemove на document по той причине, что нас интересует перемещение курсора относительно объекта document. В обработчике события mousemove нам потребуется просчитывать текущие координаты курсора мыши, и приравнивать к ним текущие координаты перетаскиваемого объекта element.

        ... Внутри обработчика ...

  // Кроссбраузерно получаем объект события:
  var e = window.event || event;
  
  // свойства объекта события clientX и clientY
  // это и есть нужные координаты
  element.style.left = e.clientX + "px";
  element.style.top  = e.clientY + "px";

Так как это происходит при каждом перемещении курсора, обработчик mousemove - это увы, самое ресурсоёмкое место в нашей функции, но без неё в данном случае ни куда.

Проблема:
всё вроде бы просто, но здесь есть одно "но" - начало координат объекта на странице - это его левый верхний угол. Т.е. объект, по сути, в системе координат страницы - это точка с координатами X и Y ( css свойства top, left, bottom, right задаются именно относительно этой точки). И если мы будем напрямую приравнивать эти координаты к курсору мыши, то наш объект будет, как будто подскакивать левым верхним углом к курсору. Что не есть "гуд" и будет порождать глюки.

Кстати:
объект, который вызвал событие
, доступен из: event.target или, в IE event.srcElement. Т.е. он есть ЦЕЛЬ! Кроссбраузерно его можно получить вот так:


  // Кроссбраузерно получаем объект события и текущую цель:
  var e      = window.event || event,
      target = e.srcElement || e.target;

Ещё кстати:
текущие координаты объекта
, который вызвал событие, доступны из: event.target.offsetLeft и event.target.offsetTop, или, в IE: event.srcElement.offsetLeft и event.srcElement.offsetTop

Выход:
нам нужно, но только в самом начале - при клике, вычислить разницу между текущими координатами объекта и координатами клика.

Назовём эту разницу deltaX и deltaX - их я и имел ввиду в начале статьи. Вот если мы будем приравнивать координаты объекта к координатам курсора, с учётом этих самых deltaX и deltaX то всё будет так, как нам надо.


  // Высчитываем дельту, обязательно только в момент нажатия кнопки мыши:
  deltaX = e.clientX - target.offsetLeft;
  deltaY = e.clientY - target.offsetTop;

Проблема:
теперь всё будет хорошо, но, до того момента, пока на странице не появляется прокрутка. Ведь css свойства top, left, bottom, right у нас отсчитываются от начала документа, а клик мы фиксируем, кроссбраузерно, относительно экрана! И если, перетаскиваемый объект находится где то снизу большой страницы, то при клике на объекте, наш объект будет "улетать в прекрасное далёко".

Выход:
нужно при возникновении события mousemove вносить поправку в расчёты, значение этой самой прокрутки. Значение прокрутки у браузеров - вещь тоже сугубо интимная. И разные браузеры позволяют получить его по разному. Мало того IE в зависимости от DOCTYPE документа, выпендривается дополнительно! Новот так мы имеем возможность получать нужное нам значение кроссбраузерно:


  // Служебная функция, позволяет кроссбраузерно получить прокрутку страницы:
  function pageOffset() {
    return {
      x : window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
      y : window.pageYOffset || document.documentElement.scrollTop  || document.body.scrollTop
    };
  }

Теперь, так как функция возвращает объект, использовать её нужно так:


  pageOffset().x
  pageOffset().y

Далее я чувствую без наглядного пособия не обойтись, поэтому нарисовал картинку:

координация объекта

    На рисунке сверху:

  • Ox, Oy - координаты объекта,
  • Cx, Cy - координаты клика мышки,
  • Px, Py - прокрутка страницы,
  • Dx, Dy - это та самая разница координат между кликом и объектом, о которой я говорил выше.

Нам нужно динамически при каждом событии mousemove высчитывать новые значения Dx, Dy. И с поправкой на них приравнивать текущие координаты объекта, к координатам клика.

    На данном этапе нам известны:

  • Cx, Cy - это event.clientX и event.clientY
  • Ox, Oy - это element.offsetLeft и element.offsetTop
  • Px, Py - это мы получим так: pageOffset().x и pageOffset().y

Теперь, если чуть - чуть вспомнить геометрию то не трудно догадаться что Dx = Px + Cx - Ox; Dy = Py + Cy - Oy; - ничего сложного. А собственно новые координаты посчитаем так:


  element.style.left = event.clientX + pageOffset().x - Dx + "px";
  element.style.top  = event.clientY + pageOffset().y - Dy + "px";

Вот теперь мы вроде бы всё учли, всё предусмотрели. Осталось теперь оформить код в виде изолированного пространства одной функции. Фактически это будет функция - конструктор, которая вернет объект, с единственным методом makeDragDrop, который будет принимать на входе аргумент - объект NodeList, содержащий, ссылки на html - элементы.

Кстати:
объект NodeList
возвращает следующая конструкция ( выбираем все элементы DIV на странице ) :


  document.getElementsByTagName("DIV");

Сама функция конструктор, назовём её Dragger - принимает единственный аргумент - глобальный контекст. Это в браузере, как известно объект window или, вне тела ких - либо функций скрипта - this. Именно так: вне функций, ключевое слово this - ссылается на объект window! В самом начале объявим зависимость:


  var DOC = GLOBAL.document;

Подобные зависимости позволяют интерпретатору быстрее разрешать имена переменных, чем цепочку вызова метода, и что не маловажно, они лучше сжимаются компрессорами Javascript. Поэтому в листинге функции вы не увидете, как в листингах сверху, цепочек вызовов, типа window.event и document.body.scrollTop. Полный листинг функции Drag and Drop, я честно старался писать минимум кода:


  function Dragger(GLOBAL) {
      "use strict";
      var DOC = GLOBAL.document;
      // Действия когда завершили перетаскивание:
      function stopDrag() {
          DOC.onmousemove = null;
          DOC.onselectstart = null;
      }
      // Служебная функция, позволяет кроссбраузерно получить прокрутку страницы:
      function pageOffset() {
          return {
               x : GLOBAL.pageXOffset || DOC.documentElement.scrollLeft || DOC.body.scrollLeft,
               y : GLOBAL.pageYOffset || DOC.documentElement.scrollTop  || DOC.body.scrollTop
          };
      }
      // Действия во время перетаскивания:
      function process(element, dx, dy) {
  
          // Корректный запрет выделения в Chrom Safari:
          DOC.onselectstart = function () {
              return false;
          };
          // Обработчик нужно ставить именно на объект document:
          DOC.onmousemove = function (event) {
              // Кроссбраузерно получаем объект события:
              var e = GLOBAL.event || event;
  
              // Запрет выделения в Opera IE
              if (GLOBAL.getSelection) {
                  GLOBAL.getSelection().removeAllRanges();
              } else if (DOC.selection && DOC.selection.clear) {
                  DOC.selection.clear();
              }
              // Высчитываем новое положение перетаскиваемого элемента, с
              // учётом дельты:
              element.style.left = e.clientX + pageOffset().x - dx + "px";
              element.style.top  = e.clientY + pageOffset().y - dy + "px";
          };
      }
      // Основная функция-метод, вызываемая из вне:
      // param elements:NodeList - содержит элементы,
      // которые нужно сделать перетаскиваемыми.
      function dragDrop(elements) {
  
          var length, deltaX, deltaY, i;
          length = elements.length;
  
          // Действия когда нажали кнопку мыши:
          function startDrag(event) {
              // Кроссбраузерно получаем объект события и текущую цель:
              var e = GLOBAL.event || event,
                  target = e.srcElement || e.target;
  
              // Высчитываем дельту, обязательно только в момент нажатия кнопки мыши:
              deltaX = e.clientX + pageOffset().x - target.offsetLeft;
              deltaY = e.clientY + pageOffset().y - target.offsetTop;
              // Запускаем процесс перетаскивания:
              process(target, deltaX, deltaY);
          }
          // В цикле проходим по всему списку элементов:
          for (i = 0; i < length; i += 1) {
              // Здесь всё начинается. Задаем позиционирование и раздаём обработчики:
              elements[i].style.position = "absolute";
              elements[i].onmousedown = startDrag;
          }
      }
      // Отпустили кнопку - конец перетаскивания:
      DOC.onmouseup = stopDrag;
      // Возвращаем объект:
      return {
          makeDragDrop : dragDrop
      };
  }

В сжатом виде этот монстр "вЕсит" 769 байта вызывается так:


  <script type="text/javascript">
  window.onload = function() {
    // Передаем глобальный контекст, внутри этой функции this = window
    var dragDrop = Dragger(this);
    // Получаем все элементы div
    var divs = document.getElementsByTagName("DIV");
    // Сделает все элементы div на странице перетаскиваемыми:
    dragDrop.makeDragDrop(divs);
  }
  </script>

А если вызов функции makeDragDrop вам потребуется осуществить только один раз, то можно и так:


  <script type="text/javascript">
  window.onload = function() {
    // Сделает все элементы div на странице перетаскиваемыми:
    Dragger(window).makeDragDrop(document.getElementsByTagName("DIV"));
  }
  </script>

Здесь стоит отметить, что в функции используется назначение обработчиков событиям "по-старинке", если делать "по-правильному" то нужно назначать обработчики через специальные методы или сделать "композицию" с объектом который я описывал здесь Javascript кроссбраузерная установка обработчиков события

ТАЩИ МЕНЯ

Добавить комментарий


Защитный код
Обновить



Кто на сайте
Сейчас 74 гостей онлайн