Files
luxtools-plugin/js/event-popup.js
2026-03-18 14:33:03 +01:00

931 lines
35 KiB
JavaScript

/* 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);
}
// ============================================================
// 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 = '<div class="luxtools-event-popup-content">';
html += '<button type="button" class="luxtools-event-popup-close" aria-label="Close">&times;</button>';
html += '<h3 class="luxtools-event-popup-title">' + escapeHtml(summary) + '</h3>';
// Date/time - hide when opened from a day context
if (!opts.hideDatetime) {
html += '<div class="luxtools-event-popup-field">';
if (allDay) {
html += '<strong>Date:</strong> ' + formatDate(start);
if (end && !isSameMoment(start, end)) {
html += ' &ndash; ' + formatDate(end);
}
} else {
html += '<strong>Time:</strong> ' + formatDateTime(start);
if (end && !isSameMoment(start, end)) {
html += ' &ndash; ' + formatDateTime(end);
}
}
html += '</div>';
} else if (!allDay && start) {
// In day context, show only time (not date)
html += '<div class="luxtools-event-popup-field">';
html += '<strong>Time:</strong> ' + formatTimeOnly(start);
if (end && !isSameMoment(start, end)) {
html += ' &ndash; ' + formatTimeOnly(end);
}
html += '</div>';
}
if (location) {
html += '<div class="luxtools-event-popup-field"><strong>Location:</strong> ' + escapeHtml(location) + '</div>';
}
if (description) {
html += '<div class="luxtools-event-popup-field luxtools-event-popup-description">'
+ '<strong>Description:</strong><br>'
+ escapeHtml(description).replace(/\n/g, '<br>')
+ '</div>';
}
if (slot) {
html += '<div class="luxtools-event-popup-slot"><em>' + escapeHtml(slot) + '</em></div>';
}
// Edit/Delete actions for authenticated users with a UID
if (uid && isAuthenticated()) {
html += '<div class="luxtools-event-popup-actions">';
html += '<button type="button" class="button luxtools-event-edit-btn"'
+ ' data-uid="' + escapeHtml(uid) + '"'
+ ' data-slot="' + escapeHtml(slot) + '"'
+ ' data-recurrence="' + escapeHtml(recurrence) + '"'
+ ' data-date="' + escapeHtml(dateIso) + '"'
+ ' data-summary="' + escapeHtml(summary) + '"'
+ ' data-start="' + escapeHtml(start) + '"'
+ ' data-end="' + escapeHtml(end) + '"'
+ ' data-location="' + escapeHtml(location) + '"'
+ ' data-description="' + escapeHtml(description) + '"'
+ ' data-allday="' + (allDay ? '1' : '0') + '"'
+ '>Edit</button> ';
html += '<button type="button" class="button luxtools-event-delete-btn"'
+ ' data-uid="' + escapeHtml(uid) + '"'
+ ' data-slot="' + escapeHtml(slot) + '"'
+ ' data-recurrence="' + escapeHtml(recurrence) + '"'
+ ' data-date="' + escapeHtml(dateIso) + '"'
+ '>Delete</button>';
html += '</div>';
}
html += '</div>';
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 = '<div class="luxtools-event-popup-content">';
html += '<button type="button" class="luxtools-event-popup-close" aria-label="Close">&times;</button>';
html += '<h3 class="luxtools-event-popup-title">' + escapeHtml(formatDate(dateIso + 'T00:00:00')) + '</h3>';
if (events.length === 0) {
html += '<div class="luxtools-event-popup-field luxtools-day-popup-empty">No events</div>';
} else {
html += '<ul class="luxtools-day-popup-events">';
for (var i = 0; i < events.length; i++) {
var ev = events[i];
var attrs = ' data-luxtools-event="1"'
+ ' data-event-summary="' + escapeHtml(ev.summary || '') + '"'
+ ' data-event-start="' + escapeHtml(ev.start || '') + '"'
+ ' data-event-end="' + escapeHtml(ev.end || '') + '"'
+ ' data-event-location="' + escapeHtml(ev.location || '') + '"'
+ ' data-event-description="' + escapeHtml(ev.description || '') + '"'
+ ' data-event-allday="' + (ev.allDay ? '1' : '0') + '"'
+ ' data-event-slot="' + escapeHtml(ev.slot || '') + '"'
+ ' data-event-uid="' + escapeHtml(ev.uid || '') + '"'
+ ' data-event-recurrence="' + escapeHtml(ev.recurrence || '') + '"'
+ ' data-event-date="' + escapeHtml(ev.date || dateIso) + '"';
var timeLabel = '';
var timeStr = '';
if (!ev.allDay) {
timeLabel = formatEventListTime(ev.start || '', ev.time || '');
}
if (timeLabel) {
timeStr = '<span class="luxtools-event-time">' + escapeHtml(timeLabel) + '</span> - ';
}
html += '<li class="luxtools-day-popup-event-item"' + attrs + '>'
+ timeStr
+ '<span class="luxtools-event-summary">' + escapeHtml(ev.summary || '') + '</span>'
+ '</li>';
}
html += '</ul>';
}
// Create Event action for authenticated users
if (isAuthenticated()) {
html += '<div class="luxtools-event-popup-actions">';
html += '<button type="button" class="button luxtools-event-create-btn" data-date="' + escapeHtml(dateIso) + '">Create Event</button>';
html += '</div>';
}
html += '</div>';
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 = '<div class="luxtools-event-popup-content luxtools-event-form">';
html += '<button type="button" class="luxtools-event-popup-close" aria-label="Close">&times;</button>';
html += '<h3 class="luxtools-event-popup-title">' + escapeHtml(title) + '</h3>';
html += '<div class="luxtools-event-form-field">';
html += '<label>Summary<br><input type="text" class="edit luxtools-form-summary" value="' + escapeHtml(data.summary) + '" /></label>';
html += '</div>';
html += '<div class="luxtools-event-form-field">';
html += '<label>Date<br><input type="date" class="edit luxtools-form-date" value="' + escapeHtml(data.date) + '" /></label>';
html += '</div>';
html += '<div class="luxtools-event-form-field">';
html += '<label><input type="checkbox" class="luxtools-form-allday"' + (data.allDay ? ' checked' : '') + ' /> All day</label>';
html += '</div>';
html += '<div class="luxtools-event-form-time-fields"' + (data.allDay ? ' style="display:none"' : '') + '>';
html += '<div class="luxtools-event-form-field">';
html += '<label>Start time<br><input type="time" step="60" class="edit luxtools-form-start-time" value="' + escapeHtml(data.startTime) + '" /></label>';
html += '</div>';
html += '<div class="luxtools-event-form-field">';
html += '<label>End time<br><input type="time" step="60" class="edit luxtools-form-end-time" value="' + escapeHtml(data.endTime) + '" /></label>';
html += '</div>';
html += '</div>';
html += '<div class="luxtools-event-form-field">';
html += '<label>Location<br><input type="text" class="edit luxtools-form-location" value="' + escapeHtml(data.location) + '" /></label>';
html += '</div>';
html += '<div class="luxtools-event-form-field">';
html += '<label>Description<br><textarea class="edit luxtools-form-description" rows="3">' + escapeHtml(data.description) + '</textarea></label>';
html += '</div>';
html += '<div class="luxtools-event-form-field">';
html += '<label>Calendar<br><select class="edit luxtools-form-slot">';
for (var i = 0; i < slots.length; i++) {
var sel = (slots[i].key === data.slot) ? ' selected' : '';
html += '<option value="' + escapeHtml(slots[i].key) + '"' + sel + '>' + escapeHtml(slots[i].label) + '</option>';
}
html += '</select></label>';
html += '</div>';
html += '<div class="luxtools-event-popup-actions">';
html += '<button type="button" class="button luxtools-event-form-save"'
+ ' data-mode="' + escapeHtml(data.mode) + '"'
+ (isEdit ? ' data-uid="' + escapeHtml(data.uid) + '"' : '')
+ (isEdit ? ' data-recurrence="' + escapeHtml(data.recurrence) + '"' : '')
+ '>Save</button> ';
html += '<button type="button" class="button luxtools-event-form-cancel">Cancel</button>';
html += '</div>';
html += '</div>';
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 = '<div class="luxtools-event-popup-content">';
html += '<button type="button" class="luxtools-event-popup-close" aria-label="Close">&times;</button>';
html += '<h3 class="luxtools-event-popup-title">Edit Recurring Event</h3>';
html += '<p>This is a recurring event. What would you like to edit?</p>';
html += '<div class="luxtools-event-popup-actions luxtools-recurrence-actions">';
var baseAttrs = ' data-uid="' + escapeHtml(uid) + '"'
+ ' data-recurrence="' + escapeHtml(recurrence) + '"'
+ ' data-mode="edit"';
html += '<button type="button" class="button luxtools-event-confirm-edit-scope"'
+ baseAttrs + ' data-scope="this">This occurrence</button> ';
html += '<button type="button" class="button luxtools-event-confirm-edit-scope"'
+ baseAttrs + ' data-scope="future">This and future</button> ';
html += '<button type="button" class="button luxtools-event-confirm-edit-scope"'
+ baseAttrs + ' data-scope="all">All occurrences</button> ';
html += '<button type="button" class="button luxtools-event-form-cancel">Cancel</button>';
html += '</div></div>';
// 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)
+ '&sectok=' + 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 = '<div class="luxtools-event-popup-content">';
html += '<button type="button" class="luxtools-event-popup-close" aria-label="Close">&times;</button>';
html += '<h3 class="luxtools-event-popup-title">Delete Event</h3>';
html += '<p>Are you sure you want to delete this event?</p>';
html += '<div class="luxtools-event-popup-actions">';
html += '<button type="button" class="button luxtools-event-confirm-delete"'
+ ' data-uid="' + escapeHtml(uid) + '"'
+ ' data-slot="' + escapeHtml(slot) + '"'
+ ' data-recurrence="' + escapeHtml(recurrence) + '"'
+ ' data-date="' + escapeHtml(dateIso) + '"'
+ ' data-scope="all"'
+ '>Delete</button> ';
html += '<button type="button" class="button luxtools-event-form-cancel">Cancel</button>';
html += '</div></div>';
PopupUI.show(html);
}
function showRecurrenceDeleteDialog(uid, slot, recurrence, dateIso) {
var html = '<div class="luxtools-event-popup-content">';
html += '<button type="button" class="luxtools-event-popup-close" aria-label="Close">&times;</button>';
html += '<h3 class="luxtools-event-popup-title">Delete Recurring Event</h3>';
html += '<p>This is a recurring event. What would you like to delete?</p>';
html += '<div class="luxtools-event-popup-actions luxtools-recurrence-actions">';
var baseAttrs = ' data-uid="' + escapeHtml(uid) + '"'
+ ' data-slot="' + escapeHtml(slot) + '"'
+ ' data-recurrence="' + escapeHtml(recurrence) + '"'
+ ' data-date="' + escapeHtml(dateIso) + '"';
html += '<button type="button" class="button luxtools-event-confirm-delete"'
+ baseAttrs + ' data-scope="this">This occurrence</button> ';
html += '<button type="button" class="button luxtools-event-confirm-delete"'
+ baseAttrs + ' data-scope="future">This and future</button> ';
html += '<button type="button" class="button luxtools-event-confirm-delete"'
+ baseAttrs + ' data-scope="all">All occurrences</button> ';
html += '<button type="button" class="button luxtools-event-form-cancel">Cancel</button>';
html += '</div></div>';
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)
+ '&sectok=' + 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)
+ '&sectok=' + 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;
})();