Javascript.RU

Drag and drop

Update: Более новый материал по этой теме находится по адресу https://learn.javascript.ru/drag-and-drop.

В свое время приходилось реализовывать кучу drag and drop'ов под самым разным соусом.

Эта статья представляет собой учебник-выжимку о том, как организовать drag'n'drop в javascript, начиная от основ и заканчивая готовым фреймворком.

Кроме того, почти все javascript-библиотеки реализуют drag and drop так, как написано (в статье дано несколько разных вариантов, не факт что ваш фреймворк использует лучший). Зная, что и как, вы сможете поправить и адаптировать существующую библиотеку под себя.

Drag'n'drop в свое время был замечательным открытием в области интерфейсов, которое позволило упростить большое количество операций.

Одно из самых очевидных применений drag'n'drop - переупорядочение данных. Это могут быть блоки, элементы списка, и вообще - любые DOM-элементы и их наборы.

Перенос мышкой может заменить целую последовательность кликов. И, самое главное, он упрощает внешний вид интерфейса: функции, реализуемые через drag'n'drop, в ином случае потребовали бы дополнительных полей, виджетов и т.п.

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

  1. При помощи события mouseDown отследить клик на переносимом элементе
  2. При каждом движении мыши в обработчике события mouseMove передвигать переносимый элемент по странице.
  3. При отпускании кнопки мыши, то есть наступлении события mouseUp - остановить перенос элемента и произвести все действия, связанные с окончанием drag and drop.

При обработке событий, связанных с мышью, нужен кроссбраузерный способ получения координат курсора из события в обработчике. Кроме того, необходимо знать нажатую кнопку мыши.

Для этого будем использовать свойства which и pageX/pageY, полное описание и механизмы кросс-браузерной реализации которых есть в статье по свойствам объекта событие.

which
кнопка мыши - 1: левая, 2: средняя, 3: правая
pageX/pageY
координаты курсора относительно верхнего-левого угла документа (с учетом прокрутки)

Кроссбраузерно ставить эти свойства на объект будет функция fixEvent (по статье свойства объекта событие):

function fixEvent(e) {
	// получить объект событие для IE
	e = e || window.event

	// добавить pageX/pageY для IE
	if ( e.pageX == null && e.clientX != null ) {
		var html = document.documentElement
		var body = document.body
		e.pageX = e.clientX + (html && html.scrollLeft || body && body.scrollLeft || 0) - (html.clientLeft || 0)
		e.pageY = e.clientY + (html && html.scrollTop || body && body.scrollTop || 0) - (html.clientTop || 0)
	}

	// добавить which для IE
	if (!e.which && e.button) {
		e.which = e.button & 1 ? 1 : ( e.button & 2 ? 3 : ( e.button & 4 ? 2 : 0 ) )
	}

	return e
}

В этом коде e.which проходит кросс-браузерную обработку, чтобы корректно отражать нажатую кнопку мыши. Вы можете подробно прочитать об этом в статье Свойства объекта событие.

На демке ниже обработчик mouseMove отслеживает координаты курсора мыши относительно левого-верхнего угла страницы, используя кроссбраузерную обертку fixEvent.

document.onmousemove = mouseMove

function mouseMove(event){ 
	event = fixEvent(event)
	document.getElementById('mouseX').value = event.pageX
	document.getElementById('mouseY').value = event.pageY
}

Координата X:
Координата Y:

Чтобы начать перенос элемента, мы должны отловить нажатие кнопки мыши на объекте.

Для этого нам пригодится событие mousedown. Повесим обработчик на те элементы, которые хотим сделать доступными для переноса.

Пока этот обработчик будет запоминать объект в глобальной переменной dragObject

element.onmousedown = function(e){
    // запомнить переносимый объект 
    // в переменной dragObject
    dragObject  = this

    // остановить обработку события
    return false
}

Остановить обработку события return false очень важно - иначе браузер может запустить свои механизмы перетаскивания элементов и все нам поломать.

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

document.onmouseup = function() {
    // опустить переносимый объект
    dragObject = null
}

При нажатии на элемент он запоминается и выделяется.
Выделение(запоминание) действует на все время, когда нажата кнопка мыши, в том числе при перемещении курсора.


Нажми меня

Остается добавить визуальное перемещение элемента - и drag and drop заработает.

Оптимизация onmousedown

Иногда бывает, что объектов, которые могут быть перенесены, много. Например, это ячейка таблицы или длинный список, или дерево статей с массой узлов.

Тогда время инициализации можно сильно сократить, если назначать обработчик onmousedown не на каждый объект переноса, а на контейнер. И уже в самом обработчике по event.target определять, где произошел клик.

Перед дальнейшим развитием проведем реорганизацию кода.

Используем способ описания объекта без new (описан здесь как фабрика объектов), чтобы объявить объект dragMaster, предоставляющий необходимый функционал и отслеживающий перенос.

var dragMaster = (function() {
	// private методы и свойства
	var dragObject
	
	function mouseDown(e) {
		клик на переносимом элементе: начать перенос
	}

	function mouseMove(e){
		if (dragObject) {
			отобразить перенос объекта
		}
	}
		
	function mouseUp(e){
		if (dragObject) {
			конец переноса
		}
	}

	// public методы и свойства	
	return {
		init: function() {
			// инициализовать контроллер
			document.onmousemove = mouseMove
			document.onmouseup = mouseUp
		},
		
		makeDraggable: function(element){
			// сделать элемент переносимым
			element.onmousedown = mouseDown
		}
	}
	
}())

При таком способе задания объекта dragMaster получает публичные свойства (например, makeDraggable), которые имеют доступ к приватным переменным и методам mouse*, dragObject, так как являются вложенными функциями.

Полученный код:

  1. Не загрязняет глобальную область видимости переменными типа dragObject.
  2. Дает единый объект-синглтон dragMaster, управляющий переносом.
  3. Приватные переменные хорошо сжимаются javascript-компрессором, что убыстряет и уменьшает код.

Последний пункт очень важен, так как обработчики mouseMove, mouseUp вызываются при каждом передвижении мыши и поднятии кнопки соответственно. Если mouseMove будет работать медленно, то передвижение курсора станет рваным, заметно тормозным.

На это натыкались многие писатели drag'n'drop приложений. Мы же будем изначально закладывать производительность во все критические участки кода.

Для того, чтобы перенести элемент, ему нужно поставить значение CSS-свойства position в absolute. Тогда он будет позиционироваться относительно верхнего-левого угла документа (точнее говоря, относительно ближайшего родителя, у которого position - relative/absolute, но у нас таких нет), и установка CSS-свойств left и top в координаты курсора мыши поместит левый-верхний угол элемента непосредственно под указатель.


Вот так:

Для перемещения элемента нам достаточно всего-лишь обновлять значения left/top при каждом движении мыши mousemove!

Посетитель обычно кликает не в левый-верхний угол, а куда угодно на элементе.

Поэтому чтобы элемент не прилипал к курсору верхним-левым углом, к позиции элемента необходимо добавить смещение мыши на момент клика.

На рисунке ниже mouseX/mouseY - координаты курсора мыши, а positionX/positionY - координаты верхнего-левого угла элемента, которые легко получить из DOM:

Отсюда легко получаем смещение курсора мыши:

mouseOffsetX = mouseX - positionX
mouseOffsetY = mouseY - positionY

Это изначальное смещение мы запоминаем при клике, прибавляем его при начале движения и сохраняем в дальнейшем.
Тогда позиция элемента относительно курсора мыши будет все время одной и той же.

var mouseOffset

element.onmousedown = function(e) {
    e = fixEvent(e)
    ...
    var pos = getPosition(element)
    
    mouseOffset= { 
        x: e.pageX - pos.x, 
        y: e.pageY - pos.y 
    }
    ...
}

document.onmousemove = function(e) {
    e = fixEvent(e)
    ...
    element.style.left = e.pageX - mouseOffset.x + 'px'
    element.style.top = e.pageY - mouseOffset.y  + 'px'
    ...
}


Перенеси меня

Код контроллера:

