renovations

This commit is contained in:
Ilya Kantor 2015-02-15 11:23:36 +03:00
parent fbc63034a4
commit 5fb0e36f13
79 changed files with 686 additions and 766 deletions

View file

@ -18,7 +18,7 @@
<div id="field"> <div id="field">
<img src="//js.cx/clipart/ball.gif" id="ball"> <img src="//js.cx/clipart/ball.svg" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div> </div>

View file

@ -33,7 +33,7 @@ ball.style.top = Math.round(field.clientHeight/2 - ball.offsetHeight/2)+'px';
Код выше стабильно работать не будет, потому что `IMG` идет без ширины/высоты: Код выше стабильно работать не будет, потому что `IMG` идет без ширины/высоты:
```html ```html
<img src="ball.gif" id="ball"> <img src="ball.svg" id="ball">
``` ```
**Высота и ширина изображения неизвестны браузеру до тех пор, пока оно не загрузится, если размер не указан явно.** **Высота и ширина изображения неизвестны браузеру до тех пор, пока оно не загрузится, если размер не указан явно.**
@ -43,7 +43,7 @@ ball.style.top = Math.round(field.clientHeight/2 - ball.offsetHeight/2)+'px';
Чтобы это исправить, добавим `width/height` к картинке: Чтобы это исправить, добавим `width/height` к картинке:
```html ```html
<img src="ball.gif" *!*width="40" height="40"*/!* id="ball"> <img src="ball.svg" *!*width="40" height="40"*/!* id="ball">
``` ```
Теперь браузер всегда знает ширину и высоту, так что все работает. Тот же эффект дало бы указание размеров в CSS. Теперь браузер всегда знает ширину и высоту, так что все работает. Тот же эффект дало бы указание размеров в CSS.

View file

@ -18,7 +18,7 @@
<div id="field"> <div id="field">
<img src="//js.cx/clipart/ball.gif" width="40" height="40" id="ball"> <img src="//js.cx/clipart/ball.svg" width="40" height="40" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div> </div>

View file

@ -18,7 +18,7 @@
<div id="field"> <div id="field">
<img src="//js.cx/clipart/ball.gif" id="ball"> <img src="//js.cx/clipart/ball.svg" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div> </div>

View file

@ -35,7 +35,7 @@
<div id="field"> <div id="field">
<img src="//js.cx/clipart/ball.gif" id="ball"> <img src="//js.cx/clipart/ball.svg" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div> </div>

View file

@ -28,7 +28,7 @@
<div id="field"> <div id="field">
<img src="//js.cx/clipart/ball.gif" id="ball"> <img src="//js.cx/clipart/ball.svg" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div> </div>

View file

@ -4,16 +4,15 @@
В браузере нет способа "просто получить" текущие координаты. Это может сделать обработчик события, в данном случае `mousemove`. Поэтому нужно будет поставить обработчик на `mousemove` и при каждом движении запоминать текущие координаты, чтобы `setInterval` мог раз в 100мс сравнивать их. В браузере нет способа "просто получить" текущие координаты. Это может сделать обработчик события, в данном случае `mousemove`. Поэтому нужно будет поставить обработчик на `mousemove` и при каждом движении запоминать текущие координаты, чтобы `setInterval` мог раз в 100мс сравнивать их.
Можно обойтись и без `setInterval` -- сравнивать координаты при каждом срабатывании `mousemove`. Если передвинулись на маленькое расстояние с последнего `mousemove` -- это "наведение на элемент", а на большое -- игнорируем. Вариант с `setInterval` теоретически надёжнее, но на практике и один `mousemove` работает. Можно обойтись и без `setInterval` -- сравнивать координаты при каждом срабатывании `mousemove`. Если передвинулись на маленькое расстояние с последнего `mousemove` -- это "наведение на элемент", а на большое -- игнорируем. Вариант с `setInterval` лучше с точки зрения производительности -- `mousemove` происходит уж очень часто, но если проверка несложная, то и `mousemove` подойдёт.
Чтобы наш код не срабатывал чересчур часто, мы будем начинать анализ координат при заходе на элемент, а заканчивать -- при выходе с него. Имеет смысл начинать анализ координат и отслеживание `mousemove` при заходе на элемент, а заканчивать -- при выходе с него.
Если выход осуществлён, и при этом на элементе зафиксировано "состояние наведения", то нужно вызвать соответствующий обработчик `options.out`.
Чтобы точно отловить момент входа и выхода, без учёта подэлементов (во избежание мигания), можно использовать `mouseenter/mouseleave`. Чтобы точно отловить момент входа и выхода, без учёта подэлементов (во избежание мигания), можно использовать `mouseenter/mouseleave`.
В решении, предложенном ниже, однако, используется `mouseover/mouseout`, так как это позволит легко "прикрутить" к такому объекту делегирование, если потребуется. А, чтобы не было лишних срабатываний, лишние переходы отфильтровываются. В решении, предложенном ниже, однако, используется `mouseover/mouseout`, так как это позволит легко "прикрутить" к такому объекту делегирование, если потребуется. А, чтобы не было лишних срабатываний, лишние переходы отфильтровываются.
При этом при обнаружении "наведения на элемент" это запоминается в переменной `isHover` и вызывается `options.over`, а затем, при выходе с элемента, если было "наведение", вызывается `options.out`.

View file

@ -42,7 +42,7 @@ new HoverIntent({
}); });
``` ```
Демо: Демо этого кода:
[iframe src="solution" height=110] [iframe src="solution" height=110]

View file

@ -20,16 +20,14 @@
Для `mouseout` всё наоборот: Для `mouseout` всё наоборот:
<ul> <ul>
<li>`event.target` -- элемент, с которого ушла мышь, то есть на котором возникло событие.</li> <li>`event.target` -- элемент, с которого ушла мышь, то есть на котором возникло событие.</li>
<li>`event.relatedTarget` -- элемент, на который перешла мышь.</li> <li>`event.relatedTarget` -- элемент, на который перешла мышь.</li>
</ul> </ul>
[online] В примере ниже, если у вас есть мышь, вы можете наглядно посмотреть события `mouseover/out`, возникающие на всех элементах.
В примере ниже вы можете наглядно посмотреть события `mouseover/out`, возникающие на всех элементах.
[codetabs src="mouseoverout" height=220] [codetabs src="mouseoverout" height=220]
[/online]
[warn header="`relatedTarget` может быть `null`"] [warn header="`relatedTarget` может быть `null`"]
Свойство `relatedTarget` может быть равно `null`. Свойство `relatedTarget` может быть равно `null`.

View file

@ -0,0 +1,6 @@
Как можно видеть из HTML/CSS, слайдер -- это `DIV`, подкрашенный фоном/градиентом, внутри которого находится другой `DIV`, оформленный как бегунок, с `position:relative`.
Бегунок немного поднят, и вылезает по высоте из родителя.
На этой основе мы реализуем горизонтальный Drag'n'Drop, ограниченный по ширине. Его особенность -- в `position:relative` у переносимого элемента, т.е. координата ставится не абсолютная, а относительно родителя.

View file

