diff --git a/public/js/directives/dragClick.js b/public/js/directives/dragClick.js index aeeafb818..5603eb93f 100644 --- a/public/js/directives/dragClick.js +++ b/public/js/directives/dragClick.js @@ -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; } } diff --git a/public/js/directives/draggable.js b/public/js/directives/draggable.js index c72ad49f2..052fcb542 100644 --- a/public/js/directives/draggable.js +++ b/public/js/directives/draggable.js @@ -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; } } diff --git a/public/js/directives/drop.js b/public/js/directives/drop.js index 12253386c..35afa66de 100644 --- a/public/js/directives/drop.js +++ b/public/js/directives/drop.js @@ -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; } } diff --git a/public/js/helpers/DragAndDrop.js b/public/js/helpers/DragAndDrop.js index be068300c..8bf574746 100644 --- a/public/js/helpers/DragAndDrop.js +++ b/public/js/helpers/DragAndDrop.js @@ -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 }; diff --git a/public/js/sharedworkers/dragend.js b/public/js/sharedworkers/dragend.js deleted file mode 100644 index 0b80e972a..000000000 --- a/public/js/sharedworkers/dragend.js +++ /dev/null @@ -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; - } - }; -};