var dragMaster = (function() {

	var dragObject
	var mouseOffset

	// получить сдвиг target относительно курсора мыши
	function getMouseOffset(target, e) {
		var docPos	= getPosition(target)
		return {x:e.pageX - docPos.x, y:e.pageY - docPos.y}
	}

	function mouseUp(){
		dragObject = null

		// очистить обработчики, т.к перенос закончен
		document.onmousemove = null
		document.onmouseup = null
		document.ondragstart = null
		document.body.onselectstart = null
	}

	function mouseMove(e){
		e = fixEvent(e)

		with(dragObject.style) {
			position = 'absolute'
			top = e.pageY - mouseOffset.y + 'px'
			left = e.pageX - mouseOffset.x + 'px'
		}
		return false
	}

	function mouseDown(e) {
		e = fixEvent(e)
		if (e.which!=1) return

		dragObject  = this

		// получить сдвиг элемента относительно курсора мыши
		mouseOffset = getMouseOffset(this, e)

		// эти обработчики отслеживают процесс и окончание переноса
		document.onmousemove = mouseMove
		document.onmouseup = mouseUp

		// отменить перенос и выделение текста при клике на тексте
		document.ondragstart = function() { return false }
		document.body.onselectstart = function() { return false }

		return false
	}

	return {
		makeDraggable: function(element){
			element.onmousedown = mouseDown
		}
	}

}())

function getPosition(e){
	var left = 0
	var top  = 0

	while (e.offsetParent){
		left += e.offsetLeft
		top  += e.offsetTop
		e	 = e.offsetParent
	}

	left += e.offsetLeft
	top  += e.offsetTop

	return {x:left, y:top}
}

В коде появилась новая функция getPosition(элемент) - она получает абсолютные координаты верхнего-правого угла элемента. Функция это стандартная и много где используемая.

Кроме того, при начале переноса останавливается выделение и перенос текста браузером:

document.ondragstart = function() { return false }
document.body.onselectstart = function() { return false }

Если этого не сделать, то движение курсора мыши при нажатой кнопке будет не только перемещать элемент, но и, например, выделять текст под собой (стандартная функция выделения текста на странице).

Иногда красивее и удобнее - визуально перемещать не сам элемент, а его клон или макет.

Например, переносимый объект очень сложен, и его передвижение целиком тормозит браузер и выглядит громоздко/неэстетично.

Сам элемент при этом скрывается display/visibility='none' или просто остается на месте, в зависимости от логики интерфейса.

Переносимый клон инициализуется в начале переноса и уничтожается в конце.

Когда иконка опущена, нам необходимо определить, куда.
В другое хранилище - переместить. В корзину - удалить, и т.п.

Существенная техническая проблема заключается в том, что событие mouseup сработает не на корзине, а на переносимом элементе, т.к. курсор мыши находится именно над ним.

Поэтому в событии будет информация об элементе непосредственно под курсором. На картинке выше event.target = сердечко, а корзина в объекте события event не присутствует.

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

В момент опускания на корзину выводится сообщение, перемещаемая иконка - в переменной dragObject, цель переноса (корзина, объект-акцептор) - в переменной currentDropTarget.


Перенеси меня в корзину


В коде контроллера: функции, тело которых заменено на "...", остались без изменения с прошлого примера.

var dragMaster = (function() {

    var dragObject
    var mouseOffset

    var dropTargets = []

    function mouseUp(e){
		e = fixEvent(e)

		for(var i=0; i<dropTargets.length; i++){
			var targ  = dropTargets[i]
			var targPos    = getPosition(targ)
			var targWidth  = parseInt(targ.offsetWidth)
			var targHeight = parseInt(targ.offsetHeight)

			if(
			(e.pageX > targPos.x)                &&
			(e.pageX < (targPos.x + targWidth))  &&
			(e.pageY > targPos.y)                &&
			(e.pageY < (targPos.y + targHeight))){
				alert("перенесен объект dragObject на акцептор currentDropTarget")
			}
		}

		dragObject = null
		removeDocumentEventHandlers()
    }

	function mouseDown(e) {
		e = fixEvent(e)
		if (e.which!=1) return

		dragObject  = this
		mouseOffset = getMouseOffset(this, e)

		addDocumentEventHandlers()

		return false
	}


	function removeDocumentEventHandlers() {
		document.onmousemove = null
		document.onmouseup = null
		document.ondragstart = null
		document.body.onselectstart = null
	}

	function addDocumentEventHandlers() {
		document.onmousemove = mouseMove
		document.onmouseup = mouseUp

		document.ondragstart = function() { return false }
		document.body.onselectstart = function() { return false }
	}

    function getMouseOffset(target, e) {...}

    function mouseMove(e) {...}

    return {
		makeDraggable: function(element){...},

		addDropTarget: function(dropTarget){
			dropTargets.push(dropTarget)
		}

    }
}())
var dragMaster2 = (function() {

    var dragObject
    var mouseOffset

    var dropTargets = []

    function mouseUp(e){
		e = fixEvent(e)

		for(var i=0; i<dropTargets.length; i++){
			var targ  = dropTargets[i]
			var targPos    = getPosition(targ)
			var targWidth  = parseInt(targ.offsetWidth)
			var targHeight = parseInt(targ.offsetHeight)

			if(
			(e.pageX > targPos.x)                &&
			(e.pageX < (targPos.x + targWidth))  &&
			(e.pageY > targPos.y)                &&
			(e.pageY < (targPos.y + targHeight))){
				alert("dragObject was dropped onto currentDropTarget!")
			}
		}

		dragObject = null
		removeDocumentEventHandlers()
    }

	function removeDocumentEventHandlers() {
		document.onmousemove = null
		document.onmouseup = null
		document.ondragstart = null
		document.body.onselectstart = null
	}

	function getMouseOffset(target, e) {
		var docPos	= getPosition(target)
		return {x:e.pageX - docPos.x, y:e.pageY - docPos.y}
	}


	function mouseMove(e){
		e = fixEvent(e)

		with(dragObject.style) {
			position = 'absolute'
			top = e.pageY - mouseOffset.y + 'px'
			left = e.pageX - mouseOffset.x + 'px'
		}
		return false
	}

	function mouseDown(e) {
		e = fixEvent(e)
		if (e.which!=1) return

		dragObject  = this
		mouseOffset = getMouseOffset(this, e)

		addDocumentEventHandlers()

		return false
	}

	function addDocumentEventHandlers() {
		document.onmousemove = mouseMove
		document.onmouseup = mouseUp

		// отменить перенос и выделение текста при клике на тексте
		document.ondragstart = function() { return false }
		document.body.onselectstart = function() { return false }
	}

    return {

		makeDraggable: function(element){
			element.onmousedown = mouseDown
		},

		addDropTarget: function(dropTarget){
			dropTargets.push(dropTarget)
		}
    }
}())

function getPosition(e){
	var left = 0;
	var top  = 0;

	while (e.offsetParent){
		left += e.offsetLeft;
		top  += e.offsetTop;
		e     = e.offsetParent;
	}

	left += e.offsetLeft;
	top  += e.offsetTop;

	return {x:left, y:top};
}

Основных изменений всего три.

Добавлен массив dropTargets и функция addDropTarget, которая добавляет в него элементы, на которые можно дропать.

Измененный обработчик mouseUp теперь проходит в цикле по возможным таким объектам и проверяет, не находится ли курсор внутри ограничивающего объект прямоугольника.

Если да, то демка всего лишь выводит сообщение. Реально приложение, конечно, может сделать более сложные действия.

Кроме того, установка и удаление обработчиков событий для document выделены в отдельные функции - просто в целях лучшей читаемости.

В удобном интерфейсе мы, скорее всего, захотим как-то показывать посетителю, над каким объектом он сейчас находится.

Единственно место, где это можно сделать - обработчик mouseMove. Сама проверка, над чем курсор сейчас находится, полностью аналогична mouseUp.

Однако, так как mouseMove выполняется при каждом передвижении мыши, его надо максимально оптимизировать.

Функция getPosition - довольно медленная: она работает с DOM, и ей надо пройти по всей цепочке offsetParent. Выполнять ее каждый раз при движении мыши для поиска текущего акцептора - все равно что нажать на большой-большой тормоз.

Стандартным выходом в такой ситуации является кеширование координат акцепторов, а точнее - их ограничивающих прямоугольников, так чтобы код mouseMove был максимально прост.


Пронеси меня над корзиной


Код уже стал довольно длинным, поэтому в листинге ниже повторяющиеся фрагменты заменены на троеточие "..."