@ -2,29 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="lib.js"></script> <link rel="stylesheet" href="style.css">
<style>
.slider {
border-radius: 5px;
background: #E0E0E0;
background: -moz-linear-gradient(left top , #E0E0E0, #EEEEEE) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, left top, right bottom, from(#E0E0E0), to(#EEEEEE));
background: linear-gradient(left top, #E0E0E0, #EEEEEE);
width: 310px;
height: 15px;
margin: 5px;
}
.thumb {
width: 10px;
height: 25px;
border-radius: 3px;
position: relative;
left: 10px;
top: -5px;
background: blue;
cursor: pointer;
}
</style>
</head> </head>
<body> <body>
@ -37,9 +15,7 @@
var sliderElem = document.getElementById('slider'); var sliderElem = document.getElementById('slider');
var thumbElem = sliderElem.children[0]; var thumbElem = sliderElem.children[0];
thumbElem.ondragstart = function() { return false; };
thumbElem.onmousedown = function(e) { thumbElem.onmousedown = function(e) {
e = fixEvent(e);
var thumbCoords = getCoords(thumbElem); var thumbCoords = getCoords(thumbElem);
var shiftX = e.pageX - thumbCoords.left; var shiftX = e.pageX - thumbCoords.left;
// shiftY здесь не нужен, слайдер двигается только по горизонтали // shiftY здесь не нужен, слайдер двигается только по горизонтали
@ -47,8 +23,6 @@ thumbElem.onmousedown = function(e) {
var sliderCoords = getCoords(sliderElem); var sliderCoords = getCoords(sliderElem);
document.onmousemove = function(e) { document.onmousemove = function(e) {
e = fixEvent(e);
// вычесть координату родителя, т.к. position: relative // вычесть координату родителя, т.к. position: relative
var newLeft = e.pageX - shiftX - sliderCoords.left; var newLeft = e.pageX - shiftX - sliderCoords.left;
@ -71,9 +45,21 @@ thumbElem.onmousedown = function(e) {
return false; // disable selection start (cursor change) return false; // disable selection start (cursor change)
}; };
thumbElem.ondragstart = function() {
return false;
};
function getCoords(elem) { // кроме IE8-
var box = elem.getBoundingClientRect();
return {
top: box.top + pageYOffset,
left: box.left + pageXOffset
};
}
</script> </script>
</body> </body>
</html> </html>

View file

@ -0,0 +1,22 @@
.slider {
border-radius: 5px;
background: #E0E0E0;
background: -moz-linear-gradient(left top , #E0E0E0, #EEEEEE) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, left top, right bottom, from(#E0E0E0), to(#EEEEEE));
background: linear-gradient(left top, #E0E0E0, #EEEEEE);
width: 310px;
height: 15px;
margin: 5px;
}
.thumb {
width: 10px;
height: 25px;
border-radius: 3px;
position: relative;
left: 10px;
top: -5px;
background: blue;
cursor: pointer;
}

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="slider" class="slider">
<div class="thumb"></div>
</div>
<script>
// ...Ваш код..
</script>
</body>
</html>

View file

@ -0,0 +1,22 @@
.slider {
border-radius: 5px;
background: #E0E0E0;
background: -moz-linear-gradient(left top , #E0E0E0, #EEEEEE) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, left top, right bottom, from(#E0E0E0), to(#EEEEEE));
background: linear-gradient(left top, #E0E0E0, #EEEEEE);
width: 310px;
height: 15px;
margin: 5px;
}
.thumb {
width: 10px;
height: 25px;
border-radius: 3px;
position: relative;
left: 10px;
top: -5px;
background: blue;
cursor: pointer;
}

View file

@ -7,13 +7,8 @@
Захватите мышкой синий бегунок и двигайте его, чтобы увидеть в работе. Захватите мышкой синий бегунок и двигайте его, чтобы увидеть в работе.
Позже к этому слайдеру можно будет добавить дополнительные функции по чтению/установке значения.
([getCoords](#getCoords) -- в lib.js).
Важно: Важно:
<ul> <ul>
<li>Слайдер должен нормально работать при резком движении мыши влево или вправо, за пределы полосы. При этом бегунок должен останавливаться четко в нужном конце полосы.</li> <li>Слайдер должен нормально работать при резком движении мыши влево или вправо, за пределы полосы. При этом бегунок должен останавливаться четко в нужном конце полосы.</li>
<li>Курсор при передвижении слайдера должен быть рукой(`hand`) или крестиком(`move`).</li> <li>При нажатом бегунке мышь может выходить за пределы полосы слайдера, но слайдер пусть все равно работает (это удобно для пользователя).</li>
<li>При нажатом бегунке мышь может выходить за пределы полосы слайдера, но слайдер пусть все равно работает (удобство для пользователя).</li>
</ul> </ul>

View file

@ -0,0 +1,5 @@
В решении этой задачи для переноса мы используем координаты относительно окна и `position:fixed`. Так проще.
А по окончании -- прибавляем прокрутку и делаем `position:absolute`, чтобы элемент был привязан к определённому месту в документе, а не в окне. Можно было и сразу `position:absolute` и оперировать в абсолютных координатах, но код был бы немного длиннее.
Детали решения расписаны в комментариях в исходном коде.

View file

@ -8,9 +8,9 @@
<h2>Расставьте супергероев по полю.</h2> <h2>Расставьте супергероев по полю.</h2>
<p>Супергерои -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.</p> <p>Супергерои и мяч -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.</p>
<p>Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Конечно, можно прокрутить и клавиатурой, но так -- удобнее. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.</p> <p>Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.</p>
<p>Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.</p> <p>Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.</p>
@ -23,15 +23,12 @@
<div class="hero draggable" id="hero3"></div> <div class="hero draggable" id="hero3"></div>
<div class="hero draggable" id="hero4"></div> <div class="hero draggable" id="hero4"></div>
<div class="hero draggable" id="hero5"></div> <div class="hero draggable" id="hero5"></div>
<div id="winnie" class="draggable"></div> <div class="hero draggable" id="hero6"></div>
<img src="//js.cx/drag-heroes/ball.png" class="draggable"> <img src="//js.cx/drag-heroes/ball.png" class="draggable">
<div style="clear:both"></div> <div style="clear:both"></div>
<script src="//js.cx/libs/getCoords.js"></script>
<script src="//js.cx/libs/documentScroll.js"></script>
<script src="soccer.js"></script> <script src="soccer.js"></script>
</body> </body>
</html> </html>

View file

@ -10,9 +10,10 @@ html, body {
float: left; float: left;
} }
/* герои и мяч (переносимые элементы) */
.hero { .hero {
background: url(//js.cx/drag-heroes/heroes.png); background: url(//js.cx/drag-heroes/heroes.png);
width: 105px; width: 130px;
height: 128px; height: 128px;
float: left; float: left;
} }
@ -26,33 +27,21 @@ html, body {
} }
#hero3 { #hero3 {
background-position: -131px 0; background-position: -120px 0;
} }
#hero4 { #hero4 {
background-position: -131px -128px; background-position: -125px -128px;
} }
#hero5 { #hero5 {
background-position: -236px 0; background-position: -248px -128px;
width: 130px;
} }
#winnie { #hero6 {
background: url(//js.cx/drag-heroes/winnie.png); background-position: -244px 0;
width: 115px;
height: 128px;
float: left;
} }
.draggable { .draggable {
cursor: pointer; cursor: pointer;
} }
.dragging {
z-index: 1000;
position: absolute;
cursor: move;
}

View file

@ -0,0 +1,103 @@
document.onmousedown = function(e) {
var dragElement = e.target;
if (!dragElement.classList.contains('draggable')) return;
var coords, shiftX, shiftY;
startDrag(e.clientX, e.clientY);
document.onmousemove = function(e) {
moveAt(e.clientX, e.clientY);
};
dragElement.onmouseup = function() {
finishDrag();
};
// -------------------------
function startDrag(clientX, clientY) {
shiftX = clientX - dragElement.getBoundingClientRect().left;
shiftY = clientY - dragElement.getBoundingClientRect().top;
dragElement.style.position = 'fixed';
document.body.appendChild(dragElement);
moveAt(clientX, clientY);
};
function finishDrag() {
// конец переноса, перейти от fixed к absolute-координатам
dragElement.style.top = parseInt(dragElement.style.top) + pageYOffset + 'px';
dragElement.style.position = 'absolute';
document.onmousemove = null;
dragElement.onmouseup = null;
}
function moveAt(clientX, clientY) {
// новые координаты
var newX = clientX - shiftX;
var newY = clientY - shiftY;
// ------- обработаем вынос за нижнюю границу окна ------
// новая нижняя граница элемента
var newBottom = newY + dragElement.offsetHeight;
// если новая нижняя граница вылезает вовне окна - проскроллим его
if (newBottom > document.documentElement.clientHeight) {
// координата нижней границы документа относительно окна
var docBottom = document.documentElement.getBoundingClientRect().bottom;
// scrollBy, если его не ограничить - может заскроллить за текущую границу документа
// обычно скроллим на 10px
// но если расстояние от newBottom до docBottom меньше, то меньше
var scrollY = Math.min(docBottom - newBottom, 10);
// ошибки округления при полностью прокрученной странице
// могут привести к отрицательному scrollY, что будет означать прокрутку вверх
// поправим эту ошибку
if (scrollY < 0) scrollY = 0;
window.scrollBy(0, scrollY);
// резким движением мыши элемент можно сдвинуть сильно вниз
// если он вышел за нижнюю границу документа -
// передвигаем на максимально возможную нижнюю позицию внутри документа
newY = Math.min(newY, document.documentElement.clientHeight - dragElement.offsetHeight);
}
// ------- обработаем вынос за верхнюю границу окна ------
if (newY < 0) {
// проскроллим вверх на 10px, либо меньше, если мы и так в самом верху
var scrollY = Math.min(-newY, 10);
if (scrollY < 0) scrollY = 0; // поправим ошибку округления
window.scrollBy(0, -scrollY);
// при резком движении мыши элемент мог "вылететь" сильно вверх, поправим его
newY = Math.max(newY, 0);
}
// зажать в границах экрана по горизонтали
// здесь прокрутки нет, всё просто
if (newX < 0) newX = 0;
if (newX > document.documentElement.clientWidth - dragElement.offsetHeight) {
newX = document.documentElement.clientWidth - dragElement.offsetHeight;
}
dragElement.style.left = newX + 'px';
dragElement.style.top = newY + 'px';
}
// отменим действие по умолчанию на mousedown (выделение текста, оно лишнее)
return false;
}

View file

@ -8,13 +8,12 @@
<h2>Расставьте супергероев по полю.</h2> <h2>Расставьте супергероев по полю.</h2>
<p>Супергерои -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.</p> <p>Супергерои и мяч -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.</p>
<p>Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Конечно, можно прокрутить и клавиатурой, но так -- удобнее. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.</p> <p>Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.</p>
<p>Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.</p> <p>Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.</p>
<div id="field"> <div id="field">
</div> </div>
@ -24,15 +23,12 @@
<div class="hero draggable" id="hero3"></div> <div class="hero draggable" id="hero3"></div>
<div class="hero draggable" id="hero4"></div> <div class="hero draggable" id="hero4"></div>
<div class="hero draggable" id="hero5"></div> <div class="hero draggable" id="hero5"></div>
<div id="winnie" class="draggable"></div> <div class="hero draggable" id="hero6"></div>
<img src="//js.cx/drag-heroes/ball.png" class="draggable"> <img src="//js.cx/drag-heroes/ball.png" class="draggable">
<div style="clear:both"></div> <div style="clear:both"></div>
<script src="//js.cx/libs/getCoords.js"></script>
<script src="//js.cx/libs/documentScroll.js"></script>
<script src="soccer.js"></script> <script src="soccer.js"></script>
</body> </body>
</html> </html>

View file

@ -10,9 +10,10 @@ html, body {
float: left; float: left;
} }
/* герои и мяч (переносимые элементы) */
.hero { .hero {
background: url(//js.cx/drag-heroes/heroes.png); background: url(//js.cx/drag-heroes/heroes.png);
width: 105px; width: 130px;
height: 128px; height: 128px;
float: left; float: left;
} }
@ -26,33 +27,21 @@ html, body {
} }
#hero3 { #hero3 {
background-position: -131px 0; background-position: -120px 0;
} }
#hero4 { #hero4 {
background-position: -131px -128px; background-position: -125px -128px;
} }
#hero5 { #hero5 {
background-position: -236px 0; background-position: -248px -128px;
width: 130px;
} }
#winnie { #hero6 {
background: url(//js.cx/drag-heroes/winnie.png); background-position: -244px 0;
width: 115px;
height: 128px;
float: left;
} }
.draggable { .draggable {
cursor: pointer; cursor: pointer;
} }
.dragging {
z-index: 1000;
position: absolute;
cursor: move;
}

View file

@ -0,0 +1 @@
// Ваш код

View file

@ -0,0 +1,20 @@
# Расставить супергероев по полю
[importance 5]
В этой задаче вы можете проверить своё понимание сразу нескольких аспектов Drag'n'Drop.
Сделайте так, чтобы элементы с классом `draggable` можно было переносить мышкой. По окончании переноса элемент остаётся на том месте в документе, где его положили.
Требования к реализации:
<ul>
<li>Должен быть 1 обработчик на `document`, использующий делегирование.</li>
<li>Если элементы подносят к вертикальным краям окна -- оно должно прокручиваться вниз/вверх.</li>
<li>Горизонтальной прокрутки в этой задаче не существует.</li>
<li>Элемент при переносе, даже при резких движениях мышкой, не должен попасть вне окна.</li>
</ul>
Футбольное поле в этой задач слишком большое, чтобы показывать его здесь, поэтому откройте его, кликнув по ссылке ниже. Там же и подробное описание задачи (осторожно, винни-пух и супергерои!).
[demo src="solution"]

View file

