Профессиональные курсы: Практика верстки HTML JavaScript для начинающих Программирование на PHP Онлайн-курсы по веб-технологиям

Drag & Drop силами JavaScript

Пишем скрипт, с помощью которого можно передвигать элементы на web-странице.

Перед тем как взяться за JavaScript проведем простое планирование и ответим на 2 вопроса:

  • Что мы будем передвигать?
  • Как мы будем передвигать?

Ответ на вопрос «Что?» крайне прост: любой абсолютно (или относительно) позиционированный элемент на странице, например DIV:

<div style='position:absolute; top:0; left:0'>Text in DIV</div>

На месте DIV-а мог быть рисунок, таблица, ссылка или любой другой элемент.

Ответ на вопрос «Как?» более сложен. Составим простой алгоритм перетаскивания любого элемента:

  1. Для начала нужно "ухватиться" за элемент, что соответсвует событию mousedown.
  2. Затем элемент начинают передвигать, что соответствует событию mousemove.
  3. Когда наш объект оказался на нужном месте его отпускают — событие mouseup.
  4. Если резко переместить указатель мыши и отпустить вне объекта, то движение тоже должно остановиться — событие mouseup вне элемента.

Для реализации этого алгоритма в жизнь нам понадобятся:

  1. Глобальные переменные показывающие начальное положение элемента, начальные координаты мыши и состояние движение.
  2. Функция для определения координат мыши.
  3. Функция запоминания начального состояния перед движением
  4. Функция для обработки движения

Соберем всю эту теорию в JavaScript-сценарий:

// Объявим глобальные переменные
// Переменная состояния, по умолчанию ничего не двигается = false
var moveState = false;
// Переменные координат мыши в начале перемещения, пока неизвестны
var x0, y0;
// Начальные координаты элемента, пока неизвестны
var divX0, divY0;

// Выведем абсолютно-позиционированный DIV размером 50 * 50
// Зальем DIV черным цветом
// Добавим прямо в DIV обработчики событий
document.write(
    "<div \
        style='position:absolute; top:0; left:0; background-color:black; width:50px; height:50px;' \
        onmousedown = 'initMove(this, event);' \
        onmouseup = 'moveState = false;' \
        onmousemove = 'moveHandler(this, event);' \
    ></div>"

);

// Объявим функцию для определения координат мыши
function defPosition(event) {
    var x = y = 0;
    if (document.attachEvent != null) { // Internet Explorer & Opera
        x = window.event.clientX + document.documentElement.scrollLeft + document.body.scrollLeft;
        y = window.event.clientY + document.documentElement.scrollTop + document.body.scrollTop;
    }
    if (!document.attachEvent && document.addEventListener) { // Gecko
        x = event.clientX + window.scrollX;
        y = event.clientY + window.scrollY;
    }
    return {x:x, y:y};
}

// Функция инициализации движения
// Записываем всё параметры начального состояния
function initMove(div, event) {
    var event = event || window.event;
    x0 = defPosition(event).x;
    y0 = defPosition(event).y;
    divX0 = parseInt(div.style.left);
    divY0 = parseInt(div.style.top);
    moveState = true;
}

// Если клавишу мыши отпустили вне элемента движение должно прекратиться
document.onmouseup = function() {
    moveState = false;
}

// И последнее
// Функция обработки движения:
function moveHandler(div, event) {
    var event = event || window.event;
    if (moveState) {
        div.style.left = divX0 + defPosition(event).x - x0;
        div.style.top  = divY0 + defPosition(event).y - y0;
    }
}

Это один из простейший способов реализации Drag&Drop силами JavaScript. Этот сценарий можно улучшить, избавивившись от глобальных переменных, перенеся добавление обработчиков событий из атрибутов тега в JavaScript и многое, многое другое... Главное, чтобы код не потерял свою ясность и работал также быстро. Удачи!

Александр Бурцев 26 января 2007

© Все права на данную статью принадлежат порталу fastcoder.org. Перепечатка в интернет-изданиях разрешается только с указанием автора и прямой ссылки на оригинальную статью. Перепечатка в печатных изданиях допускается только с разрешения редакции.

Комментарии