var dragMaster = (function() {

    var dragObject
    var mouseOffset

    var dropTargets = []

	/* кеш прямоугольников границ акцепторов */
    var dropTargetRectangles

	/* текущий акцептор, над которым объект в данный момент */
	var currentDropTarget

	function cacheDropTargetRectangles() {
		dropTargetRectangles = /* сделать кеш прямоугольников */
	}

	function mouseDown(e) {
		e = fixEvent(e)
		if (e.which!=1) return

		/* начать перенос */
		dragObject  = this
		mouseOffset = getMouseOffset(this, e)

		/* закешировать прямоугольники при начале переноса */
		cacheDropTargetRectangles()

		addDocumentEventHandlers()

		return false
	}

	function getCurrentTarget(e) {
		var dropTarget = /* взять из кеша прямоугольник, в котором мышь */

		return dropTarget /* null, если мы не над акцепторами */
	}

	function mouseMove(e){
		/* визуально показать перенос объекта */
		with(dragObject.style) {
			position = 'absolute'
			top = e.pageY - mouseOffset.y + 'px'
			left = e.pageX - mouseOffset.x + 'px'
		}

		/* newTarget = над каким акцептором сейчас объект */
		var newTarget = getCurrentTarget(e)

		/* если ушли со старого акцептора */
		if (currentDropTarget && currentDropTarget != newTarget) {
			/* убрать выделение currentDropTarget */
		}

		/* пришли на новый акцептор (возможно null) */
		currentDropTarget = newTarget

		/* если новый акцептор существует (не null) */
		if (newTarget) {
			/* выделить newTarget */
		}

		return false;
    }

    function mouseUp(ev){
		if (currentDropTarget) {
			alert("перенесен объект dragObject на акцептор currentDropTarget")
			/* убрать выделение с  currentDropTarget */
		}

		/* конец операции переноса */
		dragObject = null
		removeDocumentEventHandlers()
    }

	function getMouseOffset(target, e) {...}

	function addDocumentEventHandlers() {...}
	function removeDocumentEventHandlers() {...}

    return {
		...
    }
}())

Полностью рабочий вариант с кешем и т.п. - здесь:

var dragMaster = (function() {

    var dragObject
    var mouseOffset

    var dropTargets = []

    var dropTargetRectangles

	var currentDropTarget

	function cacheDropTargetRectangles() {
		dropTargetRectangles = []

		for(var i=0; i<dropTargets.length; i++){
			var targ  = dropTargets[i];
			var targPos    = getPosition(targ);
			var targWidth  = parseInt(targ.offsetWidth);
			var targHeight = parseInt(targ.offsetHeight);

			dropTargetRectangles.push({
				xmin: targPos.x,
				xmax: targPos.x + targWidth,
				ymin: targPos.y,
				ymax: targPos.y + targHeight,
				dropTarget: targ
			})
		}
	}

    function mouseUp(ev){
		if (currentDropTarget) {
			alert("перенесен объект dragObject на акцептор currentDropTarget")
			showRollOff(currentDropTarget)
		}

		dragObject = null
		removeDocumentEventHandlers()
    }

	function getCurrentTarget(e) {
		for(var i=0; i<dropTargetRectangles.length; i++){
			var rect  = dropTargetRectangles[i];

			if(
			(e.pageX > rect.xmin)  &&
			(e.pageX < rect.xmax)  &&
			(e.pageY > rect.ymin)  &&
			(e.pageY < rect.ymax)){
				return rect.dropTarget
			}
		}

		return null
	}

	function mouseMove(e){
		e = fixEvent(e)

		with(dragObject.style) {
			position = 'absolute'
			top = e.pageY - mouseOffset.y + 'px'
			left = e.pageX - mouseOffset.x + 'px'
		}

		var newTarget = getCurrentTarget(e)

		if (currentDropTarget && currentDropTarget != newTarget) {
			showRollOff(currentDropTarget)
		}

		currentDropTarget = newTarget
		if (newTarget) {
			showRollOn(newTarget)
		}

		return false;
    }

	function showRollOn(elem) {
		elem.className = 'uponMe'
	}

	function showRollOff(elem) {
		elem.className = ''
	}

	function mouseDown(e) {
		e = fixEvent(e)
		if (e.which!=1) return
		
		dragObject  = this
		mouseOffset = getMouseOffset(this, e)

		cacheDropTargetRectangles()

		addDocumentEventHandlers()

		return false
	}


	function getMouseOffset(target, e) {...}

	function addDocumentEventHandlers() {...}
	function removeDocumentEventHandlers() {...}

    return {
		...
    }
}())
var dragMaster3 = (function() {

    var dragObject
    var mouseOffset

    var dropTargets = []

    var dropTargetRectangles

	var currentDropTarget

	function cacheDropTargetRectangles() {
		dropTargetRectangles = []

		for(var i=0; i<dropTargets.length; i++){
			var targ  = dropTargets[i];
			var targPos    = getPosition(targ);
			var targWidth  = parseInt(targ.offsetWidth);
			var targHeight = parseInt(targ.offsetHeight);

			dropTargetRectangles.push({
				xmin: targPos.x,
				xmax: targPos.x + targWidth,
				ymin: targPos.y,
				ymax: targPos.y + targHeight,
				dropTarget: targ
			})
		}

	}

    function mouseUp(ev){
		if (currentDropTarget) {
			alert("droped dragObject into curTarget")
			showRollOff(currentDropTarget)
		}

		dragObject = null
		removeDocumentEventHandlers()
    }

	function removeDocumentEventHandlers() {
		document.onmousemove = null
		document.onmouseup = null
		document.ondragstart = null
		document.body.onselectstart = null
	}

	function getMouseOffset(target, e) {
		var docPos	= getPosition(target)
		return {x:e.pageX - docPos.x, y:e.pageY - docPos.y}
	}

	function getCurrentTarget(e) {
		for(var i=0; i<dropTargetRectangles.length; i++){
			var rect  = dropTargetRectangles[i];

			if(
			(e.pageX > rect.xmin)  &&
			(e.pageX < rect.xmax)  &&
			(e.pageY > rect.ymin)  &&
			(e.pageY < rect.ymax)){
				return rect.dropTarget
			}
		}

		return null
	}


	function mouseMove(e){
		e = fixEvent(e)

		with(dragObject.style) {
			position = 'absolute'
			top = e.pageY - mouseOffset.y + 'px'
			left = e.pageX - mouseOffset.x + 'px'
		}

		var newTarget = getCurrentTarget(e)

		if (currentDropTarget && currentDropTarget != newTarget) {
			showRollOff(currentDropTarget)
		}

		currentDropTarget = newTarget
		if (newTarget) {
			showRollOn(newTarget)
		}

		return false;
    }

	function showRollOn(elem) {
		elem.className = 'uponMe'
	}

	function showRollOff(elem) {
		elem.className = ''
	}

	function mouseDown(e) {
		e = fixEvent(e)
		if (e.which!=1) return

		dragObject  = this
		mouseOffset = getMouseOffset(this, e)

		cacheDropTargetRectangles()

		addDocumentEventHandlers()

		return false
	}

	function addDocumentEventHandlers() {
		document.onmousemove = mouseMove
		document.onmouseup = mouseUp

		// отменить перенос и выделение текста при клике на тексте
		document.ondragstart = function() { return false }
		document.body.onselectstart = function() { return false }
	}


    return {

		makeDraggable: function(element){
			element.onmousedown = mouseDown
		},

		addDropTarget: function(dropTarget){
			dropTargets.push(dropTarget)
		}


    }
}())

function getPosition(e){
	var left = 0;
	var top  = 0;

	while (e.offsetParent){
		left += e.offsetLeft;
		top  += e.offsetTop;
		e     = e.offsetParent;
	}

	left += e.offsetLeft;
	top  += e.offsetTop;

	return {x:left, y:top};
}

Следуя общему принципу отделения мух от котлет - лучше отделить простой клик на объекте от начала drag and drop.

Еще одна причина - дорогая инициализация drag & drop: нужно прокешировать все возможные акцепторы. Совершенно не обязательно это делать на mousedown, если имеем простой клик.

Как отделить? Очень просто:

  1. При mousedown запомнить координаты и объект, но пока не начинать перенос
  2. Если произошло событие mouseup - это был всего лишь клик, сбросить координаты
  3. В mousemove проверить: если есть запомненные координаты и курсор отошел от них хотя бы на 2 пикселя - начать перенос

В коде этой демки стоит расстояние не 2, а 25 пикселей, в целях наглядности происходящего.

До перемещения курсора на 25 пикселей вверх или вниз перенос не начнется.


Отрыв от земли - после перемещения на 25 или больше пикселей


Код демо:

