/* global window, document, jQuery */ /** * Event Popup, Day Popup, Event CRUD, and Maintenance Task Handling * * - Clicking an event item with data-luxtools-event="1" opens a detail popup. * - Clicking empty space in a calendar day cell opens a day popup listing all events. * - Day popup includes a "Create Event" action for authenticated users. * - Event popup includes "Edit" and "Delete" actions for authenticated users. * - Clicking a maintenance task action button sends an AJAX request to * complete/reopen the task. */ (function () { 'use strict'; var Luxtools = window.Luxtools || (window.Luxtools = {}); // Temporary storage for form data when showing the recurring edit scope dialog var _pendingEditFormData = null; // ============================================================ // Shared helpers // ============================================================ function escapeHtml(text) { var div = document.createElement('div'); div.appendChild(document.createTextNode(text)); return div.innerHTML; } function pad2(value) { return String(value).padStart(2, '0'); } function formatDate(isoStr) { if (!isoStr) return ''; var d = new Date(isoStr); if (isNaN(d.getTime())) return isoStr; return pad2(d.getDate()) + '.' + pad2(d.getMonth() + 1) + '.' + d.getFullYear(); } function formatDateTime(isoStr) { if (!isoStr) return ''; var d = new Date(isoStr); if (isNaN(d.getTime())) return isoStr; return formatDate(isoStr) + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()); } function formatTimeOnly(isoStr) { if (!isoStr) return ''; var d = new Date(isoStr); if (isNaN(d.getTime())) return isoStr; return pad2(d.getHours()) + ':' + pad2(d.getMinutes()); } function isSameMoment(left, right) { if (!left || !right) return false; return left === right; } function getAjaxUrl() { return (window.DOKU_BASE || '/') + 'lib/exe/ajax.php'; } function getSecurityToken(el) { // Try element hierarchy if (el) { var container = el.closest ? el.closest('[data-luxtools-sectok]') : null; if (container) { var tok = container.getAttribute('data-luxtools-sectok'); if (tok) return tok; } } if (window.JSINFO && window.JSINFO.sectok) return String(window.JSINFO.sectok); var input = document.querySelector('input[name="sectok"], input[name="securitytoken"]'); if (input && input.value) return String(input.value); return ''; } function isAuthenticated() { // Check if user is logged in via JSINFO if (window.JSINFO && window.JSINFO.isadmin) return true; if (window.JSINFO && window.JSINFO.id !== undefined) { // Check if user info exists (logged in users have a userinfo) var userinfo = document.querySelector('.user'); if (userinfo) return true; // Alternative: check for logout form var logoutLink = document.querySelector('a[href*="do=logout"], .action.logout'); if (logoutLink) return true; } return false; } function showNotification(message, type) { if (typeof window.msg === 'function') { var level = (type === 'error') ? -1 : ((type === 'warning') ? 0 : 1); window.msg(message, level); return; } var notif = document.createElement('div'); notif.className = 'luxtools-notification luxtools-notification-' + type; notif.textContent = message; document.body.appendChild(notif); setTimeout(function () { if (notif.parentNode) notif.parentNode.removeChild(notif); }, 5000); } // ============================================================ // Popup infrastructure (shared overlay) // ============================================================ var PopupUI = (function () { var overlay = null; var popup = null; function ensureElements() { if (overlay) return; overlay = document.createElement('div'); overlay.className = 'luxtools-event-popup-overlay'; overlay.style.display = 'none'; popup = document.createElement('div'); popup.className = 'luxtools-event-popup'; popup.setAttribute('role', 'dialog'); popup.setAttribute('aria-modal', 'true'); overlay.appendChild(popup); document.body.appendChild(overlay); overlay.addEventListener('click', function (e) { if (e.target === overlay) close(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && overlay && overlay.style.display !== 'none') { close(); } }); } function show(html) { ensureElements(); popup.innerHTML = html; overlay.style.display = 'flex'; var closeBtn = popup.querySelector('.luxtools-event-popup-close'); if (closeBtn) closeBtn.addEventListener('click', close); } function close() { if (overlay) overlay.style.display = 'none'; } function getPopup() { ensureElements(); return popup; } return { show: show, close: close, getPopup: getPopup }; })(); // ============================================================ // Event Popup (single event detail) // ============================================================ var EventPopup = (function () { /** * Open event detail popup. * @param {Element} el - Element with data-event-* attributes * @param {object} [opts] - Options * @param {boolean} [opts.hideDatetime] - Hide the date/time field (when opened from day context) */ function open(el, opts) { opts = opts || {}; var summary = el.getAttribute('data-event-summary') || ''; var start = el.getAttribute('data-event-start') || ''; var end = el.getAttribute('data-event-end') || ''; var location = el.getAttribute('data-event-location') || ''; var description = el.getAttribute('data-event-description') || ''; var allDay = el.getAttribute('data-event-allday') === '1'; var slot = el.getAttribute('data-event-slot') || ''; var uid = el.getAttribute('data-event-uid') || ''; var recurrence = el.getAttribute('data-event-recurrence') || ''; var dateIso = el.getAttribute('data-event-date') || ''; var html = '
'; html += ''; html += '

' + escapeHtml(summary) + '

'; // Date/time - hide when opened from a day context if (!opts.hideDatetime) { html += '
'; if (allDay) { html += 'Date: ' + formatDate(start); if (end && !isSameMoment(start, end)) { html += ' – ' + formatDate(end); } } else { html += 'Time: ' + formatDateTime(start); if (end && !isSameMoment(start, end)) { html += ' – ' + formatDateTime(end); } } html += '
'; } else if (!allDay && start) { // In day context, show only time (not date) html += '
'; html += 'Time: ' + formatTimeOnly(start); if (end && !isSameMoment(start, end)) { html += ' – ' + formatTimeOnly(end); } html += '
'; } if (location) { html += '
Location: ' + escapeHtml(location) + '
'; } if (description) { html += '
' + 'Description:
' + escapeHtml(description).replace(/\n/g, '
') + '
'; } if (slot) { html += '
' + escapeHtml(slot) + '
'; } // Edit/Delete actions for authenticated users with a UID if (uid && isAuthenticated()) { html += '
'; html += ' '; html += ''; html += '
'; } html += '
'; PopupUI.show(html); } function close() { PopupUI.close(); } return { open: open, close: close }; })(); // ============================================================ // Day Popup (list events for a specific day) // ============================================================ var DayPopup = (function () { function open(dayCell) { var dateIso = dayCell.getAttribute('data-luxtools-date') || ''; if (!dateIso) return; var events = []; var rawJson = dayCell.getAttribute('data-luxtools-day-events'); if (rawJson) { try { events = JSON.parse(rawJson); } catch (e) { events = []; } } var html = '
'; html += ''; html += '

' + escapeHtml(formatDate(dateIso + 'T00:00:00')) + '

'; if (events.length === 0) { html += '
No events
'; } else { html += ''; } // Create Event action for authenticated users if (isAuthenticated()) { html += '
'; html += ''; html += '
'; } html += '
'; PopupUI.show(html); } return { open: open }; })(); // ============================================================ // Event Form (create/edit) // ============================================================ var EventForm = (function () { var _calendarSlots = null; function loadSlots(callback) { if (_calendarSlots) { callback(_calendarSlots); return; } var xhr = new XMLHttpRequest(); xhr.open('GET', getAjaxUrl() + '?call=luxtools_calendar_slots', true); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.onload = function () { try { var result = JSON.parse(xhr.responseText); if (result.ok && result.slots) { _calendarSlots = result.slots; } else { _calendarSlots = []; } } catch (e) { _calendarSlots = []; } callback(_calendarSlots); }; xhr.onerror = function () { _calendarSlots = []; callback(_calendarSlots); }; xhr.send(); } function openCreate(dateIso) { loadSlots(function (slots) { renderForm({ mode: 'create', date: dateIso, summary: '', startTime: '', endTime: '', location: '', description: '', allDay: true, slot: (slots.length > 0) ? slots[0].key : 'general', }, slots); }); } function openEdit(data) { loadSlots(function (slots) { // Parse start/end times var startTime = ''; var endTime = ''; if (!data.allDay && data.start) { var sd = new Date(data.start); if (!isNaN(sd.getTime())) startTime = pad2(sd.getHours()) + ':' + pad2(sd.getMinutes()); } if (!data.allDay && data.end) { var ed = new Date(data.end); if (!isNaN(ed.getTime())) endTime = pad2(ed.getHours()) + ':' + pad2(ed.getMinutes()); } renderForm({ mode: 'edit', uid: data.uid || '', recurrence: data.recurrence || '', date: data.date || '', summary: data.summary || '', startTime: startTime, endTime: endTime, location: data.location || '', description: data.description || '', allDay: data.allDay, slot: data.slot || 'general', }, slots); }); } function renderForm(data, slots) { var isEdit = data.mode === 'edit'; var title = isEdit ? 'Edit Event' : 'Create Event'; var html = '
'; html += ''; html += '

' + escapeHtml(title) + '

'; html += '
'; html += ''; html += '
'; html += '
'; html += ''; html += '
'; html += '
'; html += ''; html += '
'; html += ''; html += '
'; html += ''; html += '
'; html += '
'; html += ''; html += '
'; html += '
'; html += ''; html += '
'; html += '
'; html += ' '; html += ''; html += '
'; html += '
'; PopupUI.show(html); // Wire up all-day checkbox toggle var popup = PopupUI.getPopup(); var allDayCheckbox = popup.querySelector('.luxtools-form-allday'); var timeFields = popup.querySelector('.luxtools-event-form-time-fields'); if (allDayCheckbox && timeFields) { allDayCheckbox.addEventListener('change', function () { timeFields.style.display = allDayCheckbox.checked ? 'none' : ''; }); } } function collectFormData() { var popup = PopupUI.getPopup(); return { summary: (popup.querySelector('.luxtools-form-summary') || {}).value || '', date: (popup.querySelector('.luxtools-form-date') || {}).value || '', allDay: !!(popup.querySelector('.luxtools-form-allday') || {}).checked, startTime: (popup.querySelector('.luxtools-form-start-time') || {}).value || '', endTime: (popup.querySelector('.luxtools-form-end-time') || {}).value || '', location: (popup.querySelector('.luxtools-form-location') || {}).value || '', description: (popup.querySelector('.luxtools-form-description') || {}).value || '', slot: (popup.querySelector('.luxtools-form-slot') || {}).value || 'general', }; } function save(saveBtn) { var mode = saveBtn.getAttribute('data-mode'); var formData = collectFormData(); if (!formData.summary.trim()) { showNotification('Summary is required', 'error'); return; } if (!formData.date) { showNotification('Date is required', 'error'); return; } // For recurring event edits, ask about scope first if (mode === 'edit') { var recurrence = saveBtn.getAttribute('data-recurrence') || ''; if (recurrence) { showRecurrenceEditScopeDialog(saveBtn, formData); return; } } submitSave(saveBtn, formData, 'all'); } function showRecurrenceEditScopeDialog(saveBtn, formData) { var uid = saveBtn.getAttribute('data-uid') || ''; var recurrence = saveBtn.getAttribute('data-recurrence') || ''; var html = '
'; html += ''; html += '

Edit Recurring Event

'; html += '

This is a recurring event. What would you like to edit?

'; html += '
'; var baseAttrs = ' data-uid="' + escapeHtml(uid) + '"' + ' data-recurrence="' + escapeHtml(recurrence) + '"' + ' data-mode="edit"'; html += ' '; html += ' '; html += ' '; html += ''; html += '
'; // Store formData on the global scope so the scope button handler can access it _pendingEditFormData = formData; PopupUI.show(html); } function submitSave(saveBtn, formData, scope) { saveBtn.disabled = true; saveBtn.textContent = 'Saving...'; var mode = saveBtn.getAttribute('data-mode') || 'create'; var params = 'call=luxtools_calendar_event' + '&action=' + encodeURIComponent(mode === 'edit' ? 'edit' : 'create') + '&summary=' + encodeURIComponent(formData.summary) + '&date=' + encodeURIComponent(formData.date) + '&allday=' + (formData.allDay ? '1' : '0') + '&start_time=' + encodeURIComponent(formData.startTime) + '&end_time=' + encodeURIComponent(formData.endTime) + '&location=' + encodeURIComponent(formData.location) + '&description=' + encodeURIComponent(formData.description) + '&slot=' + encodeURIComponent(formData.slot) + '§ok=' + encodeURIComponent(getSecurityToken(saveBtn)); if (mode === 'edit') { params += '&uid=' + encodeURIComponent(saveBtn.getAttribute('data-uid') || ''); params += '&recurrence=' + encodeURIComponent(saveBtn.getAttribute('data-recurrence') || ''); params += '&scope=' + encodeURIComponent(scope); } var xhr = new XMLHttpRequest(); xhr.open('POST', getAjaxUrl(), true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onload = function () { try { var result = JSON.parse(xhr.responseText); if (result.ok) { PopupUI.close(); showNotification(result.message || 'Saved', 'success'); window.location.reload(); } else { showNotification(result.error || 'Save failed', 'error'); saveBtn.disabled = false; saveBtn.textContent = 'Save'; } } catch (e) { showNotification('Invalid response', 'error'); saveBtn.disabled = false; saveBtn.textContent = 'Save'; } }; xhr.onerror = function () { showNotification('Network error', 'error'); saveBtn.disabled = false; saveBtn.textContent = 'Save'; }; xhr.send(params); } return { openCreate: openCreate, openEdit: openEdit, save: save, submitSave: submitSave }; })(); // ============================================================ // Event Deletion // ============================================================ var EventDelete = (function () { function confirmDelete(btn) { var uid = btn.getAttribute('data-uid') || ''; var slot = btn.getAttribute('data-slot') || ''; var recurrence = btn.getAttribute('data-recurrence') || ''; var dateIso = btn.getAttribute('data-date') || ''; if (!uid) return; // For recurring events, ask about scope if (recurrence) { showRecurrenceDeleteDialog(uid, slot, recurrence, dateIso); return; } // Simple confirmation var html = '
'; html += ''; html += '

Delete Event

'; html += '

Are you sure you want to delete this event?

'; html += '
'; html += ' '; html += ''; html += '
'; PopupUI.show(html); } function showRecurrenceDeleteDialog(uid, slot, recurrence, dateIso) { var html = '
'; html += ''; html += '

Delete Recurring Event

'; html += '

This is a recurring event. What would you like to delete?

'; html += '
'; var baseAttrs = ' data-uid="' + escapeHtml(uid) + '"' + ' data-slot="' + escapeHtml(slot) + '"' + ' data-recurrence="' + escapeHtml(recurrence) + '"' + ' data-date="' + escapeHtml(dateIso) + '"'; html += ' '; html += ' '; html += ' '; html += ''; html += '
'; PopupUI.show(html); } function executeDelete(btn) { var uid = btn.getAttribute('data-uid') || ''; var slot = btn.getAttribute('data-slot') || ''; var recurrence = btn.getAttribute('data-recurrence') || ''; var dateIso = btn.getAttribute('data-date') || ''; var scope = btn.getAttribute('data-scope') || 'all'; btn.disabled = true; btn.textContent = 'Deleting...'; var params = 'call=luxtools_calendar_event' + '&action=delete' + '&uid=' + encodeURIComponent(uid) + '&slot=' + encodeURIComponent(slot) + '&recurrence=' + encodeURIComponent(recurrence) + '&date=' + encodeURIComponent(dateIso) + '&scope=' + encodeURIComponent(scope) + '§ok=' + encodeURIComponent(getSecurityToken(btn)); var xhr = new XMLHttpRequest(); xhr.open('POST', getAjaxUrl(), true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onload = function () { try { var result = JSON.parse(xhr.responseText); if (result.ok) { PopupUI.close(); showNotification(result.message || 'Deleted', 'success'); window.location.reload(); } else { showNotification(result.error || 'Delete failed', 'error'); btn.disabled = false; btn.textContent = btn.getAttribute('data-scope') === 'all' ? 'Delete' : btn.textContent; } } catch (e) { showNotification('Invalid response', 'error'); btn.disabled = false; } }; xhr.onerror = function () { showNotification('Network error', 'error'); btn.disabled = false; }; xhr.send(params); } return { confirmDelete: confirmDelete, executeDelete: executeDelete }; })(); // ============================================================ // Maintenance Task Actions // ============================================================ var MaintenanceTasks = (function () { function handleAction(button) { var action = button.getAttribute('data-action'); if (!action) return; var item = button.closest('[data-task-uid]'); if (!item) item = button.closest('[data-uid]'); if (!item) return; var uid = item.getAttribute('data-task-uid') || item.getAttribute('data-uid') || ''; var date = item.getAttribute('data-task-date') || item.getAttribute('data-date') || ''; var recurrence = item.getAttribute('data-task-recurrence') || item.getAttribute('data-recurrence') || ''; if (!uid || !date) return; var ajaxUrl = getAjaxUrl(); var sectok = getSecurityToken(item); button.disabled = true; button.textContent = '...'; var params = 'call=luxtools_maintenance_task' + '&action=' + encodeURIComponent(action) + '&uid=' + encodeURIComponent(uid) + '&date=' + encodeURIComponent(date) + '&recurrence=' + encodeURIComponent(recurrence) + '§ok=' + encodeURIComponent(sectok); var xhr = new XMLHttpRequest(); xhr.open('POST', ajaxUrl, true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onload = function () { var result; try { result = JSON.parse(xhr.responseText); } catch (e) { result = { ok: false, error: 'Invalid response' }; } if (result.ok) { if (action === 'complete') { item.classList.add('luxtools-task-completed'); button.textContent = 'Reopen'; button.setAttribute('data-action', 'reopen'); } else { item.classList.remove('luxtools-task-completed'); item.style.opacity = '1'; button.textContent = 'Complete'; button.setAttribute('data-action', 'complete'); } button.disabled = false; if (result.remoteOk === false && result.remoteError) { showNotification(result.remoteError, 'warning'); } } else { showNotification(result.error || 'Action failed', 'error'); button.textContent = action === 'complete' ? 'Complete' : 'Reopen'; button.disabled = false; } }; xhr.onerror = function () { showNotification('Network error', 'error'); button.textContent = action === 'complete' ? 'Complete' : 'Reopen'; button.disabled = false; }; xhr.send(params); } return { handleAction: handleAction }; })(); // ============================================================ // Event Delegation // ============================================================ document.addEventListener('click', function (e) { var target = e.target; // Maintenance task action buttons (day pages) if (target.classList && target.classList.contains('luxtools-task-action')) { e.preventDefault(); MaintenanceTasks.handleAction(target); return; } // Maintenance task complete buttons (syntax plugin list) if (target.classList && target.classList.contains('luxtools-task-complete-btn')) { e.preventDefault(); MaintenanceTasks.handleAction(target); return; } // Event form save if (target.classList && target.classList.contains('luxtools-event-form-save')) { e.preventDefault(); EventForm.save(target); return; } // Event form cancel if (target.classList && target.classList.contains('luxtools-event-form-cancel')) { e.preventDefault(); PopupUI.close(); return; } // Event create button (from day popup) if (target.classList && target.classList.contains('luxtools-event-create-btn')) { e.preventDefault(); EventForm.openCreate(target.getAttribute('data-date') || ''); return; } // Event edit button if (target.classList && target.classList.contains('luxtools-event-edit-btn')) { e.preventDefault(); EventForm.openEdit({ uid: target.getAttribute('data-uid') || '', slot: target.getAttribute('data-slot') || '', recurrence: target.getAttribute('data-recurrence') || '', date: target.getAttribute('data-date') || '', summary: target.getAttribute('data-summary') || '', start: target.getAttribute('data-start') || '', end: target.getAttribute('data-end') || '', location: target.getAttribute('data-location') || '', description: target.getAttribute('data-description') || '', allDay: target.getAttribute('data-allday') === '1', }); return; } // Event delete button if (target.classList && target.classList.contains('luxtools-event-delete-btn')) { e.preventDefault(); EventDelete.confirmDelete(target); return; } // Confirm delete button if (target.classList && target.classList.contains('luxtools-event-confirm-delete')) { e.preventDefault(); EventDelete.executeDelete(target); return; } // Confirm edit scope button (recurring event edit) if (target.classList && target.classList.contains('luxtools-event-confirm-edit-scope')) { e.preventDefault(); if (_pendingEditFormData) { EventForm.submitSave(target, _pendingEditFormData, target.getAttribute('data-scope') || 'all'); _pendingEditFormData = null; } return; } // Event popup: clicking an event item within the day popup // (items inside the popup that have data-luxtools-event) var dayPopupEvent = target.closest ? target.closest('.luxtools-day-popup-event-item[data-luxtools-event]') : null; if (dayPopupEvent) { if (target.tagName === 'BUTTON' || (target.closest && target.closest('button'))) return; e.preventDefault(); EventPopup.open(dayPopupEvent, { hideDatetime: true }); return; } // Event popup: find closest element with data-luxtools-event var eventEl = target.closest ? target.closest('[data-luxtools-event]') : null; if (!eventEl) { var el = target; while (el && el !== document) { if (el.getAttribute && el.getAttribute('data-luxtools-event') === '1') { eventEl = el; break; } el = el.parentNode; } } if (eventEl) { if (target.tagName === 'BUTTON' || (target.closest && target.closest('button'))) return; // Check if this event is inside a day cell (calendar context) var dayCell = eventEl.closest ? eventEl.closest('td[data-luxtools-day="1"]') : null; e.preventDefault(); EventPopup.open(eventEl, { hideDatetime: !!dayCell }); return; } // Day cell click: open day popup when clicking empty space var clickedDayCell = target.closest ? target.closest('td[data-luxtools-day="1"]') : null; if (clickedDayCell) { // Don't interfere with link clicks inside the cell if (target.tagName === 'A' || (target.closest && target.closest('a'))) return; e.preventDefault(); DayPopup.open(clickedDayCell); return; } }, false); Luxtools.EventPopup = EventPopup; Luxtools.DayPopup = DayPopup; Luxtools.MaintenanceTasks = MaintenanceTasks; })();