/**
* DokuWiki Compact Calendar Plugin JavaScript
*/
// Navigate to different month
function navCalendar(calId, year, month, namespace) {
const params = new URLSearchParams({
call: 'plugin_calendar',
action: 'load_month',
year: year,
month: month,
namespace: namespace
});
fetch(DOKU_BASE + 'lib/exe/ajax.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
rebuildCalendar(calId, data.year, data.month, data.events, namespace);
}
})
.catch(err => console.error('Error:', err));
}
// Rebuild calendar grid after navigation
function rebuildCalendar(calId, year, month, events, namespace) {
const container = document.getElementById(calId);
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
// Update embedded events data
let eventsDataEl = document.getElementById('events-data-' + calId);
if (eventsDataEl) {
eventsDataEl.textContent = JSON.stringify(events);
} else {
eventsDataEl = document.createElement('script');
eventsDataEl.type = 'application/json';
eventsDataEl.id = 'events-data-' + calId;
eventsDataEl.textContent = JSON.stringify(events);
container.appendChild(eventsDataEl);
}
// Update header
const header = container.querySelector('.calendar-compact-header h3');
header.textContent = monthNames[month - 1] + ' ' + year;
// Update nav buttons
let prevMonth = month - 1;
let prevYear = year;
if (prevMonth < 1) {
prevMonth = 12;
prevYear--;
}
let nextMonth = month + 1;
let nextYear = year;
if (nextMonth > 12) {
nextMonth = 1;
nextYear++;
}
const navBtns = container.querySelectorAll('.cal-nav-btn');
navBtns[0].setAttribute('onclick', `navCalendar('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
navBtns[1].setAttribute('onclick', `navCalendar('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
// Rebuild calendar grid
const tbody = container.querySelector('.calendar-compact-grid tbody');
const firstDay = new Date(year, month - 1, 1);
const daysInMonth = new Date(year, month, 0).getDate();
const dayOfWeek = firstDay.getDay();
let html = '';
let currentDay = 1;
const rowCount = Math.ceil((daysInMonth + dayOfWeek) / 7);
for (let row = 0; row < rowCount; row++) {
html += '
';
for (let col = 0; col < 7; col++) {
if ((row === 0 && col < dayOfWeek) || currentDay > daysInMonth) {
html += ' ';
} else {
const dateKey = `${year}-${String(month).padStart(2, '0')}-${String(currentDay).padStart(2, '0')}`;
const today = new Date().toISOString().split('T')[0];
const isToday = dateKey === today;
const hasEvents = events[dateKey] && events[dateKey].length > 0;
let classes = 'cal-day';
if (isToday) classes += ' cal-today';
if (hasEvents) classes += ' cal-has-events';
html += ``;
html += `${currentDay} `;
if (hasEvents) {
// Sort events by time (no time first, then by time)
const sortedEvents = [...events[dateKey]].sort((a, b) => {
const timeA = a.time || '';
const timeB = b.time || '';
// Events without time go first
if (!timeA && timeB) return -1;
if (timeA && !timeB) return 1;
if (!timeA && !timeB) return 0;
// Sort by time
return timeA.localeCompare(timeB);
});
// Show colored stacked bars for each event
html += '';
for (const evt of sortedEvents) {
const eventId = evt.id || '';
const eventColor = evt.color || '#3498db';
const eventTime = evt.time || '';
const eventTitle = evt.title || 'Event';
const barClass = !eventTime ? 'event-bar-no-time' : 'event-bar-timed';
html += ` `;
}
html += '
';
}
html += ' ';
currentDay++;
}
}
html += ' ';
}
tbody.innerHTML = html;
// Rebuild event list
const eventList = container.querySelector('.event-list-compact');
eventList.innerHTML = renderEventListFromData(events, calId, namespace);
// Update title
const title = container.querySelector('#eventlist-title-' + calId);
title.textContent = 'Events';
}
// Render event list from data
function renderEventListFromData(events, calId, namespace) {
if (!events || Object.keys(events).length === 0) {
return 'No events this month
';
}
let html = '';
const sortedDates = Object.keys(events).sort();
for (const dateKey of sortedDates) {
const dayEvents = events[dateKey];
for (const event of dayEvents) {
html += renderEventItem(event, dateKey, calId, namespace);
}
}
return html;
}
// Show day popup with events when clicking a date
function showDayPopup(calId, date, namespace) {
// Get events for this calendar
const eventsDataEl = document.getElementById('events-data-' + calId);
let events = {};
if (eventsDataEl) {
try {
events = JSON.parse(eventsDataEl.textContent);
} catch (e) {
console.error('Failed to parse events data:', e);
}
}
const dayEvents = events[date] || [];
const dateObj = new Date(date + 'T00:00:00');
const displayDate = dateObj.toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
});
// Create popup
let popup = document.getElementById('day-popup-' + calId);
if (!popup) {
popup = document.createElement('div');
popup.id = 'day-popup-' + calId;
popup.className = 'day-popup';
document.body.appendChild(popup);
}
let html = '';
html += '';
popup.innerHTML = html;
popup.style.display = 'flex';
}
// Close day popup
function closeDayPopup(calId) {
const popup = document.getElementById('day-popup-' + calId);
if (popup) {
popup.style.display = 'none';
}
}
// Show events for a specific day (for event list panel)
function showDayEvents(calId, date, namespace) {
const params = new URLSearchParams({
call: 'plugin_calendar',
action: 'load_month',
year: date.split('-')[0],
month: parseInt(date.split('-')[1]),
namespace: namespace
});
fetch(DOKU_BASE + 'lib/exe/ajax.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
const eventList = document.getElementById('eventlist-' + calId);
const events = data.events;
const title = document.getElementById('eventlist-title-' + calId);
const dateObj = new Date(date + 'T00:00:00');
const displayDate = dateObj.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric'
});
title.textContent = 'Events - ' + displayDate;
// Filter events for this day
const dayEvents = events[date] || [];
if (dayEvents.length === 0) {
eventList.innerHTML = 'No events on this day+ Add Event
';
} else {
let html = '';
dayEvents.forEach(event => {
html += renderEventItem(event, date, calId, namespace);
});
eventList.innerHTML = html;
}
}
})
.catch(err => console.error('Error:', err));
}
// Render a single event item
function renderEventItem(event, date, calId, namespace) {
// Format date display
const dateObj = new Date(date + 'T00:00:00');
const displayDate = dateObj.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
});
// Convert to 12-hour format
let displayTime = '';
if (event.time) {
const timeParts = event.time.split(':');
if (timeParts.length === 2) {
let hour = parseInt(timeParts[0]);
const minute = timeParts[1];
const ampm = hour >= 12 ? 'PM' : 'AM';
hour = hour % 12 || 12;
displayTime = hour + ':' + minute + ' ' + ampm;
} else {
displayTime = event.time;
}
}
// Multi-day indicator
let multiDay = '';
if (event.endDate && event.endDate !== date) {
const endObj = new Date(event.endDate + 'T00:00:00');
multiDay = ' → ' + endObj.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
});
}
const completedClass = event.completed ? ' event-completed' : '';
const color = event.color || '#3498db';
const isTask = event.isTask || false;
const completed = event.completed || false;
let html = '';
html += '
';
html += '
';
html += '' + escapeHtml(event.title) + ' ';
html += '
';
html += '
';
html += '' + displayDate + multiDay;
if (displayTime) {
html += ' • ' + displayTime;
}
html += ' ';
html += '
';
if (event.description) {
html += '
' + renderDescription(event.description) + '
';
}
html += '
'; // event-info
html += '
';
html += '🗑️ ';
html += '✏️ ';
html += '
';
// Checkbox for tasks - ON THE FAR RIGHT
if (isTask) {
const checked = completed ? 'checked' : '';
html += '
';
}
html += '
';
return html;
}
// Render description with rich content support
function renderDescription(description) {
if (!description) return '';
let rendered = escapeHtml(description);
// Convert newlines to
rendered = rendered.replace(/\n/g, ' ');
// Convert DokuWiki image syntax {{image.jpg}} to HTML
rendered = rendered.replace(/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/g, function(match, imagePath, alt) {
imagePath = imagePath.trim();
alt = alt ? alt.trim() : '';
// Handle external URLs
if (imagePath.match(/^https?:\/\//)) {
return ' ';
}
// Handle internal DokuWiki images
const imageUrl = DOKU_BASE + 'lib/exe/fetch.php?media=' + encodeURIComponent(imagePath);
return ' ';
});
// Convert DokuWiki link syntax [[link|text]] to HTML
rendered = rendered.replace(/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/g, function(match, link, text) {
link = link.trim();
text = text ? text.trim() : link;
// Handle external URLs
if (link.match(/^https?:\/\//)) {
return '' + text + ' ';
}
// Handle internal DokuWiki links
const wikiUrl = DOKU_BASE + 'doku.php?id=' + encodeURIComponent(link);
return '' + text + ' ';
});
// Convert markdown-style links [text](url) to HTML
rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) {
text = text.trim();
url = url.trim();
if (url.match(/^https?:\/\//)) {
return '' + text + ' ';
}
return '' + text + ' ';
});
// Convert plain URLs to clickable links
rendered = rendered.replace(/(https?:\/\/[^\s<]+)/g, function(match, url) {
return '' + url + ' ';
});
return rendered;
}
// Open add event dialog
function openAddEvent(calId, namespace, date) {
const dialog = document.getElementById('dialog-' + calId);
const form = document.getElementById('eventform-' + calId);
const title = document.getElementById('dialog-title-' + calId);
const dateField = document.getElementById('event-date-' + calId);
if (!dateField) {
console.error('Date field not found! ID: event-date-' + calId);
return;
}
// Reset form
form.reset();
document.getElementById('event-id-' + calId).value = '';
// Set date
const defaultDate = date || new Date().toISOString().split('T')[0];
dateField.value = defaultDate;
dateField.removeAttribute('data-original-date');
// Set default color
document.getElementById('event-color-' + calId).value = '#3498db';
// Set title
title.textContent = 'Add Event';
// Show dialog
dialog.style.display = 'flex';
// Focus title field
setTimeout(() => {
const titleField = document.getElementById('event-title-' + calId);
if (titleField) titleField.focus();
}, 100);
}
// Edit event
function editEvent(calId, eventId, date, namespace) {
const params = new URLSearchParams({
call: 'plugin_calendar',
action: 'get_event',
namespace: namespace,
date: date,
eventId: eventId
});
fetch(DOKU_BASE + 'lib/exe/ajax.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params.toString()
})
.then(r => r.json())
.then(data => {
if (data.success && data.event) {
const event = data.event;
const dialog = document.getElementById('dialog-' + calId);
const title = document.getElementById('dialog-title-' + calId);
const dateField = document.getElementById('event-date-' + calId);
if (!dateField) {
console.error('Date field not found when editing!');
return;
}
// Populate form
document.getElementById('event-id-' + calId).value = event.id;
dateField.value = date;
dateField.setAttribute('data-original-date', date);
document.getElementById('event-end-date-' + calId).value = event.endDate || '';
document.getElementById('event-title-' + calId).value = event.title;
document.getElementById('event-time-' + calId).value = event.time || '';
document.getElementById('event-color-' + calId).value = event.color || '#3498db';
document.getElementById('event-desc-' + calId).value = event.description || '';
document.getElementById('event-is-task-' + calId).checked = event.isTask || false;
title.textContent = 'Edit Event';
dialog.style.display = 'flex';
}
})
.catch(err => console.error('Error editing event:', err));
}
// Delete event
function deleteEvent(calId, eventId, date, namespace) {
if (!confirm('Delete this event?')) return;
const params = new URLSearchParams({
call: 'plugin_calendar',
action: 'delete_event',
namespace: namespace,
date: date,
eventId: eventId
});
fetch(DOKU_BASE + 'lib/exe/ajax.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
// Extract year and month from date
const [year, month] = date.split('-').map(Number);
// Reload calendar data via AJAX
reloadCalendarData(calId, year, month, namespace);
}
})
.catch(err => console.error('Error:', err));
}
// Save event (add or edit)
function saveEventCompact(calId, namespace) {
const eventId = document.getElementById('event-id-' + calId).value;
const dateInput = document.getElementById('event-date-' + calId);
const date = dateInput.value;
const oldDate = dateInput.getAttribute('data-original-date') || date;
const endDate = document.getElementById('event-end-date-' + calId).value;
const title = document.getElementById('event-title-' + calId).value;
const time = document.getElementById('event-time-' + calId).value;
const color = document.getElementById('event-color-' + calId).value;
const description = document.getElementById('event-desc-' + calId).value;
const isTask = document.getElementById('event-is-task-' + calId).checked;
const completed = false; // New tasks are not completed
if (!title) {
alert('Please enter a title');
return;
}
if (!date) {
alert('Please select a date');
return;
}
const params = new URLSearchParams({
call: 'plugin_calendar',
action: 'save_event',
namespace: namespace,
eventId: eventId,
date: date,
oldDate: oldDate,
endDate: endDate,
title: title,
time: time,
color: color,
description: description,
isTask: isTask ? '1' : '0',
completed: completed ? '1' : '0'
});
fetch(DOKU_BASE + 'lib/exe/ajax.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
closeEventDialog(calId);
// Extract year and month from the NEW date (in case date was changed)
const [year, month] = date.split('-').map(Number);
// Reload calendar data via AJAX to the month of the event
reloadCalendarData(calId, year, month, namespace);
} else {
alert('Error: ' + (data.error || 'Unknown error'));
}
})
.catch(err => {
console.error('Error:', err);
alert('Error saving event');
});
}
// Reload calendar data without page refresh
function reloadCalendarData(calId, year, month, namespace) {
const params = new URLSearchParams({
call: 'plugin_calendar',
action: 'load_month',
year: year,
month: month,
namespace: namespace
});
fetch(DOKU_BASE + 'lib/exe/ajax.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
const container = document.getElementById(calId);
// Check if this is a full calendar or just event panel
if (container.classList.contains('calendar-compact-container')) {
rebuildCalendar(calId, data.year, data.month, data.events, namespace);
} else if (container.classList.contains('event-panel-standalone')) {
rebuildEventPanel(calId, data.year, data.month, data.events, namespace);
}
}
})
.catch(err => console.error('Error:', err));
}
// Close event dialog
function closeEventDialog(calId) {
const dialog = document.getElementById('dialog-' + calId);
dialog.style.display = 'none';
}
// Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Highlight event when clicking on bar in calendar
function highlightEvent(calId, eventId, date) {
// Find the event item in the event list
const eventList = document.querySelector('#' + calId + ' .event-list-compact');
if (!eventList) return;
const eventItem = eventList.querySelector('[data-event-id="' + eventId + '"][data-date="' + date + '"]');
if (!eventItem) return;
// Remove previous highlights
const previousHighlights = eventList.querySelectorAll('.event-highlighted');
previousHighlights.forEach(el => el.classList.remove('event-highlighted'));
// Add highlight
eventItem.classList.add('event-highlighted');
// Scroll to event
eventItem.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
// Remove highlight after 3 seconds
setTimeout(() => {
eventItem.classList.remove('event-highlighted');
}, 3000);
}
// Close dialog on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const dialogs = document.querySelectorAll('.event-dialog-compact');
dialogs.forEach(dialog => {
if (dialog.style.display === 'flex') {
dialog.style.display = 'none';
}
});
}
});
// Event panel navigation
function navEventPanel(calId, year, month, namespace) {
const params = new URLSearchParams({
call: 'plugin_calendar',
action: 'load_month',
year: year,
month: month,
namespace: namespace
});
fetch(DOKU_BASE + 'lib/exe/ajax.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
rebuildEventPanel(calId, data.year, data.month, data.events, namespace);
}
})
.catch(err => console.error('Error:', err));
}
// Rebuild event panel only
function rebuildEventPanel(calId, year, month, events, namespace) {
const container = document.getElementById(calId);
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
// Update header
const header = container.querySelector('.panel-standalone-header h3');
header.textContent = monthNames[month - 1] + ' ' + year + ' Events';
// Update nav buttons
let prevMonth = month - 1;
let prevYear = year;
if (prevMonth < 1) {
prevMonth = 12;
prevYear--;
}
let nextMonth = month + 1;
let nextYear = year;
if (nextMonth > 12) {
nextMonth = 1;
nextYear++;
}
const navBtns = container.querySelectorAll('.cal-nav-btn');
navBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
navBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
// Rebuild event list
const eventList = container.querySelector('.event-list-compact');
eventList.innerHTML = renderEventListFromData(events, calId, namespace);
}
// Open add event for panel
function openAddEventPanel(calId, namespace) {
openAddEvent(calId, namespace, new Date().toISOString().split('T')[0]);
}
// Toggle task completion
function toggleTaskComplete(calId, eventId, date, namespace, completed) {
const params = new URLSearchParams({
call: 'plugin_calendar',
action: 'toggle_task',
namespace: namespace,
date: date,
eventId: eventId,
completed: completed ? '1' : '0'
});
fetch(DOKU_BASE + 'lib/exe/ajax.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
const [year, month] = date.split('-').map(Number);
reloadCalendarData(calId, year, month, namespace);
}
})
.catch(err => console.error('Error toggling task:', err));
}
// Make dialog draggable
function makeDialogDraggable(calId) {
const dialog = document.getElementById('dialog-content-' + calId);
const handle = document.getElementById('drag-handle-' + calId);
if (!dialog || !handle) return;
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
handle.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
isDragging = true;
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
setTranslate(currentX, currentY, dialog);
}
}
function dragEnd(e) {
initialX = currentX;
initialY = currentY;
isDragging = false;
}
function setTranslate(xPos, yPos, el) {
el.style.transform = `translate(${xPos}px, ${yPos}px)`;
}
}
// Initialize dialog draggability when opened
const originalOpenAddEvent = openAddEvent;
openAddEvent = function(calId, namespace, date) {
originalOpenAddEvent(calId, namespace, date);
setTimeout(() => makeDialogDraggable(calId), 100);
};
const originalEditEvent = editEvent;
editEvent = function(calId, eventId, date, namespace) {
originalEditEvent(calId, eventId, date, namespace);
setTimeout(() => makeDialogDraggable(calId), 100);
};