var dragMaster = (function() {

	...

	var mouseDownAt

	function mouseDown(e) {
		e = fixEvent(e)
		if (e.which!=1) return

 		mouseDownAt = { x: e.pageX, y: e.pageY, dragObject: this }

		addDocumentEventHandlers()

		return false
	}

	function mouseMove(e){
		e = fixEvent(e)

		if (mouseDownAt) {
			if (Math.abs(mouseDownAt.x-e.pageX)<25 && Math.abs(mouseDownAt.y-e.pageY)<25) {
				// слишком близко, возможно это клик
				return
			}
			// курсор нажатой мыши отвели далеко - начинаем перенос
			dragObject  = mouseDownAt.dragObject
			mouseOffset = getMouseOffset(dragObject, mouseDownAt.x, mouseDownAt.y)
			cacheDropTargetRectangles()
			// запомненные координаты нам больше не нужны
			mouseDownAt = null
		}

		showDrag(e) // показать перенос

		return false;
    }

    function mouseUp(ev){
		if (!dragObject) {
			// ничего не начали нести, был просто клик
			mouseDownAt = null
		} else {
			// чего-то несем - обрабатываем конец переноса
			if (currentDropTarget) {
				alert("перенесен объект dragObject на акцептор currentDropTarget")
				showRollOff(currentDropTarget)
			}

			dragObject = null
		}

		// (возможный) drag and drop завершен
		removeDocumentEventHandlers()
    }

	function showDrag(e) {
		// перенести объект
		with(dragObject.style) {
			position = 'absolute'
			top = e.pageY - mouseOffset.y + 'px'
			left = e.pageX - mouseOffset.x + 'px'
		}

		// подсветить акцептор
		var newTarget = getCurrentTarget(e)

		if (currentDropTarget && currentDropTarget != newTarget) {
			showRollOff(currentDropTarget)
		}

		currentDropTarget = newTarget
		if (newTarget) {
			showRollOn(newTarget)
		}
	}

	function getMouseOffset(target, x, y) {
		// для удобства поменяли синтаксис: x/y вместо event
		var docPos	= getPosition(target)
		return {x:x - docPos.x, y:y - docPos.y}
	}

	function cacheDropTargetRectangles() {...}

	function removeDocumentEventHandlers() {...}

	function getCurrentTarget(e) {...}

	function showRollOn(elem) {...}

	function showRollOff(elem) {...}

	function addDocumentEventHandlers() {...}


    return {
		...
    }
}())
var dropMove2 = (function() {

    var dragObject
    var mouseOffset
	var mouseDownAt

    var dropTargets = []


    var dropTargetRectangles

	var currentDropTarget

	function cacheDropTargetRectangles() {
		dropTargetRectangles = []

		for(var i=0; i<dropTargets.length; i++){
			var targ  = dropTargets[i];
			var targPos    = getPosition(targ);
			var targWidth  = parseInt(targ.offsetWidth);
			var targHeight = parseInt(targ.offsetHeight);

			dropTargetRectangles.push({
				xmin: targPos.x,
				xmax: targPos.x + targWidth,
				ymin: targPos.y,
				ymax: targPos.y + targHeight,
				dropTarget: targ
			})
		}
	}


    function mouseUp(ev){
		if (!dragObject) {
			mouseDownAt = null
		} else {
			if (currentDropTarget) {
				alert("перенесен объект dragObject на акцептор currentDropTarget")
				showRollOff(currentDropTarget)
			}

			dragObject = null
		}

		removeDocumentEventHandlers()
    }

	function removeDocumentEventHandlers() {
		document.onmousemove = null
		document.onmouseup = null
		document.ondragstart = null
		document.body.onselectstart = null
	}

	function getMouseOffset(target, x, y) {
		var docPos	= getPosition(target)
		return {x:x - docPos.x, y:y - docPos.y}
	}

	function getCurrentTarget(e) {
		for(var i=0; i<dropTargetRectangles.length; i++){
			var rect  = dropTargetRectangles[i];

			if(
			(e.pageX > rect.xmin)  &&
			(e.pageX < rect.xmax)  &&
			(e.pageY > rect.ymin)  &&
			(e.pageY < rect.ymax)){
				return rect.dropTarget
			}
		}

		return null
	}


	function mouseMove(e){
		e = fixEvent(e)

		if (mouseDownAt) {
			if (Math.abs(mouseDownAt.x-e.pageX)<25 && Math.abs(mouseDownAt.y-e.pageY)<25) {
				return
			}
			dragObject  = mouseDownAt.dragObject
			mouseOffset = getMouseOffset(dragObject, mouseDownAt.x, mouseDownAt.y)
			cacheDropTargetRectangles()
			mouseDownAt = null
		}

		showDrag(e)

		return false;
    }

	function showDrag(e) {

		with(dragObject.style) {
			position = 'absolute'
			top = e.pageY - mouseOffset.y + 'px'
			left = e.pageX - mouseOffset.x + 'px'
		}

		var newTarget = getCurrentTarget(e)

		if (currentDropTarget && currentDropTarget != newTarget) {
			showRollOff(currentDropTarget)
		}

		currentDropTarget = newTarget
		if (newTarget) {
			showRollOn(newTarget)
		}
	}

	function showRollOn(elem) {
		elem.className = 'uponMe'
	}

	function showRollOff(elem) {
		elem.className = ''
	}

	function mouseDown(e) {
		e = fixEvent(e)
		if (e.which!=1) return

 		mouseDownAt = { x: e.pageX, y: e.pageY, dragObject: this }

		addDocumentEventHandlers()

		return false
	}

	function addDocumentEventHandlers() {
		document.onmousemove = mouseMove
		document.onmouseup = mouseUp

		// отменить перенос и выделение текста при клике на тексте
		document.ondragstart = function() { return false }
		document.body.onselectstart = function() { return false }
	}


    return {

		makeDraggable: function(element){
			element.onmousedown = mouseDown
		},

		addDropTarget: function(dropTarget){
			dropTargets.push(dropTarget)
		}


    }
}())

function getPosition(e){
	var left = 0;
	var top  = 0;

	while (e.offsetParent){
		left += e.offsetLeft;
		top  += e.offsetTop;
		e     = e.offsetParent;
	}

	left += e.offsetLeft;
	top  += e.offsetTop;

	return {x:left, y:top};
}

Бывает, что возможных акцепторов очень много. Тогда код, кеширующий прямоугольники при начале переноса, будет тормозить.

Визуально это проявляется как задержка от клика на объекте до его фактического переноса - потому что долго, с 100% поеданием одного ядра CPU обрабатывается mousedown.

Речь тут идет о 100 акцепторах или больше - например, при переносе между длинными списками или деревьями. Хотя какие-то тормоза могут быть заметны и от 50.

Принципиальных решений здесь два.

Этот малоизвестный метод работает во всех браузерах и возвращает элемент по координатам на странице.

Firefox/IE используют для этого clientX/Y, а Opera, Chrome и Safari - pageX/Y.

Возвращенный элемент является самым глубоко вложенным на точке с координатами (x,y). Это может быть текстовый узел в том числе.
Следуя по цепочке родителей, легко найти нужного акцептора.

На время вызова elementFromPoint необходимо спрятать переносимый элемент, чтобы он не закрывал акцептора.

Псевдокод будет выглядеть так:

function getCurrentTarget(e) {
  dragObject.style.display = 'none' // спрятать

  if (navigator.userAgent.match('MSIE') || navigator.userAgent.match('Gecko')) {
       // IE || FF
	var elem = document.elementFromPoint(e.clientX,e.clientY)
  } else {
	var elem = document.elementFromPoint(e.pageX,e.pageY)
  }
  dragObject.style.display = ''  // показать побыстрее

  while (elem!= null) {
        if (elem является акцептором) {
            return elem
        }
        elem = elem.parentNode
  }

  // не нашли 
  return null
}

Если структура объекта переноса нам известна - возможно, кешировать каждый элемент ни к чему?

Например, мы переносим объект в список акцепторов:

<ul class="articles-list">
  <li>...</li>
  <li>...</li> 
  <li>...</li>
</ul>

И мы знаем, что каждый элемент имеет фиксированную высоту и отступы:

.articles-list li {
  height: 20px;
  margin: 0px;
  padding: 0px;
}

В таком случае, можно вычислить номер LI, разделив общую высоту контейнера на высоту акцептора.

