Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,41 @@ add_filter('se_event_calendar_link_text', function( string $link_text ) {
}, 10, 1);
```

### Calendar Rendering

#### Filter day events before calendar markup is built
```php
add_filter( 'simple_events_calendar_day_events', function( array $day_events, DateTime $date, int $start_timestamp, int $end_timestamp ) {
$unique_events = array();
$seen_keys = array();

foreach ( $day_events as $event ) {
if ( ! is_object( $event ) || ! isset( $event->ID, $event->event_start_date, $event->event_end_date ) ) {
$unique_events[] = $event;
continue;
}

$key = implode(
'|',
array(
(string) $event->ID,
$event->event_start_date instanceof DateTime ? $event->event_start_date->format( 'U' ) : '',
$event->event_end_date instanceof DateTime ? $event->event_end_date->format( 'U' ) : '',
)
);

if ( isset( $seen_keys[ $key ] ) ) {
continue;
}

$seen_keys[ $key ] = true;
$unique_events[] = $event;
}

return $unique_events;
}, 10, 4 );
```

### Cron Tasks for Event Start Date

> When the cron task runs to update the event start date to a future date if its passed and future dates exist.
Expand Down Expand Up @@ -189,6 +224,12 @@ Copy the `team51-focal-point` folder to your `mu-plugins` directory.

## Changelog

### 2.1.2

- **Calendar loading UX:** added a loading skeleton while calendar month requests are in flight.
- **Calendar mobile interaction:** fixed day selection logic on mobile breakpoints when themes customize hidden/mobile display rules.
- **Calendar navigation:** added a "Show Months in Order" option for sequential month navigation (defaults to off for existing blocks).

### 2.1.1

- **Loop Event Info block:** added a configurable HTML wrapper element (`div`/`p`/`h1`–`h6`), per-block date and time format overrides, and a query offset control.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wpcomsp-simple-events",
"version": "2.1.1",
"version": "2.1.2",
"description": "A simple Gutenberg-first event management plugin that integrates with WooCommerce Box Office.",
"author": {
"name": "WordPress.com Special Projects Team",
Expand Down
6 changes: 3 additions & 3 deletions plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Simple Events Plugin bootstrap file.
*
* @since 1.0.0
* @version 2.1.1
* @version 2.1.2
* @author WordPress.com Special Projects
* @license GPL-3.0-or-later
*
Expand All @@ -14,7 +14,7 @@
* Description: Event management frontend for WooCommerce Box Office.
* Requires at least: 6.5
* Tested up to: 6.9
* Version: 2.1.1
* Version: 2.1.2
* Requires PHP: 8.0
* Author: WordPress.com Special Projects
* Author URI: https://wpspecialprojects.wordpress.com
Expand All @@ -32,7 +32,7 @@
function_exists( 'get_plugin_data' ) || require_once ABSPATH . 'wp-admin/includes/plugin.php';
define( 'SE_METADATA', get_plugin_data( __FILE__, false, false ) );

define( 'SE_VERSION', '2.1.1' );
define( 'SE_VERSION', '2.1.2' );
define( 'SE_BASENAME', plugin_basename( __FILE__ ) );
define( 'SE_PLUGIN_DIR', untrailingslashit( plugin_dir_path( __FILE__ ) ) );
define( 'SE_PLUGIN_URL', untrailingslashit( plugin_dir_url( __FILE__ ) ) );
Expand Down
4 changes: 4 additions & 0 deletions src/blocks/calendar/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
"type": "boolean",
"default": false
},
"sequentialMonths": {
"type": "boolean",
"default": false
},
"hideNeighbourEvents": {
"type": "boolean",
"default": false
Expand Down
93 changes: 81 additions & 12 deletions src/blocks/calendar/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,14 @@ export default class Calendar {
/**
* Check if mobile view
*
* @param calendarItem
* @return {boolean}
*/
isMobile( calendarItem ) {
const mobileElements = calendarItem.querySelectorAll( this.DOM.desktopElements );
if ( mobileElements.length ) {
if ( 'none' === window.getComputedStyle( mobileElements[ 0 ], null ).display ) {
return true;
}
isMobile() {
if ( window.matchMedia ) {
return window.matchMedia( '(max-width: 767px)' ).matches;
}

return false;
return window.innerWidth <= 767;
}

/**
Expand All @@ -70,7 +66,7 @@ export default class Calendar {
if ( calendarDays.length ) {
calendarDays.forEach( ( item ) => {
item.addEventListener( 'click', ( event ) => {
if ( ! this.isMobile( calendarItem ) ) {
if ( ! this.isMobile() ) {
return;
}

Expand Down Expand Up @@ -134,13 +130,80 @@ export default class Calendar {
}
}

/**
* Show loading skeleton and hide current calendar content.
*
* @param {Element} calendarItem Calendar container element.
*/
showLoading( calendarItem ) {
const skeleton = calendarItem.querySelector(
'[data-js="simple-events-calendar-skeleton"]'
);
const content = calendarItem.querySelector(
'[data-js="simple-events-calendar-content"]'
);

if ( skeleton ) {
skeleton.classList.remove(
'simple-events-calendar-skeleton--hidden'
);
}

if ( content ) {
content.classList.add( 'simple-events-calendar-content--hidden' );
}
}

/**
* Update the visible calendar markup after request completes.
*
* @param {Element} calendarItem Calendar container element.
* @param {string} html Updated calendar main HTML.
*/
updateContent( calendarItem, html ) {
const content = calendarItem.querySelector(
'[data-js="simple-events-calendar-content"]'
);

if ( content ) {
content.innerHTML = html;
return;
}

calendarItem.innerHTML = html;
}

/**
* Hide loading skeleton and re-show calendar content.
*
* @param {Element} calendarItem Calendar container element.
*/
hideLoading( calendarItem ) {
const skeleton = calendarItem.querySelector(
'[data-js="simple-events-calendar-skeleton"]'
);
const content = calendarItem.querySelector(
'[data-js="simple-events-calendar-content"]'
);

if ( skeleton ) {
skeleton.classList.add( 'simple-events-calendar-skeleton--hidden' );
}

if ( content ) {
content.classList.remove( 'simple-events-calendar-content--hidden' );
}
}

/**
* Send calendar API request
*
* @param date
* @param calendarItem
*/
sendCalendarRequest( date, calendarItem ) {
this.showLoading( calendarItem );

/**
* Convert GET request to POST
* Implemented to send block attributes in body instead of URL.
Expand All @@ -154,12 +217,18 @@ export default class Calendar {
},
} ).then( ( result ) => {
if ( result.html ) {
calendarItem.innerHTML = result.html;
this.initListeners( calendarItem );
this.updateContent( calendarItem, result.html );
} else {
console.log( result );
}
});
} )
.catch( () => {
// Keep the existing calendar content if request fails.
} )
.finally( () => {
this.hideLoading( calendarItem );
this.initListeners( calendarItem );
Comment on lines +228 to +230
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid rebinding global modal listeners on every request completion.

Line 230 calls initListeners() in finally, which also runs handleModalFunctionality() globally. This can stack duplicate hover handlers after repeated navigation and cause multiple modal toggles per interaction.

💡 Suggested fix
-			.finally( () => {
-				this.hideLoading( calendarItem );
-				this.initListeners( calendarItem );
-			} );
+			.finally( () => {
+				this.hideLoading( calendarItem );
+				// Rebind only listeners tied to replaced calendar markup.
+				this.addNavigationItemsListeners( calendarItem );
+				this.addCalendarDayListeners( calendarItem );
+				// Keep modal binding scoped/idempotent (adjust handleModalFunctionality separately).
+			} );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/blocks/calendar/calendar.js` around lines 228 - 230, The current finally
block calls initListeners(calendarItem) after every request which rebinds global
modal handlers (handleModalFunctionality) and causes duplicate hover/toggle
behavior; update the code so global modal listeners are not re-attached on each
request—either call initListeners only once during component initialization (not
in the per-request finally), or modify initListeners/handleModalFunctionality to
check and early-return if listeners are already bound (e.g., a mounted flag)
before adding handlers; ensure hideLoading(calendarItem) still runs in finally
but move or guard the initListeners(calendarItem) invocation to prevent
duplicate binding.

} );
}