Frip Nown 16 апреля 2009, 22:35 #
Отличный пример.
С точки зрения юзабилити, в примере есть один прокол - если быстро двигать мышкой, квадратик отваливается. Курсор вылетает за его пределы и драг заканчивается. Я вылечил это так:
document.onmousemove = function(event) {
moveHandler(document.getElementById('blackbox'), event);
}
(при создании дива придется приписать ему id="blackbox"; не уверен, что это лучшее решение, но советую так или иначе импрувнуть код : ) )

И еще одно пожелание - дайте линк на работающий пример, так читать интереснее : )
P.S. Opera 9.50, в редакторе кнопка «код» не работает.
 
Bur 17 апреля 2009, 13:52 #
Да, скрипт древний, маусмув надо вешать именно на документ. Поправлю статью.
За баг-репорт спасибо.
 
kos 6 августа 2009, 21:39 #
а если div шириной 1600 px с бэкграундом, как сделать что бы div при загрузке страницы был по середине экрана и когда его двигаешь мышкой что бы слой не двигался дальше своих краев, т.е. двигаю слой вправо как только показался левый край слоя движение прекращается, соответсвенно так во все стороны?

использую вот этот скрипт
<script type="text/javascript">
function GetLayer(layer)
         {
         var ReturnLayer = null;
         if(navigator.appName == 'Netscape')
           {
           if(parseInt(navigator.appVersion) == 5)
              ReturnLayer = document.getElementById(layer);
           else
              eval('ReturnLayer = document.' + layer + ';');
           }
         else
           {
           eval('ReturnLayer = document.all.' + layer + ';');
           }
         return ReturnLayer;
         }

function Layer(layer, x, y)
         {
         this.id = GetLayer(layer);
         Move(x, y, this.id);
         this.ClickedX = 0;
         this.ClickedY = 0;
         }

function SetZ(z, layer)
         {
         if(navigator.appName == 'Netscape')
           {
           if(parseInt(navigator.appVersion) == 5)
              layer.style.zIndex = z;
           if(parseInt(navigator.appVersion) == 4)
              layer.zIndex = z;
           }
         else
           {
           layer.style.zIndex = z;
           }
         }

function GetZ(layer)
         {
         if(navigator.appName == 'Netscape')
           {
           if(parseInt(navigator.appVersion) == 5)
              return(parseInt(layer.style.zIndex));
           if(parseInt(navigator.appVersion) == 4)
              return(layer.zIndex);
           }
         else
           {
           return(layer.style.zIndex);
           }
         }

function GetX(layer)
         {
         if(navigator.appName == 'Netscape')
           {
           if(parseInt(navigator.appVersion) == 5)
              return(parseInt(layer.style.left));
           if(parseInt(navigator.appVersion) == 4)
              return(layer.left);
           }
         else
           {
           return(layer.style.pixelLeft);
           }
         }

function GetW(layer)
         {
         if(navigator.appName == 'Netscape')
           {
           if(parseInt(navigator.appVersion) == 5)
              return(parseInt(layer.style.width));
           if(parseInt(navigator.appVersion) == 4)
              return(layer.clip.width);
           }
         else
           {
           if(navigator.appVersion.indexOf('MSIE 4') > 0)
              return(layer.style.pixelWidth);
           else
              return(layer.offsetWidth);
           }
         }

function GetY(layer)
         {
         if(navigator.appName == 'Netscape')
           {
           if(parseInt(navigator.appVersion) == 5)
              return(parseInt(layer.style.top));
           if(parseInt(navigator.appVersion) == 4)
              return(layer.top);
           }
         else
           {
           return(layer.style.pixelTop);
           }
         }

function GetH(layer)
         {
         if(navigator.appName == 'Netscape')
           {
           if(parseInt(navigator.appVersion) == 5)
              return(parseInt(layer.style.height));
           if(parseInt(navigator.appVersion) == 4)
              return(layer.clip.height);
           }
         else
           {
           if(navigator.appVersion.indexOf('MSIE 4') > 0)
              return(layer.style.pixelHeight);
           else
              return(layer.offsetHeight);
           }
         }

function Move(x, y, layer)
         {
         if(navigator.appName == 'Netscape')
           {
           if(parseInt(navigator.appVersion) == 5)
             {
             layer.style.left = x;
             layer.style.top = y;
             }
           if(parseInt(navigator.appVersion) == 4)
             {
             layer.left = x;
             layer.top = y;
             }
           }
         else
           {
           layer.style.pixelLeft = x;
           layer.style.pixelTop = y;
           }
         }