function getCurrentTarget(e) {
  var rect = /* ограничивающий прямоугольник для UL */

  // (ulX, ulY) - координаты относительно верхнего-левого угла контейнера
  var ulX= e.pageX - rect.xmin
  var ulY = e.pageY - rect.ymin

  if ( ulX < 0 || ulX > rect.xmax-rect.xmin || ulY < 0 || ulY > rect.ymax-rect.ymin) {
    /* событие за границами списка */
    return null
  }

  /* по 20px на каждого ребенка-акцептора */
  var childNum = ulY / 20  

  return rect.dropTarget.childNodes[childNum]
}

Конечно, такая оптимизация возможна не всегда и зависит от конкретной задачи.

В ряде задач допустимо не сохранять переносимый элемент под курсором, а передвинуть его на несколько пикселей в сторону, то mouseUp и mouseMove будут срабатывать уже на акцепторе, который станет возможным получить из event.target.

Да, получится не так красиво, но это может быть единственным выходом.

Можно переносить не сам объект, а его "аватар", схематическое изображение.

Это может быть полезно:

для ускорения отрисовки
например, при переносе сложного блока можно переносить такого же размера серый прямоугольник
чтобы убрать элемент из-под курсора
переносить сам объект в стороне от мыши - может быть некрасиво. А обозначить перенос небольшой аватаркой рядом с курсором - уже другое дело.

Код dragMaster'а на текущий момент сочетает весь функционал по отслеживанию переноса, отображению и опусканию на акцептор.

Целесообразно его разделить на три компоненты: переносимые объекты, цели переноса (акцепторы) и менеджер, который следит, что куда переносится.

Объект DragObject - обобщенный переносимый объект, который привязывается к DOM-элементу. Он представлен на следующем листинге.

function DragObject(element) {
	element.dragObject = this
	
	dragMaster.makeDraggable(element)
	
	var rememberPosition
	var mouseOffset
	
	this.onDragStart = function(offset) {
		var s = element.style
		rememberPosition = {top: s.top, left: s.left, position: s.position}
		s.position = 'absolute'
		
		mouseOffset = offset
	}
		
	this.hide = function() {
		element.style.display = 'none' 
	}
	
	this.show = function() {
		element.style.display = '' 
	}
	
	this.onDragMove = function(x, y) {
		element.style.top =  y - mouseOffset.y +'px'
		element.style.left = x - mouseOffset.x +'px'
	}
	
	this.onDragSuccess = function(dropTarget) { }
	
	this.onDragFail = function() {
		var s = element.style
		s.top = rememberPosition.top
		s.left = rememberPosition.left
		s.position = rememberPosition.position
	}
	
	this.toString = function() {
		return element.id
	}
}

Использование:

new DragObject(element)

Достаточно одного вызова new, т.к функция DragObject добавляет конструируемый объект к element в свойство element.dragObject:

...
element.dragObject = this
...

Методы:

onDragStart(offset)
Вызывается при начале переноса. В текущей реализации отрывает объект "от земли" и запоминает текущую позицию в rememberPosition и сдвиг курсора мыши от левого-верхнего угла объекта в mouseOffset.

В другой реализации может показывать перенос как-то по-другому, например создавать "переносимый клон" объекта.

onDragMove(x, y)
Вызывается при переносе объекта в координаты (x,y). Отображает перенос.
onDragFail()
Обрабатывает неудачный перенос. В текущей реализации возвращает объект на старые координаты.

Вообще говоря, можно делать это с анимацией, показывая как объект "перелетает" на старое место.

onDragSuccess(dropTarget)
Обрабатывает успешный перенос. В текущей реализации обработка успешного переноса целиком сосредоточена у принимающего объекта DropTaget, поэтому эта функция пустая.
show/hide()
Показать/спрятать переносимый объект - вспомогательные методы

DropTarget - обобщенный объект-акцептор, потенциальная цель переноса. Может быть большим контейнером или маленьким элементом - не важно.

Как показано дальше, поддерживаются вложенные DropTarget: объект будет положен туда, куда следует, вне зависимости от степени вложенности.

function DropTarget(element) {
	
	element.dropTarget = this
	
	this.canAccept = function(dragObject) {
		return true
	}
	
	this.accept = function(dragObject) {
		this.onLeave()
		
		dragObject.hide()
		
		alert("Акцептор '"+this+"': принял объект '"+dragObject+"'")
	}
	
	this.onLeave = function() {
		element.className =  ''
	}
	
	this.onEnter = function() {
		element.className = 'uponMe'
	}
	
	this.toString = function() {
		return element.id
	}
}

Методов у DropTarget поменьше, чем у DragObject. Еще бы, акцептору ведь не нужно анимировать собственный перенос.

canAccept(dragObject)
При проносе объекта над DropTarget, dragMaster спросит у акцептора, может ли он принять dragObject. Если нет - dragMaster проигнорирует этот акцептор.

В текущей реализации всегда возвращает true, то есть положить можно. Вообще говоря, может проверять класс переносимого объекта:

this.canAccept = function(dragObject) {
    // могу принять только объекты типа TreeNodeDragObject 
    return dragObject instanceof TreeNodeDragObject
}
accept(dragObject)
Принимает переносимый объект. Объект может быть перемещен(в другой каталог) или уничтожен(корзина) - зависит от вашей логики обработки переноса.

В конкретном приложении стоит посмотреть особо, какую часть логики конца переноса стоит поместить DragObject#onDragSuccess, а какую - в DropTarget#accept. Как правило, основную логику переноса удобно сосредоточить у DropTarget.

onLeave/onEnter
Анимация возможности положить объект на акцептор. Как правило, акцептор при этом подсвечивается. Эти методы будут вызваны только если акцептор может принять (canAccept) переносимый объект.

Ну и, наконец, похудевший и лишенный большинства обязанностей dragMaster.

var dragMaster = (function() {

    var dragObject
    var mouseDownAt

	var currentDropTarget

	
	function mouseDown(e) {
		e = fixEvent(e)
		if (e.which!=1) return

 		mouseDownAt = { x: e.pageX, y: e.pageY, element: this }

		addDocumentEventHandlers()

		return false
	}


	function mouseMove(e){
		e = fixEvent(e)

		// (1)
		if (mouseDownAt) {
			if (Math.abs(mouseDownAt.x-e.pageX)<5 && Math.abs(mouseDownAt.y-e.pageY)<5) {
				return false
			}
			// Начать перенос
			var elem  = mouseDownAt.element
			// текущий объект для переноса
			dragObject = elem.dragObject
			
			// запомнить, с каких относительных координат начался перенос
			var mouseOffset = getMouseOffset(elem, mouseDownAt.x, mouseDownAt.y)
			mouseDownAt = null // запомненное значение больше не нужно, сдвиг уже вычислен
			
			dragObject.onDragStart(mouseOffset) // начали
			
		}

		// (2)
		dragObject.onDragMove(e.pageX, e.pageY)
		
		// (3)
		var newTarget = getCurrentTarget(e)
		
		// (4)
		if (currentDropTarget != newTarget) {
			if (currentDropTarget) {
				currentDropTarget.onLeave()
			}
			if (newTarget) {
				newTarget.onEnter()
			}
			currentDropTarget = newTarget

		}
		
		// (5)
		return false
    }
	
	
    function mouseUp(){
		if (!dragObject) { // (1)
			mouseDownAt = null
		} else {
			// (2)
			if (currentDropTarget) {
				currentDropTarget.accept(dragObject)
				dragObject.onDragSuccess(currentDropTarget)
			} else {
				dragObject.onDragFail()
			}

			dragObject = null
		}

		// (3)
		removeDocumentEventHandlers()
    }


	function getMouseOffset(target, x, y) {
		var docPos	= getOffset(target)
		return {x:x - docPos.left, y:y - docPos.top}
	}

	
	function getCurrentTarget(e) {
		// спрятать объект, получить элемент под ним - и тут же показать опять
		
		if (navigator.userAgent.match('MSIE') || navigator.userAgent.match('Gecko')) {
			var x=e.clientX, y=e.clientY
		} else {
			var x=e.pageX, y=e.pageY
		}
		// чтобы не было заметно мигание - максимально снизим время от hide до show
		dragObject.hide()
		var elem = document.elementFromPoint(x,y)
		dragObject.show()
		
		// найти самую вложенную dropTarget
		while (elem) {
			// которая может принять dragObject 
			if (elem.dropTarget && elem.dropTarget.canAccept(dragObject)) {
				return elem.dropTarget
			}
			elem = elem.parentNode
		}
		
		// dropTarget не нашли
		return null
	}


	function addDocumentEventHandlers() {
		document.onmousemove = mouseMove
		document.onmouseup = mouseUp
		document.ondragstart = document.body.onselectstart = function() {return false}
	}
	function removeDocumentEventHandlers() {
		document.onmousemove = document.onmouseup = document.ondragstart = document.body.onselectstart = null
	}


    return {

		makeDraggable: function(element){
			element.onmousedown = mouseDown
		}
    }
}())
mouseDown(e)
Метод не изменился. Он запоминает позицию, на которой произошло нажатие кнопки мыши mouseDownAt и добавляет остальные обработчики слежения за переносом addDocumentEventHandlers().
mouseMove(e)
Этот обработчик присоединяется к document в момент mouseDown.

  1. Если в момент его срабатывания есть запомненное нажатие и курсор отошел больше чем на 5 пикселей - значит начался перенос. DragObject'у это сообщается вызовом onDragStart с передачей текущего сдвига курсора относительно левого-верхнего угла mouseOffset.
  2. Объект информируется о текущих координатах переноса.
  3. Вычисляем текущий акцептор при помощи модифицированного метода getCurrentTarget (см. выше в разделе "Оптимизация").
  4. Обработать нового акцептора и уход со старого акцептора.
  5. Вернуть false для блокирования действий браузера и всплывания события mouseMove
