+
+
+
+
+
$widget):
if (empty($widget['enabled'])) continue;
$span = (int)($widget['col_span'] ?? 6);
@@ -583,7 +598,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
+
{
- handle.setAttribute('draggable', 'true');
+ handle.setAttribute('draggable', 'false'); // Uitschakelen van krakkemikkige native browser drag
+ handle.style.touchAction = 'none';
+
+ handle.onpointerdown = (e) => {
+ // Alleen primaire muisklik (links)
+ if (e.button !== 0) return;
+ // Negeer als er op knoppen in de buurt geklikt wordt
+ if (e.target.closest('.widget-settings-btn') || e.target.closest('.widget-settings-dropdown')) return;
- handle.ondragstart = (e) => {
const wrapper = handle.closest('.fociz-widget-wrapper');
if (!wrapper) return;
- draggedWidgetEl = wrapper;
- // Bewaar beginpositie om te controleren op wijzigingen
- const allWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
- dragInitialIndex = allWrappers.indexOf(draggedWidgetEl);
-
- e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData('text/plain', wrapper.getAttribute('data-widget-id'));
-
- // Sluit eventuele open popover menu's
- document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
-
- setTimeout(() => {
- if (draggedWidgetEl) {
- draggedWidgetEl.classList.add('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/70', 'shadow-2xl');
- }
- }, 0);
- };
-
- handle.ondragend = (e) => {
- if (draggedWidgetEl) {
- draggedWidgetEl.classList.remove('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/70', 'shadow-2xl');
- }
- saveCurrentGridOrderIfChanged();
- draggedWidgetEl = null;
+ dragCandidate = {
+ wrapper: wrapper,
+ handle: handle,
+ startX: e.clientX,
+ startY: e.clientY
+ };
};
});
-
- // Dragover op grid: verplaatst de widget live in het DOM zodat andere blokken direct opzij schuiven
- grid.ondragover = (e) => {
- if (!draggedWidgetEl) return;
- e.preventDefault();
- e.dataTransfer.dropEffect = 'move';
-
- // Check of cursor onderaan het grid zweeft
- const gridRect = grid.getBoundingClientRect();
- if (e.clientY > gridRect.bottom - 40) {
- if (grid.lastElementChild !== draggedWidgetEl) {
- grid.appendChild(draggedWidgetEl);
- }
- return;
- }
-
- // Check of cursor bovenaan het grid zweeft
- if (e.clientY < gridRect.top + 40) {
- if (grid.firstElementChild !== draggedWidgetEl) {
- grid.prepend(draggedWidgetEl);
- }
- return;
- }
-
- const target = e.target.closest('.fociz-widget-wrapper');
- if (target && target !== draggedWidgetEl && target.parentElement === grid) {
- const rect = target.getBoundingClientRect();
- const midY = rect.top + rect.height / 2;
- const midX = rect.left + rect.width / 2;
-
- // Bepaal of de muis voorbij het midden is (verticaal of horizontaal in dezelfde rij)
- const isAfter = (e.clientY > midY) || (Math.abs(e.clientY - midY) < rect.height * 0.35 && e.clientX > midX);
-
- if (isAfter) {
- if (target.nextElementSibling !== draggedWidgetEl) {
- target.after(draggedWidgetEl);
- }
- } else {
- if (target.previousElementSibling !== draggedWidgetEl) {
- target.before(draggedWidgetEl);
- }
- }
- }
- };
-
- grid.ondrop = (e) => {
- e.preventDefault();
- if (draggedWidgetEl) {
- draggedWidgetEl.classList.remove('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/70', 'shadow-2xl');
- }
- saveCurrentGridOrderIfChanged();
- draggedWidgetEl = null;
- };
}
+// Globale pointer listeners (blijven soepel actief zelfs als cursor snel beweegt)
+window.addEventListener('pointermove', (e) => {
+ currentPointerX = e.clientX;
+ currentPointerY = e.clientY;
+
+ if (!isDraggingActive && dragCandidate) {
+ // Controleer of de gebruiker minimaal 5px heeft bewogen (voorkomt valse triggers bij simpele kliks)
+ const dist = Math.hypot(e.clientX - dragCandidate.startX, e.clientY - dragCandidate.startY);
+ if (dist > 5) {
+ startWidgetDragging(dragCandidate.wrapper, dragCandidate.startX, dragCandidate.startY);
+ }
+ }
+
+ if (isDraggingActive) {
+ updateFloatingGhostPosition();
+ updateDropTargetFromPointer();
+ }
+});
+
+window.addEventListener('pointerup', (e) => {
+ if (isDraggingActive) {
+ finishWidgetDragging();
+ }
+ dragCandidate = null;
+});
+
+window.addEventListener('pointercancel', () => {
+ if (isDraggingActive) {
+ cancelWidgetDragging();
+ }
+ dragCandidate = null;
+});
+
+window.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && isDraggingActive) {
+ cancelWidgetDragging();
+ }
+});
+
+// Start het oppakken & slepen van de widget
+function startWidgetDragging(wrapper, startX, startY) {
+ const grid = document.getElementById('focizWidgetGrid');
+ if (!grid || !wrapper) return;
+
+ isDraggingActive = true;
+ activeDraggedWrapper = wrapper;
+
+ // Sluit openstaande popovers
+ document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden'));
+
+ // Bepaal beginpositie
+ const allWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
+ dragInitialIndex = allWrappers.indexOf(wrapper);
+
+ // 1. Toon het 12-koloms Blueprint Achtergrond Raster
+ const blueprint = document.getElementById('widgetGridBlueprint');
+ if (blueprint) {
+ blueprint.classList.remove('hidden');
+ blueprint.classList.add('block');
+ }
+
+ // 2. Creëer de zwevende Hover Ghost kaart
+ const rect = wrapper.getBoundingClientRect();
+ grabOffsetX = startX - rect.left;
+ grabOffsetY = startY - rect.top;
+
+ floatingGhost = wrapper.cloneNode(true);
+ floatingGhost.id = 'widgetFloatingGhost';
+ floatingGhost.removeAttribute('data-widget-id');
+ floatingGhost.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));
+ floatingGhost.querySelectorAll('.widget-settings-dropdown').forEach(d => d.remove());
+
+ floatingGhost.style.position = 'fixed';
+ floatingGhost.style.left = `${rect.left}px`;
+ floatingGhost.style.top = `${rect.top}px`;
+ floatingGhost.style.width = `${rect.width}px`;
+ floatingGhost.style.height = `${rect.height}px`;
+ floatingGhost.style.pointerEvents = 'none';
+ floatingGhost.style.zIndex = '999999';
+ floatingGhost.style.transform = 'scale(1.02) rotate(1.2deg)';
+ floatingGhost.style.opacity = '0.94';
+ floatingGhost.style.transition = 'transform 0.15s ease, box-shadow 0.15s ease';
+ floatingGhost.style.boxShadow = '0 25px 60px -10px rgba(79, 70, 229, 0.45), 0 0 0 2px rgba(99, 102, 241, 0.7)';
+ floatingGhost.style.cursor = 'grabbing';
+ document.body.appendChild(floatingGhost);
+
+ // 3. Plaats een duidelijke Drop Slot Placeholder in het raster
+ const span = wrapper.getAttribute('data-col-span') || '6';
+ dropPlaceholder = document.createElement('div');
+ dropPlaceholder.id = 'widgetDropPlaceholder';
+ dropPlaceholder.className = `lg:col-span-${span} col-span-1 min-w-0 rounded-2xl border-2 border-dashed border-indigo-500 dark:border-indigo-400 bg-indigo-50/70 dark:bg-indigo-950/50 flex flex-col items-center justify-center p-6 transition-all duration-200 shadow-inner`;
+ dropPlaceholder.style.minHeight = `${Math.max(130, Math.min(rect.height, 280))}px`;
+ dropPlaceholder.innerHTML = `
+
+
+
Laat los om hier te plaatsen
+
+ `;
+ wrapper.after(dropPlaceholder);
+ wrapper.style.display = 'none';
+
+ document.body.classList.add('select-none', 'cursor-grabbing');
+
+ // 4. Start automatische soepele scroll bij nadering van schermranden
+ startAutoScroller();
+}
+
+function updateFloatingGhostPosition() {
+ if (!floatingGhost) return;
+ floatingGhost.style.left = `${currentPointerX - grabOffsetX}px`;
+ floatingGhost.style.top = `${currentPointerY - grabOffsetY}px`;
+}
+
+function updateDropTargetFromPointer() {
+ const grid = document.getElementById('focizWidgetGrid');
+ if (!grid || !dropPlaceholder || !activeDraggedWrapper) return;
+
+ const wrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper')).filter(w => w !== activeDraggedWrapper);
+ let targetFound = false;
+
+ for (const wrap of wrappers) {
+ const rect = wrap.getBoundingClientRect();
+ if (currentPointerY >= rect.top - 15 && currentPointerY <= rect.bottom + 15 &&
+ currentPointerX >= rect.left - 15 && currentPointerX <= rect.right + 15) {
+
+ const midY = rect.top + rect.height / 2;
+ const midX = rect.left + rect.width / 2;
+ const isAfter = (currentPointerY > midY) || (Math.abs(currentPointerY - midY) < rect.height * 0.35 && currentPointerX > midX);
+
+ if (isAfter) {
+ if (wrap.nextElementSibling !== dropPlaceholder) {
+ wrap.after(dropPlaceholder);
+ }
+ } else {
+ if (wrap.previousElementSibling !== dropPlaceholder) {
+ wrap.before(dropPlaceholder);
+ }
+ }
+ targetFound = true;
+ break;
+ }
+ }
+
+ if (!targetFound && wrappers.length > 0) {
+ const firstRect = wrappers[0].getBoundingClientRect();
+ const lastRect = wrappers[wrappers.length - 1].getBoundingClientRect();
+ if (currentPointerY < firstRect.top) {
+ if (grid.firstElementChild !== dropPlaceholder) {
+ grid.prepend(dropPlaceholder);
+ }
+ } else if (currentPointerY > lastRect.bottom) {
+ if (grid.lastElementChild !== dropPlaceholder) {
+ grid.appendChild(dropPlaceholder);
+ }
+ }
+ }
+}
+
+// Auto-scroller wanneer je met de widget naar de boven- of onderkant van het scherm beweegt
+function startAutoScroller() {
+ stopAutoScroller();
+ function scrollStep() {
+ if (!isDraggingActive) return;
+ const viewportHeight = window.innerHeight;
+ const threshold = 110;
+
+ if (currentPointerY < threshold && window.scrollY > 0) {
+ const factor = (threshold - currentPointerY) / threshold;
+ const speed = Math.max(3, Math.round(factor * 26));
+ window.scrollBy(0, -speed);
+ updateDropTargetFromPointer();
+ } else if (currentPointerY > viewportHeight - threshold) {
+ const factor = (currentPointerY - (viewportHeight - threshold)) / threshold;
+ const speed = Math.max(3, Math.round(factor * 26));
+ window.scrollBy(0, speed);
+ updateDropTargetFromPointer();
+ }
+ autoScrollAnimId = requestAnimationFrame(scrollStep);
+ }
+ autoScrollAnimId = requestAnimationFrame(scrollStep);
+}
+
+function stopAutoScroller() {
+ if (autoScrollAnimId) {
+ cancelAnimationFrame(autoScrollAnimId);
+ autoScrollAnimId = null;
+ }
+}
+
+// Voltooi het slepen (loslaten & AJAX opslaan)
+function finishWidgetDragging() {
+ isDraggingActive = false;
+ stopAutoScroller();
+
+ document.body.classList.remove('select-none', 'cursor-grabbing');
+
+ // Verberg blueprint raster
+ const blueprint = document.getElementById('widgetGridBlueprint');
+ if (blueprint) {
+ blueprint.classList.add('hidden');
+ blueprint.classList.remove('block');
+ }
+
+ // Verwijder zwevende ghost
+ if (floatingGhost) {
+ floatingGhost.remove();
+ floatingGhost = null;
+ }
+
+ // Plaats originele widget terug op de plek van de placeholder
+ if (dropPlaceholder && activeDraggedWrapper) {
+ dropPlaceholder.replaceWith(activeDraggedWrapper);
+ activeDraggedWrapper.style.display = '';
+ dropPlaceholder = null;
+
+ // Subtiel succes flitsje op de neergezette widget
+ activeDraggedWrapper.classList.add('ring-2', 'ring-indigo-500', 'shadow-xl');
+ setTimeout(() => {
+ activeDraggedWrapper?.classList.remove('ring-2', 'ring-indigo-500', 'shadow-xl');
+ }, 400);
+ }
+
+ // Controleer of de volgorde gewijzigd is en sla op via AJAX
+ saveCurrentGridOrderIfChanged();
+ activeDraggedWrapper = null;
+}
+
+// Annuleer slepen (bijv. via Escape)
+function cancelWidgetDragging() {
+ isDraggingActive = false;
+ stopAutoScroller();
+
+ document.body.classList.remove('select-none', 'cursor-grabbing');
+
+ const blueprint = document.getElementById('widgetGridBlueprint');
+ if (blueprint) {
+ blueprint.classList.add('hidden');
+ blueprint.classList.remove('block');
+ }
+
+ if (floatingGhost) {
+ floatingGhost.remove();
+ floatingGhost = null;
+ }
+
+ if (dropPlaceholder && activeDraggedWrapper) {
+ dropPlaceholder.remove();
+ dropPlaceholder = null;
+ activeDraggedWrapper.style.display = '';
+ }
+
+ activeDraggedWrapper = null;
+}
+
+// Controleer gewijzigde volgorde en sla asynchroon op via AJAX
function saveCurrentGridOrderIfChanged() {
const grid = document.getElementById('focizWidgetGrid');
if (!grid) return;
const currentWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
- const currentIndex = currentWrappers.indexOf(draggedWidgetEl);
+ const currentIndex = activeDraggedWrapper ? currentWrappers.indexOf(activeDraggedWrapper) : -1;
let hasOrderChanged = false;
currentWrappers.forEach((el, index) => {
@@ -2219,6 +2434,7 @@ function saveCurrentGridOrderIfChanged() {
});
if (hasOrderChanged || (dragInitialIndex !== -1 && currentIndex !== dragInitialIndex)) {
+ // Geruisloos opslaan via AJAX
persistWidgetLayoutAuto();
showWidgetToast('Widget verplaatst & indeling opgeslagen', 'success');
}