| 1 |
|
- |
/**
|
| 2 |
|
- |
* GoingsOn - Events Calendar Module
|
| 3 |
|
- |
* Month grid and week grid views for events.
|
| 4 |
|
- |
*/
|
| 5 |
|
- |
|
| 6 |
|
- |
(function() {
|
| 7 |
|
- |
'use strict';
|
| 8 |
|
- |
const esc = GoingsOn.utils.escapeHtml;
|
| 9 |
|
- |
const escAttr = GoingsOn.utils.escapeAttrValue;
|
| 10 |
|
- |
const escArg = GoingsOn.utils.escapeHandlerArg;
|
| 11 |
|
- |
const escAttrVal = GoingsOn.utils.escapeAttrValue;
|
| 12 |
|
- |
|
| 13 |
|
- |
let currentMonthDate = new Date();
|
| 14 |
|
- |
let currentWeekDate = new Date();
|
| 15 |
|
- |
let monthEvents = [];
|
| 16 |
|
- |
let monthTasks = [];
|
| 17 |
|
- |
let weekEvents = [];
|
| 18 |
|
- |
let weekSwipeCleanup = null;
|
| 19 |
|
- |
|
| 20 |
|
- |
// Width, and the same width the stylesheet uses: seven day columns either
|
| 21 |
|
- |
// fit or they do not, and that is a question about the window rather than
|
| 22 |
|
- |
// about the pointer. A tablet in landscape shows the week grid; a desktop
|
| 23 |
|
- |
// window dragged narrow shows one day.
|
| 24 |
|
- |
//
|
| 25 |
|
- |
// 599px is makeover-geometry's SizeClass::Compact upper bound, which is
|
| 26 |
|
- |
// where .cal-mobile-day is styled. The two have to agree, or this renders
|
| 27 |
|
- |
// a single-day DOM the stylesheet is laying out as a week; src-tauri's
|
| 28 |
|
- |
// build.rs fails the build if this string and SizeClass part ways.
|
| 29 |
|
- |
//
|
| 30 |
|
- |
// Held and watched rather than asked per call. The week view is two
|
| 31 |
|
- |
// different DOM trees, so crossing the boundary with one already on screen
|
| 32 |
|
- |
// leaves markup the stylesheet is not laying out; nothing else re-renders
|
| 33 |
|
- |
// on resize, and until this listener existed you got that until the next
|
| 34 |
|
- |
// navigation.
|
| 35 |
|
- |
const compact = window.matchMedia('(max-width: 599px)');
|
| 36 |
|
- |
|
| 37 |
|
- |
function isCompactView() {
|
| 38 |
|
- |
return compact.matches;
|
| 39 |
|
- |
}
|
| 40 |
|
- |
|
| 41 |
|
- |
compact.addEventListener('change', () => {
|
| 42 |
|
- |
// Only if the week view is the one on screen. Rebuilding it while the
|
| 43 |
|
- |
// month grid is up would drop the swipe handlers on a hidden node and
|
| 44 |
|
- |
// fight whatever the user is actually looking at.
|
| 45 |
|
- |
const container = document.getElementById('week-calendar-grid');
|
| 46 |
|
- |
if (container && container.offsetParent !== null) loadWeek();
|
| 47 |
|
- |
});
|
| 48 |
|
- |
|
| 49 |
|
- |
// Date Helpers
|
| 50 |
|
- |
|
| 51 |
|
- |
function getMonday(date) {
|
| 52 |
|
- |
const d = new Date(date);
|
| 53 |
|
- |
const day = d.getDay();
|
| 54 |
|
- |
const diff = (day + 6) % 7;
|
| 55 |
|
- |
d.setDate(d.getDate() - diff);
|
| 56 |
|
- |
d.setHours(0, 0, 0, 0);
|
| 57 |
|
- |
return d;
|
| 58 |
|
- |
}
|
| 59 |
|
- |
|
| 60 |
|
- |
function toDateKey(date) {
|
| 61 |
|
- |
const y = date.getFullYear();
|
| 62 |
|
- |
const m = String(date.getMonth() + 1).padStart(2, '0');
|
| 63 |
|
- |
const d = String(date.getDate()).padStart(2, '0');
|
| 64 |
|
- |
return `${y}-${m}-${d}`;
|
| 65 |
|
- |
}
|
| 66 |
|
- |
|
| 67 |
|
- |
function groupByDate(events) {
|
| 68 |
|
- |
const map = new Map();
|
| 69 |
|
- |
for (const e of events) {
|
| 70 |
|
- |
const d = new Date(e.startTimeEpoch || new Date(e.startTime).getTime());
|
| 71 |
|
- |
const key = toDateKey(d);
|
| 72 |
|
- |
if (!map.has(key)) map.set(key, []);
|
| 73 |
|
- |
map.get(key).push(e);
|
| 74 |
|
- |
}
|
| 75 |
|
- |
return map;
|
| 76 |
|
- |
}
|
| 77 |
|
- |
|
| 78 |
|
- |
/**
|
| 79 |
|
- |
* Group tasks by the local day they are due on. A task with no due date is
|
| 80 |
|
- |
* dropped: the grid is a calendar, and an undated task has no cell.
|
| 81 |
|
- |
*/
|
| 82 |
|
- |
function groupTasksByDueDate(tasks) {
|
| 83 |
|
- |
const map = new Map();
|
| 84 |
|
- |
for (const t of tasks) {
|
| 85 |
|
- |
if (!t.due) continue;
|
| 86 |
|
- |
const key = toDateKey(new Date(t.due));
|
| 87 |
|
- |
if (!map.has(key)) map.set(key, []);
|
| 88 |
|
- |
map.get(key).push(t);
|
| 89 |
|
- |
}
|
| 90 |
|
- |
return map;
|
| 91 |
|
- |
}
|
| 92 |
|
- |
|
| 93 |
|
- |
function truncate(str, len) {
|
| 94 |
|
- |
if (!str || str.length <= len) return str || '';
|
| 95 |
|
- |
return str.substring(0, len - 1) + '\u2026';
|
| 96 |
|
- |
}
|
| 97 |
|
- |
|
| 98 |
|
- |
function formatMonthLabel(date) {
|
| 99 |
|
- |
return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
| 100 |
|
- |
}
|
| 101 |
|
- |
|
| 102 |
|
- |
function formatWeekLabel(monday) {
|
| 103 |
|
- |
const sunday = new Date(monday);
|
| 104 |
|
- |
sunday.setDate(monday.getDate() + 6);
|
| 105 |
|
- |
const opts = { month: 'short', day: 'numeric' };
|
| 106 |
|
- |
const start = monday.toLocaleDateString('en-US', opts);
|
| 107 |
|
- |
const end = sunday.toLocaleDateString('en-US', { ...opts, year: 'numeric' });
|
| 108 |
|
- |
return `${start} \u2013 ${end}`;
|
| 109 |
|
- |
}
|
| 110 |
|
- |
|
| 111 |
|
- |
// Month View
|
| 112 |
|
- |
|
| 113 |
|
- |
async function loadMonth() {
|
| 114 |
|
- |
const first = new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth(), 1);
|
| 115 |
|
- |
const startOffset = (first.getDay() + 6) % 7;
|
| 116 |
|
- |
const gridStart = new Date(first);
|
| 117 |
|
- |
gridStart.setDate(first.getDate() - startOffset);
|
| 118 |
|
- |
|
| 119 |
|
- |
const lastDay = new Date(first.getFullYear(), first.getMonth() + 1, 0);
|
| 120 |
|
- |
const gridEnd = new Date(lastDay);
|
| 121 |
|
- |
const endOffset = (7 - ((lastDay.getDay() + 6) % 7 + 1)) % 7;
|
| 122 |
|
- |
gridEnd.setDate(lastDay.getDate() + endOffset + 1);
|
| 123 |
|
- |
|
| 124 |
|
- |
// Events and due tasks together: the month grid answers "what lands on
|
| 125 |
|
- |
// this day", and a deadline lands on a day exactly as an event does.
|
| 126 |
|
- |
// Fetched over the whole grid, so the leading and trailing days of the
|
| 127 |
|
- |
// neighbouring months are populated too.
|
| 128 |
|
- |
const [events, tasks] = await Promise.all([
|
| 129 |
|
- |
GoingsOn.api.events.listBetween(gridStart.toISOString(), gridEnd.toISOString())
|
| 130 |
|
- |
.catch(err => { console.error('Failed to load month events:', err); return []; }),
|
| 131 |
|
- |
GoingsOn.api.tasks.listDueBetween(gridStart.toISOString(), gridEnd.toISOString())
|
| 132 |
|
- |
.catch(err => { console.error('Failed to load month tasks:', err); return []; }),
|
| 133 |
|
- |
]);
|
| 134 |
|
- |
monthEvents = events;
|
| 135 |
|
- |
monthTasks = tasks;
|
| 136 |
|
- |
|
| 137 |
|
- |
renderMonthGrid(first, gridStart, gridEnd);
|
| 138 |
|
- |
const label = document.getElementById('month-calendar-label');
|
| 139 |
|
- |
if (label) label.textContent = formatMonthLabel(first);
|
| 140 |
|
- |
}
|
| 141 |
|
- |
|
| 142 |
|
- |
function renderMonthGrid(firstOfMonth, gridStart, gridEnd) {
|
| 143 |
|
- |
const container = document.getElementById('month-calendar-grid');
|
| 144 |
|
- |
if (!container) return;
|
| 145 |
|
- |
const eventsByDate = groupByDate(monthEvents);
|
| 146 |
|
- |
const tasksByDate = groupTasksByDueDate(monthTasks);
|
| 147 |
|
- |
const today = toDateKey(new Date());
|
| 148 |
|
- |
const dayHeaders = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
| 149 |
|
- |
|
| 150 |
|
- |
let html = '<div class="raised cal-month-grid">';
|
| 151 |
|
- |
html += '<div class="cal-month-header">';
|
| 152 |
|
- |
for (const d of dayHeaders) {
|
| 153 |
|
- |
html += `<div class="cal-month-day-header">${d}</div>`;
|
| 154 |
|
- |
}
|
| 155 |
|
- |
html += '</div><div class="cal-month-cells">';
|
| 156 |
|
- |
|
| 157 |
|
- |
const cursor = new Date(gridStart);
|
| 158 |
|
- |
while (cursor < gridEnd) {
|
| 159 |
|
- |
const dateKey = toDateKey(cursor);
|
| 160 |
|
- |
const isCurrentMonth = cursor.getMonth() === firstOfMonth.getMonth();
|
| 161 |
|
- |
const isToday = dateKey === today;
|
| 162 |
|
- |
const dayEvents = eventsByDate.get(dateKey) || [];
|
| 163 |
|
- |
const dayTasks = tasksByDate.get(dateKey) || [];
|
| 164 |
|
- |
|
| 165 |
|
- |
const classes = ['cal-month-cell'];
|
| 166 |
|
- |
if (!isCurrentMonth) classes.push('other-month');
|
| 167 |
|
- |
if (isToday) classes.push('today');
|
| 168 |
|
- |
|
| 169 |
|
- |
html += `<div class="${classes.join(' ')}">`;
|
| 170 |
|
- |
html += `<div class="cal-month-cell-header"><span class="cal-day-number">${cursor.getDate()}</span></div>`;
|
| 171 |
|
- |
|
| 172 |
|
- |
// Events first, then what is due: the day's fixed points, then the
|
| 173 |
|
- |
// work hanging off it. The cap is over both, so a busy day does not
|
| 174 |
|
- |
// grow a cell twice as tall as its neighbours.
|
| 175 |
|
- |
const maxShow = 3;
|
| 176 |
|
- |
let shown = 0;
|
| 177 |
|
- |
dayEvents.slice(0, maxShow).forEach(e => {
|
| 178 |
|
- |
const blockClass = e.blockType ? `block-${e.blockType}` : '';
|
| 179 |
|
- |
html += `<div class="cal-event-chip ${blockClass}" data-act="events.open" data-a1="${escAttr(e.id)}" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 18))}</div>`;
|
| 180 |
|
- |
shown++;
|
| 181 |
|
- |
});
|
| 182 |
|
- |
dayTasks.slice(0, Math.max(0, maxShow - shown)).forEach(t => {
|
| 183 |
|
- |
const overdue = t.isOverdue ? ' overdue' : '';
|
| 184 |
|
- |
const done = t.status === 'Completed' ? ' done' : '';
|
| 185 |
|
- |
html += `<div class="cal-task-chip${overdue}${done}" data-act="taskOverview.open" data-a1="${escAttr(t.id)}" title="Due: ${escAttrVal(t.title)}">${esc(truncate(t.title, 18))}</div>`;
|
| 186 |
|
- |
shown++;
|
| 187 |
|
- |
});
|
| 188 |
|
- |
const hidden = dayEvents.length + dayTasks.length - shown;
|
| 189 |
|
- |
if (hidden > 0) {
|
| 190 |
|
- |
html += `<div class="cal-event-more">+${hidden} more</div>`;
|
| 191 |
|
- |
}
|
| 192 |
|
- |
|
| 193 |
|
- |
html += '</div>';
|
| 194 |
|
- |
cursor.setDate(cursor.getDate() + 1);
|
| 195 |
|
- |
}
|
| 196 |
|
- |
|
| 197 |
|
- |
html += '</div></div>';
|
| 198 |
|
- |
container.innerHTML = html;
|
| 199 |
|
- |
}
|
| 200 |
|
- |
|
| 201 |
|
- |
function prevMonth() { currentMonthDate.setMonth(currentMonthDate.getMonth() - 1); loadMonth(); }
|
| 202 |
|
- |
function nextMonth() { currentMonthDate.setMonth(currentMonthDate.getMonth() + 1); loadMonth(); }
|
| 203 |
|
- |
function goToThisMonth() { currentMonthDate = new Date(); loadMonth(); }
|
| 204 |
|
- |
|
| 205 |
|
- |
// Week View
|
| 206 |
|
- |
|
| 207 |
|
- |
const SLOT_HEIGHT = 12;
|
| 208 |
|
- |
const HOURS_START = 6;
|
| 209 |
|
- |
const HOURS_END = 22;
|
| 210 |
|
- |
|
| 211 |
|
- |
async function loadWeek() {
|
| 212 |
|
- |
const monday = getMonday(currentWeekDate);
|
| 213 |
|
- |
const sunday = new Date(monday);
|
| 214 |
|
- |
sunday.setDate(monday.getDate() + 7);
|
| 215 |
|
- |
|
| 216 |
|
- |
try {
|
| 217 |
|
- |
weekEvents = await GoingsOn.api.events.listBetween(
|
| 218 |
|
- |
monday.toISOString(), sunday.toISOString()
|
| 219 |
|
- |
);
|
| 220 |
|
- |
} catch (err) {
|
| 221 |
|
- |
console.error('Failed to load week events:', err);
|
| 222 |
|
- |
weekEvents = [];
|
| 223 |
|
- |
}
|
| 224 |
|
- |
|
| 225 |
|
- |
renderWeekGrid(monday);
|
| 226 |
|
- |
const label = document.getElementById('week-calendar-label');
|
| 227 |
|
- |
if (label) {
|
| 228 |
|
- |
label.textContent = isCompactView()
|
| 229 |
|
- |
? currentWeekDate.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })
|
| 230 |
|
- |
: formatWeekLabel(monday);
|
| 231 |
|
- |
}
|
| 232 |
|
- |
}
|
| 233 |
|
- |
|
| 234 |
|
- |
function renderWeekGrid(monday) {
|
| 235 |
|
- |
const container = document.getElementById('week-calendar-grid');
|
| 236 |
|
- |
if (!container) return;
|
| 237 |
|
- |
if (isCompactView()) {
|
| 238 |
|
- |
renderMobileDay(container);
|
| 239 |
|
- |
return;
|
| 240 |
|
- |
}
|
| 241 |
|
- |
const eventsByDate = groupByDate(weekEvents);
|
| 242 |
|
- |
const today = toDateKey(new Date());
|
| 243 |
|
- |
const totalSlots = (HOURS_END - HOURS_START) * 4;
|
| 244 |
|
- |
const gridHeight = totalSlots * SLOT_HEIGHT;
|
| 245 |
|
- |
|
| 246 |
|
- |
let html = '<div class="raised cal-week-grid">';
|
| 247 |
|
- |
|
| 248 |
|
- |
// Header row
|
| 249 |
|
- |
html += '<div class="cal-week-header"><div class="cal-week-time-gutter"></div>';
|
| 250 |
|
- |
const cursor = new Date(monday);
|
| 251 |
|
- |
for (let d = 0; d < 7; d++) {
|
| 252 |
|
- |
const dateKey = toDateKey(cursor);
|
| 253 |
|
- |
const dayName = cursor.toLocaleDateString('en-US', { weekday: 'short' });
|
| 254 |
|
- |
html += `<div class="cal-week-day-header ${dateKey === today ? 'today' : ''}">
|
| 255 |
|
- |
<span class="cal-week-day-name">${dayName}</span>
|
| 256 |
|
- |
<span class="cal-week-day-num">${cursor.getDate()}</span>
|
| 257 |
|
- |
</div>`;
|
| 258 |
|
- |
cursor.setDate(cursor.getDate() + 1);
|
| 259 |
|
- |
}
|
| 260 |
|
- |
html += '</div>';
|
| 261 |
|
- |
|
| 262 |
|
- |
// All-day row
|
| 263 |
|
- |
html += '<div class="cal-week-allday-row"><div class="cal-week-time-gutter cal-allday-label">All Day</div>';
|
| 264 |
|
- |
const adCursor = new Date(monday);
|
| 265 |
|
- |
for (let d = 0; d < 7; d++) {
|
| 266 |
|
- |
const dateKey = toDateKey(adCursor);
|
| 267 |
|
- |
const dayEvts = (eventsByDate.get(dateKey) || []).filter(e => e.isAllDay);
|
| 268 |
|
- |
html += '<div class="cal-week-allday-cell">';
|
| 269 |
|
- |
dayEvts.forEach(e => {
|
| 270 |
|
- |
const blockClass = e.blockType ? `block-${e.blockType}` : '';
|
| 271 |
|
- |
html += `<div class="cal-event-chip ${blockClass}" data-act="events.open" data-a1="${escAttr(e.id)}" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 14))}</div>`;
|
| 272 |
|
- |
});
|
| 273 |
|
- |
html += '</div>';
|
| 274 |
|
- |
adCursor.setDate(adCursor.getDate() + 1);
|
| 275 |
|
- |
}
|
| 276 |
|
- |
html += '</div>';
|
| 277 |
|
- |
|
| 278 |
|
- |
// Time grid body
|
| 279 |
|
- |
html += `<div class="cal-week-body" style="height: ${gridHeight}px;">`;
|
| 280 |
|
- |
|
| 281 |
|
- |
// Time gutter
|
| 282 |
|
- |
html += '<div class="cal-week-time-gutter">';
|
| 283 |
|
- |
for (let h = HOURS_START; h < HOURS_END; h++) {
|
| 284 |
|
- |
const label = h === 0 ? '12 AM' : h < 12 ? `${h} AM` : h === 12 ? '12 PM' : `${h - 12} PM`;
|
| 285 |
|
- |
html += `<div class="cal-week-hour-label" style="top: ${(h - HOURS_START) * 4 * SLOT_HEIGHT}px;">${label}</div>`;
|
| 286 |
|
- |
}
|
| 287 |
|
- |
html += '</div>';
|
| 288 |
|
- |
|
| 289 |
|
- |
// Day columns
|
| 290 |
|
- |
const colCursor = new Date(monday);
|
| 291 |
|
- |
for (let d = 0; d < 7; d++) {
|
| 292 |
|
- |
const dateKey = toDateKey(colCursor);
|
| 293 |
|
- |
const dayEvts = (eventsByDate.get(dateKey) || []).filter(e => !e.isAllDay);
|
| 294 |
|
- |
html += `<div class="cal-week-day-col ${dateKey === today ? 'today' : ''}">`;
|
| 295 |
|
- |
|
| 296 |
|
- |
// Hour lines
|
| 297 |
|
- |
for (let h = HOURS_START; h < HOURS_END; h++) {
|
| 298 |
|
- |
html += `<div class="cal-week-hour-line" style="top: ${(h - HOURS_START) * 4 * SLOT_HEIGHT}px;"></div>`;
|
| 299 |
|
- |
}
|
| 300 |
|
- |
|
| 301 |
|
- |
// Positioned events
|
| 302 |
|
- |
dayEvts.forEach(e => {
|
| 303 |
|
- |
const startDate = new Date(e.startTimeEpoch || new Date(e.startTime).getTime());
|
| 304 |
|
- |
const endEpoch = e.endTimeEpoch || (e.endTime ? new Date(e.endTime).getTime() : (startDate.getTime() + 3600000));
|
| 305 |
|
- |
const endDate = new Date(endEpoch);
|
| 306 |
|
- |
|
| 307 |
|
- |
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
|
| 308 |
|
- |
const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
|
| 309 |
|
- |
const durationMinutes = Math.max(endMinutes - startMinutes, 15);
|
| 310 |
|
- |
|
| 311 |
|
- |
const topPx = ((startMinutes - HOURS_START * 60) / 15) * SLOT_HEIGHT;
|
| 312 |
|
- |
const heightPx = Math.max((durationMinutes / 15) * SLOT_HEIGHT, SLOT_HEIGHT);
|
| 313 |
|
- |
const blockClass = e.blockType ? `block-${e.blockType}` : '';
|
| 314 |
|
- |
|
| 315 |
|
- |
html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" data-act="events.open" data-a1="${escAttr(e.id)}" title="${escAttrVal(e.title + ' ' + e.timeFormatted)}">
|
| 316 |
|
- |
<div class="cal-week-event-title">${esc(truncate(e.title, 20))}</div>
|
| 317 |
|
- |
<div class="cal-week-event-time">${esc(e.timeFormatted)}</div>
|
| 318 |
|
- |
</div>`;
|
| 319 |
|
- |
});
|
| 320 |
|
- |
|
| 321 |
|
- |
html += '</div>';
|
| 322 |
|
- |
colCursor.setDate(colCursor.getDate() + 1);
|
| 323 |
|
- |
}
|
| 324 |
|
- |
|
| 325 |
|
- |
html += '</div></div>';
|
| 326 |
|
- |
container.innerHTML = html;
|
| 327 |
|
- |
|
| 328 |
|
- |
// Auto-scroll to current hour
|
| 329 |
|
- |
const body = container.querySelector('.cal-week-body');
|
| 330 |
|
- |
if (body) {
|
| 331 |
|
- |
const now = new Date();
|
| 332 |
|
- |
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
| 333 |
|
- |
const scrollTarget = ((currentMinutes - HOURS_START * 60) / 15) * SLOT_HEIGHT - 100;
|
| 334 |
|
- |
body.scrollTop = Math.max(0, scrollTarget);
|
| 335 |
|
- |
}
|
| 336 |
|
- |
}
|
| 337 |
|
- |
|
| 338 |
|
- |
function prevWeek() {
|
| 339 |
|
- |
const step = isCompactView() ? 1 : 7;
|
| 340 |
|
- |
currentWeekDate.setDate(currentWeekDate.getDate() - step);
|
| 341 |
|
- |
loadWeek();
|
| 342 |
|
- |
}
|
| 343 |
|
- |
function nextWeek() {
|
| 344 |
|
- |
const step = isCompactView() ? 1 : 7;
|
| 345 |
|
- |
currentWeekDate.setDate(currentWeekDate.getDate() + step);
|
| 346 |
|
- |
loadWeek();
|
| 347 |
|
- |
}
|
| 348 |
|
- |
function goToThisWeek() { currentWeekDate = new Date(); loadWeek(); }
|
| 349 |
|
- |
|
| 350 |
|
- |
// Mobile: Single-day swipe view
|
| 351 |
|
- |
|
| 352 |
|
- |
function renderMobileDay(container) {
|
| 353 |
|
- |
const dateKey = toDateKey(currentWeekDate);
|
| 354 |
|
- |
const today = toDateKey(new Date());
|
| 355 |
|
- |
const allDayEvts = (groupByDate(weekEvents).get(dateKey) || []).filter(e => e.isAllDay);
|
| 356 |
|
- |
const timedEvts = (groupByDate(weekEvents).get(dateKey) || []).filter(e => !e.isAllDay);
|
| 357 |
|
- |
const totalSlots = (HOURS_END - HOURS_START) * 4;
|
| 358 |
|
- |
const gridHeight = totalSlots * SLOT_HEIGHT;
|
| 359 |
|
- |
|
| 360 |
|
- |
let html = '<div class="cal-mobile-day">';
|
| 361 |
|
- |
html += `<div class="cal-mobile-day-header${dateKey === today ? ' today' : ''}">
|
| 362 |
|
- |
${esc(currentWeekDate.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' }))}
|
| 363 |
|
- |
</div>`;
|
| 364 |
|
- |
|
| 365 |
|
- |
if (allDayEvts.length) {
|
| 366 |
|
- |
html += '<div class="cal-mobile-allday">';
|
| 367 |
|
- |
allDayEvts.forEach(e => {
|
| 368 |
|
- |
const blockClass = e.blockType ? `block-${e.blockType}` : '';
|
| 369 |
|
- |
html += `<div class="cal-event-chip ${blockClass}" data-act="events.open" data-a1="${escAttr(e.id)}">${esc(truncate(e.title, 30))}</div>`;
|
| 370 |
|
- |
});
|
| 371 |
|
- |
html += '</div>';
|
| 372 |
|
- |
}
|
| 373 |
|
- |
|
| 374 |
|
- |
html += `<div class="cal-mobile-day-body" style="height: ${gridHeight}px;">`;
|
| 375 |
|
- |
html += '<div class="cal-week-time-gutter">';
|
| 376 |
|
- |
for (let h = HOURS_START; h < HOURS_END; h++) {
|
| 377 |
|
- |
const label = h === 0 ? '12 AM' : h < 12 ? `${h} AM` : h === 12 ? '12 PM' : `${h - 12} PM`;
|
| 378 |
|
- |
html += `<div class="cal-week-hour-label" style="top: ${(h - HOURS_START) * 4 * SLOT_HEIGHT}px;">${label}</div>`;
|
| 379 |
|
- |
}
|
| 380 |
|
- |
html += '</div>';
|
| 381 |
|
- |
|
| 382 |
|
- |
html += `<div class="cal-mobile-day-col">`;
|
| 383 |
|
- |
for (let h = HOURS_START; h < HOURS_END; h++) {
|
| 384 |
|
- |
html += `<div class="cal-week-hour-line" style="top: ${(h - HOURS_START) * 4 * SLOT_HEIGHT}px;"></div>`;
|
| 385 |
|
- |
}
|
| 386 |
|
- |
timedEvts.forEach(e => {
|
| 387 |
|
- |
const startDate = new Date(e.startTimeEpoch || new Date(e.startTime).getTime());
|
| 388 |
|
- |
const endEpoch = e.endTimeEpoch || (e.endTime ? new Date(e.endTime).getTime() : (startDate.getTime() + 3600000));
|
| 389 |
|
- |
const endDate = new Date(endEpoch);
|
| 390 |
|
- |
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
|
| 391 |
|
- |
const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
|
| 392 |
|
- |
const durationMinutes = Math.max(endMinutes - startMinutes, 15);
|
| 393 |
|
- |
const topPx = ((startMinutes - HOURS_START * 60) / 15) * SLOT_HEIGHT;
|
| 394 |
|
- |
const heightPx = Math.max((durationMinutes / 15) * SLOT_HEIGHT, SLOT_HEIGHT);
|
| 395 |
|
- |
const blockClass = e.blockType ? `block-${e.blockType}` : '';
|
| 396 |
|
- |
html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" data-act="events.open" data-a1="${escAttr(e.id)}">
|
| 397 |
|
- |
<div class="cal-week-event-title">${esc(truncate(e.title, 30))}</div>
|
| 398 |
|
- |
<div class="cal-week-event-time">${esc(e.timeFormatted)}</div>
|
| 399 |
|
- |
</div>`;
|
| 400 |
|
- |
});
|
| 401 |
|
- |
html += '</div></div></div>';
|
| 402 |
|
- |
|
| 403 |
|
- |
container.innerHTML = html;
|
| 404 |
|
- |
|
| 405 |
|
- |
// Auto-scroll to current hour if it's today
|
| 406 |
|
- |
const body = container.querySelector('.cal-mobile-day-body');
|
| 407 |
|
- |
if (body && dateKey === today) {
|
| 408 |
|
- |
const now = new Date();
|
| 409 |
|
- |
const minutes = now.getHours() * 60 + now.getMinutes();
|
| 410 |
|
- |
body.scrollTop = Math.max(0, ((minutes - HOURS_START * 60) / 15) * SLOT_HEIGHT - 100);
|
| 411 |
|
- |
}
|
| 412 |
|
- |
|
| 413 |
|
- |
// Swipe to change day
|
| 414 |
|
- |
if (weekSwipeCleanup) weekSwipeCleanup();
|
| 415 |
|
- |
if (GoingsOn.touch?.isTouchDevice && GoingsOn.touch.addSwipeNavigation) {
|
| 416 |
|
- |
weekSwipeCleanup = GoingsOn.touch.addSwipeNavigation(container, {
|
| 417 |
|
- |
onLeft: nextWeek,
|
| 418 |
|
- |
onRight: prevWeek,
|
| 419 |
|
- |
});
|
| 420 |
|
- |
}
|
| 421 |
|
- |
}
|
| 422 |
|
- |
|
| 423 |
|
- |
// Namespace
|
| 424 |
|
- |
|
| 425 |
|
- |
GoingsOn.eventsCalendar = {
|
| 426 |
|
- |
loadMonth,
|
| 427 |
|
- |
loadWeek,
|
| 428 |
|
- |
prevMonth,
|
| 429 |
|
- |
nextMonth,
|
| 430 |
|
- |
goToThisMonth,
|
| 431 |
|
- |
prevWeek,
|
| 432 |
|
- |
nextWeek,
|
| 433 |
|
- |
goToThisWeek,
|
| 434 |
|
- |
};
|
| 435 |
|
- |
|
| 436 |
|
- |
})();
|