mouseUp()
Этот обработчик завершает (возможный) перенос. Обратите внимание - само событие в данном случае не нужно. Весь процесс переноса отслеживается событием mouseMove.

  1. Если объект переноса не установлен, значит перед этим было простое нажатие mouseDown, элемент не отнесли на 5 пикселей в сторону - это не drag'n'drop.
  2. При наличии акцептора - перенос успешно завершается, если акцептора нет - отменяется.
  3. В любом случае в конце все обработчики с документа снимаются

Функция определения позиции элемента getPosition заменена на более точный вариант getOffset, описанный в статье про определение координат.

Скачать пакет из итогового фреймворка и демок можно здесь.

В комплекте - пара демок. На этой странице обе подключены в iframe'ах.

Открыть в отдельном окне: demo.html

В этой демке акцепторами являются два вложенных DIV'а.

Открыть в отдельном окне: nested.html

В процессе переноса акцепторы объекты могут сдвигаться, освобождая место.

Если вы используете кеш координат акцепторов, то при этом производится соответствующее обновление кеша, но вместо полного перевычисления обновляются только координаты сдвинувшихся объектов.

Например, при раздвижении списка вниз - увеличиваются Y-координаты всех сдвинувшихся LI.
При этом для удобного обновления кеш делается не массивом, а объектом с доступом по ID элемента, так чтобы можно было легко обновить именно нужные координаты.

Иногда, например, при смене позиции элемента в списке, объект переносится не на акцептор, а между акцепторами. Как правило, "между" - имеется в виду по высоте.

Для этого логику определения currentDropTarget нужно поменять. Возможно два варианта:

Допустим перенос как между, так и над
В этом случае акцептор делится на 3 части по высоте clientHeight: 25% - 50% - 25%, и определяется попадание координаты события на нужную часть.
Перенос только между
Акцептор делится на две части: 50% - 50%

Кроме того, в дополнение к текущему акцептору currentDropTarget добавляется флаг, обозначающий, куда относительно акцептора происходит перенос.

Индикацию "переноса между" удобнее всего делать либо раздвижением элементов, либо показом полосы-индикатора border-top/border-bottom, как показано на рисунке ниже:

Обычно при переносе объекта куда-либо посетитель может просто отпустить его в любом месте.

При этом drag and drop фреймворк анимирует отмену переноса. Один из частых вариантов - скольжение объекта обратно к исходному месту, откуда его взяли.
Конечно, для этого исходное место необходимо запомнить.

Если перетаскивается аватарка(клон), то можно его и просто уничтожить.

Совершенно не факт, что любой объект можно перенести на любой аксептор.
Как правило, все с точностью наоборот.

Акцептор может быть недоступен по двум причинам:

  • либо это несовпадение типов, для этого drag and drop должен предусматривать различные типы объектов/акцепторов и проверку их соответствия
  • либо у посетителя недостаточно для этого прав, в рамках наложенных CMS ограничений

При переносе над недоступным акцептором - getCurrentTarget просто возвращает null.

Иногда проверку прав и результата переноса необходимо делать на сервере. Как правило, такую проверку выполняют только при mouseUp, чтобы не нагружать сервер излишними запросами во время mouseMove.

Здесь используется два подхода

Синхронный XmlHttpRequest
Запрос отправляется синхронно, чтобы не нарушать общий поток выполнения. Все хорошо, вот только браузер видимо подвисает при отпускании кнопки мыши. Да и другие недостатки у синхронного запроса есть.
Отложенная отмена переноса
Более продвинутый и удобный вариант. Основан на "оптимистичном" сценарии, по которому перенос, как правило, проходит успешно.

  1. Посетитель кладет объект туда, куда он хочет.
  2. Фреймворк перемещает объект, и затем "в фоне" делает асинхронный запрос к серверу. На запрос вешается callback, содержащий информацию, откуда был перенесен объект.
  3. Сервер обрабатывает перенос и возвращает ответ, все ли в порядке.
  4. Если нет - callback выводит ошибку и объект возвращается туда, откуда был перенесен.

Вы узнали основные принципы и особенности реализации drag and drop в яваскрипт.

Рассмотрели ряд оптимизаций и различные организации переноса на уровне UI.

Все современные javascript-библиотеки используют описанную схему и какие-то (не все) из оптимизаций.

Пакет из итогового фреймворка и демок доступен здесь.


Автор: 토토사이트 (не зарегистрирован), дата: 1 сентября, 2023 - 04:38
#permalink

Your work is very good and I appreciate you and hopping for some more informative posts. 토토사이트


Автор: поздравления с днём рождения (не зарегистрирован), дата: 5 сентября, 2023 - 05:55
#permalink

Thank you for creating a blog that not only informs but also motivates. Your positivity and поздравление с днем рождения enthusiasm are contagious. Your words have the power to uplift and inspire readers like me. Please keep spreading your light through your incredible blog!


Автор: window replacement cost phoenix (не зарегистрирован), дата: 7 сентября, 2023 - 10:56
#permalink

Thanks for an interesting blog. What else may I get that sort of info written in such a perfect approach? I have an undertaking that I am just now operating on, and I have been on the lookout for such info.window replacement cost phoenix


Автор: shower toga (не зарегистрирован), дата: 8 сентября, 2023 - 09:12
#permalink

It is ideal to read blogs like this. Have you ever wondered about the benefits of using Shower Toga? No worries if you haven't! It enhances your outdoor experience while promoting ethical and ecological practices like reducing water waste and offering an eco-conscious option. Experience a seamless, rejuvenating, and environmentally responsible outdoor shower solution. Get more details in the article.


Автор: Private Instagram Profiles (не зарегистрирован), дата: 8 сентября, 2023 - 12:31
#permalink

Thanks for sharing such an informative post. Do you know guys how to view someone's private Instagram profiles? If you don't know, Don't worry. Now, you can view private Instagram profiles using the Instagram Private Profile Viewer free tool. More information about the tool is provided in the article. Visit the article and get more information about it.


Автор: ambbet-เว็บตรง (не зарегистрирован), дата: 12 сентября, 2023 - 09:09
#permalink

ambbet-เว็บตรง ไม่มีการบริการใดเทียบเท่า ของเราอีกแล้ว เราพร้อมให้บริการทุกท่าน ทุกระดับประทับใจแน่นอน เรามีสิ่งอำนวยความสะดวกในการ ฝากถอน ด้วยระบบไอแบ้งกิ้ง สามารถเข้าเล่นได้ทุกที่ทุกเวลา


Автор: ambbet-plus (не зарегистрирован), дата: 12 сентября, 2023 - 11:02
#permalink

ambbet-plus เดิมพัน ของเราวันนี้มีโปรโมชั่นเพียบ สมัครเลย ด้วยเกมเดิมพันที่หลากหลายให้เล่น แทงบอลเป็นหลัก คาสิโนเป็นรอง พร้อมที่จะบริการลูกค้าทุกท่านให้ไปได้ไกลที่สุด แตกล้านจ่ายล้าน แน่นอนมั่นใจที่นี่ที่เดียว


Автор: UFABET168GO (не зарегистрирован), дата: 15 сентября, 2023 - 16:50
#permalink

