mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-06-16 11:39:31 +00:00
StV Groups Drag&Drop
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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')
|
||||
+ ': <strong>' + (this.selectedcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gefiltert')
|
||||
+ ': '
|
||||
+ '<strong>' + (this.filteredcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gesamt')
|
||||
+ ': <strong>' + (this.count || 0) + '</strong>';
|
||||
},
|
||||
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')
|
||||
+ ': <strong>' + (this.selectedcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gefiltert')
|
||||
+ ': '
|
||||
+ '<strong>' + (this.filteredcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gesamt')
|
||||
+ ': <strong>' + (this.count || 0) + '</strong>';
|
||||
},
|
||||
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: `
|
||||
<div class="stv-list h-100 pt-3">
|
||||
<div class="tabulator-container d-flex flex-column h-100" :class="{'has-filter': filterKontoCount0 || filterKontoMissingCounter}" tabindex="0" @focusin="onFocus" @keydown="onKeydown">
|
||||
<div
|
||||
class="tabulator-container d-flex flex-column h-100"
|
||||
:class="{'has-filter': filterKontoCount0 || filterKontoMissingCounter}"
|
||||
tabindex="0"
|
||||
@focusin="onFocus"
|
||||
@keydown="onKeydown"
|
||||
v-draggable:copyLink.capture="selectedDragObject"
|
||||
@dragend="dragCleanup"
|
||||
>
|
||||
<core-filter-cmpt
|
||||
ref="table"
|
||||
:description="countsToHTML"
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
import ApiStvVerband from '../../../api/factory/stv/verband.js';
|
||||
import drop from '../../../directives/drop.js';
|
||||
import dragClick from '../../../directives/dragClick.js';
|
||||
|
||||
import ApiStvGroups from '../../../api/factory/stv/group.js';
|
||||
import ApiStvDetails from '../../../api/factory/stv/details.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PvTreetable: primevue.treetable,
|
||||
PvColumn: primevue.column
|
||||
},
|
||||
directives: {
|
||||
drop,
|
||||
dragClick
|
||||
},
|
||||
inject: {
|
||||
$reloadList: {
|
||||
from: '$reloadList',
|
||||
required: true
|
||||
},
|
||||
currentSemester: {
|
||||
from: 'currentSemester',
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'selectVerband'
|
||||
],
|
||||
@@ -207,6 +225,51 @@ export default {
|
||||
|
||||
this.selectedKey = {[currentNode.key]: true};
|
||||
this.onSelectTreeNode(currentNode);
|
||||
},
|
||||
async toggleTreeNode(node) {
|
||||
if (this.expandedKeys[node.key]) {
|
||||
delete this.expandedKeys[node.key];
|
||||
} else if (!node.leaf) {
|
||||
await this.onExpandTreeNode(node);
|
||||
this.expandedKeys[node.key] = true;
|
||||
}
|
||||
},
|
||||
getStudentAjaxId(student) {
|
||||
let res = student.id;
|
||||
if (student.vorname && student.nachname)
|
||||
res += ' (' + student.vorname + ' ' + student.nachname + ')';
|
||||
return res;
|
||||
},
|
||||
dropStudents(node, students, evt) {
|
||||
const data = node.data;
|
||||
|
||||
let endpoint;
|
||||
if (data.gruppe_kurzbz) {
|
||||
endpoint = students.map(student => [
|
||||
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 {
|
||||
</template>
|
||||
<template #body="{ node }">
|
||||
<span
|
||||
v-if="['semester', 'verband', 'gruppe', 'gruppe_kurzbz'].some(key => node.data.hasOwnProperty(key))"
|
||||
:data-tree-item-key="node.key"
|
||||
:title="node.data.studiengang_kz"
|
||||
v-drag-click="() => toggleTreeNode(node)"
|
||||
v-drop:link-strict.student-collection="(evt, students) => dropStudents(node, students, evt)"
|
||||
>
|
||||
{{node.data.name}}
|
||||
{{ node.data.name }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
:data-tree-item-key="node.key"
|
||||
:title="node.data.studiengang_kz"
|
||||
v-drag-click="() => toggleTreeNode(node)"
|
||||
>
|
||||
{{ node.data.name }}
|
||||
</span>
|
||||
</template>
|
||||
</pv-column>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user