3 * @file EventListView.ts
4 * @brief Event sidebar UI component
6 * @details Displays events in list format with scrollable container and navigation support.
8 * @author Arnold Schiller <calendar@projektit.de>
10 * @copyright GPL-3.0-or-later
13 * Project IT Calendar - Event List View Component
14 * ----------------------------------------------
15 * This component handles the rendering of the event list shown next to or
16 * below the calendar grid. It supports single-day views, range views, and
17 * full-month overviews with clickable event rows for navigation.
18 * * * * ARCHITECTURE OVERVIEW:
19 * 1. EventManager: Handles data fetching (ICS/Evolution/System).
20 * 2. CalendarLogic: Pure JS logic for date calculations and holiday parsing.
21 * 3. CalendarView: The complex St.Table based UI grid.
22 * 4. EventListView: Specialized view for displaying event details.
23 * * * SYSTEM INTEGRATION:
24 * - Uses 'Settings' for user-defined date formats and behavior.
25 * - Uses 'AppletPopupMenu' to host the calendar UI.
26 * - Uses 'KeybindingManager' for global hotkey support.
27 * * @author Arnold Schiller <calendar@projektit.de>
28 * @link https://github.com/ArnoldSchiller/calendar
29 * @link https://projektit.de/kalender
30 * @license GPL-3.0-or-later
34/* === GJS Imports - Shell Toolkit and Clutter for UI === */
35const { St, Clutter } = imports.gi;
36const Signals = imports.signals;
39 * Interface Merging for Signals
40 * This tells TypeScript that EventListView will have the methods
41 * from the Signals.Signals interface (connect, disconnect, emit).
43export interface EventListView extends Signals.Signals {}
49 * Manages the UI for the event sidebar/agenda.
52 * @class EventListView
53 * @brief Main eventlist view class
55 * @details For detailed documentation see the main class documentation.
58 * @class EventListView
59 * @brief Main eventlist view class
61 * @details For detailed documentation see the main class documentation.
63export class EventListView {
64 public actor: any; // The main container (St.BoxLayout)
65 private _eventsBox: any; // Container for the list of event rows
66 private _selectedDateLabel: any; // Header showing the current date or range
69 // Main layout: vertical box containing the header and the scrollable list
70 this.actor = new St.BoxLayout({
71 style_class: "calendar-events-main-box",
76 // Header Label: Shows "Monday, January 1, 2026" or "January 2026"
77 this._selectedDateLabel = new St.Label({
78 style_class: "calendar-events-date-label"
80 this.actor.add_actor(this._selectedDateLabel);
83 * ScrollView: Essential for UI usability.
84 * Policy 1 (NEVER) for horizontal: We want text to wrap or clip, not scroll sideways.
85 * Policy 2 (AUTOMATIC) for vertical: Shows scrollbar only if content exceeds height.
87 let scrollBox = new St.ScrollView({
88 style_class: 'calendar-events-scrollbox vfade',
93 // Internal box for the actual event entries
94 this._eventsBox = new St.BoxLayout({
95 style_class: 'calendar-events-event-container',
99 scrollBox.add_actor(this._eventsBox);
100 this.actor.add_actor(scrollBox);
103 * Legacy/Generic Update Method
104 * This remains as a central entry point. By default, it treats
105 * the input as a single day update.
106 * @param date - The date to display in the header.
107 * @param events - Array of events.
109 public update(date: Date, events: any[]): void {
110 this.updateForDate(date, events);
114 * Update the list for a specific day.
115 * @param date - The specific day to display.
116 * @param events - Array of events for this day.
118 public updateForDate(date: Date, events: any[] = []): void {
119 this._selectedDateLabel.set_text(date.toLocaleDateString(undefined, {
120 weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'
123 this._eventsBox.destroy_children();
125 if (!events || events.length === 0) {
126 this._showNoEvents();
130 // Render rows without showing the date (redundant in single day view)
131 events.forEach((ev) => this._addEventRow(ev, false));
135 * Update the list for a specific month overview.
136 * @param year - The year of the month.
137 * @param month - The month index (0-11).
138 * @param events - All events within this month.
140 public updateForMonth(year: number, month: number, events: any[]): void {
141 const date = new Date(year, month, 1);
142 this._selectedDateLabel.set_text(date.toLocaleDateString(undefined, {
143 month: 'long', year: 'numeric'
146 this._eventsBox.destroy_children();
148 if (!events.length) {
149 this._showNoEvents();
153 // Render rows with dates enabled so users can see which day the event belongs to
154 events.forEach(ev => this._addEventRow(ev, true));
158 * Update the list for a specific date range.
159 * @param range - Object with from and to dates.
160 * @param events - Events within this range.
162 public updateForRange(range: DateRange, events: any[]): void {
163 this._selectedDateLabel.set_text(this._formatRangeLabel(range));
164 this._eventsBox.destroy_children();
166 if (!events.length) {
167 this._showNoEvents();
171 // Range views usually benefit from seeing the date per row
172 events.forEach(ev => this._addEventRow(ev, true));
176 * Helper to format a DateRange into a readable string.
178 private _formatRangeLabel(range: DateRange): string {
179 const opts: Intl.DateTimeFormatOptions = {
184 return `${range.from.toLocaleDateString(undefined, opts)} – ${range.to.toLocaleDateString(undefined, opts)}`;
188 * Renders a placeholder state when no events are present.
190 private _showNoEvents() {
191 let box = new St.BoxLayout({
192 style_class: "calendar-events-no-events-box",
194 x_align: 2 // Clutter.ActorAlign.CENTER
197 box.add_actor(new St.Icon({
198 icon_name: 'office-calendar',
202 box.add_actor(new St.Label({
204 style_class: "calendar-events-no-events-label"
207 this._eventsBox.add_actor(box);
211 * Creates a stylized row for a single event.
212 * @param ev - The event data object.
213 * @param showDate - Whether to display the day/month prefix.
215 private _addEventRow(ev: any, showDate: boolean = false) {
216 let row = new St.BoxLayout({
217 style_class: "calendar-event-button",
223 // Event listener to trigger navigation when a row is clicked
224 row.connect('button-press-event', () => {
226 // Emit signal so CalendarView can jump to this specific date
227 this.emit('event-clicked', ev);
229 return Clutter.EVENT_STOP;
232 // Visual indicator: Color strip matching the source calendar color
233 let colorStrip = new St.Bin({
234 style_class: "calendar-event-color-strip",
235 style: `background-color: ${ev.color || '#3498db'}; width: 4px;`
237 row.add_actor(colorStrip);
239 let contentVBox = new St.BoxLayout({
240 style_class: "calendar-event-row-content",
245 // Date Label: Only shown in month/range overviews for orientation
246 if (showDate && ev.start) {
247 let dateStr = ev.start.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit' });
248 contentVBox.add_actor(new St.Label({
250 style_class: "calendar-event-date-small"
255 contentVBox.add_actor(new St.Label({
256 text: ev.summary || "Unnamed Event",
257 style_class: "calendar-event-summary"
260 // Optional: Sub-text (e.g., location or description)
261 if (ev.description) {
262 contentVBox.add_actor(new St.Label({
263 text: ev.description || "",
264 style_class: "calendar-event-time-future"
268 row.add_actor(contentVBox);
269 this._eventsBox.add_actor(row);
274 * Add GJS Signal support to the prototype.
275 * This allows the view to emit the 'event-clicked' signal.
277Signals.addSignalMethods(EventListView.prototype);
280 * HYBRID EXPORT SYSTEM
282if (typeof exports !== 'undefined') {
283 exports.EventListView = EventListView;
285(global as any).EventListView = EventListView;