/* 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 formatEventListTime(startIso, fallbackTime) { var formatted = formatTimeOnly(startIso); if (!formatted || formatted === startIso) { return fallbackTime || ""; } return formatted; } 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); } // Lazy reference to the shared dialog infrastructure (dialog.js). // Accessed via function to handle any script-loading order variation. function getDialog() { return Luxtools.Dialog; } // ============================================================ // 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 += "
"; getDialog().show(html); } function close() { getDialog().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 += "
"; getDialog().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 += "
"; getDialog().show(html); // Wire up all-day checkbox toggle var popup = getDialog().getContainer(); 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 = getDialog().getContainer(); 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; getDialog().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) { getDialog().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 += "
"; getDialog().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 += "
"; getDialog().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) { getDialog().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(); getDialog().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; })();