finetuning drag&drop

This commit is contained in:
chfhtw
2025-09-11 13:13:57 +02:00
parent 112211fb0b
commit ee6e7f3a48
5 changed files with 180 additions and 162 deletions
+26 -74
View File
@@ -1,3 +1,5 @@
import { bindDragEnterLeave } from '../helpers/DragAndDrop.js';
export default {
mounted(el, binding) {
const delay = parseInt(binding.arg) || 300;
@@ -12,90 +14,40 @@ export default {
timeout = null;
}
let lastTarget;
let lastX;
let lastY;
function onDragOver(evt) {
if (lastX != evt.offsetX || lastY != evt.offsetY || lastTarget != evt.target) {
// moved
lastTarget = evt.target;
lastX = evt.offsetX;
lastY = evt.offsetY;
stopCountdown();
startCountdown();
}
}
function onEnter(evt) {
let lastTarget = evt.target;
let lastX = evt.offsetX;
let lastY = evt.offsetY;
lastTarget = evt.target;
lastX = evt.offsetX;
lastY = evt.offsetY;
el.addEventListener('dragover', evt => {
if (lastX != evt.offsetX || lastY != evt.offsetY || lastTarget != evt.target) {
// moved
lastTarget = evt.target;
lastX = evt.offsetX;
lastY = evt.offsetY;
stopCountdown();
startCountdown();
}
});
el.addEventListener('dragover', onDragOver);
startCountdown();
}
function onLeave() {
stopCountdown();
el.removeEventListener('dragover', onDragOver);
}
// NOTE(chris): add save dragenter and dragleave events
// that won't fire when hovering over child elements
let skipLeave = false;
let skipLeaveParent = true;
function init(evt) {
skipLeave = false;
skipLeaveParent = true;
// add global listeners
window.addEventListener('dragenter', globalDragenter, true);
window.addEventListener('dragleave', globalDragleave, true);
window.addEventListener('drop', globalDrop, true);
// call enter
onEnter(evt);
// remove self
el.removeEventListener('dragenter', init);
}
function cleanup() {
// remove global listeners
window.removeEventListener('dragenter', globalDragenter, true);
window.removeEventListener('dragleave', globalDragleave, true);
window.removeEventListener('drop', globalDrop, true);
// call leave
onLeave();
// add init
el.addEventListener('dragenter', init);
}
function globalDragenter(evt) {
skipLeaveParent = false;
if (el != evt.target && !el.contains(evt.target)) {
cleanup();
} else {
skipLeave = true;
}
}
function globalDragleave(evt) {
if (el != evt.target && !el.contains(evt.target)) {
if (skipLeaveParent) {
skipLeaveParent = false;
return;
}
} else {
if (skipLeave) {
skipLeave = false;
return;
}
}
cleanup();
}
function globalDrop(evt) {
cleanup();
}
el.addEventListener('dragenter', init);
el.initFunc = init;
el.fhcDragClickCleanup = bindDragEnterLeave(el, onEnter, onLeave);
},
beforeUnmount(el) {
el.removeEventListener('dragenter', el.initFunc);
delete el.initFunc;
el.fhcDragClickCleanup();
delete el.fhcDragClickCleanup;
}
}
+40 -36
View File
@@ -1,4 +1,4 @@
import { setTransferData, convertToValidDragObject, dragendWorker } from '../helpers/DragAndDrop.js';
import { setTransferData, convertToValidDragObject } from '../helpers/DragAndDrop.js';
const EFFECTS = [
'none',
@@ -24,58 +24,62 @@ export default {
el.draggable = true;
}
el.addEventListener('dragstart', evt => {
const bcc = new BroadcastChannel('fhc-dnd');
let blocked = false;
function onStart(evt) {
const value = el.dataset.fhcDraggableValue;
if (value) {
setTransferData(evt, JSON.parse(value), true);
if (el.dataset.fhcEffectAllowed)
evt.dataTransfer.effectAllowed = el.dataset.fhcEffectAllowed;
blockDragend();
bcc.onmessage = e => {
if (e.data == 'block') {
blocked = true;
} else if (e.data == 'release') {
let evt = null;
if (blocked && blocked.evt) {
evt = blocked.evt;
}
blocked = false;
if (evt)
el.dispatchEvent(evt);
}
};
} else {
evt.preventDefault();
}
}, binding.modifiers.capture);
let id;
let evt = null;
let dataTransfer = null;
function blockDragend() {
id = el.dataset.fhcDraggableValue;
dragendWorker.port.postMessage(['init', id]);
window.addEventListener('dragend', blockHandler, true);
}
function unblockDragend(e) {
if (e) {
evt = e;
dataTransfer = e.dataTransfer;
function onEnd(evt) {
if (blocked) {
blocked = {
evt,
dt: evt.dataTransfer
};
evt.stopPropagation();
el.dispatchEvent(new DragEvent("beforedragend", evt));
} else {
bcc.onmessage = () => {};
}
window.removeEventListener('dragend', blockHandler, true);
}
el.addEventListener('dragstart', onStart, binding.modifiers.capture);
function blockHandler(evt) {
if (evt.dataTransfer.dropEffect == 'none')
return unblockDragend();
unblockDragend(evt);
evt.stopPropagation();
dragendWorker.port.postMessage(['request']);
}
el.addEventListener('dragend', onEnd, true);
dragendWorker.port.onmessage = e => {
const [ func, ...args ] = e.data;
if (func != 'fire')
return;
const [ targetId ] = args;
if (targetId != id)
return;
if (evt === null)
unblockDragend();
else
el.dispatchEvent(evt);
}
el.fhcDraggableCleanup = () => {
el.removeEventListener('dragstart', onStart, binding.modifiers.capture);
el.removeEventListener('dragend', onEnd, true);
};
},
updated(el, binding) {
updateValue(el, binding.value);
updateEffectAllowed(el, binding.arg);
},
beforeUnmount(el) {
el.fhcDraggableCleanup();
delete el.fhcDraggableCleanup;
}
}
+33 -11
View File
@@ -1,4 +1,4 @@
import { getValidTransferData, eventHasTypes, dragendWorker } from '../helpers/DragAndDrop.js';
import { getValidTransferData, eventHasTypes, bindDragEnterLeave } from '../helpers/DragAndDrop.js';
const EFFECTS = [
'move',
@@ -24,34 +24,56 @@ export default {
const arg = binding.arg.replace(/(strict-|-strict)/, '');
const effect = EFFECTS.includes(arg) ? arg : null;
const bcc = new BroadcastChannel('fhc-dnd');
let allowed = false;
el.addEventListener('dragenter', evt => {
function onEnter(evt) {
allowed = eventHasTypes(evt, allowedTypes, strict);
if (allowed)
if (allowed) {
evt.preventDefault();
});
el.addEventListener('dragover', evt => {
bcc.postMessage('block');
}
}
function onLeave(evt, wasDropped) {
if (allowed && !wasDropped) {
bcc.postMessage('release');
}
}
function onOver(evt) {
if (allowed) {
evt.preventDefault();
if (effect)
evt.dataTransfer.dropEffect = effect;
}
});
el.addEventListener('drop', evt => {
}
function onDrop(evt) {
let result = getValidTransferData(evt, allowedTypes, strict);
if (!Array.isArray(result) && !binding.modifiers[result.type] && allowedTypes.includes(result.type + '-collection'))
result = [result];
const res = binding.value(evt, result);
if (res instanceof Promise) {
const localId = id++;
dragendWorker.port.postMessage(['block', localId]);
res.then(r => {
dragendWorker.port.postMessage(['unblock', localId]);
bcc.postMessage('release');
return r;
});
} else {
bcc.postMessage('release');
}
});
}
const cleanupEnterLeave = bindDragEnterLeave(el, onEnter, onLeave);
el.addEventListener('dragover', onOver);
el.addEventListener('drop', onDrop);
el.fhcDropCleanup = () => {
cleanupEnterLeave();
el.removeEventListener('dragover', onOver);
el.removeEventListener('drop', onDrop);
};
},
beforeUnmount(el) {
el.fhcDropCleanup();
delete el.fhcDropCleanup;
}
}
+81 -10
View File
@@ -2,8 +2,6 @@
* TODO(chris): This is only a prototype!!!
*/
const dragendWorker = new SharedWorker(new URL("../sharedworkers/dragend.js", import.meta.url));
const TYPE_DEFINITION = {
lehreinheit: {
id: "lehreinheit_id",
@@ -119,12 +117,12 @@ function getTransferData(event, strict) {
const result = [];
for (const type of event.dataTransfer.types) {
if (type.substr(0, 4) != 'fhc/') {
if (type.substr(0, 16) != 'application/fhc-') {
if (strict)
return null;
continue;
}
let base_type = type.substr(4);
let base_type = type.substr(16);
let collection = false;
if (base_type.substr(-11) == '-collection') {
base_type = base_type.substr(0, base_type.length-11);
@@ -251,7 +249,7 @@ function setTransferData(event, validDragObject, setDragImage = false) {
return validDragObject.forEach(data => setTransferData(event, data));
}
event.dataTransfer.setData('fhc/' + validDragObject.type, JSON.stringify(validDragObject));
event.dataTransfer.setData('application/fhc-' + validDragObject.type, JSON.stringify(validDragObject));
}
/**
@@ -266,12 +264,85 @@ function setTransferData(event, validDragObject, setDragImage = false) {
function eventHasTypes(event, allowedTypes, strict) {
if (!allowedTypes || !allowedTypes.length)
allowedTypes = VALID_TYPES;
allowedTypes = allowedTypes.map(type => 'fhc/' + type);
allowedTypes = allowedTypes.map(type => 'application/fhc-' + type);
const dataTypes = [...event.dataTransfer.types];
// NOTE(chris): if dragging across browsers the dataTransfer object is
// set to a default one without data. Since we do not support dragging
// across browsers (yet) we return false which will disallow dropping.
if (!dataTypes.length)
return false;
if (!strict)
return allowedTypes.some(type => event.dataTransfer.types.includes(type));
return allowedTypes.some(type => [...event.dataTransfer.types].includes(type));
return event.dataTransfer.types.every(type => allowedTypes.includes(type));
return [...event.dataTransfer.types].every(type => allowedTypes.includes(type));
}
function bindDragEnterLeave(el, onEnter, onLeave) {
// NOTE(chris): add save dragenter and dragleave events
// that won't fire when hovering over child elements
let skipLeave = false;
let skipLeaveParent = true;
function init(evt) {
skipLeave = false;
skipLeaveParent = true;
// add global listeners
window.addEventListener('dragenter', globalDragenter, true);
window.addEventListener('dragleave', globalDragleave, true);
window.addEventListener('drop', globalDrop, true);
// call enter
onEnter(evt);
// remove self
el.removeEventListener('dragenter', init);
}
function cleanup(evt, wasDropped) {
// remove global listeners
window.removeEventListener('dragenter', globalDragenter, true);
window.removeEventListener('dragleave', globalDragleave, true);
window.removeEventListener('drop', globalDrop, true);
// call leave
onLeave(evt, wasDropped);
// add init
el.addEventListener('dragenter', init);
}
function globalDragenter(evt) {
skipLeaveParent = false;
if (el != evt.target && !el.contains(evt.target)) {
cleanup(evt);
} else {
skipLeave = true;
}
}
function globalDragleave(evt) {
if (el != evt.target && !el.contains(evt.target)) {
if (skipLeaveParent) {
skipLeaveParent = false;
return;
}
} else {
if (skipLeave) {
skipLeave = false;
return;
}
}
cleanup(evt);
}
function globalDrop(evt) {
cleanup(evt, true);
}
el.addEventListener('dragenter', init);
return () => {
// cleanup
el.removeEventListener('dragenter', init);
}
}
export {
@@ -282,7 +353,7 @@ export {
convertToValidDragObject,
setTransferData,
eventHasTypes,
dragendWorker
bindDragEnterLeave
};
export default {
...TYPE_CONSTANTS,
@@ -293,5 +364,5 @@ export default {
convertToValidDragObject,
setTransferData,
eventHasTypes,
dragendWorker
bindDragEnterLeave
};
-31
View File
@@ -1,31 +0,0 @@
let dragEndCallback = null;
onconnect = e => {
const port = e.ports[0];
const cbList = [];
port.onmessage = e => {
const [ func, ...args ] = e.data;
switch (func) {
case 'init':
dragEndCallback = () => {
port.postMessage(['fire', args]);
};
break;
case 'block':
cbList[args[0]] = dragEndCallback;
dragEndCallback = null;
break;
case 'unblock':
cbList[args[0]]();
break;
case 'request':
if (dragEndCallback) {
dragEndCallback();
dragEndCallback = null;
}
break;
}
};
};