whisper.cat/weboasis/todo/scripts/AppDraggable.js

350 lines
7.7 KiB
JavaScript
Raw Permalink Normal View History

2023-10-05 23:28:32 +11:00
/* global VT */
window.VT = window.VT || {};
VT.AppDraggable = function (el, options) {
var dragThreshold = options.dragThreshold || 5;
var dropRange = options.dropRange || 50;
var dropRangeSquared = dropRange * dropRange;
var originX, originY;
var clientX, clientY;
var startTime;
var dragging = false;
var clicked = false;
var data;
var image;
var imageSource;
var imageX, imageY;
var currentTarget;
el.addEventListener('touchstart', start);
el.addEventListener('mousedown', start);
// maybe prevent click
el.addEventListener(
'click',
function (e) {
if (dragging || clicked) {
e.preventDefault();
e.stopImmediatePropagation();
}
},
true
);
function start(e) {
if (el.classList.contains('_nodrag')) return;
if (e.type === 'mousedown' && e.button !== 0) return;
if (e.touches && e.touches.length > 1) return;
e.preventDefault();
var p = getPositionHost(e);
clientX = originX = p.clientX || p.pageX;
clientY = originY = p.clientY || p.pageY;
startTime = Date.now();
startListening();
}
function move(e) {
e.preventDefault();
var p = getPositionHost(e);
clientX = p.clientX || p.pageX;
clientY = p.clientY || p.pageY;
if (dragging) {
dispatchDrag();
dispatchTarget();
return;
}
var deltaX = clientX - originX;
var deltaY = clientY - originY;
if (Math.abs(deltaX) < dragThreshold && Math.abs(deltaY) < dragThreshold) {
return;
}
// prevent unintentional dragging on touch devices
if (e.touches && Date.now() - startTime < 50) {
stopListening();
return;
}
dragging = true;
data = {};
dispatchStart();
dispatchDrag();
dispatchTarget();
dispatchOverContinuously();
}
function end(e) {
e.preventDefault();
if (!dragging) {
e.target.click();
clicked = true;
}
stopListening();
requestAnimationFrame(function () {
clicked = false;
if (dragging) {
dispatchTarget();
dispatchEnd();
}
});
}
function startListening() {
el.addEventListener('touchmove', move);
el.addEventListener('touchend', end);
window.addEventListener('mousemove', move);
window.addEventListener('mouseup', end);
}
function stopListening() {
el.removeEventListener('touchmove', move);
el.removeEventListener('touchend', end);
window.removeEventListener('mousemove', move);
window.removeEventListener('mouseup', end);
}
//
function dispatchStart() {
setImage(el);
el.dispatchEvent(
new CustomEvent('draggableStart', {
detail: buildDetail(),
bubbles: true,
})
);
}
function dispatchDrag() {
image.dispatchEvent(
new CustomEvent('draggableDrag', {
detail: buildDetail(),
bubbles: true,
})
);
}
function dispatchTarget() {
if (!dragging) return;
var nextTarget = getTarget();
if (nextTarget === currentTarget) return;
if (currentTarget) {
currentTarget.addEventListener('draggableLeave', removeDropClassOnce);
currentTarget.dispatchEvent(
new CustomEvent('draggableLeave', {
detail: buildDetail(),
bubbles: true,
})
);
}
if (nextTarget) {
nextTarget.addEventListener('draggableEnter', addDropClassOnce);
nextTarget.dispatchEvent(
new CustomEvent('draggableEnter', {
detail: buildDetail(),
bubbles: true,
})
);
}
currentTarget = nextTarget;
}
function dispatchOverContinuously() {
if (!dragging) return;
dispatchOver();
setTimeout(dispatchOver, 50);
}
function dispatchOver() {
if (currentTarget) {
currentTarget.dispatchEvent(
new CustomEvent('draggableOver', {
detail: buildDetail(),
bubbles: true,
})
);
}
setTimeout(dispatchOver, 50);
}
function dispatchEnd() {
if (currentTarget) {
currentTarget.addEventListener('draggableDrop', cleanUpOnce);
currentTarget.dispatchEvent(
new CustomEvent('draggableDrop', {
detail: buildDetail(),
bubbles: true,
})
);
} else {
image.dispatchEvent(
new CustomEvent('draggableCancel', {
detail: buildDetail(),
bubbles: true,
})
);
}
}
//
function buildDetail() {
var detail = {
el: el,
data: data,
image: image,
imageSource: imageSource,
originX: originX,
originY: originY,
clientX: clientX,
clientY: clientY,
imageX: imageX,
imageY: imageY,
setImage: function (source) {
setImage(source);
detail.image = image;
},
};
return detail;
}
function setImage(source) {
if (imageSource === source) return;
imageSource = source;
removeImage();
image = imageSource.cloneNode(true);
image.style.position = 'fixed';
image.style.left = '0';
image.style.top = '0';
image.style.width = imageSource.offsetWidth + 'px';
image.style.height = imageSource.offsetHeight + 'px';
image.style.margin = '0';
image.style.zIndex = 9999;
image.classList.add('-dragging');
var rect = source.getBoundingClientRect();
imageX = originX - rect.left;
imageY = originY - rect.top;
image.addEventListener('draggableDrag', function (e) {
var x = e.detail.clientX - e.detail.imageX;
var y = e.detail.clientY - e.detail.imageY;
image.style.transition = 'none';
image.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
});
image.addEventListener('draggableCancel', cleanUp);
document.body.appendChild(image);
}
function addDropClassOnce(e) {
e.target.removeEventListener(e.type, addDropClassOnce);
e.target.classList.add('-drop');
}
function removeDropClassOnce(e) {
e.target.removeEventListener(e.type, addDropClassOnce);
e.target.classList.remove('-drop');
}
function cleanUpOnce(e) {
e.target.removeEventListener(e.type, cleanUpOnce);
cleanUp();
}
function cleanUp() {
if (currentTarget) {
currentTarget.classList.remove('-drop');
}
removeImage();
data = null;
image = null;
imageSource = null;
currentTarget = null;
}
function removeImage() {
if (image && image.parentNode) {
image.parentNode.removeChild(image);
}
}
function getTarget() {
var candidates = [];
document.querySelectorAll(options.dropSelector).forEach(function (el) {
var rect = el.getBoundingClientRect();
var distanceSquared = pointDistanceToRectSquared(clientX, clientY, rect);
if (distanceSquared > dropRangeSquared) return;
candidates.push({
el: el,
distance2: distanceSquared,
});
});
candidates.sort(function (a, b) {
if (a.distance2 === 0 && b.distance2 === 0) {
// in this case, the client position is inside both rectangles
// if A contains B, B is the correct target and vice versa
// TODO sort by z-index somehow?
return a.el.contains(b.el) ? -1 : b.el.contains(a.el) ? 1 : 0;
}
// sort by distance, ascending
return a.distance2 - b.distance2;
});
return candidates.length > 0 ? candidates[0].el : null;
}
function pointDistanceToRectSquared(x, y, rect) {
var dx =
x < rect.left ? x - rect.left : x > rect.right ? x - rect.right : 0;
var dy =
y < rect.top ? y - rect.top : y > rect.bottom ? y - rect.bottom : 0;
return dx * dx + dy * dy;
}
function getPositionHost(e) {
if (e.targetTouches && e.targetTouches.length > 0) {
return e.targetTouches[0];
}
if (e.changedTouches && e.changedTouches.length > 0) {
return e.changedTouches[0];
}
return e;
}
};