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">
<img src="//js.cx/clipart/ball.gif" id="ball">
<img src="//js.cx/clipart/ball.svg" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div>

View file

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

View file

@ -18,7 +18,7 @@
<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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,16 +20,14 @@
Для `mouseout` всё наоборот:
<ul>
<li>`event.target` -- элемент, с которого ушла мышь, то есть на котором возникло событие.</li>
<li>`event.relatedTarget` -- элемент, на который перешла мышь.</li>
</ul>
[online]
В примере ниже вы можете наглядно посмотреть события `mouseover/out`, возникающие на всех элементах.
В примере ниже, если у вас есть мышь, вы можете наглядно посмотреть события `mouseover/out`, возникающие на всех элементах.
[codetabs src="mouseoverout" height=220]
[/online]
[warn header="`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>
<head>
<meta charset="utf-8">
<script src="lib.js"></script>
<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>
<link rel="stylesheet" href="style.css">
</head>
<body>
@ -37,9 +15,7 @@
var sliderElem = document.getElementById('slider');
var thumbElem = sliderElem.children[0];
thumbElem.ondragstart = function() { return false; };
thumbElem.onmousedown = function(e) {
e = fixEvent(e);
var thumbCoords = getCoords(thumbElem);
var shiftX = e.pageX - thumbCoords.left;
// shiftY здесь не нужен, слайдер двигается только по горизонтали
@ -47,8 +23,6 @@ thumbElem.onmousedown = function(e) {
var sliderCoords = getCoords(sliderElem);
document.onmousemove = function(e) {
e = fixEvent(e);
// вычесть координату родителя, т.к. position: relative
var newLeft = e.pageX - shiftX - sliderCoords.left;
@ -71,9 +45,21 @@ thumbElem.onmousedown = function(e) {
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>
</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>
<li>Слайдер должен нормально работать при резком движении мыши влево или вправо, за пределы полосы. При этом бегунок должен останавливаться четко в нужном конце полосы.</li>
<li>Курсор при передвижении слайдера должен быть рукой(`hand`) или крестиком(`move`).</li>
<li>При нажатом бегунке мышь может выходить за пределы полосы слайдера, но слайдер пусть все равно работает (удобство для пользователя).</li>
<li>При нажатом бегунке мышь может выходить за пределы полосы слайдера, но слайдер пусть все равно работает (это удобно для пользователя).</li>
</ul>

View file

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

View file

@ -8,9 +8,9 @@
<h2>Расставьте супергероев по полю.</h2>
<p>Супергерои -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.</p>
<p>Супергерои и мяч -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.</p>
<p>Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Конечно, можно прокрутить и клавиатурой, но так -- удобнее. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.</p>
<p>Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.</p>
<p>Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.</p>
@ -23,15 +23,12 @@
<div class="hero draggable" id="hero3"></div>
<div class="hero draggable" id="hero4"></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">
<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>
</body>
</html>

View file

@ -10,9 +10,10 @@ html, body {
float: left;
}
/* герои и мяч (переносимые элементы) */
.hero {
background: url(//js.cx/drag-heroes/heroes.png);
width: 105px;
width: 130px;
height: 128px;
float: left;
}
@ -26,33 +27,21 @@ html, body {
}
#hero3 {
background-position: -131px 0;
background-position: -120px 0;
}
#hero4 {
background-position: -131px -128px;
background-position: -125px -128px;
}
#hero5 {
background-position: -236px 0;
width: 130px;
background-position: -248px -128px;
}
#winnie {
background: url(//js.cx/drag-heroes/winnie.png);
width: 115px;
height: 128px;
float: left;
#hero6 {
background-position: -244px 0;
}
.draggable {
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>
<p>Супергерои -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.</p>
<p>Супергерои и мяч -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.</p>
<p>Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Конечно, можно прокрутить и клавиатурой, но так -- удобнее. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.</p>
<p>Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.</p>
<p>Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.</p>
<div id="field">
</div>
@ -24,15 +23,12 @@
<div class="hero draggable" id="hero3"></div>
<div class="hero draggable" id="hero4"></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">
<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>
</body>
</html>

View file

@ -10,9 +10,10 @@ html, body {
float: left;
}
/* герои и мяч (переносимые элементы) */
.hero {
background: url(//js.cx/drag-heroes/heroes.png);
width: 105px;
width: 130px;
height: 128px;
float: left;
}
@ -26,33 +27,21 @@ html, body {
}
#hero3 {
background-position: -131px 0;
background-position: -120px 0;
}
#hero4 {
background-position: -131px -128px;
background-position: -125px -128px;
}
#hero5 {
background-position: -236px 0;
width: 130px;
background-position: -248px -128px;
}
#winnie {
background: url(//js.cx/drag-heroes/winnie.png);
width: 115px;
height: 128px;
float: left;
#hero6 {
background-position: -244px 0;
}
.draggable {
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, в ином случае потребовали бы дополнительных полей, виджетов и т.п.
[cut]
## Отличия от 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>
<li>При помощи события `mousedown` отследить нажатие кнопки на переносимом элементе.</li>
<li>При нажатии -- подготовить элемент к перемещению: обычно ему назначается `position:absolute` и ставятся координаты `left/top` по координатам курсора.</li>
<li>Далее отслеживаем движение мыши через <code>mousemove</code> и передвигаем переносимый элемент на новые координаты путём смены `left/top`.</li>
<li>Отслеживаем нажатие кнопки мыши на переносимом элементе при помощи события `mousedown`.</li>
<li>При нажатии -- подготовить элемент к перемещению.</li>
<li>Далее отслеживаем движение мыши через <code>mousemove</code> и передвигаем переносимый элемент на новые координаты путём смены `left/top` и `position:absolute`.</li>
<li>При отпускании кнопки мыши, то есть наступлении события <code>mouseup</code> -- остановить перенос элемента и произвести все действия, связанные с окончанием Drag'n'Drop.</li>
</ol>
В следующем примере эти шаги реализованы для переноса мяча:
```js
//+ autorun
var ball = document.getElementById('ball');
ball.onmousedown = function(e) { // 1. отследить нажатие*!*
var self = this;
// подготовить к перемещению
// 2. разместить на том же месте, но в абсолютных координатах*!*
this.style.position = 'absolute';
ball.style.position = 'absolute';
moveAt(e);
// переместим в body, чтобы мяч был точно не внутри position:relative
document.body.appendChild(this);
document.body.appendChild(ball);
this.style.zIndex = 1000; // показывать мяч над другими элементами
ball.style.zIndex = 1000; // показывать мяч над другими элементами
// передвинуть мяч под координаты курсора
// передвинуть мяч под координаты курсора
// и сдвинуть на половину ширины/высоты для центрирования
function moveAt(e) {
self.style.left = e.pageX-20+'px'; // 20 - половина ширины/высоты мяча
self.style.top = e.pageY-20+'px';
ball.style.left = e.pageX - ball.offsetWidth/2 + 'px';
ball.style.top = e.pageY - ball.offsetHeight/2 + 'px';
}
// 3, перемещать по экрану*!*
@ -55,27 +58,24 @@ ball.onmousedown = function(e) { // 1. отследить нажатие*!*
}
// 4. отследить окончание переноса *!*
this.onmouseup = function() {
document.onmousemove = self.onmouseup = null;
ball.onmouseup = function() {
document.onmousemove = null;
ball.onmouseup = null;
}
}
```
В действии:
<div style="height:80px">
Кликните по мячу и тяните, чтобы двигать его.
<img src="//js.cx/clipart/ball.gif" style="cursor:pointer" width="40" height="40" id="ball">
</div>
Если запустить этот код на картинке `#ball`, то мы заметим нечто странное. При начале переноса мяч "раздваивается" и переносится не сам мяч, а его "клон".
**Попробуйте этот пример. Он не совсем работает, мячик "раздваивается".**
[online]
В действии (внутри ифрейма):
Сейчас мы это исправим.
[iframe src="ball" height=230]
[/online]
## Отмена переноса браузера
Это потому, что браузер имеет свой собственный Drag'n'Drop, который автоматически запускается и вступает в конфликт с нашим. Это происходит именно для картинок и некоторых других элементов.
При нажатии мышью на `<img>` браузер начинает выполнять свой собственный, встроенный Drag'n'Drop, который и портит наш перенос.
Чтобы браузер не вмешивался, нужно отменить действие по умолчанию для события `dragstart`:
Его нужно отключить:
```js
ball.ondragstart = function() {
@ -83,78 +83,45 @@ ball.ondragstart = function() {
};
```
Исправленный пример:
Теперь всё будет в порядке.
```js
//+ autorun
var ball = document.getElementById('ball2');
[online]
В действии (внутри ифрейма):
ball.onmousedown = function(e) {
var self = this;
[iframe src="ball2" height=230]
[/online]
this.style.position = 'absolute';
moveAt(e);
document.body.appendChild(this);
Ещё одна особенность правильного Drag'd'Drop -- событие `mousemove` отслеживается на `document`, а не на `ball`.
this.style.zIndex = 1000;
function moveAt(e) {
self.style.left = e.pageX-20+'px';
self.style.top = e.pageY-20+'px';
}
С первого взгляда кажется, что мышь всегда над мячом и обработчик `mousemove` можно повесить на сам мяч, а не на документ.
document.onmousemove = function(e) {
moveAt(e);
};
Однако, на самом деле мышь во время переноса не всегда над мячом.
this.onmouseup = function() {
document.onmousemove = self.onmouseup = null;
};
}
Вспомним, событие `mousemove` возникает хоть и часто, но не для каждого пикселя. Быстрое движение курсора вызовет `mousemove` уже не над мячом, а, например, в дальнем конце страницы.
*!*
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` на всём `document`.
[/smart]
## Правильное позиционирование
В примерах выше мяч позиционируется в центре под курсором мыши:
```js
self.style.left = e.pageX - 20 + 'px';
self.style.top = e.pageY - 20 + 'px';
self.style.left = e.pageX - ball.offsetWidth/2 + 'px';
self.style.top = e.pageY - ball.offsetHeight/2 + 'px';
```
Число `20` здесь -- половина длины мячика. Оно использовано здесь потому, что если поставить `left/top` ровно в `pageX/pageY`, то мячик прилипнет верхним-левым углом к курсору мыши. Будет некрасиво.
Если поставить `left/top` ровно в `pageX/pageY`, то мячик прилипнет верхним-левым углом к курсору мыши. Будет некрасиво. Поэтому мы сдвигаем его на половину высоты/ширины, чтобы был центром под мышью. Уже лучше.
**Для правильного переноса необходимо, чтобы изначальный сдвиг курсора относительно элемента сохранялся: где захватили, за ту "часть элемента" и переносим.**
Но не идеально. В частности, в самом начале переноса, особенно если мячик "взят" за край -- он резко "прыгает" центром под курсор мыши.
**Для правильного переноса необходимо, чтобы изначальный сдвиг курсора относительно элемента сохранялся.**
Где захватили, за ту "часть элемента" и переносим:
<img src="ball_shift.png">
<ul>
<li>Когда человек нажимает на мячик `mousedown` -- курсор сдвинут относительно левого-верхнего угла мяча на расстояние `shiftX/shiftY`. И мы хотели бы сохранить этот сдвиг.
<ol>
<li>Когда человек нажимает на мячик `mousedown` -- курсор сдвинут относительно левого-верхнего угла мяча на расстояние, которое мы обозначим `shiftX/shiftY`. И нужно при переносе сохранить этот сдвиг.
Получить значения `shiftX/shiftY` легко: достаточно вычесть из координат курсора `pageX/pageY` левую-верхнюю границу мячика, полученную при помощи функции [getCoords](#getCoords).
@ -173,48 +140,45 @@ shiftY = e.pageY - getCoords(ball).top;
```js
// onmousemove
self.style.left = e.pageX - *!*shiftX*/!* + 'px';
self.style.top = e.pageY - *!*shiftY*/!* + 'px';
ball.style.left = e.pageX - *!*shiftX*/!* + 'px';
ball.style.top = e.pageY - *!*shiftY*/!* + 'px';
```
</li>
</ul>
</ol>
**Пример с правильным позиционированием:**
Итоговый код с правильным позиционированием:
В этом примере позиционирование осуществляется не на `20px`, а с учётом изначального сдвига.
```js
//+ autorun
var ball = document.getElementById('ball3');
var ball = document.getElementById('ball');
ball.onmousedown = function(e) {
var self = this;
var coords = getCoords(this);
var coords = getCoords(ball);
*!*
var shiftX = e.pageX - coords.left;
var shiftY = e.pageY - coords.top;
*/!*
this.style.position = 'absolute';
document.body.appendChild(this);
ball.style.position = 'absolute';
document.body.appendChild(ball);
moveAt(e);
this.style.zIndex = 1000; // над другими элементами
ball.style.zIndex = 1000; // над другими элементами
function moveAt(e) {
self.style.left = e.pageX - *!*shiftX*/!* + 'px';
self.style.top = e.pageY - *!*shiftY*/!* + 'px';
ball.style.left = e.pageX - *!*shiftX*/!* + 'px';
ball.style.top = e.pageY - *!*shiftY*/!* + 'px';
}
document.onmousemove = function(e) {
moveAt(e);
};
this.onmouseup = function() {
document.onmousemove = self.onmouseup = null;
ball.onmouseup = function() {
document.onmousemove = null;
ball.onmouseup = null;
};
}
@ -224,12 +188,11 @@ ball.ondragstart = function() {
};
```
В действии:
[online]
В действии (внутри ифрейма):
<div style="height:80px">
Кликните по мячу и тяните, чтобы двигать его.
<img src="//js.cx/clipart/ball.gif" style="cursor:pointer" width="40" height="40" id="ball3">
</div>
[iframe src="ball3" height=230]
[/online]
Различие особенно заметно, если захватить мяч за правый-нижний угол. В предыдущем примере мячик "прыгнет" серединой под курсор, в этом -- будет плавно переноситься с текущей позиции.
@ -240,7 +203,7 @@ ball.ondragstart = function() {
Его компоненты:
<ol>
<li>События `mousedown` -> `document.mousemove` -> `mouseup`.</li>
<li>События `ball.mousedown` -> `document.mousemove` -> `ball.mouseup`.</li>
<li>Передвижение с учётом изначального сдвига `shiftX/shiftY`.</li>
<li>Отмена действия браузера по событию `dragstart`.</li>
</ol>
@ -250,10 +213,7 @@ ball.ondragstart = function() {
<ul>
<li>При `mouseup` можно обработать окончание переноса, произвести изменения в данных, если они нужны.</li>
<li>Во время самого переноса можно подсвечивать элементы, над которыми проходит элемент.</li>
<li>При обработке событий `mousedown` и `mouseup` можно использовать делегирование, так что одного обработчика достаточно для управления переносом в зоне с сотнями элементов.
</ul>
Это и многое другое мы рассмотрим в статье про [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]
## Документ
Как пример задачи -- возьмём документ с иконками браузера ("объекты переноса"), которые можно переносить в компьютер ("цель переноса"):
<ul>
<li>Элементы, которые можно переносить (иконки браузеров), помечены атрибутом `draggable`.</li>
<li>Элементы, на которые можно положить (компьютер), имеют атрибут `droppable`.</li>
<li>Элементы, которые можно переносить (иконки браузеров), помечены классом `draggable`.</li>
<li>Элементы, на которые можно положить (компьютер), имеют класс `droppable`.</li>
</ul>
```html
<img src="chrome.png" *!*draggable*/!*>
<img src="firefox.png" *!*draggable*/!*>
<img src="ie.png" *!*draggable*/!*>
<img src="opera.png" *!*draggable*/!*>
<img src="safari.png" *!*draggable*/!*>
<img src="chrome.png" class="*!*draggable*/!*">
<img src="firefox.png" class="*!*draggable*/!*">
<img src="ie.png" class="*!*draggable*/!*">
<img src="opera.png" class="*!*draggable*/!*">
<img src="safari.png" class="*!*draggable*/!*">
<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,35 +47,32 @@
## Начало переноса
Чтобы начать перенос элемента, мы должны отловить нажатие левой кнопки мыши на нём. Для этого используем событие `mousedown`.
Чтобы начать перенос элемента, мы должны отловить нажатие левой кнопки мыши на нём. Для этого используем событие `mousedown`... И, конечно, делегирование.
..И здесь нас ждёт первая особенность переноса объектов. На что ставить обработчик?
Переносимых элементов может быть много. В нашем документе-примере это всего лишь несколько иконок, но если мы хотим переносить элементы списка или дерева, то их может быть 100 штук и более.
**Переносимых элементов может быть много.** В нашем документе-примере это всего лишь несколько иконок, но если мы хотим переносить элементы списка или дерева, то их может быть 100 штук и более.
Поэтому повесим обработчик `mousedown` на контейнер, который содержит переносимые элементы, и будем определять нужный элемент поиском ближайшего `draggable` вверх по иерархии от `event.target`.
Назначать обработчик на каждый DOM-элемент -- нецелесообразно. Проще всего решить эту задачу делегированием.
В качестве контейнера здесь будем брать `document`, хотя это может быть и любой элемент.
**Повесим обработчик `mousedown` на контейнер, который содержит переносимые элементы,** и будем определять нужный элемент поиском ближайшего `draggable` вверх по иерархии от `event.target`. В качестве контейнера здесь и далее будем брать `document`.
Найденный переносимый объект сохраним в переменной `dragObject` и начнём двигать.
Найденный `draggable`-элемент сохраним в свойстве `dragObject.elem` и начнём двигать.
Код обработчика `mousedown`:
```js
var dragObject = {};
document.onmousedown = function(e){
document.onmousedown = function(e) {
if (e.which != 1) {
return; // нажатие правой кнопкой не запускает перенос
if (e.which != 1) { // если клик правой кнопкой мыши
return; // то он не запускает перенос
}
// найти ближайший draggable, пройдя по цепочке родителей target
var elem = findDraggable(e.target, document);
var elem = e.target.closest('.draggable');
if (!elem) return; // не нашли, клик вне draggable объекта
if (!elem) return; // не нашли, клик вне draggable-объекта
// запомнить переносимый объект
// запомнить переносимый объект
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;
}
```
Это важное различие. Снимать элемент со своего места и куда-то двигать нужно только при переносе.
**Здесь мы пока не начинаем перенос, потому что нажатие на элемент вовсе не означает, что его собираются куда-то двигать.** Возможно, на нём просто кликают.
Это важное различие. Нужно отличать перенос от клика, в том числе -- от клика, который сопровождается нечаянным перемещением на пару пикселей (рука дрогнула).
Иначе при клике элемент будет сниматься со своего места, и потом тут же возвращаться обратно (никуда не положили). Это лишняя работа и, вообще, выглядит некрасиво.
**Поэтому в `mousedown` мы запоминаем, какой элемент и где был зажат, а начало переноса будем инициировать из `mousemove`,** если он передвинут хотя бы на несколько пикселей.
Чтобы отличить перенос от клика, в том числе -- от клика, который сопровождается нечаянным перемещением на пару пикселей (рука дрогнула), мы будем запоминать в `dragObject`, какой элемент (`elem`) и где (`downX/downY`) был зажат, а начало переноса будем инициировать из `mousemove`, если он передвинут хотя бы на несколько пикселей.
[/warn]
## Перенос элемента
Первой задачей обработчика `mousemove` является инициировать начало переноса, если элемент передвинули в зажатом состоянии.
Ну а второй задачей -- отображать его перенос при каждом передвижении мыши. Схематично, обработчик имеет такой вид:
Ну а второй задачей -- отображать его перенос при каждом передвижении мыши.
Схематично, обработчик будет иметь такой вид:
```js
document.onmousemove = function(e) {
@ -118,13 +111,13 @@ document.onmousemove = function(e) {
}
```
Здесь мы видим новое свойство `dragObject.avatar`. При начале переноса *аватар* делается из элемента и сохраняется в свойство `dragObject.avatar`.
Здесь мы видим новое свойство `dragObject.avatar`. При начале переноса "аватар" делается из элемента и сохраняется в свойство `dragObject.avatar`.
***"Аватар"* -- это DOM-элемент, который перемещается по экрану.**
**"Аватар" -- это DOM-элемент, который перемещается по экрану.**
Почему бы не перемещать по экрану сам `draggable`-элемент? Зачем, вообще, нужен аватар?
Дело в том, что иногда сам элемент передвигать неудобно, например он слишком большой. А удобно создать некоторое визуальное представление элемента, и его уже переносить. Аватар дает такую возможность.
Дело в том, что иногда сам элемент передвигать неудобно, например потому, что он слишком большой. А удобно создать некоторое визуальное представление элемента, и его уже переносить. Аватар дает такую возможность.
А в простейшем случае аватаром можно будет сделать сам элемент, и это не повлечёт дополнительных расходов.
@ -154,7 +147,7 @@ avatar.style.left = новая координата + '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';
```
### Полный код mousemove
## Полный код mousemove
Код `mousemove`, решающий задачу начала переноса и позиционирования аватара:
@ -213,12 +206,11 @@ document.onmousemove = function(e) {
Функция `createAvatar(e)` создает аватар. В нашем случае в качестве аватара берется сам `draggable` элемент. После создания аватара в него записывается функция `avatar.rollback`, которая задает поведение при отмене переноса.
Как правило, отмена переноса влечет за собой разрушение аватара, если это был клон, или возвращение его на прежнее место, если это сам элемент.
Как правило, отмена переноса влечет за собой разрушение аватара, если это был клон, или возвращение его на прежнее место, если это сам элемент.
В нашем случае для отмены переноса нужно запомнить старую позицию элемента и его родителя.
```js
//+ hide="Раскрыть код createAvatar"
function createAvatar(e) {
// запомнить старые свойства, чтобы вернуться к ним при отмене переноса
@ -245,7 +237,7 @@ function createAvatar(e) {
}
```
Функция `startDrag(e)` инициирует начало переноса и позиционирует аватар на странице.
Функция `startDrag(e)`, которую вызывает `mousemove`, если видит, что элемент в "зажатом" состоянии перенесли достаточно далеко, инициирует начало переноса и позиционирует аватар на странице:
```js
function startDrag(e) {
@ -257,7 +249,7 @@ function startDrag(e) {
}
```
### Окончание переноса
## Окончание переноса
Окончание переноса происходит по событию `mouseup`.
@ -306,18 +298,16 @@ function finishDrag(e) {
```js
// возвратит ближайший droppable или null
// *!*предварительный вариант findDroppable, исправлен ниже!*/!*
*!*
// это предварительный вариант findDroppable, исправлен ниже!
*/!*
function findDroppable(event) {
// взять элемент на данных координатах
var elem = document.elementFromPoint(event.clientX, event.clientY);
// пройти вверх по цепочке родителей в поисках droppable
while(elem != document && elem.getAttribute('droppable') == null) {
elem = elem.parentNode;
}
return elem == document ? null : elem;
// найти ближайший сверху droppable
return elem.closest('.droppable');
}
```
@ -325,67 +315,48 @@ function findDroppable(event) {
Вариант выше -- предварительный. Он не будет работать. Если попробовать применить эту функцию, будет все время возвращать один и тот же элемент! А именно -- *текущий переносимый*. Почему так?
..Дело в том, что в процессе переноса под мышкой находится именно аватар. При начале переноса ему даже `z-index` ставится большой, чтобы он был поверх всех остальных.
...Дело в том, что в процессе переноса под мышкой находится именно аватар. При начале переноса ему даже `z-index` ставится большой, чтобы он был поверх всех остальных.
**Аватар перекрывает остальные элементы. Поэтому функция `document.elementFromPoint()` увидит на текущих координатах именно его.**
Чтобы это изменить, нужно либо поправить код переноса, чтобы аватар двигался *рядом* с курсором мыши, либо поступить проще:
Чтобы это изменить, нужно либо поправить код переноса, чтобы аватар двигался *рядом* с курсором мыши, либо дать аватару стиль `pointer-events:none` (кроме IE10-), либо:
<ol><li>Спрятать аватар.</li>
<li>Вызывать `elementFromPoint`.</li>
<li>Показать аватар.</li></ol>
Напишем вспомогательную функцию `getElementUnderClientXY(elem, clientX, clientY)`, которая это делает:
```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`:
Напишем функцию `findDroppable(event)`, которая это делает:
```js
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`. Это позволит прямо при создании объявить дополнительные переменные и функции в замыкании, которыми могут пользоваться методы объекта, а также назначить обработчики:
```js
var dragManager = new function() {
var DragManager = new function() {
var dragObject = {};
@ -404,11 +375,13 @@ var dragManager = new function() {
}
```
Всю работу будут выполнять обработчики `onMouse*`, которые оформлены как локальные функции. В данном случае они ставятся на `document` через `on...`, но вы легко можете поменять это на `addEventListener/attachEvent`.
Всю работу будут выполнять обработчики `onMouse*`, которые оформлены как локальные функции. В данном случае они ставятся на `document` через `on...`, но это легко поменять это на `addEventListener`.
Внутренний объект `dragObject` будет содержать информацию об объекте переноса.
Код функция `onMouse*` мы подробно рассмотрели ранее, но вы сможете увидеть их в полном примере ниже.
У него будут следующие свойства:
Внутренний объект `dragObject` будет содержать информацию об объекте переноса.
У него будут следующие свойства, которые также разобраны выше:
<dl>
<dt>`elem`</dt>
<dd>Текущий зажатый мышью объект, если есть (ставится в `mousedown`).</dd>
@ -420,63 +393,56 @@ var dragManager = new function() {
<dd>Относительный сдвиг курсора от угла элемента, вспомогательное свойство вычисляется в начале переноса.</dd>
</dl>
**Задачей `dragManager` является общее управление переносом.** Для обработки его окончания вызываются методы `onDrag*`, которые назначаются внешним кодом.
Задачей `DragManager` является общее управление переносом. Что же касается действий при его окончании -- их должен назначить внешний код, который использует `DragManager`.
Разработчик, подключив `dragManager`, описывает в этих методах, что делать при начале и завершении переноса.
Можно сделать это через вспомогательные методы `onDrag*`, которые устанавливаются внешним кодом и затем вызываются фреймворком. Разработчик, подключив `DragManager`, описывает в этих методах, что делать при начале и завершении переноса. Конечно же, можно заменить методы `onDrag*` на генерацию "своих" событий.
[smart]
Если в вашем распоряжении есть современный JavaScript-фреймворк, то можно заменить вызовы методов `onDrag*` на генерацию соответствующих событий.
[/smart]
### Реализация переноса иконок
С использованием этого фреймворка перенос иконок браузеров в компьютер реализуется просто:
С использованием `DragManager` пример, с которого начиналась эта глава -- перенос иконок браузеров в компьютер, реализуется совсем просто:
```js
dragManager.onDragEnd = function(dragObject, dropElem) {
DragManager.onDragEnd = function(dragObject, dropElem) {
// скрыть/удалить переносимый объект
dragObject.elem.hidden = true;
// успешный перенос, показать улыбку классом computer-smile
dropElem.className = 'computer computer-smile';
// скрыть переносимый объект
dragObject.elem.style.display = 'none';
// убрать улыбку через 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();
};
```
Результат:
Полный пример с кодом:
[iframe border=1 src="dragDemo" height=250 link edit]
[codetabs src="dragDemo" height=280]
## Варианты расширения этого кода
## Расширения
Существует масса возможных применений 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>
</ul>
Эта задача решается добавлением проверки в `findDroppable(e)`. Эта функция знает и об аватаре и о событии. При попытке положить в "неправильное" место функция `findDroppable(e)` должна возвращать `null`.
Здесь решение: "можно или нет" переносить или нельзя зависит от "типа" переносимого объекта.
Рассмотрим эту задачу поглубже, так как она встречается часто.
Есть и более сложные варианты, когда решение зависит от конкретного места в `droppable`, над которым посетитель отпустил кнопку мыши. К примеру, переносить в верхнюю часть можно, а в нижнюю -- нет.
**Есть две наиболее частые причины, по которым аватар нельзя положить на потенциальный `droppable`.**
Эта задача решается добавлением проверки в `findDroppable(e)`. Эта функция знает и об аватаре и о событии, включая координаты. При попытке положить в "неправильное" место функция `findDroppable(e)` должна возвращать `null`.
Первую мы уже рассмотрели: "несовпадение типов". Это как раз тот случай, когда "узлы-разделы" дерева админки могут принимать только "статьи", и не могут -- "посетителей".
Однако, на практике бывают ситуации, когда решение "прямо сейчас" принять невозможно. Например, нужно сделать запрос на сервер: "А разрешено ли текущему посетителю производить такую операцию?"
В этом случае мы можем проверить это в `findDroppable`, так как вся необходимая информация о типах у нас есть.
Как при этом должен вести себя интерфейс? Можно, конечно сделать, чтобы элемент после отпускания кнопки мыши "завис" над `droppable`, ожидая ответа. Однако, такое решение неудобно в реализации и странновато выглядит для посетителя.
Вторая причина -- посложнее.
Как правило, применяют "оптимистичный" алгоритм, по которому мы считаем, что перенос обычно успешен, но при необходимости можем отменить его.
**Может не хватать прав на такое действие с `draggable/droppable`, в рамках наложенных на посетителя ограничений.**
Например, человек не может положить статью именно в данный раздел.
А возможно, объект переноса удален из базы администратором, и браузер об этом (пока) не знает -- мы должны корректно обрабатывать все случаи, включая этот.
Здесь сложность в том, что **окончательное решение знает только сервер**. Значит, нужно сделать запрос. А элемент после отпускания мыши не может "зависнуть" над элементом в ожидании ответа, нужно его куда-то положить.
**Как правило, применяют "оптимистичный" сценарий, по которому мы считаем, что перенос обычно успешен.**
При нём посетитель кладет объект туда, куда он хочет.
Затем, в коде `onDragEnd`:
При нём посетитель кладет объект туда, куда он хочет, а затем, в коде `onDragEnd`:
<ol>
<li>Визуально обрабатывается завершение переноса, как будто все ок.</li>
<li>Производится асинхронный запрос к серверу, содержащий информацию о переносе.</li>
@ -518,93 +472,58 @@ dragManager.onDragCancel = function(dragObject) {
<li>Если нет -- выводится ошибка и возвращается `avatar.rollback()`. Аватар в этом случае должен предусматривать возможность отката после успешного завершения.</li>
</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` происходит перенос.
</dd>
<dt>Поддержка переноса только "между"</dt>
<dd>Здесь есть два варианта.
<ol>
<li>Во-первых, можно разделить `droppable` в отношении 50%/50% и по координатам смотреть, куда мы попали.</li>
<li>В некоторых случаях, например при вставке между `droppable`-элементами списка, можно считать, что *элемент вставляется перед тем `LI`, над которым проходит*.
<ul>
<li>Если перенос в верхнюю четверть, то это -- "над".</li>
<li>Если перенос в середину, то это "внутрь".</li>
<li>Если перенос в нижнюю четверть, то это -- "под".</li>
</ul>
А чтобы вставить после последнего -- нужно перетащить аватар на сам DOM-элемент списка, но не на существующий `droppable`, а в пустое место, которое оставляется внизу для этой цели.</li>
</ol>
</dd>
</dl>
Индикацию "переноса между" удобнее всего делать либо раздвижением элементов, либо показом полосы-индикатора <code>border-top/border-bottom</code>, как показано на рисунке ниже:
Текущий `droppable` и позиция относительно него при этом могут помечаться подсветкой и жирной чертой над/под, если требуется.
Пример индикации из Firefox:
<img src="between.png">
### Анимация отмены переноса
Отмену переноса и возврат аватара на место можно красиво анимировать.
Один из частых вариантов -- скольжение объекта обратно к исходному месту, откуда его взяли. Для этого достаточно поправить `avatar.revert()` соотвествующим образом.
Один из частых вариантов -- скольжение объекта обратно к исходному месту, откуда его взяли. Для этого достаточно поправить `avatar.rollback()`.
## Итого
Алгоритм Drag'n'Drop:
Уточнённый алгоритм Drag'n'Drop:
<ol>
<li>При `mousedown` запомнить координаты нажатия.</li>
<li>При `mousemove` инициировать перенос, как только зажатый элемент передвинули на 3 пикселя или больше.
<li>При `mousemove` инициировать перенос, как только зажатый элемент передвинули на 3 пикселя или больше. Сообщить во внешний код вызовом `onDragStart`.</li>
<ol>
<li>Создать аватар, если можно начать перенос с этой точки `draggable`.</li>
<li>Перемещать его по экрану. Новые координаты ставить по `e.pageX/pageY` с учетом изначального сдвига элемента относительно курсора.</li>
<li>Перемещать его по экрану, новые координаты ставить по `e.pageX/pageY` с учетом изначального сдвига элемента относительно курсора.</li>
<li>Сообщать во внешний код о текущем `droppable` под курсором и позиции над ним вызовами `onDragEnter`, `onDragMove`, `onDragLeave`.</li>
</ol>
</li>
<li>При `mouseup` обработать завершение переноса. Элемент под аватаром получить по координатам, предварительно спрятав аватар.</li>
<li>При `mouseup` обработать завершение переноса. Элемент под аватаром получить по координатам, предварительно спрятав аватар. Сообщить во внешний код вызовом `onDragEnd`.</li>
</ol>
Получившийся попутно с обсуждением технических моментов Drag'n'Drop фреймворк обладает рядом особенностей:
Получившаяся реализация Drag'n'Drop проста, эффективна, изящна.
[compare]
+Он прост. Он действительно очень прост.
+Он полностью кросс-браузерный, не содержит хаков.
+Он позволяет работать с большим количеством потенциальных `draggable`/`droppable`.
+Его легко расширить и поменять.
[/compare]
Вы можете получить файлы и посмотреть итоговое демо [edit src="dragDemo"]в песочнице[/edit].
В зависимости от ваших задач, вы можете либо использовать его как отправную точку, либо реализовать свой.
Её очень легко поменять или адаптировать под "особые" потребности.
ООП-вариант фреймворка находится в статье [](/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;
var elem = findDraggable(e);
var elem = e.target.closest('.draggable');
if (!elem) return;
dragObject.elem = elem;
@ -118,23 +118,22 @@ var dragManager = new function() {
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) {
// спрячем переносимый элемент
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;
@ -145,3 +144,15 @@ var dragManager = new function() {
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 {
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

@ -9,15 +9,15 @@
Несмотря на то, что колёсико мыши обычно ассоциируется с прокруткой, это совсем разные вещи.
<ul>
<li>При прокрутке срабатывает событие [onscroll](/event-onscroll) -- рассмотрим его в дальнейшем. Оно произойдёт *при любой прокрутке*, в том числе через клавиатурy, но *только на прокручиваемых элементах*. Например, элемент с `overflow: hidden` в принципе не может сгенерировать `onscroll`.</li>
<li>При прокрутке срабатывает событие [onscroll](/event-onscroll) -- рассмотрим его в дальнейшем. Оно произойдёт *при любой прокрутке*, в том числе через клавиатурy, но *только на прокручиваемых элементах*. Например, элемент с `overflow:hidden` в принципе не может сгенерировать `onscroll`.</li>
<li>А событие `wheel` является чисто "мышиным". Оно генерируется *над любым элементом* при передвижении колеса мыши. При этом не важно, прокручиваемый он или нет. В частности, `overflow:hidden` никак не препятствует обработке колеса мыши.</li>
</ul>
Кроме того, событие `onscroll` происходит после прокрутки, а `onwheel` -- до прокрутки, поэтому в нём можно отменить саму прокрутку (действие браузера).
Кроме того, событие `onscroll` происходит *после* прокрутки, а `onwheel` -- *до* прокрутки, поэтому в нём можно отменить саму прокрутку (действие браузера).
## Зоопарк 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).
@ -27,26 +27,24 @@
<dd>Свойство `deltaY` -- количество прокрученных пикселей по горизонтали и вертикали. Существуют также свойства `deltaX` и `deltaZ` для других направлений прокрутки.</dd>
<dt>`MozMousePixelScroll`</dt>
<dd>Срабатывает, начиная с Firefox 3.5, только в Firefox. Даёт возможность отменить прокрутку и получить размер в пикселях через свойство `detail`, ось прокрутки в свойстве `axis`.</dd>
<dt>`DOMMouseScroll`</dt>
<dd>Существует в Firefox очень давно, отличается от предыдущего тем, что даёт в `detail` количество строк. Если не нужна поддержка Firefox < 3.5, то не нужно и это событие.</dd>
<dt>`mousewheel`</dd>
<dd>Срабатывает в браузерах, которые ещё не реализовали `wheel`. В свойстве `wheelDelta` -- условный "размер прокрутки", обычно равен `120` для прокрутки вверх и `-120` -- вниз. Он не соответствует какому-либо конкретному количеству пикселей.</dd>
</dl>
Чтобы кросс-браузерно отловить прокрутку и, при необходимости, отменить её, можно использовать все эти события.
Пример:
Пример, включающий поддержку IE8-:
```js
if (elem.addEventListener) {
if ('onwheel' in document) {
// IE9+, FF17+
// IE9+, FF17+, Ch31+
elem.addEventListener ("wheel", onWheel, false);
} else if ('onmousewheel' in document) {
// устаревший вариант события
elem.addEventListener ("mousewheel", onWheel, false);
} else {
// 3.5 <= Firefox < 17, более старое событие DOMMouseScroll пропустим
// Firefox < 17
elem.addEventListener ("MozMousePixelScroll", onWheel, false);
}
} 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">
<img src="//js.cx/clipart/ball.gif" id="ball">
<img src="//js.cx/clipart/ball.svg" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div>

View file

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

View file

@ -18,7 +18,7 @@
<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>
<script>

View file

@ -13,7 +13,7 @@
<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>

View file

@ -18,7 +18,7 @@
<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>
<script>

View file

@ -13,7 +13,7 @@
<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>

View file

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