/**
* 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,
_: new Date().getTime() // Cache buster
});
fetch(DOKU_BASE + 'lib/exe/ajax.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache'
},
body: params.toString()
})
.then(r => r.json())
.then(data => {
console.log('Month navigation data:', data);
if (data.success) {
console.log('Rebuilding calendar for', year, month, 'with', Object.keys(data.events || {}).length, 'date entries');
rebuildCalendar(calId, data.year, data.month, data.events, namespace);
} else {
console.error('Failed to load month:', data.error);
}
})
.catch(err => {
console.error('Error loading month:', err);
});
}
// Jump to current month
function jumpToToday(calId, namespace) {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth() + 1; // JavaScript months are 0-indexed
navCalendar(calId, year, month, namespace);
}
// Jump to today for event panel
function jumpTodayPanel(calId, namespace) {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth() + 1;
navEventPanel(calId, year, month, namespace);
}
// Open month picker dialog
function openMonthPicker(calId, currentYear, currentMonth, namespace) {
console.log('openMonthPicker called:', calId, currentYear, currentMonth, namespace);
const overlay = document.getElementById('month-picker-overlay-' + calId);
console.log('Overlay element:', overlay);
const monthSelect = document.getElementById('month-picker-month-' + calId);
console.log('Month select:', monthSelect);
const yearSelect = document.getElementById('month-picker-year-' + calId);
console.log('Year select:', yearSelect);
if (!overlay) {
console.error('Month picker overlay not found! ID:', 'month-picker-overlay-' + calId);
return;
}
if (!monthSelect || !yearSelect) {
console.error('Select elements not found!');
return;
}
// Set current values
monthSelect.value = currentMonth;
yearSelect.value = currentYear;
// Show overlay
overlay.style.display = 'flex';
console.log('Overlay display set to flex');
}
// Open month picker dialog for event panel
function openMonthPickerPanel(calId, currentYear, currentMonth, namespace) {
openMonthPicker(calId, currentYear, currentMonth, namespace);
}
// Close month picker dialog
function closeMonthPicker(calId) {
const overlay = document.getElementById('month-picker-overlay-' + calId);
overlay.style.display = 'none';
}
// Jump to selected month
function jumpToSelectedMonth(calId, namespace) {
const monthSelect = document.getElementById('month-picker-month-' + calId);
const yearSelect = document.getElementById('month-picker-year-' + calId);
const month = parseInt(monthSelect.value);
const year = parseInt(yearSelect.value);
closeMonthPicker(calId);
// Check if this is a calendar or event panel
const container = document.getElementById(calId);
if (container && container.classList.contains('event-panel-standalone')) {
navEventPanel(calId, year, month, namespace);
} else {
navCalendar(calId, year, month, namespace);
}
}
// 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();
// Calculate month boundaries
const monthStart = new Date(year, month - 1, 1);
const monthEnd = new Date(year, month - 1, daysInMonth);
// Build a map of all events with their date ranges
const eventRanges = {};
for (const [dateKey, dayEvents] of Object.entries(events)) {
// Only process events that could possibly overlap with this month/year
const dateYear = parseInt(dateKey.split('-')[0]);
const dateMonth = parseInt(dateKey.split('-')[1]);
// Skip events from completely different years (unless they're very long multi-day events)
if (Math.abs(dateYear - year) > 1) {
continue;
}
for (const evt of dayEvents) {
const startDate = dateKey;
const endDate = evt.endDate || dateKey;
// Check if event overlaps with current month
const eventStart = new Date(startDate + 'T00:00:00');
const eventEnd = new Date(endDate + 'T00:00:00');
// Skip if event doesn't overlap with current month
if (eventEnd < monthStart || eventStart > monthEnd) {
continue;
}
// Create entry for each day the event spans
const start = new Date(startDate + 'T00:00:00');
const end = new Date(endDate + 'T00:00:00');
const current = new Date(start);
while (current <= end) {
const currentKey = current.toISOString().split('T')[0];
// Check if this date is in current month
const currentDate = new Date(currentKey + 'T00:00:00');
if (currentDate.getFullYear() === year && currentDate.getMonth() === month - 1) {
if (!eventRanges[currentKey]) {
eventRanges[currentKey] = [];
}
// Add event with span information
const eventCopy = {...evt};
eventCopy._span_start = startDate;
eventCopy._span_end = endDate;
eventCopy._is_first_day = (currentKey === startDate);
eventCopy._is_last_day = (currentKey === endDate);
eventCopy._original_date = dateKey;
// Check if event continues from previous month or to next month
eventCopy._continues_from_prev = (eventStart < monthStart);
eventCopy._continues_to_next = (eventEnd > monthEnd);
eventRanges[currentKey].push(eventCopy);
}
current.setDate(current.getDate() + 1);
}
}
}
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')}`;
// Get today's date in local timezone
const todayObj = new Date();
const today = `${todayObj.getFullYear()}-${String(todayObj.getMonth() + 1).padStart(2, '0')}-${String(todayObj.getDate()).padStart(2, '0')}`;
const isToday = dateKey === today;
const hasEvents = eventRanges[dateKey] && eventRanges[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 = [...eventRanges[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 += '
';
}
tbody.innerHTML = html;
// Rebuild event list - show events that overlap with current month
const currentMonthEvents = {};
for (const [dateKey, dayEvents] of Object.entries(events)) {
for (const event of dayEvents) {
const startDate = dateKey;
const endDate = event.endDate || dateKey;
// Check if event overlaps with current month
// Event starts before month ends AND ends after month starts
const eventStartObj = new Date(startDate);
const eventEndObj = new Date(endDate);
const monthFirstDay = new Date(year, month - 1, 1);
const monthLastDay = new Date(year, month - 1, daysInMonth);
// Include if event overlaps this month at all
if (eventEndObj >= monthFirstDay && eventStartObj <= monthLastDay) {
if (!currentMonthEvents[dateKey]) {
currentMonthEvents[dateKey] = [];
}
currentMonthEvents[dateKey].push(event);
}
}
}
const eventList = container.querySelector('.event-list-compact');
eventList.innerHTML = renderEventListFromData(currentMonthEvents, calId, namespace);
// Update title
const title = container.querySelector('#eventlist-title-' + calId);
title.textContent = 'Events';
}
// Render event list from data
function renderEventListFromData(events, calId, namespace, year, month) {
if (!events || Object.keys(events).length === 0) {
return '
No events this month
';
}
let html = '';
const sortedDates = Object.keys(events).sort();
// Filter events to only current month if year/month provided
const monthStart = year && month ? new Date(year, month - 1, 1) : null;
const monthEnd = year && month ? new Date(year, month, 0, 23, 59, 59) : null;
for (const dateKey of sortedDates) {
// Skip events not in current month if filtering
if (monthStart && monthEnd) {
const eventDate = new Date(dateKey + 'T00:00:00');
if (eventDate < monthStart || eventDate > monthEnd) {
continue;
}
}
const dayEvents = events[dateKey];
for (const event of dayEvents) {
html += renderEventItem(event, dateKey, calId, namespace);
}
}
if (!html) {
return '
No events this month
';
}
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 += '
';
html += '
';
html += '
' + displayDate + '
';
html += '';
html += '
';
html += '
';
if (dayEvents.length === 0) {
html += '
No events on this day
';
} else {
html += '
';
dayEvents.forEach(event => {
const color = event.color || '#3498db';
html += '