/**
Expand Down
14 changes: 14 additions & 0 deletions src/blocks/calendar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ registerBlockType( 'simple-events/calendar', {
</BlockControls>
<InspectorControls>
<PanelBody title="Event Configuration" initialOpen={ true }>
<ToggleControl
label={ __(
'Show Months in Order',
'simple-events'
) }
help={ __(
'When enabled, months move one at a time and the calendar opens on the first month that has events (from today if the current month is empty). When disabled, navigation jumps only between months that have events.',
'simple-events'
) }
checked={ attributes?.sequentialMonths === true }
onChange={ ( value ) =>
setAttributes( { sequentialMonths: value } )
}
/>
<ToggleControl
label={ __(
'Hide Events on Neighbouring Months',
Expand Down
88 changes: 87 additions & 1 deletion src/blocks/calendar/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,92 @@
*/

@import '../common.scss';

.simple-events-calendar-skeleton {
&--hidden {
display: none;
}

&:not(.simple-events-calendar-skeleton--hidden) {
.simple-events-calendar-skeleton__top-bar > span,
.simple-events-calendar-skeleton__header > span,
.simple-events-calendar-skeleton__day {
background: linear-gradient(90deg, #454545 25%, #515151 50%, #454545 75%);
background-size: 200% 100%;
animation: simple-events-skeleton-shimmer 2.5s ease-in-out infinite;
border-radius: 4px;
}
}

.simple-events-calendar-skeleton__top-bar {
display: flex;
flex-direction: row;
gap: 20px;
align-items: center;
margin-bottom: 20px;
justify-content: space-between;

> span {
&:first-child {
width: 50px;
height: 28px;
}
&:nth-child(2) {
flex: 1;
max-width: 200px;
height: 28px;
}
&:nth-child(3) {
width: 80px;
height: 28px;
}
}
}

.simple-events-calendar-skeleton__header {
display: flex;
gap: 4px;
margin-bottom: 12px;

> span {
flex: 1;
height: 20px;
}
}

.simple-events-calendar-skeleton__body {
min-height: 500px;
}

.simple-events-calendar-skeleton__week {
display: flex;
gap: 4px;
margin-bottom: 4px;
}

.simple-events-calendar-skeleton__day {
flex: 1;
min-height: 100px;

@include max-mobile {
min-height: 50px;
}
}
}

@keyframes simple-events-skeleton-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}

.simple-events-calendar-content--hidden {
display: none;
}

.simple-events-calendar {
margin-left: auto;
margin-right: auto;
Expand Down Expand Up @@ -272,4 +358,4 @@
font-size: 14px;
}
}
}
}
Loading
Loading