function PickUp(layer)
         {
         var CurrentZ = GetZ(Layers[layer].id);
         SetZ(Layers.length - 1, Layers[layer].id);
         var currentZ;
         for(var index = 0; index < Layers.length; index++)
            {
            currentZ = GetZ(Layers[index].id);
            if(currentZ >= CurrentZ && index != layer)
              {
              SetZ(currentZ - 1, Layers[index].id);
              }
            }
         }

function Selected(x, y, layer)
         {
         if(navigator.appName == 'Netscape')
           {
           if(parseInt(navigator.appVersion) == 5)
              if(x > parseInt(layer.style.left) && x < parseInt(layer.style.left) + parseInt(layer.style.width) &&
                 y > parseInt(layer.style.top) && y < parseInt(layer.style.top) + parseInt(layer.style.height))
                 return(true);
           if(parseInt(navigator.appVersion) == 4)
              if(x > layer.left && x < layer.left + layer.clip.width &&
                 y > layer.top && y < layer.top + layer.clip.height)
                 return(true);
           }
         else
           {
           if(navigator.appVersion.indexOf('MSIE 4') > 0)
             {
             if(x > layer.style.pixelLeft && x < layer.style.pixelLeft + layer.offsetWidth &&
                y > layer.style.pixelTop && y < layer.style.pixelTop + layer.offsetHeight)
                return(true);
             }
           else
             {
             if(x > layer.style.pixelLeft && x < layer.style.pixelLeft + layer.offsetWidth &&
                y > layer.style.pixelTop && y < layer.style.pixelTop + layer.offsetHeight)
                return(true);
             }
           }

         return(false);
         }

function mouseDown(e)
         {
         if ((navigator.appName == 'Netscape' && e.which!=1) || (navigator.appName == 'Microsoft Internet Explorer' && event.button!=1)) return true;
         var x = (navigator.appName == 'Netscape')? e.pageX : event.x+document.body.scrollLeft;
         var y = (navigator.appName == 'Netscape')? e.pageY : event.y+document.body.scrollTop;
         if (navigator.appName == 'Netscape' && e.target!=document) routeEvent(e);

         ClickedLayer = -1;

         //Check if a div was clicked on.
         for(var layer = 0; layer < Layers.length; layer++)
            {
            if(Selected(x, y, Layers[layer].id))
              {
              if(ClickedLayer == -1)
                {
                Layers[layer].ClickedX = x - GetX(Layers[layer].id);
                Layers[layer].ClickedY = y - GetY(Layers[layer].id);
                ClickedLayer = layer;
                }
              else
                //If divs are overlapping, pick the one on top.
                if(GetZ(Layers[layer].id) > GetZ(Layers[ClickedLayer].id))
                  {
                  Layers[layer].ClickedX = x - GetX(Layers[layer].id);
                  Layers[layer].ClickedY = y - GetY(Layers[layer].id);
                  ClickedLayer = layer;
                  }
              }
            }

         if(ClickedLayer != -1)
           {
           PickUp(ClickedLayer);
           }

         if(ClickedLayer == -1) return true;
         else return false;
         }

function mouseMove(e)
         {
         var x = (navigator.appName == 'Netscape')? e.pageX : event.x+document.body.scrollLeft;
         var y = (navigator.appName == 'Netscape')? e.pageY : event.y+document.body.scrollTop;
         if (navigator.appName == 'Netscape' && e.target!=document) routeEvent(e);

         //If a div is selected,
         //make it follow the mouse cursor.

         if(ClickedLayer != -1)
           {
           Move(x - Layers[ClickedLayer].ClickedX,
                y - Layers[ClickedLayer].ClickedY,
           Layers[ClickedLayer].id);
           }

         if(ClickedLayer == -1) return true;
         else return false;
         }

function mouseUp(e)
         {
         var x = (navigator.appName == 'Netscape')? e.pageX : event.x+document.body.scrollLeft;
         var y = (navigator.appName == 'Netscape')? e.pageY : event.y+document.body.scrollTop;
         if (navigator.appName == 'Netscape' && e.target!=document) routeEvent(e);

         ClickedLayer = -1;

         return true;
         }

