1102 lines
35 KiB
JavaScript
1102 lines
35 KiB
JavaScript
/* global window, document, jQuery */
|
|
|
|
/**
|
|
* Event Popup, Day Popup, and Event CRUD
|
|
*
|
|
* - 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.
|
|
*/
|
|
(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 match = isoStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
if (!match) return isoStr;
|
|
return match[3] + "." + match[2] + "." + match[1];
|
|
}
|
|
|
|
function formatDateTime(isoStr) {
|
|
if (!isoStr) return "";
|
|
return formatDate(isoStr) + " " + formatTimeOnly(isoStr);
|
|
}
|
|
|
|
function formatTimeOnly(isoStr) {
|
|
if (!isoStr) return "";
|
|
var match = isoStr.match(/T(\d{2}):(\d{2})/);
|
|
if (!match) return isoStr;
|
|
return match[1] + ":" + match[2];
|
|
}
|
|
|
|
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 = '<div class="luxtools-dialog-content">';
|
|
html +=
|
|
'<button type="button" class="luxtools-dialog-close" aria-label="Close">×</button>';
|
|
html +=
|
|
'<h3 class="luxtools-dialog-title">' + escapeHtml(summary) + "</h3>";
|
|
|
|
// Date/time - hide when opened from a day context
|
|
if (!opts.hideDatetime) {
|
|
html += '<div class="luxtools-dialog-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-dialog-field">';
|
|
html += "<strong>Time:</strong> " + formatTimeOnly(start);
|
|
if (end && !isSameMoment(start, end)) {
|
|
html += " – " + formatTimeOnly(end);
|
|
}
|
|
html += "</div>";
|
|
}
|
|
|
|
if (location) {
|
|
html +=
|
|
'<div class="luxtools-dialog-field"><strong>Location:</strong> ' +
|
|
escapeHtml(location) +
|
|
"</div>";
|
|
}
|
|
|
|
if (description) {
|
|
html +=
|
|
'<div class="luxtools-dialog-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-dialog-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>";
|
|
|
|
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 = '<div class="luxtools-dialog-content">';
|
|
html +=
|
|
'<button type="button" class="luxtools-dialog-close" aria-label="Close">×</button>';
|
|
html +=
|
|
'<h3 class="luxtools-dialog-title">' +
|
|
escapeHtml(formatDate(dateIso + "T00:00:00")) +
|
|
"</h3>";
|
|
|
|
if (events.length === 0) {
|
|
html +=
|
|
'<div class="luxtools-dialog-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-dialog-actions">';
|
|
html +=
|
|
'<button type="button" class="button luxtools-event-create-btn" data-date="' +
|
|
escapeHtml(dateIso) +
|
|
'">Create Event</button>';
|
|
html += "</div>";
|
|
}
|
|
|
|
html += "</div>";
|
|
|
|
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) {
|
|
startTime = formatTimeOnly(data.start);
|
|
if (startTime === data.start) startTime = "";
|
|
}
|
|
if (!data.allDay && data.end) {
|
|
endTime = formatTimeOnly(data.end);
|
|
if (endTime === data.end) endTime = "";
|
|
}
|
|
|
|
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 setupTimeInputNormalization(input) {
|
|
// Track digits typed into the hours sub-field so we can reconstruct
|
|
// "HH:00" when the user fills hours but leaves minutes as "--".
|
|
var typedDigits = "";
|
|
|
|
input.addEventListener("keydown", function (e) {
|
|
if (this.value) {
|
|
// Field already has a valid complete value — reset tracking.
|
|
typedDigits = "";
|
|
return;
|
|
}
|
|
if (/^\d$/.test(e.key)) {
|
|
typedDigits = (typedDigits + e.key).slice(-2);
|
|
} else if (e.key === "Backspace" || e.key === "Delete") {
|
|
typedDigits = "";
|
|
}
|
|
});
|
|
|
|
input.addEventListener("change", function () {
|
|
typedDigits = "";
|
|
});
|
|
|
|
input.addEventListener("blur", function () {
|
|
if (!this.value && this.validity && this.validity.badInput) {
|
|
var h = parseInt(typedDigits, 10);
|
|
if (!isNaN(h) && h >= 0 && h <= 23) {
|
|
this.value = pad2(h) + ":00";
|
|
}
|
|
typedDigits = "";
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderForm(data, slots) {
|
|
var isEdit = data.mode === "edit";
|
|
var title = isEdit ? "Edit Event" : "Create Event";
|
|
|
|
var html = '<div class="luxtools-dialog-content luxtools-event-form">';
|
|
html +=
|
|
'<button type="button" class="luxtools-dialog-close" aria-label="Close">×</button>';
|
|
html +=
|
|
'<h3 class="luxtools-dialog-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-dialog-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>";
|
|
|
|
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" : "";
|
|
});
|
|
}
|
|
|
|
// Wire up time normalization: auto-substitute :00 for missing minutes
|
|
var startTimeInput = popup.querySelector(".luxtools-form-start-time");
|
|
var endTimeInput = popup.querySelector(".luxtools-form-end-time");
|
|
if (startTimeInput) setupTimeInputNormalization(startTimeInput);
|
|
if (endTimeInput) setupTimeInputNormalization(endTimeInput);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (!formData.allDay) {
|
|
var popup = getDialog().getContainer();
|
|
var startInput = popup.querySelector(".luxtools-form-start-time");
|
|
var endInput = popup.querySelector(".luxtools-form-end-time");
|
|
if (
|
|
(startInput && startInput.validity && startInput.validity.badInput) ||
|
|
(endInput && endInput.validity && endInput.validity.badInput)
|
|
) {
|
|
showNotification(
|
|
"Please complete the time fields or clear them.",
|
|
"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-dialog-content">';
|
|
html +=
|
|
'<button type="button" class="luxtools-dialog-close" aria-label="Close">×</button>';
|
|
html += '<h3 class="luxtools-dialog-title">Edit Recurring Event</h3>';
|
|
html += "<p>This is a recurring event. What would you like to edit?</p>";
|
|
html +=
|
|
'<div class="luxtools-dialog-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;
|
|
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 = '<div class="luxtools-dialog-content">';
|
|
html +=
|
|
'<button type="button" class="luxtools-dialog-close" aria-label="Close">×</button>';
|
|
html += '<h3 class="luxtools-dialog-title">Delete Event</h3>';
|
|
html += "<p>Are you sure you want to delete this event?</p>";
|
|
html += '<div class="luxtools-dialog-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>";
|
|
|
|
getDialog().show(html);
|
|
}
|
|
|
|
function showRecurrenceDeleteDialog(uid, slot, recurrence, dateIso) {
|
|
var html = '<div class="luxtools-dialog-content">';
|
|
html +=
|
|
'<button type="button" class="luxtools-dialog-close" aria-label="Close">×</button>';
|
|
html += '<h3 class="luxtools-dialog-title">Delete Recurring Event</h3>';
|
|
html +=
|
|
"<p>This is a recurring event. What would you like to delete?</p>";
|
|
html +=
|
|
'<div class="luxtools-dialog-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>";
|
|
|
|
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 };
|
|
})();
|
|
|
|
// ============================================================
|
|
// Event Delegation
|
|
// ============================================================
|
|
document.addEventListener(
|
|
"click",
|
|
function (e) {
|
|
var target = e.target;
|
|
|
|
// 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;
|
|
})();
|