@ -5,48 +5,51 @@ Drag'n'Drop -- это возможность захватить мышью эл
Перенос мышкой может заменить целую последовательность кликов. И, самое главное, он упрощает внешний вид интерфейса: функции, реализуемые через Drag'n'Drop, в ином случае потребовали бы дополнительных полей, виджетов и т.п. Перенос мышкой может заменить целую последовательность кликов. И, самое главное, он упрощает внешний вид интерфейса: функции, реализуемые через Drag'n'Drop, в ином случае потребовали бы дополнительных полей, виджетов и т.п.
[cut] [cut]
## Отличия от HTML5 Drag'n'Drop ## Отличия от HTML5 Drag'n'Drop
В современном стандарте HTML5 есть поддержка Drag'n'Drop при помощи [специальных событий](http://www.html5rocks.com/en/tutorials/dnd/basics/). Эти события поддерживаются всеми браузерами, в мелочах отстаёт от них IE. В современном стандарте HTML5 есть поддержка Drag'n'Drop при помощи [специальных событий](http://www.html5rocks.com/en/tutorials/dnd/basics/).
У них есть своя область применения, например можно перетащить файл в браузер, но здесь сосредоточимся на реализации техник Drag'n'Drop в более широком смысле, для более обширного класса задач. Эти события поддерживаются всеми современными браузерами, и у них есть свои интересные особенности, например, можно перетащить файл в браузер, так что JS получит доступ к его содержимому. Они заслуживают отдельного рассмотрения.
**Далее речь пойдет о реализации Drag'n'Drop при помощи событий мыши.** Но в плане именно Drag'n'Drop у них есть существенные ограничения. Например, нельзя организовать перенос "только по горизонтали" или "только по вертикали". Также нельзя ограничить перенос внутри заданной зоны. Есть и другие интерфейсные задачи, которые такими встроенными событиями нереализуемы.
**Изложенные методы применяются в элементах управления для обработки любых действий вида "захватить - потянуть - отпустить".** Поэтому здесь мы будем рассматривать Drag'n'Drop при помощи событий мыши.
## Основная логика Drag'n'Drop Рассматриваемые приёмы, вообще говоря, применяются не только в Drag'n'Drop, но и для любых интерфейсных взаимодействий вида "захватить - потянуть - отпустить".
Для организации Drag'n'Drop нужно: ## Алгоритм Drag'n'Drop
Основной алгоритм Drag'n'Drop выглядит так:
<ol> <ol>
<li>При помощи события `mousedown` отследить нажатие кнопки на переносимом элементе.</li> <li>Отслеживаем нажатие кнопки мыши на переносимом элементе при помощи события `mousedown`.</li>
<li>При нажатии -- подготовить элемент к перемещению: обычно ему назначается `position:absolute` и ставятся координаты `left/top` по координатам курсора.</li> <li>При нажатии -- подготовить элемент к перемещению.</li>
<li>Далее отслеживаем движение мыши через <code>mousemove</code> и передвигаем переносимый элемент на новые координаты путём смены `left/top`.</li> <li>Далее отслеживаем движение мыши через <code>mousemove</code> и передвигаем переносимый элемент на новые координаты путём смены `left/top` и `position:absolute`.</li>
<li>При отпускании кнопки мыши, то есть наступлении события <code>mouseup</code> -- остановить перенос элемента и произвести все действия, связанные с окончанием Drag'n'Drop.</li> <li>При отпускании кнопки мыши, то есть наступлении события <code>mouseup</code> -- остановить перенос элемента и произвести все действия, связанные с окончанием Drag'n'Drop.</li>
</ol> </ol>
В следующем примере эти шаги реализованы для переноса мяча: В следующем примере эти шаги реализованы для переноса мяча:
```js ```js
//+ autorun
var ball = document.getElementById('ball'); var ball = document.getElementById('ball');
ball.onmousedown = function(e) { // 1. отследить нажатие*!* ball.onmousedown = function(e) { // 1. отследить нажатие*!*
var self = this;
// подготовить к перемещению // подготовить к перемещению
// 2. разместить на том же месте, но в абсолютных координатах*!* // 2. разместить на том же месте, но в абсолютных координатах*!*
this.style.position = 'absolute'; ball.style.position = 'absolute';
moveAt(e); moveAt(e);
// переместим в body, чтобы мяч был точно не внутри position:relative // переместим в body, чтобы мяч был точно не внутри position:relative
document.body.appendChild(this); document.body.appendChild(ball);
this.style.zIndex = 1000; // показывать мяч над другими элементами ball.style.zIndex = 1000; // показывать мяч над другими элементами
// передвинуть мяч под координаты курсора // передвинуть мяч под координаты курсора
// и сдвинуть на половину ширины/высоты для центрирования
function moveAt(e) { function moveAt(e) {
self.style.left = e.pageX-20+'px'; // 20 - половина ширины/высоты мяча ball.style.left = e.pageX - ball.offsetWidth/2 + 'px';
self.style.top = e.pageY-20+'px'; ball.style.top = e.pageY - ball.offsetHeight/2 + 'px';
} }
// 3, перемещать по экрану*!* // 3, перемещать по экрану*!*
@ -55,27 +58,24 @@ ball.onmousedown = function(e) { // 1. отследить нажатие*!*
} }
// 4. отследить окончание переноса *!* // 4. отследить окончание переноса *!*
this.onmouseup = function() { ball.onmouseup = function() {
document.onmousemove = self.onmouseup = null; document.onmousemove = null;
ball.onmouseup = null;
} }
} }
``` ```
В действии: Если запустить этот код на картинке `#ball`, то мы заметим нечто странное. При начале переноса мяч "раздваивается" и переносится не сам мяч, а его "клон".
<div style="height:80px">
Кликните по мячу и тяните, чтобы двигать его.
<img src="//js.cx/clipart/ball.gif" style="cursor:pointer" width="40" height="40" id="ball">
</div>
**Попробуйте этот пример. Он не совсем работает, мячик "раздваивается".** [online]
В действии (внутри ифрейма):
Сейчас мы это исправим. [iframe src="ball" height=230]
[/online]
## Отмена переноса браузера Это потому, что браузер имеет свой собственный Drag'n'Drop, который автоматически запускается и вступает в конфликт с нашим. Это происходит именно для картинок и некоторых других элементов.
При нажатии мышью на `<img>` браузер начинает выполнять свой собственный, встроенный Drag'n'Drop, который и портит наш перенос. Его нужно отключить:
Чтобы браузер не вмешивался, нужно отменить действие по умолчанию для события `dragstart`:
```js ```js
ball.ondragstart = function() { ball.ondragstart = function() {
@ -83,78 +83,45 @@ ball.ondragstart = function() {
}; };
``` ```
Исправленный пример: Теперь всё будет в порядке.
```js [online]
//+ autorun В действии (внутри ифрейма):
var ball = document.getElementById('ball2');
ball.onmousedown = function(e) { [iframe src="ball2" height=230]
var self = this; [/online]
this.style.position = 'absolute'; Ещё одна особенность правильного Drag'd'Drop -- событие `mousemove` отслеживается на `document`, а не на `ball`.
moveAt(e);
document.body.appendChild(this);
this.style.zIndex = 1000;
function moveAt(e) {
self.style.left = e.pageX-20+'px';
self.style.top = e.pageY-20+'px';
}
document.onmousemove = function(e) {
moveAt(e);
};
this.onmouseup = function() {
document.onmousemove = self.onmouseup = null;
};
}
*!*
ball.ondragstart = function() {
return false;
};
*/!*
```
В действии:
<div style="height:80px">
Кликните по мячу и тяните, чтобы двигать его.
<img src="//js.cx/clipart/ball.gif" style="cursor:pointer" width="40" height="40" id="ball2">
</div>
[smart header="Обработчик `mousemove` ставим на `document`"]
**Почему событие `mousemove` в примере отслеживается на `document`, а не на `ball`?**
С первого взгляда кажется, что мышь всегда над мячом и обработчик `mousemove` можно повесить на сам мяч, а не на документ. С первого взгляда кажется, что мышь всегда над мячом и обработчик `mousemove` можно повесить на сам мяч, а не на документ.
Однако, на самом деле **мышь во время переноса не всегда над мячом**. Вспомните, браузер регистрирует `mousemove` часто, но не для каждого пикселя. Однако, на самом деле мышь во время переноса не всегда над мячом.
Вспомним, событие `mousemove` возникает хоть и часто, но не для каждого пикселя. Быстрое движение курсора вызовет `mousemove` уже не над мячом, а, например, в дальнем конце страницы.
Быстрое движение курсора вызовет `mousemove` уже не над мячом, а, например, в дальнем конце страницы.
Вот почему мы должны отслеживать `mousemove` на всём `document`. Вот почему мы должны отслеживать `mousemove` на всём `document`.
[/smart]
## Правильное позиционирование ## Правильное позиционирование
В примерах выше мяч позиционируется в центре под курсором мыши: В примерах выше мяч позиционируется в центре под курсором мыши:
```js ```js
self.style.left = e.pageX - 20 + 'px'; self.style.left = e.pageX - ball.offsetWidth/2 + 'px';
self.style.top = e.pageY - 20 + 'px'; self.style.top = e.pageY - ball.offsetHeight/2 + 'px';
``` ```
Число `20` здесь -- половина длины мячика. Оно использовано здесь потому, что если поставить `left/top` ровно в `pageX/pageY`, то мячик прилипнет верхним-левым углом к курсору мыши. Будет некрасиво. Если поставить `left/top` ровно в `pageX/pageY`, то мячик прилипнет верхним-левым углом к курсору мыши. Будет некрасиво. Поэтому мы сдвигаем его на половину высоты/ширины, чтобы был центром под мышью. Уже лучше.
**Для правильного переноса необходимо, чтобы изначальный сдвиг курсора относительно элемента сохранялся: где захватили, за ту "часть элемента" и переносим.** Но не идеально. В частности, в самом начале переноса, особенно если мячик "взят" за край -- он резко "прыгает" центром под курсор мыши.
**Для правильного переноса необходимо, чтобы изначальный сдвиг курсора относительно элемента сохранялся.**
Где захватили, за ту "часть элемента" и переносим:
<img src="ball_shift.png"> <img src="ball_shift.png">
<ul> <ol>
<li>Когда человек нажимает на мячик `mousedown` -- курсор сдвинут относительно левого-верхнего угла мяча на расстояние `shiftX/shiftY`. И мы хотели бы сохранить этот сдвиг. <li>Когда человек нажимает на мячик `mousedown` -- курсор сдвинут относительно левого-верхнего угла мяча на расстояние, которое мы обозначим `shiftX/shiftY`. И нужно при переносе сохранить этот сдвиг.
Получить значения `shiftX/shiftY` легко: достаточно вычесть из координат курсора `pageX/pageY` левую-верхнюю границу мячика, полученную при помощи функции [getCoords](#getCoords). Получить значения `shiftX/shiftY` легко: достаточно вычесть из координат курсора `pageX/pageY` левую-верхнюю границу мячика, полученную при помощи функции [getCoords](#getCoords).
@ -173,48 +140,45 @@ shiftY = e.pageY - getCoords(ball).top;
```js ```js
// onmousemove // onmousemove
self.style.left = e.pageX - *!*shiftX*/!* + 'px'; ball.style.left = e.pageX - *!*shiftX*/!* + 'px';
self.style.top = e.pageY - *!*shiftY*/!* + 'px'; ball.style.top = e.pageY - *!*shiftY*/!* + 'px';
``` ```
</li> </li>
</ul> </ol>
**Пример с правильным позиционированием:** Итоговый код с правильным позиционированием:
В этом примере позиционирование осуществляется не на `20px`, а с учётом изначального сдвига.
```js ```js
//+ autorun var ball = document.getElementById('ball');
var ball = document.getElementById('ball3');
ball.onmousedown = function(e) { ball.onmousedown = function(e) {
var self = this;
var coords = getCoords(this); var coords = getCoords(ball);
*!* *!*
var shiftX = e.pageX - coords.left; var shiftX = e.pageX - coords.left;
var shiftY = e.pageY - coords.top; var shiftY = e.pageY - coords.top;
*/!* */!*
this.style.position = 'absolute'; ball.style.position = 'absolute';
document.body.appendChild(this); document.body.appendChild(ball);
moveAt(e); moveAt(e);
this.style.zIndex = 1000; // над другими элементами ball.style.zIndex = 1000; // над другими элементами
function moveAt(e) { function moveAt(e) {
self.style.left = e.pageX - *!*shiftX*/!* + 'px'; ball.style.left = e.pageX - *!*shiftX*/!* + 'px';
self.style.top = e.pageY - *!*shiftY*/!* + 'px'; ball.style.top = e.pageY - *!*shiftY*/!* + 'px';
} }
document.onmousemove = function(e) { document.onmousemove = function(e) {
moveAt(e); moveAt(e);
}; };
this.onmouseup = function() { ball.onmouseup = function() {
document.onmousemove = self.onmouseup = null; document.onmousemove = null;
ball.onmouseup = null;
}; };
} }
@ -224,12 +188,11 @@ ball.ondragstart = function() {
}; };
``` ```
В действии: [online]
В действии (внутри ифрейма):
<div style="height:80px"> [iframe src="ball3" height=230]
Кликните по мячу и тяните, чтобы двигать его. [/online]
<img src="//js.cx/clipart/ball.gif" style="cursor:pointer" width="40" height="40" id="ball3">
</div>
Различие особенно заметно, если захватить мяч за правый-нижний угол. В предыдущем примере мячик "прыгнет" серединой под курсор, в этом -- будет плавно переноситься с текущей позиции. Различие особенно заметно, если захватить мяч за правый-нижний угол. В предыдущем примере мячик "прыгнет" серединой под курсор, в этом -- будет плавно переноситься с текущей позиции.
@ -240,7 +203,7 @@ ball.ondragstart = function() {
Его компоненты: Его компоненты:
<ol> <ol>
<li>События `mousedown` -> `document.mousemove` -> `mouseup`.</li> <li>События `ball.mousedown` -> `document.mousemove` -> `ball.mouseup`.</li>
<li>Передвижение с учётом изначального сдвига `shiftX/shiftY`.</li> <li>Передвижение с учётом изначального сдвига `shiftX/shiftY`.</li>
<li>Отмена действия браузера по событию `dragstart`.</li> <li>Отмена действия браузера по событию `dragstart`.</li>
</ol> </ol>
@ -250,10 +213,7 @@ ball.ondragstart = function() {
<ul> <ul>
<li>При `mouseup` можно обработать окончание переноса, произвести изменения в данных, если они нужны.</li> <li>При `mouseup` можно обработать окончание переноса, произвести изменения в данных, если они нужны.</li>
<li>Во время самого переноса можно подсвечивать элементы, над которыми проходит элемент.</li> <li>Во время самого переноса можно подсвечивать элементы, над которыми проходит элемент.</li>
<li>При обработке событий `mousedown` и `mouseup` можно использовать делегирование, так что одного обработчика достаточно для управления переносом в зоне с сотнями элементов.
</ul> </ul>
Это и многое другое мы рассмотрим в статье про [Drag'n'Drop объектов](/drag-and-drop-objects). Это и многое другое мы рассмотрим в статье про [Drag'n'Drop объектов](/drag-and-drop-objects).
[libs]
getCoords.js
[/libs]

View file

@ -0,0 +1,44 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body style="height: 200px">
<p>Кликните по мячу и тяните, чтобы двигать его.</p>
<img src="//js.cx/clipart/ball.svg" style="cursor:pointer" width="40" height="40" id="ball">
<script>
var ball = document.getElementById('ball');
ball.onmousedown = function(e) {
ball.style.position = 'absolute';
moveAt(e);
document.body.appendChild(ball);
ball.style.zIndex = 1000; // показывать мяч над другими элементами
function moveAt(e) {
ball.style.left = e.pageX - ball.offsetWidth/2 + 'px';
ball.style.top = e.pageY - ball.offsetHeight/2 + 'px';
}
document.onmousemove = function(e) {
moveAt(e);
};
ball.onmouseup = function() {
document.onmousemove = null;
ball.onmouseup = null;
};
}
</script>
</body>
</html>

View file

@ -0,0 +1,49 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body style="height: 200px">
<p>Кликните по мячу и тяните, чтобы двигать его.</p>
<img src="//js.cx/clipart/ball.svg" style="cursor:pointer" width="40" height="40" id="ball">
<script>
var ball = document.getElementById('ball');
ball.onmousedown = function(e) {
ball.style.position = 'absolute';
moveAt(e);
document.body.appendChild(ball);
ball.style.zIndex = 1000; // показывать мяч над другими элементами
function moveAt(e) {
ball.style.left = e.pageX - ball.offsetWidth/2 + 'px';
ball.style.top = e.pageY - ball.offsetHeight/2 + 'px';
}
document.onmousemove = function(e) {
moveAt(e);
};
ball.onmouseup = function() {
document.onmousemove = null;
ball.onmouseup = null;
};
}
ball.ondragstart = function() {
return false;
};
</script>
</body>
</html>

View file

@ -0,0 +1,63 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body style="height: 200px">
<p>Кликните по мячу и тяните, чтобы двигать его.</p>
<img src="//js.cx/clipart/ball.svg" style="cursor:pointer" width="40" height="40" id="ball">
<script>
var ball = document.getElementById('ball');
ball.onmousedown = function(e) {
var coords = getCoords(ball);
var shiftX = e.pageX - coords.left;
var shiftY = e.pageY - coords.top;
ball.style.position = 'absolute';
document.body.appendChild(ball);
moveAt(e);
ball.style.zIndex = 1000; // над другими элементами
function moveAt(e) {
ball.style.left = e.pageX - shiftX + 'px';
ball.style.top = e.pageY - shiftY + 'px';
}
document.onmousemove = function(e) {
moveAt(e);
};
ball.onmouseup = function() {
document.onmousemove = null;
ball.onmouseup = null;
};
}
ball.ondragstart = function() {
return false;
};
function getCoords(elem) { // кроме IE8-
var box = elem.getBoundingClientRect();
return {
top: box.top + pageYOffset,
left: box.left + pageXOffset
};
}
</script>
</body>
</html>

View file

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

View file

@ -1,34 +1,40 @@
# Мышь: Drag'n'Drop объектов # Мышь: Drag'n'Drop более глубоко
В [предыдущей статье](/drag-and-drop) мы рассмотрели основы Drag'n'Drop. Здесь мы построим на этой основе фреймворк, предназначенный для переноса *объектов* -- элементов списка, узлов дерева и т.п. В [предыдущей статье](/drag-and-drop) мы рассмотрели основы Drag'n'Drop. Здесь мы разберём дополнительные "тонкие места" и приёмы реализации, которые возникают на практике.
Почти все javascript-библиотеки реализуют Drag'n'Drop так, как написано (хотя бывает что и менее эффективно).
Зная, что и как, вы сможете легко написать свой код переноса или поправить, адаптировать существующую библиотеку под себя.
Этот материал не строго обязателен для изучения, он специфичен именно для Drag'n'Drop.
Почти все javascript-библиотеки реализуют Drag'n'Drop так, как написано (хотя бывает что и менее эффективно ;)). Зная, что и как, вы сможете легко написать свой код переноса или поправить, адаптировать существующую библиотеку под себя.
[cut] [cut]
## Документ ## Документ
Как пример задачи -- возьмём документ с иконками браузера ("объекты переноса"), которые можно переносить в компьютер ("цель переноса"): Как пример задачи -- возьмём документ с иконками браузера ("объекты переноса"), которые можно переносить в компьютер ("цель переноса"):
<ul> <ul>
<li>Элементы, которые можно переносить (иконки браузеров), помечены атрибутом `draggable`.</li> <li>Элементы, которые можно переносить (иконки браузеров), помечены классом `draggable`.</li>
<li>Элементы, на которые можно положить (компьютер), имеют атрибут `droppable`.</li> <li>Элементы, на которые можно положить (компьютер), имеют класс `droppable`.</li>
</ul> </ul>
```html ```html
<img src="chrome.png" *!*draggable*/!*> <img src="chrome.png" class="*!*draggable*/!*">
<img src="firefox.png" *!*draggable*/!*> <img src="firefox.png" class="*!*draggable*/!*">
<img src="ie.png" *!*draggable*/!*> <img src="ie.png" class="*!*draggable*/!*">
<img src="opera.png" *!*draggable*/!*> <img src="opera.png" class="*!*draggable*/!*">
<img src="safari.png" *!*draggable*/!*> <img src="safari.png" class="*!*draggable*/!*">
<p>Браузер переносить сюда:</p> <p>Браузер переносить сюда:</p>
<img src="computer.gif" *!*droppable*/!*> <img src="computer.gif" class="*!*droppable*/!*">
``` ```
Работающий пример с переносом.
Работающий пример с переносом:
[iframe border=1 src="dragDemo" height=250 link edit] [iframe border=1 src="dragDemo" height=280 link edit]
Далее мы рассмотрим, как делается фреймворк для таких переносов, а в перспективе -- и для более сложных. Далее мы рассмотрим, как делается фреймворк для таких переносов, а в перспективе -- и для более сложных.
@ -41,17 +47,15 @@
## Начало переноса ## Начало переноса
Чтобы начать перенос элемента, мы должны отловить нажатие левой кнопки мыши на нём. Для этого используем событие `mousedown`. Чтобы начать перенос элемента, мы должны отловить нажатие левой кнопки мыши на нём. Для этого используем событие `mousedown`... И, конечно, делегирование.
..И здесь нас ждёт первая особенность переноса объектов. На что ставить обработчик? Переносимых элементов может быть много. В нашем документе-примере это всего лишь несколько иконок, но если мы хотим переносить элементы списка или дерева, то их может быть 100 штук и более.
**Переносимых элементов может быть много.** В нашем документе-примере это всего лишь несколько иконок, но если мы хотим переносить элементы списка или дерева, то их может быть 100 штук и более. Поэтому повесим обработчик `mousedown` на контейнер, который содержит переносимые элементы, и будем определять нужный элемент поиском ближайшего `draggable` вверх по иерархии от `event.target`.
Назначать обработчик на каждый DOM-элемент -- нецелесообразно. Проще всего решить эту задачу делегированием. В качестве контейнера здесь будем брать `document`, хотя это может быть и любой элемент.
**Повесим обработчик `mousedown` на контейнер, который содержит переносимые элементы,** и будем определять нужный элемент поиском ближайшего `draggable` вверх по иерархии от `event.target`. В качестве контейнера здесь и далее будем брать `document`. Найденный `draggable`-элемент сохраним в свойстве `dragObject.elem` и начнём двигать.
Найденный переносимый объект сохраним в переменной `dragObject` и начнём двигать.
Код обработчика `mousedown`: Код обработчика `mousedown`:
@ -60,14 +64,13 @@ var dragObject = {};
document.onmousedown = function(e) { document.onmousedown = function(e) {
if (e.which != 1) { if (e.which != 1) { // если клик правой кнопкой мыши
return; // нажатие правой кнопкой не запускает перенос return; // то он не запускает перенос
} }
// найти ближайший draggable, пройдя по цепочке родителей target var elem = e.target.closest('.draggable');
var elem = findDraggable(e.target, document);
if (!elem) return; // не нашли, клик вне draggable объекта if (!elem) return; // не нашли, клик вне draggable-объекта
// запомнить переносимый объект // запомнить переносимый объект
dragObject.elem = elem; dragObject.elem = elem;
@ -78,33 +81,23 @@ document.onmousedown = function(e){
} }
``` ```
В обработчике используется функция `findDraggable` для поиска переносимого элемента по `event.target`: [warn header="Не начинаем перенос по `mousedown`"]
Ранее мы по `mousedown` начинали перенос.
```js Но на самом деле нажатие на элемент вовсе не означает, что его собираются куда-то двигать. Возможно, на нём просто кликают.
function findDraggable(event) {
var elem = event.target;
// найти ближайший draggable, пройдя по цепочке родителей target Это важное различие. Снимать элемент со своего места и куда-то двигать нужно только при переносе.
while(elem != document && elem.getAttribute('draggable') == null) {
elem = elem.parentNode;
}
return elem == document ? null : elem;
}
```
**Здесь мы пока не начинаем перенос, потому что нажатие на элемент вовсе не означает, что его собираются куда-то двигать.** Возможно, на нём просто кликают. Чтобы отличить перенос от клика, в том числе -- от клика, который сопровождается нечаянным перемещением на пару пикселей (рука дрогнула), мы будем запоминать в `dragObject`, какой элемент (`elem`) и где (`downX/downY`) был зажат, а начало переноса будем инициировать из `mousemove`, если он передвинут хотя бы на несколько пикселей.
[/warn]
Это важное различие. Нужно отличать перенос от клика, в том числе -- от клика, который сопровождается нечаянным перемещением на пару пикселей (рука дрогнула).
Иначе при клике элемент будет сниматься со своего места, и потом тут же возвращаться обратно (никуда не положили). Это лишняя работа и, вообще, выглядит некрасиво.
**Поэтому в `mousedown` мы запоминаем, какой элемент и где был зажат, а начало переноса будем инициировать из `mousemove`,** если он передвинут хотя бы на несколько пикселей.
## Перенос элемента ## Перенос элемента
Первой задачей обработчика `mousemove` является инициировать начало переноса, если элемент передвинули в зажатом состоянии. Первой задачей обработчика `mousemove` является инициировать начало переноса, если элемент передвинули в зажатом состоянии.
Ну а второй задачей -- отображать его перенос при каждом передвижении мыши. Схематично, обработчик имеет такой вид: Ну а второй задачей -- отображать его перенос при каждом передвижении мыши.
Схематично, обработчик будет иметь такой вид:
```js ```js
document.onmousemove = function(e) { document.onmousemove = function(e) {
@ -118,13 +111,13 @@ document.onmousemove = function(e) {
} }
``` ```
Здесь мы видим новое свойство `dragObject.avatar`. При начале переноса *аватар* делается из элемента и сохраняется в свойство `dragObject.avatar`. Здесь мы видим новое свойство `dragObject.avatar`. При начале переноса "аватар" делается из элемента и сохраняется в свойство `dragObject.avatar`.
***"Аватар"* -- это DOM-элемент, который перемещается по экрану.** **"Аватар" -- это DOM-элемент, который перемещается по экрану.**
Почему бы не перемещать по экрану сам `draggable`-элемент? Зачем, вообще, нужен аватар? Почему бы не перемещать по экрану сам `draggable`-элемент? Зачем, вообще, нужен аватар?
Дело в том, что иногда сам элемент передвигать неудобно, например он слишком большой. А удобно создать некоторое визуальное представление элемента, и его уже переносить. Аватар дает такую возможность. Дело в том, что иногда сам элемент передвигать неудобно, например потому, что он слишком большой. А удобно создать некоторое визуальное представление элемента, и его уже переносить. Аватар дает такую возможность.
А в простейшем случае аватаром можно будет сделать сам элемент, и это не повлечёт дополнительных расходов. А в простейшем случае аватаром можно будет сделать сам элемент, и это не повлечёт дополнительных расходов.
@ -154,7 +147,7 @@ avatar.style.left = новая координата + 'px';
avatar.style.top = новая координата + 'px'; avatar.style.top = новая координата + 'px';
``` ```
**Как вычислять новые координаты `left/top` при переносе?** Как вычислять новые координаты `left/top` при переносе?
Чтобы элемент сохранял свою позицию под курсором, необходимо при нажатии запомнить его изначальный сдвиг относительно курсора, и сохранять его при переносе. Чтобы элемент сохранял свою позицию под курсором, необходимо при нажатии запомнить его изначальный сдвиг относительно курсора, и сохранять его при переносе.
@ -169,7 +162,7 @@ avatar.style.left = e.pageX - shiftX + 'px';
avatar.style.top = e.pageY - shiftY + 'px'; avatar.style.top = e.pageY - shiftY + 'px';
``` ```
### Полный код mousemove ## Полный код mousemove
Код `mousemove`, решающий задачу начала переноса и позиционирования аватара: Код `mousemove`, решающий задачу начала переноса и позиционирования аватара:
@ -218,7 +211,6 @@ document.onmousemove = function(e) {
В нашем случае для отмены переноса нужно запомнить старую позицию элемента и его родителя. В нашем случае для отмены переноса нужно запомнить старую позицию элемента и его родителя.
```js ```js
//+ hide="Раскрыть код createAvatar"
function createAvatar(e) { function createAvatar(e) {
// запомнить старые свойства, чтобы вернуться к ним при отмене переноса // запомнить старые свойства, чтобы вернуться к ним при отмене переноса
@ -245,7 +237,7 @@ function createAvatar(e) {
} }
``` ```
Функция `startDrag(e)` инициирует начало переноса и позиционирует аватар на странице. Функция `startDrag(e)`, которую вызывает `mousemove`, если видит, что элемент в "зажатом" состоянии перенесли достаточно далеко, инициирует начало переноса и позиционирует аватар на странице:
```js ```js
function startDrag(e) { function startDrag(e) {
@ -257,7 +249,7 @@ function startDrag(e) {
} }
``` ```
### Окончание переноса ## Окончание переноса
Окончание переноса происходит по событию `mouseup`. Окончание переноса происходит по событию `mouseup`.
@ -306,18 +298,16 @@ function finishDrag(e) {
```js ```js
// возвратит ближайший droppable или null // возвратит ближайший droppable или null
// *!*предварительный вариант findDroppable, исправлен ниже!*/!* *!*
// это предварительный вариант findDroppable, исправлен ниже!
*/!*
function findDroppable(event) { function findDroppable(event) {
// взять элемент на данных координатах // взять элемент на данных координатах
var elem = document.elementFromPoint(event.clientX, event.clientY); var elem = document.elementFromPoint(event.clientX, event.clientY);
// пройти вверх по цепочке родителей в поисках droppable // найти ближайший сверху droppable
while(elem != document && elem.getAttribute('droppable') == null) { return elem.closest('.droppable');
elem = elem.parentNode;
}
return elem == document ? null : elem;
} }
``` ```
@ -325,67 +315,48 @@ function findDroppable(event) {
Вариант выше -- предварительный. Он не будет работать. Если попробовать применить эту функцию, будет все время возвращать один и тот же элемент! А именно -- *текущий переносимый*. Почему так? Вариант выше -- предварительный. Он не будет работать. Если попробовать применить эту функцию, будет все время возвращать один и тот же элемент! А именно -- *текущий переносимый*. Почему так?
..Дело в том, что в процессе переноса под мышкой находится именно аватар. При начале переноса ему даже `z-index` ставится большой, чтобы он был поверх всех остальных. ...Дело в том, что в процессе переноса под мышкой находится именно аватар. При начале переноса ему даже `z-index` ставится большой, чтобы он был поверх всех остальных.
**Аватар перекрывает остальные элементы. Поэтому функция `document.elementFromPoint()` увидит на текущих координатах именно его.** **Аватар перекрывает остальные элементы. Поэтому функция `document.elementFromPoint()` увидит на текущих координатах именно его.**
Чтобы это изменить, нужно либо поправить код переноса, чтобы аватар двигался *рядом* с курсором мыши, либо поступить проще: Чтобы это изменить, нужно либо поправить код переноса, чтобы аватар двигался *рядом* с курсором мыши, либо дать аватару стиль `pointer-events:none` (кроме IE10-), либо:
<ol><li>Спрятать аватар.</li> <ol><li>Спрятать аватар.</li>
<li>Вызывать `elementFromPoint`.</li> <li>Вызывать `elementFromPoint`.</li>
<li>Показать аватар.</li></ol> <li>Показать аватар.</li></ol>
Напишем вспомогательную функцию `getElementUnderClientXY(elem, clientX, clientY)`, которая это делает: Напишем функцию `findDroppable(event)`, которая это делает:
```js
/* получить элемент на координатах clientX/clientY, под elem */
function getElementUnderClientXY(elem, clientX, clientY) {
// сохранить старый display и спрятать переносимый элемент
var display = elem.style.display || '';
elem.style.display = 'none';
// получить самый вложенный элемент под курсором мыши
var target = document.elementFromPoint(clientX, clientY);
// показать переносимый элемент обратно
elem.style.display = display;
if (!target || target == document) { // такое бывает при выносе за границы окна
target = document.body; // поправить значение, чтобы был именно элемент
}
return target;
}
```
Правильный код `findDroppable`:
```js ```js
function findDroppable(event) { function findDroppable(event) {
var elem = getElementUnderClientXY(dragObject.avatar, event.clientX, event.clientY); // спрячем переносимый элемент
dragObject.avatar.hidden = true;
while(elem != document && elem.getAttribute('droppable') == null) { // получить самый вложенный элемент под курсором мыши
elem = elem.parentNode; var elem = document.elementFromPoint(event.clientX, event.clientY);
// показать переносимый элемент обратно
dragObject.avatar.hidden = false;
if (elem == null) {
// такое возможно, если курсор мыши "вылетел" за границу окна
return null;
} }
return elem == document ? null : elem; return target.closest('.droppable');
} }
``` ```
## Сводим части фреймворка вместе ## DragManager
Сейчас в нашем распоряжении находятся основные фрагменты кода и решения всех технических подзадач. Из фрагментов кода, разобранных выше, можно собрать мини-фреймворк.
Приведем их в нормальный вид. Объект `DragManager` будет запоминать текущий переносимый объект и отслеживать его перенос.
### dragManager
Перенос будет координироваться единым объектом. Назовем его `dragManager`.
Для его создания используем не обычный синтаксис `{...}`, а вызов `new function`. Это позволит прямо при создании объявить дополнительные переменные и функции в замыкании, которыми могут пользоваться методы объекта, а также назначить обработчики: Для его создания используем не обычный синтаксис `{...}`, а вызов `new function`. Это позволит прямо при создании объявить дополнительные переменные и функции в замыкании, которыми могут пользоваться методы объекта, а также назначить обработчики:
```js ```js
var dragManager = new function() { var DragManager = new function() {
var dragObject = {}; var dragObject = {};
@ -404,11 +375,13 @@ var dragManager = new function() {
} }
``` ```
Всю работу будут выполнять обработчики `onMouse*`, которые оформлены как локальные функции. В данном случае они ставятся на `document` через `on...`, но вы легко можете поменять это на `addEventListener/attachEvent`. Всю работу будут выполнять обработчики `onMouse*`, которые оформлены как локальные функции. В данном случае они ставятся на `document` через `on...`, но это легко поменять это на `addEventListener`.
Код функция `onMouse*` мы подробно рассмотрели ранее, но вы сможете увидеть их в полном примере ниже.
Внутренний объект `dragObject` будет содержать информацию об объекте переноса. Внутренний объект `dragObject` будет содержать информацию об объекте переноса.
У него будут следующие свойства: У него будут следующие свойства, которые также разобраны выше:
<dl> <dl>
<dt>`elem`</dt> <dt>`elem`</dt>
<dd>Текущий зажатый мышью объект, если есть (ставится в `mousedown`).</dd> <dd>Текущий зажатый мышью объект, если есть (ставится в `mousedown`).</dd>
@ -420,63 +393,56 @@ var dragManager = new function() {
<dd>Относительный сдвиг курсора от угла элемента, вспомогательное свойство вычисляется в начале переноса.</dd> <dd>Относительный сдвиг курсора от угла элемента, вспомогательное свойство вычисляется в начале переноса.</dd>
</dl> </dl>
**Задачей `dragManager` является общее управление переносом.** Для обработки его окончания вызываются методы `onDrag*`, которые назначаются внешним кодом. Задачей `DragManager` является общее управление переносом. Что же касается действий при его окончании -- их должен назначить внешний код, который использует `DragManager`.
Разработчик, подключив `dragManager`, описывает в этих методах, что делать при начале и завершении переноса. Можно сделать это через вспомогательные методы `onDrag*`, которые устанавливаются внешним кодом и затем вызываются фреймворком. Разработчик, подключив `DragManager`, описывает в этих методах, что делать при начале и завершении переноса. Конечно же, можно заменить методы `onDrag*` на генерацию "своих" событий.
[smart] С использованием `DragManager` пример, с которого начиналась эта глава -- перенос иконок браузеров в компьютер, реализуется совсем просто:
Если в вашем распоряжении есть современный JavaScript-фреймворк, то можно заменить вызовы методов `onDrag*` на генерацию соответствующих событий.
[/smart]
### Реализация переноса иконок
С использованием этого фреймворка перенос иконок браузеров в компьютер реализуется просто:
```js ```js
dragManager.onDragEnd = function(dragObject, dropElem) { DragManager.onDragEnd = function(dragObject, dropElem) {
// скрыть/удалить переносимый объект
dragObject.elem.hidden = true;
// успешный перенос, показать улыбку классом computer-smile // успешный перенос, показать улыбку классом computer-smile
dropElem.className = 'computer computer-smile'; dropElem.className = 'computer computer-smile';
// скрыть переносимый объект
dragObject.elem.style.display = 'none';
// убрать улыбку через 0.2 сек // убрать улыбку через 0.2 сек
setTimeout(function() { dropElem.className = 'computer'; }, 200); setTimeout(function() {
dropElem.classList.remove('computer-smile';
}, 200);
}; };
dragManager.onDragCancel = function(dragObject) { DragManager.onDragCancel = function(dragObject) {
// откат переноса // откат переноса
dragObject.avatar.rollback(); dragObject.avatar.rollback();
}; };
``` ```
Результат: Полный пример с кодом:
[codetabs src="dragDemo" height=280]
[iframe border=1 src="dragDemo" height=250 link edit]
## Варианты расширения этого кода ## Расширения
Существует масса возможных применений Drag'n'Drop. Реализациях всех из них здесь превратила бы довольно простой фреймворк в страшнейшего монстра. Существует масса возможных применений Drag'n'Drop. Здесь мы не будем реализовывать их все, поскольку не стоит цель создать фреймворк-монстр.
Поэтому мы разберем варианты расширений и их реализации, чтобы вы, при необходимости, легко могли написать то, что нужно. Однако, мы рассмотрим их, чтобы, при необходимости, легко было написать то, что нужно.
### Захватывать элемент можно только за "ручку" ### Захватывать элемент можно только за "ручку"
Функция `createAvatar(e)` может быть модифицирована, чтобы захватывать элемент можно было, только ухватившись за определенную зону. Часто бывает, что перенос должен быть инициирован только при захвате за определённую зону элемента. К примеру, модальное окно можно "взять", только захватив его за заголовок.
Например, окно чата можно "взять", только захватив его за заголовок. Для этого достаточно добавить необходимую проверку, к примеру, в функцию `createAvatar` или перед её запуском.
Для этого достаточно проверять по `e.target`, куда, всё же, нажал посетитель. Если взять элемент на данных координатах нельзя, то `createAvatar` может вернуть `false`, и перенос не будет начат. Если `mousedown` был внутри элемента, помеченного, к примеру, классом `draghandle`, то начинаем перенос, иначе -- нет.
### Проверка прав положить на droppable ### Проверка прав на droppable
Бывает так, что не на любое место в `droppable` можно положить элемент. Бывает и так, что не на любое место в `droppable` можно положить элемент.
Само решение "можно или нет" может зависеть как от переносимого элемента (или "аватара" как его полномочного представителя), так и от конкретного места в `droppable`, над которым посетитель отпустил кнопку мыши.
Например: в админке есть дерево всех объектов сайта: статей, разделов, посетителей и т.п. Например: в админке есть дерево всех объектов сайта: статей, разделов, посетителей и т.п.
@ -486,31 +452,19 @@ dragManager.onDragCancel = function(dragObject) {
<li>Узел "статья" (draggable) можно переносить в "раздел" (droppable), а узел "пользователи" -- нельзя. Но и то и другое можно поместить в "корзину".</li> <li>Узел "статья" (draggable) можно переносить в "раздел" (droppable), а узел "пользователи" -- нельзя. Но и то и другое можно поместить в "корзину".</li>
</ul> </ul>
Эта задача решается добавлением проверки в `findDroppable(e)`. Эта функция знает и об аватаре и о событии. При попытке положить в "неправильное" место функция `findDroppable(e)` должна возвращать `null`. Здесь решение: "можно или нет" переносить или нельзя зависит от "типа" переносимого объекта.
Рассмотрим эту задачу поглубже, так как она встречается часто. Есть и более сложные варианты, когда решение зависит от конкретного места в `droppable`, над которым посетитель отпустил кнопку мыши. К примеру, переносить в верхнюю часть можно, а в нижнюю -- нет.
**Есть две наиболее частые причины, по которым аватар нельзя положить на потенциальный `droppable`.** Эта задача решается добавлением проверки в `findDroppable(e)`. Эта функция знает и об аватаре и о событии, включая координаты. При попытке положить в "неправильное" место функция `findDroppable(e)` должна возвращать `null`.
Первую мы уже рассмотрели: "несовпадение типов". Это как раз тот случай, когда "узлы-разделы" дерева админки могут принимать только "статьи", и не могут -- "посетителей". Однако, на практике бывают ситуации, когда решение "прямо сейчас" принять невозможно. Например, нужно сделать запрос на сервер: "А разрешено ли текущему посетителю производить такую операцию?"
В этом случае мы можем проверить это в `findDroppable`, так как вся необходимая информация о типах у нас есть. Как при этом должен вести себя интерфейс? Можно, конечно сделать, чтобы элемент после отпускания кнопки мыши "завис" над `droppable`, ожидая ответа. Однако, такое решение неудобно в реализации и странновато выглядит для посетителя.
Вторая причина -- посложнее. Как правило, применяют "оптимистичный" алгоритм, по которому мы считаем, что перенос обычно успешен, но при необходимости можем отменить его.
**Может не хватать прав на такое действие с `draggable/droppable`, в рамках наложенных на посетителя ограничений.** При нём посетитель кладет объект туда, куда он хочет, а затем, в коде `onDragEnd`:
Например, человек не может положить статью именно в данный раздел.
А возможно, объект переноса удален из базы администратором, и браузер об этом (пока) не знает -- мы должны корректно обрабатывать все случаи, включая этот.
Здесь сложность в том, что **окончательное решение знает только сервер**. Значит, нужно сделать запрос. А элемент после отпускания мыши не может "зависнуть" над элементом в ожидании ответа, нужно его куда-то положить.
**Как правило, применяют "оптимистичный" сценарий, по которому мы считаем, что перенос обычно успешен.**
При нём посетитель кладет объект туда, куда он хочет.
Затем, в коде `onDragEnd`:
<ol> <ol>
<li>Визуально обрабатывается завершение переноса, как будто все ок.</li> <li>Визуально обрабатывается завершение переноса, как будто все ок.</li>
<li>Производится асинхронный запрос к серверу, содержащий информацию о переносе.</li> <li>Производится асинхронный запрос к серверу, содержащий информацию о переносе.</li>
@ -518,93 +472,58 @@ dragManager.onDragCancel = function(dragObject) {
<li>Если нет -- выводится ошибка и возвращается `avatar.rollback()`. Аватар в этом случае должен предусматривать возможность отката после успешного завершения.</li> <li>Если нет -- выводится ошибка и возвращается `avatar.rollback()`. Аватар в этом случае должен предусматривать возможность отката после успешного завершения.</li>
</ol> </ol>
**Процесс общения с сервером сопровождается индикацией загрузки и, при необходимости, блокировкой новых операций переноса до получения подтверждения.** Процесс общения с сервером сопровождается индикацией загрузки и, при необходимости, блокировкой новых операций переноса до получения подтверждения.
</dd>
</dl>
### Индикация переноса ### Подсветка текущего droppable
Можно добавить методы `onDragEnter/onDragMove/onDragLeave` для интеграции с внешним кодом, которые вызываются при заходе (`onDragEnter`), при каждом передвижении (`onDragMove`), и при выходе из `droppable` (`onDragLeave`). Удобно, когда пользователь во время переноса наглядно видит, куда он сейчас положит draggable. Например, текущий droppable (или его часть) подсвечиваются.
При этом бывает, что нужно поддерживать перенос *в элемент*, но и перенос *между элементами*. Разберем два варианта такой ситуации: Для этого в `DragManager` можно добавить дополнительные методы интеграции с внешним кодом:
<ul>
<li>`onDragEnter` -- будет вызываться при заходе на `droppable`, из `onMouseMove`.</li>
<li>`onDragMove` -- при каждом передвижении внутри `droppable`, из `onMouseMove`.</li>
<li>`onDragLeave` -- при выходе с `droppable`, из `onMouseMove` и `onMouseUp`.</li>
</ul>
<dl> Возможен более сложный вариант, когда нужно поддерживать не только перенос *в элемент*, но и перенос *между элементами*, например вставку одной статьи между двумя другими.
<dt>Поддержка трех видов переноса: "между", "под", "над" `droppable`</dt>
<dd>Этот сценарий встречается, когда можно вставить статью как *в* существующий "узел-раздел" дерева, так и *между* разделами.
В этом случае `droppable` делится на 3 части по высоте: 25% - 50% - 25%, берутся координаты элемента и определяется попадание координаты события на нужную часть. Для этого код, который обрабатывает перенос, может "делить на части" droppable, к примеру, в отношении 25% - 50% - 25%, и смотреть:
В параметры `dragObject` добавляется флаг, обозначающий, куда по отношению к найденному `droppable` происходит перенос. <ul>
</dd> <li>Если перенос в верхнюю четверть, то это -- "над".</li>
<dt>Поддержка переноса только "между"</dt> <li>Если перенос в середину, то это "внутрь".</li>
<dd>Здесь есть два варианта. <li>Если перенос в нижнюю четверть, то это -- "под".</li>
<ol> </ul>
<li>Во-первых, можно разделить `droppable` в отношении 50%/50% и по координатам смотреть, куда мы попали.</li>
<li>В некоторых случаях, например при вставке между `droppable`-элементами списка, можно считать, что *элемент вставляется перед тем `LI`, над которым проходит*.
А чтобы вставить после последнего -- нужно перетащить аватар на сам DOM-элемент списка, но не на существующий `droppable`, а в пустое место, которое оставляется внизу для этой цели.</li> Текущий `droppable` и позиция относительно него при этом могут помечаться подсветкой и жирной чертой над/под, если требуется.
</ol>
</dd>
</dl>
Индикацию "переноса между" удобнее всего делать либо раздвижением элементов, либо показом полосы-индикатора <code>border-top/border-bottom</code>, как показано на рисунке ниже:
Пример индикации из Firefox:
<img src="between.png"> <img src="between.png">
### Анимация отмены переноса ### Анимация отмены переноса
Отмену переноса и возврат аватара на место можно красиво анимировать. Отмену переноса и возврат аватара на место можно красиво анимировать.
Один из частых вариантов -- скольжение объекта обратно к исходному месту, откуда его взяли. Для этого достаточно поправить `avatar.revert()` соотвествующим образом. Один из частых вариантов -- скольжение объекта обратно к исходному месту, откуда его взяли. Для этого достаточно поправить `avatar.rollback()`.
## Итого ## Итого
Алгоритм Drag'n'Drop: Уточнённый алгоритм Drag'n'Drop:
<ol> <ol>
<li>При `mousedown` запомнить координаты нажатия.</li> <li>При `mousedown` запомнить координаты нажатия.</li>
<li>При `mousemove` инициировать перенос, как только зажатый элемент передвинули на 3 пикселя или больше. <li>При `mousemove` инициировать перенос, как только зажатый элемент передвинули на 3 пикселя или больше. Сообщить во внешний код вызовом `onDragStart`.</li>
<ol> <ol>
<li>Создать аватар, если можно начать перенос с этой точки `draggable`.</li> <li>Создать аватар, если можно начать перенос с этой точки `draggable`.</li>
<li>Перемещать его по экрану. Новые координаты ставить по `e.pageX/pageY` с учетом изначального сдвига элемента относительно курсора.</li> <li>Перемещать его по экрану, новые координаты ставить по `e.pageX/pageY` с учетом изначального сдвига элемента относительно курсора.</li>
<li>Сообщать во внешний код о текущем `droppable` под курсором и позиции над ним вызовами `onDragEnter`, `onDragMove`, `onDragLeave`.</li>
</ol> </ol>
</li> </li>
<li>При `mouseup` обработать завершение переноса. Элемент под аватаром получить по координатам, предварительно спрятав аватар.</li> <li>При `mouseup` обработать завершение переноса. Элемент под аватаром получить по координатам, предварительно спрятав аватар. Сообщить во внешний код вызовом `onDragEnd`.</li>
</ol> </ol>
Получившийся попутно с обсуждением технических моментов Drag'n'Drop фреймворк обладает рядом особенностей: Получившаяся реализация Drag'n'Drop проста, эффективна, изящна.
[compare] Её очень легко поменять или адаптировать под "особые" потребности.
+Он прост. Он действительно очень прост.
+Он полностью кросс-браузерный, не содержит хаков.
+Он позволяет работать с большим количеством потенциальных `draggable`/`droppable`.
+Его легко расширить и поменять.
[/compare]
Вы можете получить файлы и посмотреть итоговое демо [edit src="dragDemo"]в песочнице[/edit].
В зависимости от ваших задач, вы можете либо использовать его как отправную точку, либо реализовать свой.
ООП-вариант фреймворка находится в статье [](/drag-and-drop-plus). ООП-вариант фреймворка находится в статье [](/drag-and-drop-plus).
[head]
<script>
function getElementUnderClientXY(elem, clientX, clientY) {
var display = elem.style.display || '';
elem.style.display = 'none';
var target = document.elementFromPoint(clientX, clientY);
elem.style.display = display;
if (!target || target == document) {
target = document.body;
}
return target;
}
</script>
[/head]
[libs]
getCoords.js
[/libs]

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before After
Before After

View file

@ -1,4 +1,4 @@
var dragManager = new function() { var DragManager = new function() {
/** /**
* составной объект для хранения информации о переносе: * составной объект для хранения информации о переносе:
@ -17,7 +17,7 @@ var dragManager = new function() {
if (e.which != 1) return; if (e.which != 1) return;
var elem = findDraggable(e); var elem = e.target.closest('.draggable');
if (!elem) return; if (!elem) return;
dragObject.elem = elem; dragObject.elem = elem;
@ -118,23 +118,22 @@ var dragManager = new function() {
avatar.style.position = 'absolute'; avatar.style.position = 'absolute';
} }
function findDraggable(event) {
var elem = event.target;
while(elem != document && elem.getAttribute('draggable') == null) {
elem = elem.parentNode;
}
return elem == document ? null : elem;
}
function findDroppable(event) { function findDroppable(event) {
// спрячем переносимый элемент
dragObject.avatar.hidden = true;
var elem = getElementUnderClientXY(dragObject.avatar, event.clientX, event.clientY); // получить самый вложенный элемент под курсором мыши
var elem = document.elementFromPoint(event.clientX, event.clientY);
while(elem != document && elem.getAttribute('droppable') == null) { // показать переносимый элемент обратно
elem = elem.parentNode; dragObject.avatar.hidden = false;
if (elem == null) {
// такое возможно, если курсор мыши "вылетел" за границу окна
return null;
} }
return elem == document ? null : elem; return elem.closest('.droppable');
} }
document.onmousemove = onMouseMove; document.onmousemove = onMouseMove;
@ -145,3 +144,15 @@ var dragManager = new function() {
this.onDragCancel = function(dragObject) { }; this.onDragCancel = function(dragObject) { };
}; };
function getCoords(elem) { // кроме IE8-
var box = elem.getBoundingClientRect();
return {
top: box.top + pageYOffset,
left: box.left + pageXOffset
};
}

View file

@ -9,3 +9,7 @@
.computer-smile { .computer-smile {
background: url(//js.cx/clipart/computer-smile.gif) no-repeat; background: url(//js.cx/clipart/computer-smile.gif) no-repeat;
} }
.draggable {
cursor: pointer;
}

View file

@ -0,0 +1,38 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script src="http://polyfill.webservices.ft.com/v1/polyfill.js?features=Element.prototype.closest"></script>
<script src="DragManager.js"></script>
<link rel="stylesheet" href="dragDemo.css">
<script>
DragManager.onDragCancel = function(dragObject) {
dragObject.avatar.rollback();
};
DragManager.onDragEnd = function(dragObject, dropElem) {
dragObject.elem.style.display = 'none';
dropElem.classList.add('computer-smile');
setTimeout(function() {
dropElem.classList.remove('computer-smile');
}, 200);
};
</script>
</head>
<body>
<div style="height:64px">
<img src="//js.cx/browsers/chrome.svg" class="draggable">
<img src="//js.cx/browsers/firefox.svg" class="draggable">
<img src="//js.cx/browsers/ie.svg" class="draggable">
<img src="//js.cx/browsers/opera.svg" class="draggable">
<img src="//js.cx/browsers/safari.svg" class="draggable">
</div>
<p>Браузер переносить сюда:</p>
<div class="computer droppable">
</div>
</body>
</html>

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

View file

@ -1,46 +0,0 @@
# HTML/CSS, подсказка
Слайдер -- это `DIV`, подкрашенный фоном/градиентом, внутри которого находится другой `DIV`, оформленный как бегунок, с `position:relative`.
Бегунок немного поднят, и вылезает по высоте из родителя.
# HTML/CSS для слайдера
Например, вот так:
```html
<!--+ run -->
<style>
.slider {
border-radius: 5px;
background: #E0E0E0;
background: -moz-linear-gradient(left top , #E0E0E0, #EEEEEE) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, left top, right bottom, from(#E0E0E0), to(#EEEEEE));
background: linear-gradient(left top, #E0E0E0, #EEEEEE);
width: 310px;
height: 15px;
margin: 5px;
}
.thumb {
width: 10px;
height: 25px;
border-radius: 3px;
position: relative;
left: 10px;
top: -5px;
background: blue;
cursor: pointer;
}
</style>
<div class="slider">
<div class="thumb"></div>
</div>
```
Теперь на этом реализуйте перенос бегунка.
# Полное решение
Это горизонтальный Drag'n'Drop, ограниченный по ширине. Его особенность -- в `position:relative` у переносимого элемента, т.е. координата ставится не абсолютная, а относительно родителя.

View file

@ -1,44 +0,0 @@
function fixEvent(e) {
e = e || window.event;
if (!e.target) e.target = e.srcElement;
if (e.pageX == null && e.clientX != null ) { // если нет pageX..
var html = document.documentElement;
var body = document.body;
e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0);
e.pageX -= html.clientLeft || 0;
e.pageY = e.clientY + (html.scrollTop || body && body.scrollTop || 0);
e.pageY -= html.clientTop || 0;
}
if (!e.which && e.button) {
e.which = e.button & 1 ? 1 : ( e.button & 2 ? 3 : ( e.button & 4 ? 2 : 0 ) )
}
return e;
}
function getCoords(elem) {
var box = elem.getBoundingClientRect();
var body = document.body;
var docElem = document.documentElement;
var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
var clientTop = docElem.clientTop || body.clientTop || 0;
var clientLeft = docElem.clientLeft || body.clientLeft || 0;
var top = box.top + scrollTop - clientTop;
var left = box.left + scrollLeft - clientLeft;
return { top: Math.round(top), left: Math.round(left) };
}

View file

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="lib.js"></script>
</head>
<body>
<!-- Ваш HTML/CSS -->
<script>
// ваш код
</script>
</body>
</html>

View file

@ -1,44 +0,0 @@
function fixEvent(e) {
e = e || window.event;
if (!e.target) e.target = e.srcElement;
if (e.pageX == null && e.clientX != null ) { // если нет pageX..
var html = document.documentElement;
var body = document.body;
e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0);
e.pageX -= html.clientLeft || 0;
e.pageY = e.clientY + (html.scrollTop || body && body.scrollTop || 0);
e.pageY -= html.clientTop || 0;
}
if (!e.which && e.button) {
e.which = e.button & 1 ? 1 : ( e.button & 2 ? 3 : ( e.button & 4 ? 2 : 0 ) )
}
return e;
}
function getCoords(elem) {
var box = elem.getBoundingClientRect();
var body = document.body;
var docElem = document.documentElement;
var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
var clientTop = docElem.clientTop || body.clientTop || 0;
var clientLeft = docElem.clientLeft || body.clientLeft || 0;
var top = box.top + scrollTop - clientTop;
var left = box.left + scrollLeft - clientLeft;
return { top: Math.round(top), left: Math.round(left) };
}

View file

@ -1,80 +0,0 @@
document.body.onmousedown = function(e) {
var dragElement = e.target;
if (!dragElement.classList.contains('draggable')) return;
var coords, shiftX, shiftY;
startDrag(e.pageX, e.pageY);
document.onmousemove = function(e) {
moveAt(e.pageX, e.pageY);
};
dragElement.onmouseup = function() {
finishDrag();
};
// -------------------------
function startDrag(pageX, pageY) {
coords = getCoords(dragElement);
shiftX = pageX - coords.left;
shiftY = pageY - coords.top;
dragElement.classList.add("dragging");
dragElement.style.position = 'absolute';
document.body.appendChild(dragElement);
moveAt(pageX, pageY);
};
function finishDrag() {
dragElement.classList.remove('dragging');
document.onmousemove = dragElement.onmouseup = null;
}
function moveAt(pageX, pageY) {
var newX = pageX - shiftX;
var newY = pageY - shiftY;
var newBottom = newY + dragElement.offsetHeight;
var docScroll = getDocumentScroll();
// прокрутить вниз, если нужно
if (newBottom > docScroll.bottom) {
// ...но не за конец документа
var scrollSizeToEnd = docScroll.height - docScroll.bottom;
// scrollBy, если его не ограничить,
// может заскроллить за текущую границу страницы
var toScrollY = Math.min(scrollSizeToEnd, 10);
window.scrollBy(0, toScrollY );
// при необходимости двигаем элемент вверх, чтобы поместился
// метод scrollBy асинхронный, поэтому учитываем будущую прокрутку (+toScrollY)
newY = Math.min(newY, docScroll.bottom - dragElement.offsetHeight + toScrollY);
}
if (newY < docScroll.top) {
var toScrollY = Math.min(docScroll.top, 10);
window.scrollBy(0, -toScrollY );
newY = Math.max(newY, docScroll.top - toScrollY );
}
// зажать в границах экрана по горизонтали
newX = Math.max(newX, 0);
newX = Math.min(newX, document.documentElement.clientWidth - dragElement.offsetHeight);
dragElement.style.left = newX + 'px';
dragElement.style.top = newY + 'px';
}
return false;
}

View file

@ -1,2 +0,0 @@
/* ваш код */

View file

@ -1,13 +0,0 @@
# Расставить супергероев по полю
[importance 5]
Сделайте так, чтобы элементы с классом `draggable` можно было переносить мышкой.
Футбольное поле в этой задач слишком большое, чтобы показывать его здесь, поэтому откройте его, кликнув по ссылке ниже. Там же и подробное описание задачи (осторожно, винни-пух и супергерои!).
[demo src="solution"]
P.S. Для вашего удобства добавлены функции `getCoords` -- для координат и `getDocumentScroll` -- для получения границ видимой области и прокрутки в документе.

View file

@ -13,11 +13,11 @@
<li>А событие `wheel` является чисто "мышиным". Оно генерируется *над любым элементом* при передвижении колеса мыши. При этом не важно, прокручиваемый он или нет. В частности, `overflow:hidden` никак не препятствует обработке колеса мыши.</li> <li>А событие `wheel` является чисто "мышиным". Оно генерируется *над любым элементом* при передвижении колеса мыши. При этом не важно, прокручиваемый он или нет. В частности, `overflow:hidden` никак не препятствует обработке колеса мыши.</li>
</ul> </ul>
Кроме того, событие `onscroll` происходит после прокрутки, а `onwheel` -- до прокрутки, поэтому в нём можно отменить саму прокрутку (действие браузера). Кроме того, событие `onscroll` происходит *после* прокрутки, а `onwheel` -- *до* прокрутки, поэтому в нём можно отменить саму прокрутку (действие браузера).
## Зоопарк wheel в разных браузерах ## Зоопарк wheel в разных браузерах
Событие `wheel` появилось в [стандарте](http://www.w3.org/TR/DOM-Level-3-Events/#event-type-wheel) не так давно. Оно поддерживается IE9+, Firefox 17+. Возможно, другими браузерами на момент чтения этой статьи. Событие `wheel` появилось в [стандарте](http://www.w3.org/TR/DOM-Level-3-Events/#event-type-wheel) не так давно. Оно поддерживается Chrome 31+, IE9+, Firefox 17+.
До него браузеры обрабатывали прокрутку при помощи событий [mousewheel](http://msdn.microsoft.com/en-us/library/ie/ms536951.aspx) (все кроме Firefox) и [DOMMouseScroll](https://developer.mozilla.org/en-US/docs/DOM/DOM_event_reference/DOMMouseScroll), [MozMousePixelScroll](https://developer.mozilla.org/en-US/docs/DOM/DOM_event_reference/MozMousePixelScroll) (только Firefox). До него браузеры обрабатывали прокрутку при помощи событий [mousewheel](http://msdn.microsoft.com/en-us/library/ie/ms536951.aspx) (все кроме Firefox) и [DOMMouseScroll](https://developer.mozilla.org/en-US/docs/DOM/DOM_event_reference/DOMMouseScroll), [MozMousePixelScroll](https://developer.mozilla.org/en-US/docs/DOM/DOM_event_reference/MozMousePixelScroll) (только Firefox).
@ -27,26 +27,24 @@
<dd>Свойство `deltaY` -- количество прокрученных пикселей по горизонтали и вертикали. Существуют также свойства `deltaX` и `deltaZ` для других направлений прокрутки.</dd> <dd>Свойство `deltaY` -- количество прокрученных пикселей по горизонтали и вертикали. Существуют также свойства `deltaX` и `deltaZ` для других направлений прокрутки.</dd>
<dt>`MozMousePixelScroll`</dt> <dt>`MozMousePixelScroll`</dt>
<dd>Срабатывает, начиная с Firefox 3.5, только в Firefox. Даёт возможность отменить прокрутку и получить размер в пикселях через свойство `detail`, ось прокрутки в свойстве `axis`.</dd> <dd>Срабатывает, начиная с Firefox 3.5, только в Firefox. Даёт возможность отменить прокрутку и получить размер в пикселях через свойство `detail`, ось прокрутки в свойстве `axis`.</dd>
<dt>`DOMMouseScroll`</dt>
<dd>Существует в Firefox очень давно, отличается от предыдущего тем, что даёт в `detail` количество строк. Если не нужна поддержка Firefox < 3.5, то не нужно и это событие.</dd>
<dt>`mousewheel`</dd> <dt>`mousewheel`</dd>
<dd>Срабатывает в браузерах, которые ещё не реализовали `wheel`. В свойстве `wheelDelta` -- условный "размер прокрутки", обычно равен `120` для прокрутки вверх и `-120` -- вниз. Он не соответствует какому-либо конкретному количеству пикселей.</dd> <dd>Срабатывает в браузерах, которые ещё не реализовали `wheel`. В свойстве `wheelDelta` -- условный "размер прокрутки", обычно равен `120` для прокрутки вверх и `-120` -- вниз. Он не соответствует какому-либо конкретному количеству пикселей.</dd>
</dl> </dl>
Чтобы кросс-браузерно отловить прокрутку и, при необходимости, отменить её, можно использовать все эти события. Чтобы кросс-браузерно отловить прокрутку и, при необходимости, отменить её, можно использовать все эти события.
Пример: Пример, включающий поддержку IE8-:
```js ```js
if (elem.addEventListener) { if (elem.addEventListener) {
if ('onwheel' in document) { if ('onwheel' in document) {
// IE9+, FF17+ // IE9+, FF17+, Ch31+
elem.addEventListener ("wheel", onWheel, false); elem.addEventListener ("wheel", onWheel, false);
} else if ('onmousewheel' in document) { } else if ('onmousewheel' in document) {
// устаревший вариант события // устаревший вариант события
elem.addEventListener ("mousewheel", onWheel, false); elem.addEventListener ("mousewheel", onWheel, false);
} else { } else {
// 3.5 <= Firefox < 17, более старое событие DOMMouseScroll пропустим // Firefox < 17
elem.addEventListener ("MozMousePixelScroll", onWheel, false); elem.addEventListener ("MozMousePixelScroll", onWheel, false);
} }
} else { // IE<9 } else { // IE<9

View file

@ -1,35 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script src="lib.js"></script>
<script src="DragManager.js"></script>
<link rel="stylesheet" type="text/css" href="dragDemo.css">
<script>
dragManager.onDragCancel = function(dragObject) {
dragObject.avatar.rollback();
};
dragManager.onDragEnd = function(dragObject, dropElem) {
dropElem.className = 'computer computer-smile';
dragObject.elem.style.display = 'none';
setTimeout(function() { dropElem.className = 'computer'; }, 200);
};
</script>
</head>
<body>
<img src="//js.cx/browsers/chrome_64.png" draggable>
<img src="//js.cx/browsers/firefox_64.png" draggable>
<img src="//js.cx/browsers/ie_64.png" draggable>
<img src="//js.cx/browsers/opera_64.png" draggable>
<img src="//js.cx/browsers/safari_64.png" draggable>
<p>Браузер переносить сюда:</p>
<div class="computer" droppable>
</div>
</body>
</html>

View file

@ -1,30 +0,0 @@
function getCoords(elem) {
var box = elem.getBoundingClientRect();
var body = document.body;
var docElem = document.documentElement;
var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
var clientTop = docElem.clientTop || body.clientTop || 0;
var clientLeft = docElem.clientLeft || body.clientLeft || 0;
var top = box.top + scrollTop - clientTop;
var left = box.left + scrollLeft - clientLeft;
return { top: Math.round(top), left: Math.round(left) };
}
function getElementUnderClientXY(elem, clientX, clientY) {
var display = elem.style.display || '';
elem.style.display = 'none';
var target = document.elementFromPoint(clientX, clientY);
elem.style.display = display;
return target;
}

View file

@ -22,7 +22,7 @@
<div id="field"> <div id="field">
<img src="//js.cx/clipart/ball.gif" id="ball"> <img src="//js.cx/clipart/ball.svg" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div> </div>

View file

@ -18,7 +18,7 @@
<div id="field"> <div id="field">
<img src="//js.cx/clipart/ball.gif" id="ball"> <img src="//js.cx/clipart/ball.svg" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div> </div>

View file

@ -18,7 +18,7 @@
<div id="field"> <div id="field">
<img src="//js.cx/clipart/ball.gif" width="40" height="40" id="ball"> <img src="//js.cx/clipart/ball.svg" width="40" height="40" id="ball">
</div> </div>
<script> <script>

View file

@ -13,7 +13,7 @@
<div id="field"> <div id="field">
<img src="//js.cx/clipart/ball.gif" width="40" height="40" id="ball"> <img src="//js.cx/clipart/ball.svg" width="40" height="40" id="ball">
</div> </div>

View file

@ -18,7 +18,7 @@
<div id="field"> <div id="field">
<img src="//js.cx/clipart/ball.gif" width="40" height="40" id="ball"> <img src="//js.cx/clipart/ball.svg" width="40" height="40" id="ball">
</div> </div>
<script> <script>

View file

@ -13,7 +13,7 @@
<div id="field"> <div id="field">
<img src="//js.cx/clipart/ball.gif" width="40" height="40" id="ball"> <img src="//js.cx/clipart/ball.svg" width="40" height="40" id="ball">
</div> </div>

View file

@ -1,6 +1,10 @@
# Применяем ООП: Drag'n'Drop++ # Применяем ООП: Drag'n'Drop++
Эта статья представляет собой продолжение статьи [](/drag-and-drop-objects). Она посвящена более гибкой и расширяемой реализации переноса. Эта статья представляет собой продолжение главы [](/drag-and-drop-objects).
Она посвящена более гибкой и расширяемой реализации переноса.
Рекомендуется прочитать указанную главу перед тем, как двигаться дальше.
[cut] [cut]
В сложных приложениях Drag'n'Drop обладает рядом особенностей: В сложных приложениях Drag'n'Drop обладает рядом особенностей:
<ol> <ol>

View file

@ -1,2 +0,0 @@
#!/bin/bash
mongoexport -d js -c plunks --jsonArray >plunks.json

View file

@ -1,2 +0,0 @@
#!/bin/bash
mongoimport -d js -c plunks --jsonArray --drop <plunks.json

File diff suppressed because one or more lines are too long