function init()
         {
         ClickedLayer = -1;
         document.onmousedown = mouseDown;
         document.onmousemove = mouseMove;
         document.onmouseup = mouseUp;
         if(navigator.appName == 'Netscape')
            document.captureEvents(Event.MOUSEDOWN | Event.MOUSEMOVE | Event.MOUSEUP);

         Layers = new Array();
         Layers[0] = new Layer('Drag', 0, 0);
         Layers[1] = new Layer('Drop', 0, 0);
         }
</script>
 
Bur 6 августа 2009, 21:52 #
Разбираться в полотне вашего скрипта не хочется, т.к. вы тоже не пожелали этого делать.
Алгоритм решения задачи прост:
- В процессе мувинга вам известны: ширина блока (divWidth), ширина внешнего контейнера (bodyWidth) и выставляемая left-координата (x).
Далее идут проверки:
if ( x > 0 ) x = 0;
else if ( x <  bodyWidth - divWidth  ) x  = bodyWidth - divWidth ;
div.style.left = x + 'px';

Для y аналогично, только с высотами.
 
pachlava 17 ноября 2009, 14:52 #
Большое спасибо за пример! Очень помог.
P.S. Улучшение, предложенное Frip Nown, можно импрувнуть еще сильнее, если из него сделать функцию такого вида:

function makeFastDrag(div, event)
{
document.onmousemove = function(event){moveHandler(div, event);}
}


и в div'е заменить
onmousemove = 'moveHandler(this, event);'
на
 
onmousemove = 'moveHandler(this, event);'

в этом случае не нужно указывать id для div'а и их может быть много
 
pachlava 17 ноября 2009, 14:54 #
* заменить
onmousemove = 'moveHandler(this, event);'
на
onmousemove="makeFastDrag(this, event);"
 
Bur 17 ноября 2009, 14:58 #
Вы правы в главном, mousemove нужно вешать на document. Cтатья старая, никак не доберусь уточнить этот момент...
 
mishko 24 февраля 2010, 20:28 #
Все вроде работает, но возник такой вопрос: как модифицировать скрипт, чтобы он вместо пикселей воспринимал проценты? Нужны именно проценты..
 
Bur 24 февраля 2010, 22:20 #
Подобные задачи мувинга как правило решают с использованием пикслелей. Если начальное положение блока задается через top-left в процентах можно воспользоваться функцией определения координат элемента на странице.
 
mishko 25 февраля 2010, 00:20 #
Просто на странице присутствует несколько блоков, которые необходимо двигать. 2 из них, которые расположены в левой части великолепно позиционируются и в пикселях. Но те, что справа, так просто не расположить..Хотелось бы чтобы правый блок был на заданном расстоянии от правого края окна. Если использовать position: absolute; right: ... - то элемент почему-то двигается только по вертикали. нужно чтобы и при разрешении 1600px и 1024 div был на заданном расстоянии от правого края.
 
Bur 25 февраля 2010, 00:55 #
Для right скрипт необходимо модифицировать повсеместно заменив left на right. Так и не понял причем тут проценты :-)
 
mishko 25 февраля 2010, 20:06 #
Да, с процентами я что-то корявое хотел сказать)
Все разобрался!
Огромное спасибо за помощь! ;)
 
mamas 9 июля 2010, 20:08 #
Уважаемый Александр! Не подскажете ли мне, как новичку в JavaScript, один момент? Если в вашем коде вместо блока <div></div> использовать тег с картинкой, то картинку можно перемещать таким же образом, как и черный квадрат в примере, но с одним исключением: после нажатия кнопки мыши и перемещении, картинка двигается на несколько пикселов и замирает. Если же после этого отпустить кнопку мыши, то картинка двигается как надо, и еще один щелчек и отпускание фиксируют ее на новом месте. То есть чтобы сдвинуть картинку надо нажать-отпустить-протащить-нажать-отпустить, вместо нажать-протащить-отпустить, как с черным квадратом. Хотя тег с картинкой и стилем элементарный, приведу его на всякий случай.

<img src="1.jpg" style='position:absolute; top:300; left:200'
onmousedown = 'initMove(this, event)'
onmouseup = 'moveState = false'
onmousemove = 'moveHandler(this, event)'>
 
Bur 18 июля 2010, 21:50 #
В каком браузере наблюдается артефакт? В ИЕ? Если да, добавьте атрибут ondragstart="return false;".
 
 
Rambler's Top100 Flede HTML valid CSS valid