ufabet168 vip เว็บตรงแทงบอล และคาสิโนออนไลน์ดีที่สุด สล็อต PG ทดลองเล่น เล่นฟรีทุกค่าย เล่นได้ก่อนใคร


Автор: หวยออนไลน์-เล่นยังไง (не зарегистрирован), дата: 18 сентября, 2023 - 11:45
#permalink

มาเองงานนี้รับรองว่า ปลอดภัย 100% ลูกค้าจะได้รับประสบการ์ณการเดิมพันที่ดี เรามีโปรโมชั่นให้เลือกมากมาย พร้อมกับการบริการตอบแทนลูกค้าที่ไว้วางใจเล่นกับเราตลอดมา
หวยออนไลน์-เล่นยังไง


Автор: SERAS (не зарегистрирован), дата: 23 сентября, 2023 - 16:45
#permalink

https://bigbrothertitansn.com/

bigbrothertitansn.com

Big Brother Titans is the joint South African and Nigerian edition of the Big Brother franchise. The theme for the first season was "Ziyakhala Wahala".


Автор: SERAS (не зарегистрирован), дата: 23 сентября, 2023 - 16:45
#permalink

https://onlineiptvplayerz.com/

onlineiptvplayerz.com

IPTV is defined as the secure and reliable delivery to subscribers of entertainment video and related services.


Автор: SERAS (не зарегистрирован), дата: 23 сентября, 2023 - 16:46
#permalink

https://dramaz24.com/

Watch best and latest K-Dramas on interenet online, without ads or anything for Free

dramaz24.com

Watch best and latest K-Dramas on interenet online, without ads or anything for Free.


Автор: SERAS (не зарегистрирован), дата: 23 сентября, 2023 - 16:46
#permalink

https://bigbrothervipkosovalive.com/

bigbrothervipkosovalive.com

Kur do filloj big brother vip kosova dhe si do te jete edicioni i radhes? Njeri nder formatet me te medha televizive ne Kosove.

===

https://bbalbaniavip.com/

bbalbaniavip.com

Edicioni dhe teleshou me i ndjekur ne te gjitha trojet shqiptare Big Brother VIP Albania. Shiko Live Big Brother Albania LIVE ne webfaqen tone.


Автор: 먹튀위키 (не зарегистрирован), дата: 26 сентября, 2023 - 08:35
#permalink

We still cannot quite believe that I was able to often be any type of those staring at the important points located on your blog post. 먹튀위키


Автор: Гость (не зарегистрирован), дата: 6 октября, 2023 - 13:44
#permalink

Everyone is well aware of the importance of safety when
it comes to online sports betting. My website has a lot of
useful sports news information, check it out now.
토토사이트 안전성


Автор: Гость (не зарегистрирован), дата: 8 октября, 2023 - 15:23
#permalink

يتميز فلتر مياه كوجين بفعاليته في تنقية المياه ب سعر فلتر كوجين المناسب، وهو ما يجعله الخيار المثالي لكل الفئات. ومن خلال ، يمكن الحصول على فلتر مياه كوجين 7 مراحل تايواني بسعر مناسب في عام 2023. بالإضافة إلى ذلك، يتميز هذا الفلتر بالهاوسنجات القوية والمضخة الفعالة، بالإضافة إلى أنه قابل للصيانة على المدى الطويل. كما يتضمن هذا الفلتر الشمعات القادرة على تنقية المياه بشكل مثالي، مما يجعله الخيار الأمثل لمن يبحثون عن جودة عالية بأسعار مناسبة. ويمكن الحصول على فلتر كوجين 7 مراحل تايواني عبر بسعر مناسب وبضمان الوكيل.3400 جنيه

https://waterfiltereg.com/


Автор: bchilwell168 (не зарегистрирован), дата: 15 октября, 2023 - 22:12
#permalink

Your article is very good, I can help you if you need mobile sounds now free ringtones sonnerieandroid.com will help you.


Автор: Гость (не зарегистрирован), дата: 18 октября, 2023 - 14:39
#permalink

https://fsonline.cc/

fsonline.cc

Filme si Seriale Online cu Subtitrare in Romana - FS Online Filme online HD si seriale online HD cu subtitrare in romana pe fsonlinehd
================================================================

https://yabancidizion.com/

yabancidizion.com

Yabancı Dizi – Yabancı Diziler ve Filmler Izle - Yabancı Dizi izle Türkçe Altyazılı Orjinal Diziler ve Türkçe Dublaj Yabancı Diziler.

==================================================================

https://turksub24.net/

turksub24.net

Turkish Series with English Subtitles - Full HD. Watch Turkish Series with English Subtitles for free!

---

https://diziizle.cc/

diziizle.cc

Dizi Izle – Diziler Izleme, Yerli Diziler, Full Dizi - Kaçırdığınız dizi bölümlerini ve sinema filmlerini full hd kalitesinde.


Автор: Гость (не зарегистрирован), дата: 18 октября, 2023 - 14:39
#permalink

https://fsonline.cc/

fsonline.cc

Filme si Seriale Online cu Subtitrare in Romana - FS Online Filme online HD si seriale online HD cu subtitrare in romana pe fsonlinehd
================================================================

https://yabancidizion.com/

yabancidizion.com

Yabancı Dizi – Yabancı Diziler ve Filmler Izle - Yabancı Dizi izle Türkçe Altyazılı Orjinal Diziler ve Türkçe Dublaj Yabancı Diziler.

==================================================================

https://turksub24.net/

turksub24.net

Turkish Series with English Subtitles - Full HD. Watch Turkish Series with English Subtitles for free!

---

https://diziizle.cc/

diziizle.cc

Dizi Izle – Diziler Izleme, Yerli Diziler, Full Dizi - Kaçırdığınız dizi bölümlerini ve sinema filmlerini full hd kalitesinde.


Автор: 1980s women's fashion (не зарегистрирован), дата: 19 октября, 2023 - 19:50
#permalink

Pale skin was considered fashionable for both men and women. Lead-based cosmetics were used to achieve this look, which unfortunately had harmful health effects.


Автор: ranker (не зарегистрирован), дата: 28 октября, 2023 - 12:35
#permalink

Glad to chat your blog, I seem to be forward to more reliable articles and I think we all wish to thank so many good articles, blog to share with us. Suika


Автор: ufabet168 (не зарегистрирован), дата: 11 ноября, 2023 - 21:11
#permalink

Helpful information. Fortunate me I found your site unintentionally, and I am shocked why this coincidence did not took place earlier! I bookmarked it. ufabet168


Автор: BIOBETGAMING (не зарегистрирован), дата: 12 ноября, 2023 - 17:44
#permalink

I really appreciate this wonderful post that you have provided for us. I assure this would be beneficial for most of the people. ทางเข้า biobet" title="ทางเข้า biobet">ทางเข้า biobet


Автор: UFABET168GO (не зарегистрирован), дата: 15 ноября, 2023 - 17:22
#permalink

ufabet168 เว็บตรง เสถียร ใช้งานง่ายที่สุด


Автор: betflix vip (не зарегистрирован), дата: 19 ноября, 2023 - 17:20
#permalink

Hello. Cool article. There’s an issue with the website and you might want to test this… The browser is the marketplace chief and a large element of other folks will miss your great writing due to this problem. betflix vip


Автор: Гость (не зарегистрирован), дата: 20 ноября, 2023 - 10:36
#permalink

فلتر فلوكستك 7 مراحل فخر الصناعة التايواني
يتميز فلتر فلوكستك بالكفاءة والجوده العالمية
فهو يعمل بنظام التناضح العكسي RO SYSTEM
ويتميز فلتر فلوكس تك بجودة خاماته واحتوائه على شمعة الكالاين التي ترفع قلوية المياه وخزان تايواني أصلي ٣ طبقات ١٢ لتر وحنفية من الاستانلس ستيل الخالص غير القابل للصدا وشمعات من كربون جوز الهند ، كما أن موتور الفلتر وكل شمعة من شمعات عليها باركود دولي مرتبط بمصنع فلوكستك مباشرة ، ويمكنك فحص الباركود من خلال موبايلك والتأكد من أنه أصلي .

تعرف معنا على افضل خصومات وعروض سعر فلتر فلوكستك فى مصر


Автор: Гость (не зарегистрирован), дата: 21 ноября, 2023 - 18:06
#permalink

