diff --git a/application/controllers/api/frontend/v1/stv/Student.php b/application/controllers/api/frontend/v1/stv/Student.php index 0cfd82c36..5cff2b173 100644 --- a/application/controllers/api/frontend/v1/stv/Student.php +++ b/application/controllers/api/frontend/v1/stv/Student.php @@ -36,6 +36,7 @@ class Student extends FHCAPI_Controller parent::__construct([ 'get' => ['admin:r', 'assistenz:r'], 'save' => ['admin:rw', 'assistenz:rw'], + 'saveStudent' => ['admin:rw', 'assistenz:rw'], 'check' => ['admin:rw', 'assistenz:rw'], 'add' => ['admin:rw', 'assistenz:rw'] // TODO(chris): extra permissions ]); @@ -414,6 +415,31 @@ class Student extends FHCAPI_Controller ), '')); } + /** + * Saves data to a prestudent using their student_uid + * + * @param string $student_uid + * @param string $studiensemester_kurzbz + * @return void + */ + public function saveStudent($student_uid, $studiensemester_kurzbz) + { + $this->load->model('crm/Student_model', 'StudentModel'); + + $result = $this->StudentModel->load([$student_uid]); + + $data = $this->getDataOrTerminateWithError($result); + + if (!$data) + show_404(); // No Student with that ID + + $student = current($data); + + $this->checkPermissionsForPrestudent($student->prestudent_id, ['admin:rw', 'assistenz:rw']); + + return $this->save($student->prestudent_id, $studiensemester_kurzbz); + } + public function check() { $this->load->library('form_validation'); diff --git a/application/controllers/api/frontend/v1/stv/Verband.php b/application/controllers/api/frontend/v1/stv/Verband.php index 4060704de..4ff2c74fe 100644 --- a/application/controllers/api/frontend/v1/stv/Verband.php +++ b/application/controllers/api/frontend/v1/stv/Verband.php @@ -271,6 +271,7 @@ class Verband extends FHCAPI_Controller $this->StudiengangModel->addSelect("CONCAT(UPPER(CONCAT(typ, kurzbz)), '-', semester, verband, (SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END FROM public.tbl_lehrverband WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester AND verband=v.verband ORDER BY gruppe LIMIT 1)) AS name", false); $this->StudiengangModel->addSelect("CASE WHEN MAX(gruppe)='' OR MAX(gruppe)=' ' THEN TRUE ELSE FALSE END AS leaf"); + $this->StudiengangModel->addSelect($this->StudiengangModel->escape($semester) . ' AS semester'); $this->StudiengangModel->addSelect('verband'); $this->StudiengangModel->addSelect($this->StudiengangModel->escape($studiengang_kz) . '::integer AS stg_kz', false); @@ -319,6 +320,8 @@ class Verband extends FHCAPI_Controller $this->StudiengangModel->addSelect("CONCAT(UPPER(CONCAT(typ, kurzbz)), '-', semester, verband, gruppe, (SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END FROM public.tbl_lehrverband WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester AND verband=v.verband AND gruppe=v.gruppe ORDER BY gruppe LIMIT 1)) AS name", false); $this->StudiengangModel->addSelect("TRUE AS leaf", false); + $this->StudiengangModel->addSelect('v.semester'); + $this->StudiengangModel->addSelect('v.verband'); $this->StudiengangModel->addSelect('gruppe'); $this->StudiengangModel->addSelect($this->StudiengangModel->escape($studiengang_kz) . '::integer AS stg_kz', false); diff --git a/public/js/api/factory/stv/details.js b/public/js/api/factory/stv/details.js index 1077de22d..577bc14fb 100644 --- a/public/js/api/factory/stv/details.js +++ b/public/js/api/factory/stv/details.js @@ -35,4 +35,14 @@ export default { params }; }, + saveStudent(student_uid, studiensemester_kurzbz, params) { + return { + method: 'post', + url: 'api/frontend/v1/stv/student/saveStudent/' + + encodeURIComponent(student_uid) + + '/' + + encodeURIComponent(studiensemester_kurzbz), + params + }; + } }; \ No newline at end of file diff --git a/public/js/components/Calendar/Base/DragAndDrop.js b/public/js/components/Calendar/Base/DragAndDrop.js index 0be17ddb4..631a792a8 100644 --- a/public/js/components/Calendar/Base/DragAndDrop.js +++ b/public/js/components/Calendar/Base/DragAndDrop.js @@ -96,8 +96,11 @@ export default { }, methods: { onDragstart(evt) { - DragAndDrop.setTransferData(evt.detail.originalEvent, evt.detail.item.orig); - this.draggedInternalEvent = evt.detail.item; + const data = DragAndDrop.convertToTransferData(evt.detail.item.orig); + if (DragAndDrop.isValidDragObject(data)) { + DragAndDrop.setTransferData(evt.detail.originalEvent, data); + this.draggedInternalEvent = evt.detail.item; + } }, onDragend() { this.draggedInternalEvent = null; diff --git a/public/js/components/Stv/Studentenverwaltung/List.js b/public/js/components/Stv/Studentenverwaltung/List.js index 67cada523..9e4cd4296 100644 --- a/public/js/components/Stv/Studentenverwaltung/List.js +++ b/public/js/components/Stv/Studentenverwaltung/List.js @@ -1,6 +1,7 @@ import {CoreFilterCmpt} from "../../filter/Filter.js"; import ListNew from './List/New.js'; +import draggable from '../../../directives/draggable.js'; export default { name: "ListPrestudents", @@ -8,11 +9,18 @@ export default { CoreFilterCmpt, ListNew }, + directives: { + draggable + }, inject: { - 'lists': { + lists: { from: 'lists', required: true }, + $reloadList: { + from: '$reloadList', + required: true + }, currentSemester: { from: 'currentSemester', required: true @@ -165,6 +173,39 @@ export default { currentEndpointRawUrl: '' } }, + computed: { + countsToHTML: function() { + return this.$p.t('global/ausgewaehlt') + + ': ' + (this.selectedcount || 0) + '' + + ' | ' + + this.$p.t('global/gefiltert') + + ': ' + + '' + (this.filteredcount || 0) + '' + + ' | ' + + this.$p.t('global/gesamt') + + ': ' + (this.count || 0) + ''; + }, + selectedDragObject() { + return this.selected.map(item => { + let type, id; + if (item.uid) { + type = 'student'; + id = item.uid; + } else if (item.prestudent_id) { + type = 'prestudent'; + id = item.prestudent_id; + } else if (item.person_id) { + type = 'person'; + id = item.person_id; + } + return { + ...item, + type, + id + }; + }); + } + }, methods: { reload() { this.$refs.table.reloadTable(); @@ -172,10 +213,19 @@ export default { actionNewPrestudent() { this.$refs.new.open(); }, - rowSelectionChanged(data) { + rowSelectionChanged(data, rows) { this.selectedcount = data.length; this.lastSelected = this.selected; this.$emit('update:selected', data); + + // set selected elements draggable + const tableEl = this.$refs.table?.$refs?.table; + if (tableEl) { + const oldDragables = tableEl.querySelectorAll('[draggable]'); + for (const draggable of oldDragables) + draggable.removeAttribute('draggable'); + } + rows.forEach(row => row.getElement().draggable = true); }, autoSelectRows(data) { if (this.lastSelected) { @@ -294,26 +344,27 @@ export default { if (el != this.focusObj) this.changeFocus(this.focusObj, el); } - } - }, - computed: { - countsToHTML: function() { - return this.$p.t('global/ausgewaehlt') - + ': ' + (this.selectedcount || 0) + '' - + ' | ' - + this.$p.t('global/gefiltert') - + ': ' - + '' + (this.filteredcount || 0) + '' - + ' | ' - + this.$p.t('global/gesamt') - + ': ' + (this.count || 0) + ''; + }, + dragCleanup(evt) { + if (evt.dataTransfer.dropEffect == 'none') + return; // aborted or wrong target + + this.$reloadList(); } }, // TODO(chris): focusin, focusout, keydown and tabindex should be in the filter component // TODO(chris): filter component column chooser has no accessibilty features template: `
-
+
[ + this.getStudentAjaxId(student), + ApiStvGroups.add( + student.id, + data.gruppe_kurzbz, + this.currentSemester + ) + ]); + } else { + const { semester, verband, gruppe } = data; + const params = { semester, verband, gruppe }; + endpoint = students.map(student => [ + this.getStudentAjaxId(student), + ApiStvDetails.saveStudent( + student.id, + this.currentSemester, + params + ) + ]); + } + + return this.$api + .call(endpoint) + .then(this.$reloadList) + .catch(this.$fhcAlert.handleSystemError); } }, mounted() { @@ -268,10 +331,21 @@ export default { diff --git a/public/js/directives/dragClick.js b/public/js/directives/dragClick.js new file mode 100644 index 000000000..aeeafb818 --- /dev/null +++ b/public/js/directives/dragClick.js @@ -0,0 +1,101 @@ +export default { + mounted(el, binding) { + const delay = parseInt(binding.arg) || 300; + + let timeout = null; + function startCountdown() { + timeout = window.setTimeout(binding.value, delay); + } + function stopCountdown() { + if (timeout) + window.clearTimeout(timeout); + timeout = null; + } + + function onEnter(evt) { + let lastTarget = evt.target; + let lastX = evt.offsetX; + let 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(); + } + }); + + startCountdown(); + } + function onLeave() { + stopCountdown(); + } + + // 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; + }, + beforeUnmount(el) { + el.removeEventListener('dragenter', el.initFunc); + delete el.initFunc; + } +} diff --git a/public/js/directives/draggable.js b/public/js/directives/draggable.js new file mode 100644 index 000000000..c72ad49f2 --- /dev/null +++ b/public/js/directives/draggable.js @@ -0,0 +1,97 @@ +import { setTransferData, convertToValidDragObject, dragendWorker } from '../helpers/DragAndDrop.js'; + +const EFFECTS = [ + 'none', + 'copy', + 'copyLink', + 'copyMove', + 'link', + 'linkMove', + 'move', + 'all', + 'uninitialized' +]; + +export default { + mounted(el, binding, vnode) { + updateValue(el, binding.value); + updateEffectAllowed(el, binding.arg); + + // if modifier capture is set we assume it's on a parent element + // i.e: for dragging multiple elements + // otherwise set draggable attribute + if (!binding.modifiers.capture) { + el.draggable = true; + } + + el.addEventListener('dragstart', 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(); + } 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; + } + window.removeEventListener('dragend', blockHandler, true); + } + + function blockHandler(evt) { + if (evt.dataTransfer.dropEffect == 'none') + return unblockDragend(); + unblockDragend(evt); + evt.stopPropagation(); + dragendWorker.port.postMessage(['request']); + } + + 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); + } + }, + updated(el, binding) { + updateValue(el, binding.value); + updateEffectAllowed(el, binding.arg); + } +} + +// Helper functions +function updateValue(el, value) { + value = convertToValidDragObject(value); + if (value) { + el.dataset.fhcDraggableValue = JSON.stringify(value); + } else if (el.dataset.fhcDraggableValue) { + delete el.dataset.fhcDraggableValue; + } +} +function updateEffectAllowed(el, effectAllowed) { + if (effectAllowed && EFFECTS.includes(effectAllowed)) { + el.dataset.fhcEffectAllowed = effectAllowed; + } else if (el.dataset.fhcEffectAllowed) { + delete el.dataset.fhcEffectAllowed; + } +} diff --git a/public/js/directives/drop.js b/public/js/directives/drop.js new file mode 100644 index 000000000..12253386c --- /dev/null +++ b/public/js/directives/drop.js @@ -0,0 +1,57 @@ +import { getValidTransferData, eventHasTypes, dragendWorker } from '../helpers/DragAndDrop.js'; + +const EFFECTS = [ + 'move', + 'copy', + 'link', + 'none' +]; + +let id = 0; + +export default { + mounted(el, binding, vnode) { + const allowedTypes = Object.keys(binding.modifiers); + allowedTypes.forEach(type => { + if (type.substr(-11) == '-collection') { + const singleType = type.substr(0, type.length-11); + if (!allowedTypes.includes(singleType)) + allowedTypes.push(singleType); + } + }); + + const strict = binding.arg.match(/(strict-|-strict)/); + const arg = binding.arg.replace(/(strict-|-strict)/, ''); + const effect = EFFECTS.includes(arg) ? arg : null; + + let allowed = false; + + el.addEventListener('dragenter', evt => { + allowed = eventHasTypes(evt, allowedTypes, strict); + if (allowed) + evt.preventDefault(); + }); + el.addEventListener('dragover', evt => { + if (allowed) { + evt.preventDefault(); + if (effect) + evt.dataTransfer.dropEffect = effect; + } + }); + el.addEventListener('drop', 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]); + return r; + }); + } + }); + } +} diff --git a/public/js/helpers/DragAndDrop.js b/public/js/helpers/DragAndDrop.js index 1160400f7..be068300c 100644 --- a/public/js/helpers/DragAndDrop.js +++ b/public/js/helpers/DragAndDrop.js @@ -1,67 +1,297 @@ /** * TODO(chris): This is only a prototype!!! */ -const DragAndDrop = { - TYPE_LE: "lehreinheit", - TYPE_VEVENT: "vevent", - getValidTransferData(event, allowedTypes) { - const json = event.dataTransfer.getData('text'); - let obj; - try { - obj = JSON.parse(json); - if (!obj.type) - return null; - if (allowedTypes && !allowedTypes.includes(obj.type)) - return null; - } catch (error) { - return null; - } - return obj; +const dragendWorker = new SharedWorker(new URL("../sharedworkers/dragend.js", import.meta.url)); + +const TYPE_DEFINITION = { + lehreinheit: { + id: "lehreinheit_id", + dragIcon: "fa-solid fa-chalkboard-user", + extras: [ + "stundenblockung" + ] }, - isValidTransferData(event, allowedTypes) { - return this.getValidTransferData(event, allowedTypes) ? true : false; + vevent: { + id: "uid", + dragIcon: "fa-solid fa-calendar", + extras: [ + "dtstart", + "dtend", + "summary" + ] }, - getTransferData(event) { - const json = event.dataTransfer.getData('text'); - return JSON.parse(json); + person: { + id: "person_id", + dragIcon: "fa-solid fa-user" }, - setTransferData(event, data) { - switch (data.type) { - case DragAndDrop.TYPE_LE: - data = DragAndDrop.fromLe(data); - break; - default: - if (data.dtstart && data.dtend && data.uid && data.summary) { - data = DragAndDrop.fromVEvent(data); - break; - } - return false; // No type found => abort - } - - event.dataTransfer.setData('text', JSON.stringify(data)); - return true; + student: { + id: "student_uid", + dragIcon: "fa-solid fa-user-graduate" }, - fromLe(data) { - const { - type = DragAndDrop.TYPE_LE, - lehreinheit_id: id, - stundenblockung - } = data; - - return { type, id, stundenblockung }; - }, - fromVEvent(data) { - const { - type = DragAndDrop.TYPE_VEVENT, - uid: id, - dtstart, - dtend, - summary - } = data; - - return { type, id, dtstart, dtend, summary }; + prestudent: { + id: "prestudent_id", + dragIcon: "fa-solid fa-user-graduate text-muted" } + // TODO: IMPLEMENT OTHER TYPES }; -export default DragAndDrop; +const VALID_TYPES = Object.keys(TYPE_DEFINITION); + +const TYPE_CONSTANTS = Object.keys(TYPE_DEFINITION).reduce((res, type) => { + res['TYPE_' + type.toUpperCase()] = type; + return res; +}, {}); + +function isValidDragObject(value) { + if (!value) + return false; + if (Array.isArray(value)) + return value.every(isValidDragObject); + if (!value.type) + return false; + + if (value.type.substr(-11) == '-collection') { + if (!value.hasOwnProperty('values')) + return false; + + if (!VALID_TYPES.includes(value.type.substr(0, value.type.length-11))) + return false; + } else { + if (!value.hasOwnProperty('id')) + return false; + + if (!VALID_TYPES.includes(value.type)) + return false; + + if (TYPE_DEFINITION[value.type].extras) { + if (!TYPE_DEFINITION[value.type].extras.every(extra => value.hasOwnProperty(extra))) + return false; + } + } + + return true; +} + +function getValidTransferData(event, allowedTypes, strict) { + let obj = null; + + try { + obj = getTransferData(event, strict); + if (!obj) + return null; + + if (!strict && Array.isArray(obj)) { + obj = obj.filter(isValidDragObject); + if (!obj.length) + return null; + } else if (!isValidDragObject(obj)) + return null; + + if (allowedTypes && allowedTypes.length) { + if (Array.isArray(obj)) { + if (strict && !obj.every(v => allowedTypes.includes(v.type))) { + return null; + } else if (!strict) { + obj = obj.filter(v => allowedTypes.includes(v.type)); + if (!obj.length) + return null; + } + } else if (!allowedTypes.includes(obj.type)) { + return null; + } + } + } catch(error) { + return null; + } + + if (Array.isArray(obj) && obj.length == 1) + return obj.find(Boolean); + + return obj; +} + +function isValidTransferData(event, allowedTypes, strict) { + return getValidTransferData(event, allowedTypes, strict) ? true : false; +} + +function getTransferData(event, strict) { + const result = []; + + for (const type of event.dataTransfer.types) { + if (type.substr(0, 4) != 'fhc/') { + if (strict) + return null; + continue; + } + let base_type = type.substr(4); + let collection = false; + if (base_type.substr(-11) == '-collection') { + base_type = base_type.substr(0, base_type.length-11); + collection = true; + } + if (!VALID_TYPES.includes(base_type)) { + if (strict) + return null; + continue; + } + let data = JSON.parse(event.dataTransfer.getData(type)); + if (collection) + result.push(...data.values); + else + result.push(data); + } + + if (!result.length) + return null; + + if (result.length == 1) + return result[0]; + + return result; +} + +function convertToValidDragObject(data, strict) { + if (Array.isArray(data)) { + const converted = data.map(convertToValidDragObject).filter(Boolean); + if (!converted.length) + return undefined; + if (strict && converted.length != data.length) + return undefined; + + const sorted = converted.reduce((res, item) => { + if (!res[item.type]) + res[item.type] = []; + res[item.type].push(item); + return res; + }, {}); + + return Object.entries(sorted).map(([type, values]) => { + if (values.length > 1) { + return { + type: type + '-collection', + values + }; + } + return values[0]; + }); + } + + if (data.hasOwnProperty('type') && isValidDragObject(data)) { + return data; + } + + const found = Object.entries(TYPE_DEFINITION).find(([type, typedef]) => { + if (!data.hasOwnProperty(typedef.id)) + return false; + if (typedef.extras) { + if (!typedef.extras.every(extra => data.hasOwnProperty("extra"))) + return false; + } + return true; + }); + + if (!found) { + return undefined; + } + + const [ type, typedef ] = found; + + const newData = {}; + newData.type = type; + newData.id = data[typedef.id]; + if (typedef.extras) + typedef.extras.forEach(extra => newData[extras] = data[extra]); + + return newData; +} + +function setTransferData(event, validDragObject, setDragImage = false) { + if (setDragImage) { + const dragItems = Array.isArray(validDragObject) ? validDragObject : [ validDragObject ]; + const dragElements = dragItems.map(item => { + const icon = document.createElement('i'); + const label = document.createElement('span'); + const iconContainer = document.createElement('span'); + + iconContainer.className = 'btn btn-outline-dark bg-light'; + label.className = 'small'; + + if (TYPE_DEFINITION[item.type]) { + icon.className = TYPE_DEFINITION[item.type].dragIcon || 'fa-solid fa-question'; + label.textContent = item.id; + } else if (item.type.substr(-11) == '-collection' && TYPE_DEFINITION[item.type.substr(0, item.type.length-11)]) { + iconContainer.style.boxShadow = '3px 3px var(--bs-btn-border-color)'; + icon.className = TYPE_DEFINITION[item.type.substr(0, item.type.length-11)].dragIcon || 'fa-solid fa-question'; + label.textContent = 'x' + item.values.length; + } else { + icon.className = 'fa-solid fa-question'; + label.textContent = item.id || ''; + } + + iconContainer.append(icon); + + const itemContainer = document.createElement('div'); + itemContainer.className = 'd-flex flex-column align-items-center gap-2 small'; + itemContainer.append(iconContainer, label); + return itemContainer; + }); + + const container = document.createElement('div'); + container.className = 'd-flex flex-row gap-2 small'; + container.append(...dragElements); + + document.body.append(container); + event.dataTransfer.setDragImage(container, -25, 0); + requestAnimationFrame(() => { + document.body.removeChild(container); + }); + } + if (Array.isArray(validDragObject)) { + return validDragObject.forEach(data => setTransferData(event, data)); + } + + event.dataTransfer.setData('fhc/' + validDragObject.type, JSON.stringify(validDragObject)); +} + +/** + * check if the dataTransfer types are in the allowed types array + * if strict is disabled at least one type must be the allowed array + * otherwise all types have to be in the allowed array + * + * @param Event event + * @param Array allowedTypes + * @param Boolean strict + */ +function eventHasTypes(event, allowedTypes, strict) { + if (!allowedTypes || !allowedTypes.length) + allowedTypes = VALID_TYPES; + allowedTypes = allowedTypes.map(type => 'fhc/' + type); + + if (!strict) + return allowedTypes.some(type => event.dataTransfer.types.includes(type)); + + return event.dataTransfer.types.every(type => allowedTypes.includes(type)); +} + +export { + isValidDragObject, + getValidTransferData, + isValidTransferData, + getTransferData, + convertToValidDragObject, + setTransferData, + eventHasTypes, + dragendWorker +}; +export default { + ...TYPE_CONSTANTS, + isValidDragObject, + getValidTransferData, + isValidTransferData, + getTransferData, + convertToValidDragObject, + setTransferData, + eventHasTypes, + dragendWorker +}; diff --git a/public/js/sharedworkers/dragend.js b/public/js/sharedworkers/dragend.js new file mode 100644 index 000000000..64179afa6 --- /dev/null +++ b/public/js/sharedworkers/dragend.js @@ -0,0 +1,30 @@ +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; + } + } + }; +};