931 lines
35 KiB
JavaScript
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">×</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 += ' – ' + formatDate(end);
|
|
}
|
|
} else {
|
|
html += '<strong>Time:</strong> ' + formatDateTime(start);
|
|
if (end && !isSameMoment(start, end)) {
|
|
html += ' – ' + 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 += ' – ' + 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">×</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">×</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">×</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)
|
|
+ '§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 = '<div class="luxtools-event-popup-content">';
|
|
html += '<button type="button" class="luxtools-event-popup-close" aria-label="Close">×</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">×</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)
|
|
+ '§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;
|
|
})();
|