1/** 2 * Hydrogen Calendar Embeds - Main Calendar Renderer 3 * 4 * Initializes and renders FullCalendar instances with the plugin's 5 * configuration. Handles view resolution, event sources, and toolbar setup. 6 * 7 * @package hydrogen-calendar-embeds 8 * @since 1.0.0 9 */ 10 11function hycal_render_calendar(hycalSettings) { 12 /** 13 * Action: hycal.beforeRender 14 * 15 * Fires before the calendar starts rendering. 16 * Use this to perform setup tasks or modify the DOM. 17 * 18 * @since 1.0.0 19 * 20 * @param {object} hycalSettings The calendar settings. 21 */ 22 hycalHooks.doAction("hycal.beforeRender", hycalSettings); 23 24 /** 25 * Filter: hycal.settings 26 * 27 * Filter the calendar settings before processing. 28 * Use this to modify or add custom settings. 29 * 30 * @since 1.0.0 31 * 32 * @param {object} hycalSettings The calendar settings. 33 * @returns {object} Modified settings. 34 */ 35 hycalSettings = hycalHooks.applyFilters("hycal.settings", hycalSettings); 36 37 const currCal = `hycal-${hycalSettings["instance_id"]}`; 38 const calendarEl = document.getElementById(currCal); 39 calendarEl.innerHTML = ""; 40 let width = window.innerWidth; 41 42 const views = hycal_resolve_views(hycalSettings); 43 const calData = hycal_resolve_cals(hycalSettings); 44 const cals = calData.eventSources; 45 46 // console.table(cals); // DEBUG 47 // console.table(hycalSettings); // DEBUG 48 // console.table(views); // DEBUG 49 50 const toolbarLeft = hycal_is_truthy(hycalSettings["show_today_button"]) 51 ? "prev,next today" 52 : "prev,next"; 53 const toolbarCenter = hycal_is_truthy(hycalSettings["show_title"]) 54 ? "title" 55 : ""; 56 const toolbarRight = views.length > 1 ? views.all.join(",") : ""; 57 58 let selectedView = views.initial; 59 60 const hycalDefaults = { 61 locale: hycalSettings["locale"], 62 63 eventSources: cals, 64 65 views: { 66 // Options apply to dayGridMonth, dayGridWeek, and dayGridDay views 67 dayGrid: { 68 eventTimeFormat: { 69 hour: "numeric", 70 minute: "2-digit", 71 meridiem: "short", 72 }, 73 }, 74 // Custom List View - only custom one needs definition 75 listCustom: { 76 type: "list", 77 duration: { days: parseInt(hycalSettings["custom_days"]) }, 78 buttonText: hycalSettings["custom_list_button"], 79 }, 80 }, 81 82 // Day grid options 83 eventDisplay: "block", // Adds border and bocks to events instead of bulleted list (default) 84 height: "auto", 85 fixedWeekCount: false, // True: 6 weeks, false: flex for month 86 87 // List options 88 listDayFormat: { weekday: "long", month: "long", day: "numeric" }, 89 90 initialView: views.initial, 91 92 headerToolbar: { 93 left: toolbarLeft, 94 center: toolbarCenter, 95 right: toolbarRight, 96 }, 97 98 eventDidMount: function (info) { 99 // Handle free/busy calendars with undefined titles 100 // Google Calendar API returns the string "undefined" for free/busy events 101 if (!info.event.title || info.event.title === "undefined") { 102 info.event.setProp( 103 "title", 104 wp.i18n.__("Busy", "hydrogen-calendar-embeds"), 105 ); 106 } 107 108 if (hycalSettings["use_tooltip"] === "true") { 109 hycal_tippyRender(info, currCal, hycalSettings); 110 } 111 112 /** 113 * Action: hycal.eventDidMount 114 * 115 * Fires after each event element is added to the DOM. 116 * Use this to add custom attributes, styles, or behaviors to events. 117 * 118 * @since 1.0.0 119 * 120 * @param {object} info FullCalendar event info object. 121 * @param {object} hycalSettings The calendar settings. 122 * @param {string} currCal The calendar container ID. 123 */ 124 hycalHooks.doAction("hycal.eventDidMount", info, hycalSettings, currCal); 125 }, 126 127 eventClick: function (info) { 128 if ( 129 hycalSettings["use_tooltip"] === "true" || 130 hycalSettings["no_link"] === "true" 131 ) { 132 info.jsEvent.preventDefault(); // Prevent following link 133 } 134 }, 135 136 // Handle calendar source loading errors (ICS fetch failures, etc.) 137 eventSourceFailure: function (error) { 138 const details = []; 139 const failureTypes = []; 140 const normalizedError = error || {}; 141 142 if (normalizedError.message) { 143 details.push(normalizedError.message); 144 145 const lowerMessage = normalizedError.message.toLowerCase(); 146 if ( 147 /(failed to fetch|network error|networkerror|dns|socket|timed out|timeout)/.test( 148 lowerMessage, 149 ) 150 ) { 151 failureTypes.push("ICS address is unreachable"); 152 } 153 if (
154 /malformed|invalid ical|invalid icalendar|parser error|parse error|unable to parse|missing begin:vcalendar/.test( 155 lowerMessage, 156 ) 157 ) { 158 failureTypes.push("Malformed ICS feed"); 159 } 160 if (/403|unauthorized|forbidden|401|404|not found/.test(lowerMessage)) { 161 failureTypes.push( 162 "REST API endpoint is not public or the remote feed returned an HTTP error", 163 ); 164 } 165 } 166 167 if (normalizedError.response) { 168 const code = normalizedError.response.status; 169 const statusText = normalizedError.response.statusText || ""; 170 if (code) { 171 details.push(`HTTP ${code}${statusText ? ` ${statusText}` : ""}`); 172 if (code === 401 || code === 403) { 173 failureTypes.push( 174 "REST API endpoint is not public or access is blocked", 175 ); 176 } else if (code === 404) { 177 failureTypes.push("REST API endpoint or ICS URL not found"); 178 } else if (code >= 500) { 179 failureTypes.push( 180 "Remote calendar service returned a server error", 181 ); 182 } 183 } 184 } 185 186 if (normalizedError.error) { 187 details.push(normalizedError.error); 188 } 189 190 if (normalizedError.source && normalizedError.source.url) { 191 details.push(`Source URL: ${normalizedError.source.url}`); 192 } 193 194 const consoleData = { 195 reason: failureTypes.length 196 ? failureTypes.join(" | ") 197 : "Unable to determine failure reason", 198 details: details.length ? details.join(" | ") : "No details available", 199 error: normalizedError, 200 }; 201 202 console.error( 203 "Hydrogen Calendar Embeds: Failed to load event source.", 204 consoleData, 205 ); 206 207 // Display user-friendly message in calendar container 208 const errorMsg = document.createElement("div"); 209 errorMsg.className = "hycal-error"; 210 errorMsg.style.cssText = 211 "color: #721c24; background: #f8d7da; padding: 12px; border-radius: 4px; margin: 8px 0;"; 212 errorMsg.textContent = wp.i18n.__( 213 "Unable to load calendar data. Please try again later.", 214 "hydrogen-calendar-embeds", 215 ); 216 // Only show one error message 217 if (!calendarEl.querySelector(".hycal-error")) { 218 calendarEl.insertBefore(errorMsg, calendarEl.firstChild); 219 } 220 }, 221 222 // // Change view on window resize 223 // windowResize: function (view) { 224 // // Catch mobile chrome, which changes window size as nav bar appears 225 // // so only fire if width has changed. 226 // if ( 227 // window.innerWidth !== width && 228 // views.hasList && 229 // views.wantsToEnforceListviewOnMobile 230 // ) { 231 // const currentView = calendar.view.type; 232 // const isCurrentlyListView = currentView.toLowerCase().includes("list"); 233 234 // if (hycal_is_mobile(views.mobileBreakpoint)) { 235 // // Only switch to list view if not already on one 236 // if (!isCurrentlyListView) { 237 // calendar.changeView(views.listView); 238 // } 239 // } else { 240 // // Only switch away from list view if we enforced it 241 // if ( 242 // isCurrentlyListView && 243 // !selectedView.toLowerCase().includes("list") 244 // ) { 245 // calendar.changeView(selectedView); 246 // } 247 // } 248 // } 249 // }, 250 }; 251 252 // Hide past events if requested 253 if (hycal_is_truthy(hycalSettings["hide_past"])) { 254 const today = new Date(); 255 const year = today.getFullYear();
256 const month = String(today.getMonth() + 1).padStart(2, "0"); 257 const day = String(today.getDate()).padStart(2, "0"); 258 const todayString = `${year}-${month}-${day}`; 259 260 hycalDefaults.validRange = { 261 start: todayString, 262 }; 263 } 264 265 let hycalOverrides = {}; 266 try { 267 hycalOverrides = JSON.parse(hycalSettings["fc_args"]); 268 } catch (e) { 269 console.error("Hydrogen Calendar Embeds: Invalid JSON in fc_args", e); 270 } 271 let hycalArgs = hycal_argmerge(hycalDefaults, hycalOverrides); 272 273 /** 274 * Filter: hycal.fullcalendarOptions 275 * 276 * Filter the FullCalendar options before initialization. 277 * Use this to modify any FullCalendar configuration. 278 * 279 * @since 1.0.0 280 * 281 * @param {object} hycalArgs The merged FullCalendar options. 282 * @param {object} hycalSettings The calendar settings. 283 * @param {object} hycalDefaults The default options. 284 * @returns {object} Modified FullCalendar options. 285 */ 286 hycalArgs = hycalHooks.applyFilters( 287 "hycal.fullcalendarOptions", 288 hycalArgs, 289 hycalSettings, 290 hycalDefaults, 291 ); 292 293 // console.log(hycalSettings["fc_args"]); // DEBUG 294 // console.log(JSON.stringify(hycalDefaults, null, 2)); // DEBUG 295 // console.log(JSON.stringify(hycalArgs, null, 2)); // DEBUG 296 297 const calendar = new FullCalendar.Calendar(calendarEl, hycalArgs); 298 calendar.render(); 299 300 /**
301 * Action: hycal.afterRender 302 * 303 * Fires after the calendar has rendered. 304 * Use this to access the FullCalendar instance or modify the rendered calendar. 305 * 306 * @since 1.0.0 307 * 308 * @param {object} calendar The FullCalendar instance. 309 * @param {object} hycalSettings The calendar settings. 310 * @param {Element} calendarEl The calendar DOM element. 311 */ 312 hycalHooks.doAction("hycal.afterRender", calendar, hycalSettings, calendarEl); 313}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.