/** * GlobalNotify JavaScript * Floating messenger-style notification widget */ var GlobalNotify = { isOpen: false, isDragging: false, dragOffset: { x: 0, y: 0 }, urlRoot: (function() { var scripts = document.querySelectorAll('script[src*="globalnotify"]'); if (scripts.length > 0) { var match = scripts[0].getAttribute('src').match(/^(.*?)\/custom\/globalnotify\//); if (match) return match[1]; } return ''; })(), /** * Toggle panel visibility */ toggle: function() { var panel = document.getElementById('globalnotify-panel'); if (!panel) return; if (this.isOpen) { this.close(); } else { this.open(); } }, /** * Open panel */ open: function() { var panel = document.getElementById('globalnotify-panel'); if (!panel) return; panel.style.display = 'flex'; this.isOpen = true; // Close when clicking outside setTimeout(function() { document.addEventListener('click', GlobalNotify.handleOutsideClick); }, 10); }, /** * Close panel */ close: function() { var panel = document.getElementById('globalnotify-panel'); if (!panel) return; panel.style.display = 'none'; this.isOpen = false; document.removeEventListener('click', GlobalNotify.handleOutsideClick); }, /** * Handle click outside */ handleOutsideClick: function(event) { var widget = document.getElementById('globalnotify-widget'); if (widget && !widget.contains(event.target)) { GlobalNotify.close(); } }, /** * Toggle history section */ toggleHistory: function() { var toggle = document.querySelector('.globalnotify-history-toggle'); var history = document.getElementById('globalnotify-history'); if (!toggle || !history) return; if (history.classList.contains('open')) { history.classList.remove('open'); toggle.classList.remove('open'); } else { history.classList.add('open'); toggle.classList.add('open'); } }, /** * Toggle item (mark as read/unread) */ toggleItem: function(notificationId, event) { if (event) { event.stopPropagation(); } var item = document.querySelector('.globalnotify-item[data-id="' + notificationId + '"]'); if (!item) return; // Visual feedback item.style.opacity = '0.5'; // AJAX call to toggle read status this.ajaxCall('dismiss', { id: notificationId }, function(data) { if (data.success) { // Move item to history or remove item.style.transition = 'opacity 0.3s, transform 0.3s'; item.style.opacity = '0'; item.style.transform = 'translateX(-100%)'; setTimeout(function() { item.remove(); GlobalNotify.updateBadge(); GlobalNotify.updateCounts(); }, 300); } else { item.style.opacity = '1'; } }); }, /** * Go to action URL and mark as read */ goToAction: function(notificationId, url) { // Mark as read first this.ajaxCall('dismiss', { id: notificationId }, function() { // Navigate to URL window.location.href = url; }); }, /** * Mark all as read */ markAllRead: function() { var section = document.querySelector('.globalnotify-section'); var items = section ? section.querySelectorAll('.globalnotify-item') : []; // Visual feedback items.forEach(function(item) { item.style.opacity = '0.5'; }); this.ajaxCall('markallread', {}, function(data) { if (data.success) { // Clear the section if (section) { section.innerHTML = '

Keine Benachrichtigungen
'; } GlobalNotify.updateBadge(); GlobalNotify.updateCounts(); } }); }, /** * Update badge on FAB */ updateBadge: function() { var badge = document.querySelector('.globalnotify-fab-badge'); var items = document.querySelectorAll('.globalnotify-section .globalnotify-item'); var count = items.length; if (count > 0) { if (badge) { badge.textContent = count; } else { // Create badge var fab = document.getElementById('globalnotify-fab'); if (fab) { badge = document.createElement('span'); badge.className = 'globalnotify-fab-badge'; badge.textContent = count; fab.appendChild(badge); } } } else if (badge) { badge.remove(); } // Update urgent state var fab = document.getElementById('globalnotify-fab'); if (fab) { var hasUrgent = document.querySelector('.globalnotify-item-error, .globalnotify-item-action'); if (hasUrgent && count > 0) { fab.classList.add('globalnotify-fab-urgent'); } else { fab.classList.remove('globalnotify-fab-urgent'); } } }, /** * Update header counts */ updateCounts: function() { var title = document.querySelector('.globalnotify-panel-title'); if (!title) return; var unreadItems = document.querySelectorAll('.globalnotify-section .globalnotify-item'); var historyItems = document.querySelectorAll('.globalnotify-history .globalnotify-item'); var unreadCount = unreadItems.length; var totalCount = unreadCount + historyItems.length; title.textContent = 'Benachrichtigungen (' + unreadCount + '/' + totalCount + ')'; // Hide/show mark all link var markAll = document.querySelector('.globalnotify-panel-actions .globalnotify-action-link'); if (markAll) { markAll.style.display = unreadCount > 0 ? '' : 'none'; } }, /** * AJAX helper */ ajaxCall: function(action, params, callback) { var url = this.urlRoot + '/custom/globalnotify/ajax/action.php'; var body = 'action=' + encodeURIComponent(action); for (var key in params) { body += '&' + key + '=' + encodeURIComponent(params[key]); } if (typeof TOKEN !== 'undefined') { body += '&token=' + TOKEN; } fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: body }) .then(function(response) { return response.json(); }) .then(function(data) { if (callback) callback(data); }) .catch(function(error) { console.error('GlobalNotify error:', error); if (callback) callback({ success: false, error: error }); }); }, /** * Initialize dragging - FAB button is draggable */ initDrag: function() { var widget = document.getElementById('globalnotify-widget'); var fab = document.getElementById('globalnotify-fab'); if (!widget || !fab) return; var dragStartTime = 0; var dragStartPos = { x: 0, y: 0 }; // Make FAB draggable fab.addEventListener('mousedown', function(e) { e.preventDefault(); dragStartTime = Date.now(); dragStartPos = { x: e.clientX, y: e.clientY }; GlobalNotify.isDragging = true; var rect = widget.getBoundingClientRect(); GlobalNotify.dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; document.body.style.userSelect = 'none'; fab.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', function(e) { if (!GlobalNotify.isDragging) return; var x = e.clientX - GlobalNotify.dragOffset.x; var y = e.clientY - GlobalNotify.dragOffset.y; // Keep within viewport x = Math.max(0, Math.min(x, window.innerWidth - 60)); y = Math.max(0, Math.min(y, window.innerHeight - 60)); widget.style.left = x + 'px'; widget.style.top = y + 'px'; widget.style.bottom = 'auto'; widget.style.right = 'auto'; }); document.addEventListener('mouseup', function(e) { if (!GlobalNotify.isDragging) return; GlobalNotify.isDragging = false; document.body.style.userSelect = ''; fab.style.cursor = 'pointer'; // Only toggle if it was a click (not a drag) var timeDiff = Date.now() - dragStartTime; var distX = Math.abs(e.clientX - dragStartPos.x); var distY = Math.abs(e.clientY - dragStartPos.y); // If moved less than 5px and less than 200ms, treat as click if (distX < 5 && distY < 5 && timeDiff < 200) { GlobalNotify.toggle(); } // Save position to localStorage GlobalNotify.savePosition(); }); // Load saved position this.loadPosition(); }, /** * Save widget position to localStorage */ savePosition: function() { var widget = document.getElementById('globalnotify-widget'); if (!widget) return; var pos = { left: widget.style.left, top: widget.style.top, bottom: widget.style.bottom }; try { localStorage.setItem('globalnotify_position', JSON.stringify(pos)); } catch (e) {} }, /** * Load widget position from localStorage */ loadPosition: function() { var widget = document.getElementById('globalnotify-widget'); if (!widget) return; try { var saved = localStorage.getItem('globalnotify_position'); if (saved) { var pos = JSON.parse(saved); if (pos.left && pos.left !== 'auto') { widget.style.left = pos.left; widget.style.bottom = 'auto'; } if (pos.top && pos.top !== 'auto') { widget.style.top = pos.top; widget.style.bottom = 'auto'; } } } catch (e) {} }, /** * Track last known count for new notification detection */ lastKnownCount: 0, /** * Refresh notifications via AJAX */ refresh: function() { this.ajaxCall('getcount', {}, function(data) { if (data.success) { var oldCount = GlobalNotify.lastKnownCount; var newCount = data.count; // Check if new notifications arrived if (newCount > oldCount && oldCount >= 0) { GlobalNotify.playNotificationSound(); GlobalNotify.animateFab(); } GlobalNotify.lastKnownCount = newCount; GlobalNotify.updateBadgeFromServer(newCount); } }); }, /** * Play notification sound */ playNotificationSound: function() { try { // Use Web Audio API for notification sound var audioContext = new (window.AudioContext || window.webkitAudioContext)(); var oscillator = audioContext.createOscillator(); var gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); // Pleasant notification tone oscillator.frequency.setValueAtTime(880, audioContext.currentTime); // A5 oscillator.type = 'sine'; gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.3); // Second tone for double-beep effect setTimeout(function() { var osc2 = audioContext.createOscillator(); var gain2 = audioContext.createGain(); osc2.connect(gain2); gain2.connect(audioContext.destination); osc2.frequency.setValueAtTime(1100, audioContext.currentTime); // C#6 osc2.type = 'sine'; gain2.gain.setValueAtTime(0.3, audioContext.currentTime); gain2.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2); osc2.start(audioContext.currentTime); osc2.stop(audioContext.currentTime + 0.2); }, 150); } catch (e) { console.log('GlobalNotify: Audio not available'); } }, /** * Animate FAB on new notification */ animateFab: function() { var fab = document.getElementById('globalnotify-fab'); if (!fab) return; // Add shake animation fab.classList.add('globalnotify-fab-shake'); // Remove class after animation completes setTimeout(function() { fab.classList.remove('globalnotify-fab-shake'); // Add bounce effect fab.classList.add('globalnotify-fab-bounce'); setTimeout(function() { fab.classList.remove('globalnotify-fab-bounce'); }, 600); }, 800); }, /** * Update badge from server count */ updateBadgeFromServer: function(count) { var badge = document.querySelector('.globalnotify-fab-badge'); var fab = document.getElementById('globalnotify-fab'); if (count > 0) { // Add active class (red) if (fab) { fab.classList.add('globalnotify-fab-active'); } if (badge) { badge.textContent = count; } else if (fab) { badge = document.createElement('span'); badge.className = 'globalnotify-fab-badge'; badge.textContent = count; fab.appendChild(badge); } } else { // Remove active class (back to gray) if (fab) { fab.classList.remove('globalnotify-fab-active'); fab.classList.remove('globalnotify-fab-urgent'); } if (badge) { badge.remove(); } } }, /** * Initialize */ init: function() { this.initDrag(); // Initialize last known count from current badge var badge = document.querySelector('.globalnotify-fab-badge'); this.lastKnownCount = badge ? parseInt(badge.textContent) || 0 : 0; // Set initial FAB state based on count var fab = document.getElementById('globalnotify-fab'); if (fab && this.lastKnownCount > 0) { fab.classList.add('globalnotify-fab-active'); } // Periodic refresh every 2 minutes setInterval(function() { GlobalNotify.refresh(); }, 120000); } }; // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', function() { GlobalNotify.init(); });