افضل الخصومات لتكييف تكييف يونيون اير 2023 استمتع الان مع شركة تكييف يونيون اير بافضل الخصوصات الجديده على كل الاجهزه التى توجد لدينا ، يعنى هتقدر تشترى الجهاز اللى نفسك فيه وباقل الاسعار اللى مش هتلاقيها غير بس عندنا . تمتعنا شركة تكييف يونيون اير بافضل العروض على الاجهزه المكيفه لانها تريد دائما توفير ما يرضى العميل وليس المال وده ما يجعلنا دائما الشركة الاولى فى مصر وخارجها . الان هتقدر تحصل على تكييف يونيون اير من اقرب فرع لكم لاننا بنوفر لكم فروع لنا فى جميع المحافظات يعنى هيكون متوافر فى كل مكان ، وهتحصل على كل العروض والخصومات التى نوفرها لكم من اى فرع معتمد لنا . خلى صيفك مميزه مع تكييفات يونيون اير وستكون دائما مستمتع باوقاتك مع اسرتك .

https://unionairemasr.com/


Автор: 먹튀위키 (не зарегистрирован), дата: 23 ноября, 2023 - 16:58
#permalink

I am not sure where your getting your information, but good topic. I needs to spend some time learning much more or understanding more. 먹튀위키


Автор: Гость (не зарегистрирован), дата: 24 ноября, 2023 - 16:25
#permalink

فلاتر مياه كوجين: الحل الأمثل لتنقية وتحسين جودة المياه
يعد فلتر كوجين من الخيارات الرائدة للأسر والمؤسسات لتحسين جودة المياه التي يتناولونها يومياً. تجمع هذه الفلاتر بين التكنولوجيا المبتكرة والتصميم المتقدم لتقديم مياه صحية ونقية للشرب والاستخدام المنزلي.


Автор: Гость (не зарегистрирован), дата: 24 ноября, 2023 - 17:02
#permalink

تحليل سعر فلتر مياه عمومى
مواصفات فلتر مياه عمومى
جهاز شمعه
وحده تزيل الشواب والاتربه العلقه بالمياه
فلتر مياة مرحلة واحدة 20 بوصة مع شمعة بولى صنع فى تيوانى لتنقية مياة المنزل بالكامل ويتم تركيبة على الموتور او على الخزان الرئيسي للفيلا او العمارة او غسلات الملابس او الاطباق للحفاظ عليها والاطاله عمر الغسلات ولذلك لمنع الشواب والرمل


Автор: ufabet168 (не зарегистрирован), дата: 29 ноября, 2023 - 18:32
#permalink

Thanks for your personal marvelous posting! I quite enjoyed reading it, you will be a great author. I will always bookmark your blog and will come back down the road. I want to encourage one to continue your great posts, have a nice morning! ufabet168


Автор: cucukakek89vip (не зарегистрирован), дата: 30 ноября, 2023 - 15:40
#permalink

Raih Keuntungan Besama Cucukakek89 Situs Slot Online Gacor yang menyediakan Bonus Mingguan Setiap hari Selesa Sebagai bentuk appresiasi untuk member aktif yang sudah bermain di cucukakek89. Kunjungi Dan Mainkan di Website Cucukakek89 Terpercaya!!. Mainkan Segera Permainan dengan pilihan Game Terlengkap dengan minimal deposit 25rb.

This is My Website:

<a href="https://cucukakek89vip.autos/" title="cucukakek89">cucukakek89</a>
<a href="https://cucukakek89.id/" title="cucukakek89 slot">cucukakek89 slot</a>
<a href="https://cucukakek89vip.autos/" title="cucukakek89 apk">cucukakek89 apk</a>
<a href="https://cucukakek89vip.autos/" title="cucukakek89 gacor">cucukakek89 gacor</a>
<a href="https://cucukakek89vip.autos/" title="cucukakek89 daftar">cucukakek89 daftar</a>
<a href="https://britishdebate.com/" title="slot terpercaya">slot terpercaya</a>
<a href="https://cucukakek89vip.autos/" title="cucukakek89 login">cucukakek89 login</a>

Автор: custom printed boxes (не зарегистрирован), дата: 1 декабря, 2023 - 16:29
#permalink

great content custom printed boxes


Автор: Гость (не зарегистрирован), дата: 11 декабря, 2023 - 12:11
#permalink

Best Dash Cam, DVR, Cheapest, Best 2024, Lowest Price; Most Features


Автор: ufabet (не зарегистрирован), дата: 13 декабря, 2023 - 13:24
#permalink

After searching for a great site. I was so impressed to yours. This will probably give me ideas for my work. Thank you ufabet


Автор: BIOBETGAMING (не зарегистрирован), дата: 13 декабря, 2023 - 17:54
#permalink

Does your blog have a contact page? I’m having problems locating it but, I’d like to shoot you an email. I’ve got some ideas for your blog you might be interested in hearing. Either way, great site and I look forward to seeing it develop over time.biogame


Автор: BIOBETGAMING (не зарегистрирован), дата: 13 декабря, 2023 - 17:54
#permalink

Does your blog have a contact page? I’m having problems locating it but, I’d like to shoot you an email. I’ve got some ideas for your blog you might be interested in hearing. Either way, great site and I look forward to seeing it develop over time.biogame


Автор: ยูฟ่าสล็อต (не зарегистрирован), дата: 16 декабря, 2023 - 13:36
#permalink

Impressive web site, Distinguished feedback that I can tackle. Im moving forward and may apply to my current job as a pet sitter, which is very enjoyable, but I need to additional expand. ยูฟ่าสล็อต


Автор: water damage las vegas (не зарегистрирован), дата: 19 декабря, 2023 - 18:50
#permalink

You there, this is really good post here. Thanks for taking the time to post such valuable information. water damage las vegas


Автор: russian blue kittens for sale (не зарегистрирован), дата: 23 декабря, 2023 - 20:13
#permalink

Автор: betflik vip (не зарегистрирован), дата: 29 декабря, 2023 - 10:02
#permalink

I’m not that much of a online reader to be honest but your blogs really nice, keep it up! I’ll go ahead and bookmark your site to come back later. All the best betflik vip


Автор: judi online (не зарегистрирован), дата: 30 декабря, 2023 - 17:26
#permalink

sangat bermanfaat sekali informasinya


Автор: betflix vip (не зарегистрирован), дата: 2 января, 2024 - 12:42
#permalink

Great post! I am actually getting ready to across this information, is very helpful my friend. I also like to recommend a site that is all about buy telegram members. Keep up the good work you are doing here. betflix vip


Автор: BETFLIXSUPERVIP (не зарегистрирован), дата: 9 января, 2024 - 18:25
#permalink

Thank you for another excellent article. Where else could anybody get that kind oof information in such an ideall means of writing? I’ve a presentation next week, and I’m at the look for such info.ทางเข้า betflix


Автор: momok (не зарегистрирован), дата: 12 января, 2024 - 09:35
#permalink

Автор: BETFLIXSUPERVIP (не зарегистрирован), дата: 17 января, 2024 - 07:31
#permalink

I think your blog might be having browser compatibility issues. When I look at your website in Ie, it looks fine but when opening in Internet Explorer, it has some overlapping. I just wanted to give you a quick heads up! Other then that, terrific blog!betflixvip


Автор: viaviaa179 (не зарегистрирован), дата: 27 января, 2024 - 10:01
#permalink

I must say you’ve done a amazing job with this. In addition, the blog loads very fast for me on Chrome. Excellent Blog!


Автор: slot kamboja (не зарегистрирован), дата: 5 февраля, 2024 - 16:06
#permalink

Отправить комментарий

Приветствуются комментарии:
  • Полезные.
  • Дополняющие прочитанное.
  • Вопросы по прочитанному. Именно по прочитанному, чтобы ответ на него помог другим разобраться в предмете статьи. Другие вопросы могут быть удалены.
    Для остальных вопросов и обсуждений есть форум.
P.S. Лучшее "спасибо" - не комментарий, как все здорово, а рекомендация или ссылка на статью.
Содержание этого поля является приватным и не предназначено к показу.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Разрешены HTML-таги: <strike> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <u> <i> <b> <pre> <img> <abbr> <blockquote> <h1> <h2> <h3> <h4> <h5> <p> <div> <span> <sub> <sup>
  • Строки и параграфы переносятся автоматически.
  • Текстовые смайлы будут заменены на графические.

Подробнее о форматировании

CAPTCHA
Антиспам
10 + 0 =
Введите результат. Например, для 1+3, введите 4.
 
Текущий раздел
Поиск по сайту
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Последние комментарии
Последние темы на форуме
Forum