/* 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 +=
'
";
// 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 +=
'";
}
if (slot) {
html +=
'";
}
// Edit/Delete actions for authenticated users with a UID
if (uid && isAuthenticated()) {
html += '
';
html +=
'Edit ";
html +=
'Delete ";
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 +=
'
";
if (events.length === 0) {
html +=
'';
} else {
html += '";
}
// Create Event action for authenticated users
if (isAuthenticated()) {
html += '
';
html +=
'Create Event ';
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 +=
'
";
html += '
';
html +=
'Summary ';
html += "
";
html += '
';
html +=
'Date ';
html += "
";
html += '
';
html +=
' All day ";
html += "
";
html +=
'
";
html += '
';
html +=
'Location ';
html += "
";
html += '
';
html +=
'Description ";
html += "
";
html += '
';
html += 'Calendar';
for (var i = 0; i < slots.length; i++) {
var sel = slots[i].key === data.slot ? " selected" : "";
html +=
'" +
escapeHtml(slots[i].label) +
" ";
}
html += " ";
html += "
";
html += '
';
html +=
'Save ";
html +=
'Cancel ';
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 += '
';
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 +=
'This occurrence ';
html +=
'This and future ';
html +=
'All occurrences ';
html +=
'Cancel ';
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 += '
';
html += "
Are you sure you want to delete this event?
";
html += '
';
html +=
'Delete ";
html +=
'Cancel ';
html += "
";
getDialog().show(html);
}
function showRecurrenceDeleteDialog(uid, slot, recurrence, dateIso) {
var html = '';
html +=
'
× ';
html += '
';
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 +=
'This occurrence ';
html +=
'This and future ';
html +=
'All occurrences ';
html +=
'Cancel ';
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;
})();