$attributes */
public function render(array $attributes = []): string
{
wp_enqueue_style('pcwp-calendar-blocks');
$attributes = $this->normalize($attributes);
$view = $attributes['view'];
[$rangeStart, $rangeEnd] = $this->range($view, $attributes['start']);
$query = array_merge($attributes, [
'start' => $rangeStart->format(DATE_ATOM),
'end' => $rangeEnd->format(DATE_ATOM),
]);
$paged = in_array($view, ['list', 'photo', 'map'], true);
if ($paged) {
$query['limit'] = $attributes['limit'] + 1;
$query['offset'] = ($attributes['page'] - 1) * $attributes['limit'];
}
if (in_array($view, ['month', 'day', 'week'], true)) {
$query['schedule_local_start'] = $rangeStart->format('Y-m-d H:i:s');
$query['schedule_local_end'] = $rangeEnd->format('Y-m-d H:i:s');
}
$occurrences = $this->events->occurrences($query);
$hasMore = $paged && count($occurrences) > $attributes['limit'];
if ($paged) {
$occurrences = array_slice($occurrences, 0, $attributes['limit']);
}
if (in_array($view, ['month', 'day', 'week'], true)) {
$occurrences = \ProCalendarWP\Events\ScheduleQuery::calendarIntervals($occurrences, $query);
}
$context = [
'attributes' => $attributes,
'events' => $this->events,
'occurrences' => apply_filters('pcwp_view_occurrences', $occurrences, $attributes),
'rangeStart' => $rangeStart,
'rangeEnd' => $rangeEnd,
'previousUrl' => $this->navigationUrl($view, $rangeStart, -1, $attributes['instance']),
'nextUrl' => $this->navigationUrl($view, $rangeStart, 1, $attributes['instance']),
];
do_action('pcwp_before_view', $view, $context);
$html = TemplateLoader::render('calendar-' . $view, $context);
do_action('pcwp_after_view', $view, $context);
$pagination = '';
if ($paged && ($attributes['page'] > 1 || $hasMore)) {
$pagination = '';
}
return '
' . $this->filterForm($attributes) . $html . $pagination . '
';
}
/**
* @param array $input Input attributes.
* @return array
*/
public function normalize(array $input): array
{
$view = sanitize_key((string) ($input['view'] ?? get_option('pcwp_default_view', 'list')));
$allowedViews = apply_filters('pcwp_allowed_calendar_views', ['list', 'photo', 'month', 'day', 'week', 'map']);
if (! is_array($allowedViews) || ! in_array($view, $allowedViews, true)) {
$view = 'list';
}
$start = sanitize_text_field((string) ($input['start'] ?? ''));
try {
$start = ($start !== '' ? new DateTimeImmutable($start, wp_timezone()) : new DateTimeImmutable('now', wp_timezone()))->format('Y-m-d');
} catch (\Exception) {
$start = current_time('Y-m-d');
}
$spec = EventQuerySpec::from($input, true);
if (($instance = QueryIntegration::sanitizeInstance((string) ($input['instance'] ?? ''))) !== 'events' || isset($input['instance'])) {
$spec = $spec->withOverrides(QueryIntegration::requestOverrides($instance));
}
$spec = $spec->toRepositoryQuery();
return array_merge($spec, [
'view' => $view,
'start' => $spec['start'] !== '' ? substr((string) $spec['start'], 0, 10) : $start,
'filter_bar' => (bool) filter_var($input['filter_bar'] ?? true, FILTER_VALIDATE_BOOLEAN),
'instance' => QueryIntegration::sanitizeInstance((string) ($input['instance'] ?? 'events')),
'page' => max(1, min(100000, absint($_GET[QueryIntegration::parameterName($instance, 'page')] ?? $input['page'] ?? 1))),
'limit' => min(499, $spec['limit']),
'show_featured_image' => (bool) filter_var($input['show_featured_image'] ?? true, FILTER_VALIDATE_BOOLEAN),
'wrap_titles' => (bool) filter_var($input['wrap_titles'] ?? true, FILTER_VALIDATE_BOOLEAN),
'max_per_day' => min(20, max(1, absint($input['max_per_day'] ?? 4))),
]);
}
/** @return array{DateTimeImmutable,DateTimeImmutable} */
private function range(string $view, string $start): array
{
$date = new DateTimeImmutable($start . ' 00:00:00', wp_timezone());
if ($view === 'month') {
return [$date->modify('first day of this month'), $date->modify('last day of this month 23:59:59')];
}
if ($view === 'day') {
return [$date, $date->setTime(23, 59, 59)];
}
if ($view === 'week') {
$first = $date->modify('monday this week');
return [$first, $first->add(new DateInterval('P6D'))->setTime(23, 59, 59)];
}
return [$date, $date->add(new DateInterval('P1Y'))->setTime(23, 59, 59)];
}
private function navigationUrl(string $view, DateTimeImmutable $date, int $direction, string $instance): string
{
$unit = $view === 'month' ? 'month' : ($view === 'day' ? 'day' : ($view === 'week' ? 'week' : 'year'));
$target = $date->modify(($direction > 0 ? '+' : '-') . '1 ' . $unit);
$params = [QueryIntegration::parameterName($instance, 'start') => $target->format('Y-m-d'), QueryIntegration::parameterName($instance, 'page') => false];
// Keep the original query arguments for legacy shortcode/widget navigation.
if ($instance === 'events') {
$params['pcwp_view'] = $view;
$params['pcwp_start'] = $target->format('Y-m-d');
}
return strtok((string) add_query_arg($params), '#') . '#pcwp-calendar-' . $instance;
}
/** @param array $attributes */
private function filterForm(array $attributes): string
{
if (! $attributes['filter_bar']) {
return '';
}
$parameter = static fn (string $field): string => QueryIntegration::parameterName($attributes['instance'], $field);
ob_start();
?>