diff --git a/civicrm-ux.php b/civicrm-ux.php index 88f5113..d4e99cd 100644 --- a/civicrm-ux.php +++ b/civicrm-ux.php @@ -9,7 +9,7 @@ * Plugin Name: WP CiviCRM UX * Plugin URI: https://github.com/agileware/wp-civicrm-ux * Description: A better user experience for integrating WordPress and CiviCRM - * Version: 1.19.0 + * Version: 1.20.0 * Requires at least: 5.8 * Requires PHP: 7.4 * Requires Plugins: civicrm diff --git a/includes/class-civicrm-ux.php b/includes/class-civicrm-ux.php index 34bb4fd..ca5c2d7 100755 --- a/includes/class-civicrm-ux.php +++ b/includes/class-civicrm-ux.php @@ -160,6 +160,7 @@ private function load_dependencies() { */ require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-civicrm-ux-helper.php'; require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-civicrm-ux-option-store.php'; + require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/utils/class-civicrm-ux-validators.php'; // All module mangers and instances class require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/interface/interface-civicrm-ux-managed-instance.php'; diff --git a/includes/utils/class-civicrm-ux-validators.php b/includes/utils/class-civicrm-ux-validators.php new file mode 100644 index 0000000..5824621 --- /dev/null +++ b/includes/utils/class-civicrm-ux-validators.php @@ -0,0 +1,57 @@ + * { z-index: 1; } - - -.tippy-box[data-theme~='fcvic'] { - background-color: #f0e8ce; - border-color: #f0e8ce; - color: black; -} - -.tippy-box[data-theme~='fcvic'][data-placement^='top'] > .tippy-arrow::before { - border-top-color: #f0e8ce; -} -.tippy-box[data-theme~='fcvic'][data-placement^='bottom'] > .tippy-arrow::before { - border-bottom-color: #f0e8ce; -} -.tippy-box[data-theme~='fcvic'][data-placement^='left'] > .tippy-arrow::before { - border-left-color: #f0e8ce; -} -.tippy-box[data-theme~='fcvic'][data-placement^='right'] > .tippy-arrow::before { - border-right-color: #f0e8ce; -} - - -.tippy-box[data-theme~='fcvic'] img { - max-width: 90%; - max-height: 50px; - height: auto; - margin: 10px 10px 0px 10px; - display: block; -} - - .event-holder { overflow: hidden; padding: 3px 3px 3px 3px; @@ -289,6 +245,7 @@ div.civicrm-event-listing-item { .event-time { margin: 5px 0px 0px 10px; display: inline-flex; + align-items: center; } .event-time-text { @@ -297,6 +254,10 @@ div.civicrm-event-listing-item { margin-left: 5px; } +.tippy-box .event-time-text { + color: #ffffff; +} + .event-location { margin: 0px 0px 5px 10px; } diff --git a/public/js/event-fullcalendar.js b/public/js/event-fullcalendar.js index 6289351..04216f0 100644 --- a/public/js/event-fullcalendar.js +++ b/public/js/event-fullcalendar.js @@ -1,320 +1,326 @@ +(function() { const domevent = function (eventName, detail) { if (typeof Window.CustomEvent === "function") { - return new CustomEvent(eventName, { - bubbles: true, - cancelable: true, - detail, - }); + return new CustomEvent(eventName, { + bubbles: true, + cancelable: true, + detail, + }); } else { - const event = document.createEvent("CustomEvent"); - event.initCustomEvent(eventName, true, true, detail); - return event; + const event = document.createEvent("CustomEvent"); + event.initCustomEvent(eventName, true, true, detail); + return event; } - }; - - /* - Convert ISO8601 date object to AM/PM time format - E.g. 2021-02-02T09:30:00+0000 => 9:30AM - */ - function formatAMPM(date) { - return date.toLocaleTimeString([], { timeStyle: "short" }); - } - - function hidePopup() { +}; + +/* + Convert ISO8601 date object to AM/PM time format + E.g. 2021-02-02T09:30:00+0000 => 9:30AM +*/ +const formatAMPM = function (date) { + return date.toLocaleTimeString([], {timeStyle: "short"}); +} + +window.hideEventsCalendarPopup = function() { document.querySelector(".civicrm-ux-event-popup").style.display = "none"; document.getElementById("civicrm-event-fullcalendar").style.display = "flex"; - } +} - function sameDay(d1, d2) { +const sameDay = function (d1, d2) { return d1.getFullYear() === d2.getFullYear() && - d1.getMonth() === d2.getMonth() && - d1.getDate() === d2.getDate(); - } - - // Used to convert an index to a day-of-the-week/month - // E.g. days[Date.getDay()] => days[1] => Monday - const days = [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - ]; - const months = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ]; - - /* - Convert ISO8601 date object to human readable format - E.g. 2021-02-02T09:30:00+0000 => Tuesday, 2 February 2021 - */ - function formatDay(date) { - return ( - days[date.getDay()] + - ", " + - date.getDate() + - " " + - months[date.getMonth()] + - " " + - date.getFullYear() - ); - } - - let events_loaded = false; - let prev_rendered_date; - let prev_rendered_date_visible = false; - - document.addEventListener("DOMContentLoaded", function () { - const calendarEl = document.getElementById("civicrm-event-fullcalendar"); - - // ColoUr scheme for different event type labels - const colors = wp_site_obj.colors; - const event_types = wp_site_obj.types.split(','); + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate(); +} - /* - This object defines the custom parameters for FullCalendar's library incl. buttons, logic, views - */ - let calendarParams = { - initialView: "dayGridMonth", - nextDayThreshold: '09:00:00', - events: function (info, successCallback, failureCallback) { - if (!events_loaded) { - // Make AJAX request (themes > functions.php) to get all events from 1 year ago until now - jQuery.ajax({ +let events_loaded = false; +let prev_rendered_date; +let prev_rendered_date_visible = false; + +// Colour scheme for different event type labels +const colors = uxFullcalendar.colors || {}; +const event_types = uxFullcalendar.filterTypes || []; + +const events = function (info, successCallback, failureCallback) { + if (!events_loaded) { + // Make AJAX request (themes > functions.php) to get all events from 1 year ago until now + wp.apiRequest({ method: "GET", dataType: "json", - url: wp_site_obj.ajax_url + 'civicrm_ux/get_events_all', + url: uxFullcalendar.ajax_url + 'civicrm_ux/get_events_all', data: { - type: wp_site_obj.types, - upload: wp_site_obj.upload, - colors: colors, - start_date: wp_site_obj.start, - image_id_field: wp_site_obj.image_id_field, - image_src_field: wp_site_obj.image_src_field, - force_login: wp_site_obj.force_login, - redirect_after_login: wp_site_obj.redirect_after_login, - extra_fields: wp_site_obj.extra_fields + type: uxFullcalendar.types, + upload: uxFullcalendar.upload, + colors: colors, + start_date: uxFullcalendar.start, + image_id_field: uxFullcalendar.image_id_field, + image_src_field: uxFullcalendar.image_src_field, + force_login: uxFullcalendar.force_login, + redirect_after_login: uxFullcalendar.redirect_after_login, + extra_fields: uxFullcalendar.extra_fields }, // Store events in client's browser after success to prevent further AJAX requests success: function (response) { - all_the_events = response.result; - events_loaded = true; - successCallback(response.result); + all_the_events = response.result; + events_loaded = true; + successCallback(response.result); }, - }); - } else { - successCallback(all_the_events); + }); + } else { + successCallback(all_the_events); + } +}; + +const eventContent = function (arg) { + const selector_val = document.querySelector("#event-selector")?.value ?? 'all'; + + const { + extendedProps: { html_entry, event_type } + } = arg.event; + + if ( + selector_val == "all" || + selector_val == arg.event.extendedProps.event_type + ) { + const eventTemplate = document.createElement("template"); + eventTemplate.innerHTML = html_entry; + + const eventHolder = eventTemplate.content.firstElementChild; + eventHolder.classList.add("event-holder"); + + if (colors.hasOwnProperty(event_type)) + eventHolder.style.backgroundColor = colors[event_type]; + + return { domNodes: eventTemplate.content.childNodes }; + } + + return {}; +} + +const eventDidMount = function (info) { + let selector_val = document.querySelector("#event-selector")?.value ?? 'all'; + + const event_start = info.event.start; + const event_end = info.event.end; + + const { + extendedProps: { + event_type, + timeZone, + 'file.uri': fileUri = undefined, + image_url, + country, + html_render = undefined } - }, - eventTimeFormat: { - hour: "numeric", - minute: "2-digit", - omitZeroMinute: false, - meridiem: "short", - }, - customButtons: { - sortByType: { - text: "Filter by Event Type", - }, - }, - headerToolbar: { - left: "dayGridMonth,listMonth sortByType", - center: "", - end: "prevYear,prev title next,nextYear", - }, - buttonText: { - dayGridMonth: "Calendar View", - listMonth: "List View", - prevYear: "« Previous year", - prev: "‹ Previous month", - nextYear: "Next year »", - next: "Next month ›", - }, - buttonIcons: { - prevYear: "chevrons-left", - prev: "chevron-left", - nextYear: "chevrons-right", - next: "chevron-right", - }, - firstDay: 1, - // Event render hooks (eventContent, eventDidMount, eventClick) define the custom logic for FCVIC's fullcalendar - - // Content injection to change the colour of event labels (by type) - eventContent: function (arg) { - let selector_val = document.querySelector("#event-selector").value; - - if ( - selector_val == "all" || - selector_val == arg.event.extendedProps.event_type - ) { - let eventHolder = document.createElement("div"); - eventHolder.classList.add("event-holder"); - eventHolder.style.backgroundColor = - Object.keys(colors).length > 0 ? '#' + colors[arg.event.extendedProps.event_type] : '#333333'; - eventHolder.innerHTML = - '
' + - arg.event.title + - "
"; - - let arrayOfDomNodes = [eventHolder]; - return { domNodes: arrayOfDomNodes }; - } else return {}; - }, - - // Executed for each event when they are added to the DOM - eventDidMount: function (info) { - let selector_val = document.querySelector("#event-selector").value; - - const event_start = new Date(info.event.start); - const event_end = new Date(info.event.end); + } = info.event; - if (info.view.type == "listMonth") { - if (prev_rendered_date != event_start.getDate()) { + if (info.view.type == "listMonth") { + if (prev_rendered_date != event_start.getDate()) { prev_rendered_date = event_start.getDate; prev_rendered_date_visible = false; - } - const onthisdate = + } + const onthisdate = event_start.getFullYear() + "-" + - (event_start.getMonth() + 1 < 10 - ? "0" + (event_start.getMonth() + 1) - : event_start.getMonth() + 1) + + (event_start.getMonth() + 1 + '').padStart(2, '0') "-" + - (event_start.getDate() < 10 - ? "0" + event_start.getDate() - : event_start.getDate()); - - let date_row = document.querySelectorAll( - "[data-date='" + onthisdate + "']" - )[0]; - - if (!prev_rendered_date_visible) { - date_row.style.display = "none"; - } else { - date_row.style.display = "table-row"; - } - - if (info.event.extendedProps.event_type == selector_val) { - prev_rendered_date_visible = true; - date_row.style.display = "table-row"; - } - - if (selector_val == "all") { - date_row.style.display = "table-row"; - } - - prev_rendered_date = event_start.getDate(); - - if ( - info.event.extendedProps.event_type != selector_val && - !prev_rendered_date_visible - ) { - prev_rendered_date_visible = false; - } + (event_start.getDate() + '').padStart(2, '0'); + + let date_row = document.querySelector("[data-date='" + onthisdate + "']"); + + if(date_row) { + if (!prev_rendered_date_visible) { + date_row.style.display = "none"; + } else { + date_row.style.display = "table-row"; + } + + if (event_type == selector_val) { + prev_rendered_date_visible = true; + date_row.style.display = "table-row"; + } + + if (selector_val == "all") { + date_row.style.display = "table-row"; + } } - + + prev_rendered_date = event_start.getDate(); + if ( - selector_val == "all" || - selector_val == info.event.extendedProps.event_type + event_type != selector_val && + !prev_rendered_date_visible ) { - let event_img = info.event.extendedProps["file.uri"] ? info.event.extendedProps["image_url"] : ""; - - - if (info.view.type == "listMonth") { - const template = info.event.extendedProps.html_render; - info.el.innerHTML = template; - } else { - const event_location = info.event.extendedProps.country; - - const template = ` -
${info.event.title}
- ${ - info.event.extendedProps["file.uri"] - ? '' - : "" - } -
${ - formatAMPM(event_start) + " to " + formatAMPM(event_end) - }
- ${ - event_location - ? '
' + event_location + '
' - : "" - } - `; - + prev_rendered_date_visible = false; + } + } + + if ( + selector_val == "all" || + selector_val == event_type + ) { + let event_img = (fileUri ?? false) ? image_url : ""; + + if (info.view.type == "listMonth") { + if(html_render) { + info.el.innerHTML = html_render; + + for (const el of Array.from(info.el.children).filter((el) => !(el instanceof HTMLTableCellElement))) { + const wrapEl = document.createElement('TD'); + el.replaceWith(wrapEl); + wrapEl.append(el); + } + + const colSpan = Math.max(1, 4 - info.el.children.length); + info.el.lastElementChild.setAttribute('colspan', colSpan); + } + } else { + const event_location = country; + + const fmt_AMPM = new Intl.DateTimeFormat([], { timeStyle: 'short', timeZone }) + + const eventElements = { + eventImg: '', + eventLocation: '', + eventTimeText: ` + + to + `, + } + + if(info.event.extendedProps["file.uri"]) { + eventElements.eventImg = ``; + } + + if(event_location) { + eventElements.eventLocation = `
${event_location}
` + } + + info.view.calendar.el.dispatchEvent(domevent('fullcalendar:buildTippy', eventElements)); + + const template = document.createElement('template'); + + template.innerHTML = + `
${info.event.title}
+ ${eventElements.eventImg} +
${eventElements.eventTimeText}
+ ${eventElements.eventLocation}`; + + if(colors.hasOwnProperty(event_type)) { + template.content.querySelector('.event-name').style.backgroundColor = colors[event_type]; + } + + info.view.calendar.el.dispatchEvent(domevent('fullcalendar:alterTippy', template.content)); + let tooltip = new tippy(info.el, { - interactive: true, - delay: 300, - maxWidth: 400, - allowHTML: true, - placement: "top", - content(reference) { - return template; - }, + interactive: true, + delay: 300, + maxWidth: 400, + allowHTML: true, + placement: "top", + content: template.content, }); - } - } else { - info.el.innerHTML = ""; } - }, - - eventClick: function (eventClickInfo) { - let jsEvent = eventClickInfo.jsEvent; - jsEvent.preventDefault(); - - let event_container = document.querySelector( - ".civicrm-ux-event-popup-container" - ); - let popup = document.querySelector(".civicrm-ux-event-popup"); - let popup_container = document.getElementById("civicrm-ux-event-popup-content"); - event_container.style.display = "none"; - calendarEl.style.display = "none"; - popup.style.display = "block"; - popup_container.innerHTML = eventClickInfo.event.extendedProps.html_render; - - event_container.style.display = "block"; - }, + } else { + info.el.innerHTML = ""; + } +}; + +const eventClick = function ({el, event, jsEvent, view}) { + jsEvent.preventDefault(); + + let event_container = document.querySelector( + ".civicrm-ux-event-popup-container" + ); + let popup = document.querySelector(".civicrm-ux-event-popup"); + let popup_container = document.getElementById("civicrm-ux-event-popup-content"); + event_container.style.display = "none"; + view.calendar.el.style.display = "none"; + popup.style.display = "block"; + popup_container.innerHTML = event.extendedProps.html_render; + + event_container.style.display = "block"; +} + +document.addEventListener("DOMContentLoaded", function () { + const calendarEl = document.getElementById("civicrm-event-fullcalendar"); + + /* + This object defines the custom parameters for FullCalendar's library incl. buttons, logic, views + */ + let calendarParams = { + initialView: "dayGridMonth", + nextDayThreshold: '09:00:00', + events, + eventTimeFormat: { + hour: "numeric", + minute: "2-digit", + omitZeroMinute: false, + meridiem: "short", + }, + customButtons: { + sortByType: { + text: "Filter by Event Type", + }, + }, + headerToolbar: { + left: "dayGridMonth,listMonth sortByType", + center: "", + end: "prevYear,prev title next,nextYear", + }, + buttonText: { + dayGridMonth: "Calendar View", + listMonth: "List View", + prevYear: "« Previous year", + prev: "‹ Previous month", + nextYear: "Next year »", + next: "Next month ›", + }, + buttonIcons: { + prevYear: "chevrons-left", + prev: "chevron-left", + nextYear: "chevrons-right", + next: "chevron-right", + }, + firstDay: 1, + // Event render hooks (eventContent, eventDidMount, eventClick) define the custom logic for FCVIC's fullcalendar + + // Content injection to change the colour of event labels (by type) + eventContent, + + // Executed for each event when they are added to the DOM + eventDidMount, + + // Callback to be run when the calendar entry is clicked + eventClick, + + ...(window.uxFullcalendar.calendar_params || {}) }; - + calendarEl.dispatchEvent( - domevent("fullcalendar:buildparams", calendarParams) + domevent("fullcalendar:buildparams", calendarParams) ); - + const calendar = new FullCalendar.Calendar(calendarEl, calendarParams); - + calendarEl.dispatchEvent(domevent("fullcalendar:prerender")); calendar.render(); const sortByTypeBtn = document.querySelector(".fc-sortByType-button"); + + if (!sortByTypeBtn) + return; + const sortByTypeSelect = document.createElement("select"); sortByTypeSelect.classList.add("fc-button"); sortByTypeSelect.classList.add("fc-button-primary"); sortByTypeSelect.innerHTML = ''; - for (const i in event_types) { - sortByTypeSelect.innerHTML += ''; + for (const type of event_types) { + sortByTypeSelect.innerHTML += ''; } sortByTypeSelect.setAttribute("id", "event-selector"); sortByTypeBtn.style.display = "none"; sortByTypeBtn.after(sortByTypeSelect); - + sortByTypeSelect.addEventListener("change", () => calendar.refetchEvents()); - }); - \ No newline at end of file +}); +})(); \ No newline at end of file diff --git a/rest/json-all-events.php b/rest/json-all-events.php index dff5900..da4a7ab 100644 --- a/rest/json-all-events.php +++ b/rest/json-all-events.php @@ -2,6 +2,8 @@ use Civi\Api4\Event; +use Civicrm_Ux_Shortcode_Event_FullCalendar as Shortcode; + class Civicrm_Ux_REST_JSON_All_Events extends Abstract_Civicrm_Ux_REST { /** @@ -44,192 +46,141 @@ public function rest_api_callback( $data ) { protected function get_events_all() { $types = array(); $start_date = preg_replace("([^0-9-])", "", $_REQUEST['start_date']); - $force_login = rest_sanitize_boolean($_REQUEST['force_login']); + $force_login = rest_sanitize_boolean($_REQUEST['force_login'] ?? Shortcode::getDefaultForceLogin()); $redirect_after_login = esc_url($_REQUEST['redirect_after_login']); - $extra_fields = $_REQUEST['extra_fields'] != '' ? explode(',', filter_var($_REQUEST['extra_fields'], FILTER_SANITIZE_STRING)) : array(); - $colors = $_REQUEST['colors'] ?? []; - filter_var($_REQUEST['upload'], FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED); - $upload = $_REQUEST['upload']; + $extra_fields = !empty($_REQUEST['extra_fields']) ? explode(',', filter_var($_REQUEST['extra_fields'], FILTER_SANITIZE_STRING)) : []; + if(!empty($_REQUEST['colors']) && !is_array($_REQUEST['colors'])) { + $_REQUEST['colors'] = explode(',', $_REQUEST['colors']); + } + $colors = array_map ( [ 'Civicrm_Ux_Validators', 'validateCssColor' ], $_REQUEST['colors'] ?? [] ); + $upload = filter_var($_REQUEST['upload'], FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED); + + $colors = array_filter($colors); + $colors[ 'default' ] ??= Shortcode::getDefaultColor(); if (isset($_REQUEST['type'])) { $types_tmp = explode(",", $_REQUEST['type']); for ($i = 0; $i < count($types_tmp); $i++) { array_push($types, preg_replace('/[^a-zA-Z0-9 ]/', '', $types_tmp[$i])); } - - } - - foreach ($colors as $k => $v) { - $colors[$k] = sanitize_hex_color_no_hash($v); - if (!ctype_alnum($k)) { - unset($colors[$k]); - } } + foreach($types as $idx => $type) { + $colors[$type] ??= $colors[$idx] ?? Shortcode::getDefaultColor(); + } $res = array('success' => true); try { $events = array(); - if ($_REQUEST['image_id_field'] != "") { - $image_id_field = $_REQUEST['image_id_field']; - $image_src_field = $_REQUEST['image_src_field']; - - $events = Event::get(FALSE) - ->addSelect('id', 'title', 'summary', 'description', 'event_type_id:label', 'start_date', 'end_date', 'file.id', $image_src_field, 'address.street_address', 'address.street_number', 'address.street_number_suffix', 'address.street_name', 'address.street_type', 'address.country_id:label', 'is_online_registration', ...$extra_fields) - ->addJoin('File AS file', 'LEFT', ['file.id', '=', $image_id_field]) - ->addJoin('LocBlock AS loc_block', 'LEFT', ['loc_block_id', '=', 'loc_block_id.id']) - ->addJoin('Address AS address', 'LEFT', ['loc_block.address_id', '=', 'address.id']) - ->addWhere('event_type_id:label', 'IN', $types) - ->addWhere('start_date', '>=', $start_date) - ->addWhere('is_public', '=', TRUE) - ->addWhere('is_active', '=', TRUE) - ->addOrderBy('start_date', 'ASC') - ->execute(); - - $res['result'] = array(); - - $tz = wp_timezone(); - - foreach ($events as $event) { - if ( !empty($redirect_after_login) ) { - // If we have specified a custom redirection after login, apply that - $params = array( - 'id' => $event['id'], - 'reset' => 1 - ); - $url = add_query_arg($params, $redirect_after_login); - } else { - // Otherwise redirect to the standard civicrm event registration page - $url = CRM_Utils_System::url('civicrm/event/register', ['id' => $event['id'], 'reset' => 1]); - } - - if (!is_user_logged_in() and $force_login) { - $url = get_site_url() . '/wp-login.php?redirect_to=' . urlencode($url); - } - - if (str_ends_with($upload, '/civicrm/custom')) { - $fileHash = CRM_Core_BAO_File::generateFileHash($event['id'], $event['file.id']); - $image_url = CRM_Utils_System::url('civicrm/file/imagefile',"reset=1&id={$event['file.id']}&eid={$event['id']}&fcs={$fileHash}"); - } else { - $image_url = $upload . '/' . $event[$image_src_field]; - } - - $event_obj = array( - 'id' => $event['id'], - 'title' => $event['title'], - 'start' => (new DateTimeImmutable($event['start_date'], $tz))->setTimezone($tz)->format(DateTime::ISO8601), - 'end' => (new DateTimeImmutable($event['end_date'], $tz))->setTimezone($tz)->format(DateTime::ISO8601), - 'display' => 'auto', - 'startStr' => $event['start_date'], - 'endStr' => $event['end_date'], - 'url' => $url, - 'extendedProps' => array( - 'html_render' => $this->generate_event_html($event, $upload, $colors, $image_src_field, $url), - 'summary' => $event['summary'], - 'description' => $event['description'], - 'event_type' => $event['event_type_id:label'], - 'file.uri' => $event[$image_src_field], - 'image_url' => $image_url, - 'street_address' => $event['address.street_address'], - 'street_number' => $event['address.street_number'], - 'street_number_suffix' => $event['address.street_number_suffix'], - 'street_name' => $event['address.street_name'], - 'street_type' => $event['address.street_type'], - 'country' => $event['address.country_id:label'], - 'is_online_registration' => $event['is_online_registration'], - 'extra_fields' => array() - ) - ); - - for ($i = 0; $i < count($extra_fields); $i++) { - $event_obj['extra_fields'][$extra_fields[$i]] = $event[$extra_fields[$i]]; - } - - $event_obj = apply_filters( 'wp_civi_ux_event_inject_content', $event_obj ); - - array_push($res['result'], $event_obj); - } - } else { - - $image_src_field = $_REQUEST['image_src_field']; - - $events = Event::get(FALSE) - ->addSelect('id', 'title', 'summary', 'description', 'event_type_id:label', 'start_date', 'end_date', 'address.street_address', 'address.street_number', 'address.street_number_suffix', 'address.street_name', 'address.street_type', 'address.country_id:label', 'is_online_registration', ...$extra_fields) - ->addJoin('LocBlock AS loc_block', 'LEFT', ['loc_block_id', '=', 'loc_block_id.id']) - ->addJoin('Address AS address', 'LEFT', ['loc_block.address_id', '=', 'address.id']) - ->addWhere('event_type_id:label', 'IN', $types) - ->addWhere('start_date', '>=', $start_date) - ->addWhere('is_public', '=', TRUE) - ->addWhere('is_active', '=', TRUE) - ->addOrderBy('start_date', 'ASC') - ->execute(); - - $res['result'] = array(); - - - foreach ($events as $event) { - if ( !empty($redirect_after_login) ) { - // If we have specified a custom redirection after login, apply that - $params = array( - 'id' => $event['id'], - 'reset' => 1 - ); - $url = add_query_arg($params, $redirect_after_login); - } else { - // Otherwise redirect to the standard civicrm event registration page - $url = CRM_Utils_System::url('civicrm/event/register', ['id' => $event['id'], 'reset' => 1]); - } - - if (!is_user_logged_in() and $force_login) { - $url = get_site_url() . '/wp-login.php?redirect_to=' . urlencode($url); - } - - $event_obj = array( - 'id' => $event['id'], - 'title' => $event['title'], - 'start' => date(DATE_ISO8601, strtotime($event['start_date'])), - 'end' => date(DATE_ISO8601, strtotime($event['end_date'])), - 'display' => 'auto', - 'startStr' => $event['start_date'], - 'endStr' => $event['end_date'], - 'url' => $url, - 'extendedProps' => array( - 'html_render' => $this->generate_event_html($event, $upload, $colors, $image_src_field, $url), - 'summary' => $event['summary'], - 'description' => $event['description'], - 'event_type' => $event['event_type_id:label'], - 'street_address' => $event['address.street_address'], - 'street_number' => $event['address.street_number'], - 'street_number_suffix' => $event['address.street_number_suffix'], - 'street_name' => $event['address.street_name'], - 'street_type' => $event['address.street_type'], - 'country' => $event['address.country_id:label'], - 'is_online_registration' => $event['is_online_registration'], - 'extra_fields' => array() - ) - ); - - for ($i = 0; $i < count($extra_fields); $i++) { - $event_obj['extra_fields'][$extra_fields[$i]] = $event[$extra_fields[$i]]; - } - - $event_obj = apply_filters( 'wp_civi_ux_event_inject_content', $event_obj ); - - array_push($res['result'], $event_obj); - } + $eventQuery = Event::get(FALSE) + ->addSelect('id', 'title', 'summary', 'description', 'event_type_id:label', 'start_date', 'end_date', 'address.street_address', 'address.street_number', 'address.street_number_suffix', 'address.street_name', 'address.street_type', 'address.country_id:label', 'is_online_registration', ...$extra_fields) + ->addJoin('LocBlock AS loc_block', 'LEFT', ['loc_block_id', '=', 'loc_block_id.id']) + ->addJoin('Address AS address', 'LEFT', ['loc_block.address_id', '=', 'address.id']) + ->addWhere('start_date', '>=', $start_date) + ->addWhere('is_public', '=', TRUE) + ->addWhere('is_active', '=', TRUE) + ->addOrderBy('start_date', 'ASC'); + + if(!empty($types)) { + $eventQuery->addWhere('event_type_id:name', 'IN', $types); + } + + $image_src_field = Civicrm_Ux_Validators::validateAPIFieldName($_REQUEST['image_src_field']); + + if (!empty($_REQUEST['image_id_field'])) { + $image_id_field = Civicrm_Ux_Validators::validateAPIFieldName($_REQUEST['image_id_field']); + + $eventQuery + ->addJoin('File AS file', 'LEFT', ['file.id', '=', $image_id_field]) + ->addSelect('file.id', $image_src_field); + } + + $events = $eventQuery->execute(); + + $res['result'] = array(); + + $tz = wp_timezone(); + + foreach ($events as $event) { + if ( !empty($redirect_after_login) ) { + // If we have specified a custom redirection after login, apply that + $params = [ 'id' => $event['id'], 'reset' => 1, ]; + + $url = add_query_arg($params, $redirect_after_login); + + } else { + // Otherwise redirect to the standard civicrm event registration page + $url = CRM_Utils_System::url('civicrm/event/register', ['id' => $event['id'], 'reset' => 1]); + } + + if (!is_user_logged_in() and $force_login) { + $url = get_site_url() . '/wp-login.php?redirect_to=' . urlencode($url); + } + + $event += [ + 'url' => $url, + 'start' => new DateTimeImmutable($event['start_date'], $tz), + 'end' => new DateTimeImmutable($event['end_date'], $tz), + ]; + + $event_obj = array( + 'id' => $event['id'], + 'title' => $event['title'], + 'start' => $event['start']->format( DateTime::ATOM ), + 'end' => $event[ 'end' ]->format( DateTime::ATOM ), + + 'display' => 'auto', + 'startStr' => $event['start_date'], + 'endStr' => $event['end_date'], + 'url' => $url, + 'extendedProps' => array( + 'html_render' => $this->generate_event_html($event, $upload, $colors, $image_src_field, $url), + 'html_entry' => $this->get_output_template(['event' => $event]), + 'timeZone' => $tz->getName(), + 'summary' => $event['summary'], + 'description' => $event['description'], + 'event_type' => $event['event_type_id:label'], + 'street_address' => $event['address.street_address'], + 'street_number' => $event['address.street_number'], + 'street_number_suffix' => $event['address.street_number_suffix'], + 'street_name' => $event['address.street_name'], + 'street_type' => $event['address.street_type'], + 'country' => $event['address.country_id:label'], + 'is_online_registration' => $event['is_online_registration'], + 'extra_fields' => [] + ) + ); + + if(!empty($image_src_field) && !empty($event['file.id'])) { + if (str_ends_with($upload, '/civicrm/custom')) { + $fileHash = CRM_Core_BAO_File::generateFileHash($event['id'], $event['file.id']); + $image_url = CRM_Utils_System::url('civicrm/file/imagefile', "reset=1&id={$event['file.id']}&eid={$event['id']}&fcs={$fileHash}"); + } else { + $image_url = $upload . '/' . $event[$image_src_field]; + } + + $event_obj['extendedProps']['file.uri'] = $event[$image_src_field]; + $event_obj['extendedProps']['image_url'] = $image_url; + } + + foreach ($extra_fields as $field) { + $event_obj['extra_fields'][$field] = $event[$field]; + } + + $event_obj = apply_filters( 'wp_civi_ux_event_inject_content', $event_obj ); + + $res['result'][] = $event_obj; } - } catch (CiviCRM_API4_Exception $e) { - $res['err'] = $e; + } catch (CRM_Core_Exception $e) { + http_response_code(500); + $res['err'] = $e->getMessage(); } echo json_encode($res); } - /** - * TODO: WPCIVIUX-149 Convert to a template file - */ protected function generate_event_html($event, $upload, $colors, $image_src_field, $url) { $date_start = date_create($event['start_date']); $date_end = date_create($event['end_date']); @@ -249,40 +200,27 @@ protected function generate_event_html($event, $upload, $colors, $image_src_fiel $event_location = $event['address.street_address'] ? $event['address.street_address'] . ', ' : ''; $event_location .= $event['address.country_id:label'] ? $event['address.country_id:label'] : ''; - if (str_ends_with($upload, '/civicrm/custom') && $event['file.id']) { + if (str_ends_with($upload, '/civicrm/custom') && !empty($event['file.id'])) { $fileHash = CRM_Core_BAO_File::generateFileHash($event['id'], $event['file.id']); $image_url = CRM_Utils_System::url('civicrm/file/imagefile',"reset=1&id={$event['file.id']}&eid={$event['id']}&fcs={$fileHash}", true, "", false, true, false); + } elseif ( ! empty($event[$image_src_field]) ) { + $image_url = $upload . '/' . $event[$image_src_field]; } else { - $image_url = $upload . '/' . $event[$image_src_field]; - } - - - $template = '
'; - $template .= $event[$image_src_field] ? '
' : ''; - - $template .= '
'; - $template .= '
'; - $template .= '
' . $event['event_type_id:label'] . '
-
' . $event['title'] . '
-
' . $event_time . '
'; - - if ( !empty($event_location) ) { - $template .= '
' . $event_location . '
'; - } - $template .= '
'; - - $template .= '
'; - $template .= $event['is_online_registration'] ? '' : ''; - $template .= '
'; - $template .= '
'; - - $template .= '
'; - $template .= $event['description'] ? $event['description'] . '
' : 'No event description provided
-
- '; - - return $template; - } + $image_url = ''; + } + + $template_args = compact( + 'event', + 'image_src_field', + 'image_url', + 'colors', + 'event_time', + 'event_location', + 'url' + ); + + return civicrm_ux_get_template_part('event-fullcalendar', 'event', $template_args); + } protected static function formatDay( $date ) { return $date->format( 'l' ) . ', ' . $date->format( 'j' ) . ' ' . $date->format( 'F' ) . ' ' . $date->format( 'Y' ); @@ -296,4 +234,7 @@ protected static function sameDay( $d1, $d2 ) { return ( $d1->format( 'j' ) == $d2->format( 'j' ) ) && ( $d1->format( 'f' ) == $d2->format( 'f' ) ) && ( $d1->format( 'Y' ) == $d2->format( 'Y' ) ); } + protected function get_output_template($args) { + return civicrm_ux_get_template_part('event-fullcalendar', 'event-entry', $args); + } } diff --git a/shortcodes/event/event-fullcalendar.php b/shortcodes/event/event-fullcalendar.php index 4847f13..c3b8a48 100644 --- a/shortcodes/event/event-fullcalendar.php +++ b/shortcodes/event/event-fullcalendar.php @@ -62,26 +62,15 @@ public function shortcode_callback( $atts = [], $content = null, $tag = '' ) { } - $types = array(); - $types_ = Event::getFields(FALSE) - ->setLoadOptions([ - 'name', - 'label' - ]) - ->addWhere('name', '=', 'event_type_id') - ->addSelect('options') - ->execute()[0]['options']; - foreach ($types_ as $type) { - array_push($types, $type['name']); - } - // Shortcode parameters defaults $wporg_atts = shortcode_atts( array( - 'types' => join(',', $types), + 'types' => NULL, + 'colors' => NULL, 'upload' => wp_upload_dir()['baseurl'] . '/civicrm/custom', - 'force_login' => true, + 'force_login' => FALSE, 'start' => date('Y-m-d', strtotime('-1 year')), + 'image_id_field' => NULL, 'image_src_field' => 'file.uri', 'extra_fields' => join(",", $extra_fields_arr) ), $atts, $tag @@ -89,51 +78,67 @@ public function shortcode_callback( $atts = [], $content = null, $tag = '' ) { $redirect_after_login = isset($atts['redirect_after_login']) ? $atts['redirect_after_login'] : ''; - $colors = array(); - - if (isset($atts['colors'])) { - $types_arr = explode(',', $wporg_atts['types']); - for ($i = 0; $i < count($colors_arr); $i++) { - if ($i >= count($types_arr)) { - break; - } - $colors[$types_arr[$i]] = $colors_arr[$i]; - } - } else { - $types_arr = explode(',', $wporg_atts['types']); - for ($i = 0; $i < count($types_arr); $i++) { - $colors[$types_arr[$i]] = '333333'; - } - } + $colors = [ 'default' => static::getDefaultColor() ]; + if(!empty($wporg_atts['types']) && !empty($wporg_atts['colors'])) { + $types_arr = explode(',', $wporg_atts['types']); + $limit = min(count($types_arr), count($colors_arr)); + for ($i = 0; $i < $limit; $i++) { + $colors[$types_arr[$i]] = Civicrm_Ux_Validators::validateCssColor($colors_arr[$i]); + } + } + wp_enqueue_style( 'font-awesome', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css', [] ); + wp_enqueue_style( 'fullcalendar-styles', 'https://cdn.jsdelivr.net/npm/fullcalendar@5.11.5/main.min.css', [] ); wp_enqueue_style( 'ux-fullcalendar-styles', WP_CIVICRM_UX_PLUGIN_URL . WP_CIVICRM_UX_PLUGIN_NAME . '/public/css/event-fullcalendar.css', [] ); - wp_enqueue_style( 'fullcalendar-styles', 'https://cdn.jsdelivr.net/npm/fullcalendar@5.9.0/main.min.css', [] ); - wp_enqueue_style( 'font-awesome', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css', [] ); - wp_enqueue_script( 'fullcalendar-base', 'https://cdn.jsdelivr.net/combine/npm/fullcalendar@5.9.0/main.js', [] ); + wp_enqueue_script( 'fullcalendar-base', 'https://cdn.jsdelivr.net/combine/npm/fullcalendar@5.11.5/main.js', [] ); wp_enqueue_script( 'popper', 'https://unpkg.com/@popperjs/core@2/dist/umd/popper.min.js', [] ); - wp_enqueue_script( 'tippy', 'https://unpkg.com/tippy.js@6/dist/tippy-bundle.umd.js', [] ); - wp_enqueue_script( 'ux-fullcalendar', WP_CIVICRM_UX_PLUGIN_URL . WP_CIVICRM_UX_PLUGIN_NAME . '/public/js/event-fullcalendar.js', [] ); - - - wp_localize_script( 'ux-fullcalendar', 'wp_site_obj', - array( 'ajax_url' => get_rest_url(), - 'upload' => $wporg_atts['upload'], - 'types' => $wporg_atts['types'], - 'colors' => $colors, - 'start' => $wporg_atts['start'], - 'image_id_field' => $atts['image_id_field'], - 'image_src_field' => $wporg_atts['image_src_field'], - 'force_login' => $wporg_atts['force_login'], - 'redirect_after_login' => $redirect_after_login, - 'extra_fields' => $wporg_atts['extra_fields'])); + wp_enqueue_script( 'tippy', 'https://unpkg.com/tippy.js@6/dist/tippy-bundle.umd.js', [ 'popper' ] ); + wp_enqueue_script( 'ux-fullcalendar', WP_CIVICRM_UX_PLUGIN_URL . WP_CIVICRM_UX_PLUGIN_NAME . '/public/js/event-fullcalendar.js', [ 'fullcalendar-base', 'popper', 'tippy' ] ); + + $ux_fullcalendar = [ + 'ajax_url' => get_rest_url(), + 'redirect_after_login' => $redirect_after_login, + ]; + + if(!empty($colors)) { + $ux_fullcalendar['colors'] = $colors; + } + + $ux_fullcalendar = $ux_fullcalendar + array_filter($wporg_atts); + + if(!empty($ux_fullcalendar['types'])) { + $ux_fullcalendar['filterTypes'] = explode(',', $ux_fullcalendar['types']); + } else { + $options = Event::getFields(FALSE) + ->setLoadOptions([ 'name', 'label' ]) + ->addWhere('name', '=', 'event_type_id') + ->addSelect('options') + ->execute() + ->first()['options']; + + $ux_fullcalendar['filterTypes'] = array_map( + fn($_type) => $_type['label'], + $options ?: [] + ); + } + + wp_localize_script( 'ux-fullcalendar', 'uxFullcalendar', $ux_fullcalendar ); return '
- +
'; } + + public static function getDefaultColor() { + return apply_filters( 'ux_event_fullcalendar/default_color', 'transparent' ); + } + + public static function getDefaultForceLogin() { + apply_filters( 'ux_event_fullcalendar/force_login', false ); + } } diff --git a/templates/event-fullcalendar/event-fullcalendar-event-entry.php b/templates/event-fullcalendar/event-fullcalendar-event-entry.php new file mode 100644 index 0000000..0d769a8 --- /dev/null +++ b/templates/event-fullcalendar/event-fullcalendar-event-entry.php @@ -0,0 +1,15 @@ + + +
+
+ +
+
\ No newline at end of file diff --git a/templates/event-fullcalendar/event-fullcalendar-event.php b/templates/event-fullcalendar/event-fullcalendar-event.php new file mode 100644 index 0000000..e16bc36 --- /dev/null +++ b/templates/event-fullcalendar/event-fullcalendar-event.php @@ -0,0 +1,53 @@ + +
+ +
Event Image
+ + +
+
+
+ +
+
+
+ + +
+ + +
+ + +
+ +
+ +
+ + + +
+
+ +
+ +
+
+