PluginProbe
Booking Calendar / 11.7
Booking Calendar v11.7
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
booking / includes / booking-resource-selector / catalog / class-wpbc-booking-resource-catalog-presenter.php

class-wpbc-booking-resource-catalog-presenter.php in Booking Calendar 11.7, at includes/booking-resource-selector/catalog/class-wpbc-booking-resource-catalog-presenter.php

248 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Dedicated frontend Booking Resource catalog presenter.
4 *
5 * @package Booking Calendar
6 * @since 11.6.0
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Render public Resource cards without depending on admin listing markup.
15 */
16 final class WPBC_Booking_Resource_Catalog_Presenter {
17
18 /** Horizontal gap mirrored by the frontend catalog stylesheet. */
19 private const CATALOG_COLUMN_GAP_PX = 10;
20
21 /**
22 * Render filters and a selectable public Resource collection.
23 *
24 * @param array<int,array<string,mixed>> $resources Public Resource DTO arrays.
25 * @param array<string,mixed> $args Presentation arguments.
26 *
27 * @return string Escaped public catalog markup.
28 */
29 public function render_selectable_catalog( $resources, $args = array() ) {
30 $args = wp_parse_args(
31 is_array( $args ) ? $args : array(),
32 array(
33 'layout' => 'grid',
34 'show_filters' => false,
35 'show_image' => true,
36 'show_title' => true,
37 'show_description' => true,
38 'item_width' => '',
39 'item_max_width' => 0,
40 'grid_items_per_row' => 0,
41 'list_items_per_row' => 0,
42 'show_hierarchy' => true,
43 'show_availability' => true,
44 'show_price' => true,
45 'selected_resource_id' => 0,
46 'input_name' => 'wpbc_resource_selector_resource',
47 'input_id_prefix' => 'wpbc_resource_selector',
48 'catalog_label' => __( 'Booking Resources', 'booking' ),
49 )
50 );
51 $layout = 'list' === $args['layout'] ? 'list' : 'grid';
52 $item_width = wpbc_booking_resource_selector_normalize_css_width( $args['item_width'] );
53 $item_max_width = absint( $args['item_max_width'] );
54 $requested_column_count = 'list' === $layout ? absint( $args['list_items_per_row'] ) : absint( $args['grid_items_per_row'] );
55 $column_count = min( 12, $requested_column_count );
56 $catalog_class = 'wpbc_booking_resource_catalog wpbc_booking_resource_catalog--' . $layout;
57 $catalog_styles = array();
58 if ( $item_max_width > 0 ) {
59 $catalog_class .= ' wpbc_booking_resource_catalog--bounded';
60 $catalog_styles[] = '--wpbc-resource-catalog-item-max-width:' . min( 1200, max( 280, $item_max_width ) ) . 'px';
61 }
62 if ( empty( $args['show_image'] ) ) {
63 $catalog_class .= ' wpbc_booking_resource_catalog--without-images';
64 }
65 if ( $column_count > 0 ) {
66 $catalog_class .= ' wpbc_booking_resource_catalog--explicit-columns wpbc_booking_resource_catalog--columns-' . $column_count;
67 $catalog_styles[] = '--wpbc-resource-catalog-items-per-row:' . $column_count;
68 } elseif ( '' !== $item_width ) {
69 $catalog_class .= ' wpbc_booking_resource_catalog--explicit-item-width';
70 $catalog_styles[] = '--wpbc-resource-catalog-item-width:' . $item_width;
71 $catalog_styles[] = '--wpbc-resource-catalog-item-track-width:' . $this->get_item_track_width( $item_width );
72 }
73 $catalog_style = empty( $catalog_styles ) ? '' : ' style="' . esc_attr( implode( ';', $catalog_styles ) . ';' ) . '"';
74 $html = '<div class="' . esc_attr( $catalog_class ) . '" data-wpbc-resource-catalog="" data-layout="' . esc_attr( $layout ) . '"' . $catalog_style . '>';
75 if ( ! empty( $args['show_filters'] ) && count( $resources ) > 1 ) {
76 $html .= $this->render_search_field();
77 }
78 $html .= '<div class="wpbc_booking_resource_catalog__items wpbc_booking_resource_selector__choices" role="radiogroup" aria-label="' . esc_attr( $args['catalog_label'] ) . '" data-wpbc-resource-catalog-items="">';
79 foreach ( $resources as $resource ) {
80 $html .= $this->render_selectable_card( $resource, $args );
81 }
82 $html .= '</div>';
83 $html .= '<p class="wpbc_booking_resource_catalog__empty" data-wpbc-resource-catalog-empty="" role="status" hidden>' . esc_html__( 'No Booking Resources match your search.', 'booking' ) . '</p>';
84 $html .= '<p class="screen-reader-text" data-wpbc-resource-catalog-status="" aria-live="polite"></p>';
85 $html .= '</div>';
86
87 return $html;
88 }
89
90 /**
91 * Return a grid track width that preserves percentage widths beside gaps.
92 *
93 * Percentage tracks subtract their proportional share of the fixed catalog
94 * gap so familiar values such as 50% and 25% produce two and four columns
95 * without horizontal overflow. Other safe units are returned unchanged.
96 *
97 * @param string $item_width Normalized item width.
98 *
99 * @return string Safe CSS grid track width.
100 */
101 private function get_item_track_width( $item_width ) {
102 if ( ! preg_match( '/^(\d+(?:\.\d+)?)%$/', $item_width, $matches ) ) {
103 return $item_width;
104 }
105
106 $percentage_width = (float) $matches[1];
107 $estimated_columns = max( 1, (int) floor( ( 100 + 0.0001 ) / $percentage_width ) );
108 if ( $estimated_columns <= 1 ) {
109 return $item_width;
110 }
111
112 $gap_adjustment = self::CATALOG_COLUMN_GAP_PX * ( $estimated_columns - 1 ) / $estimated_columns;
113 $gap_adjustment = rtrim( rtrim( number_format( $gap_adjustment, 4, '.', '' ), '0' ), '.' );
114
115 return 'calc(' . $item_width . ' - ' . $gap_adjustment . 'px)';
116 }
117
118 /**
119 * Render the public Resource search field.
120 *
121 * @return string Escaped filter markup.
122 */
123 private function render_search_field() {
124 $filter_id = wp_unique_id( 'wpbc_resource_catalog_search_' );
125 $html = '<div class="wpbc_booking_resource_catalog__filters" role="search" aria-label="' . esc_attr__( 'Filter Booking Resources', 'booking' ) . '">';
126 $html .= '<label class="wpbc_booking_resource_catalog__search" for="' . esc_attr( $filter_id ) . '"><span class="screen-reader-text">' . esc_html__( 'Search Booking Resources', 'booking' ) . '</span><input id="' . esc_attr( $filter_id ) . '" type="search" placeholder="' . esc_attr__( 'Search Booking Resources', 'booking' ) . '" data-wpbc-resource-catalog-search="" autocomplete="off"></label>';
127 $html .= '</div>';
128
129 return $html;
130 }
131
132 /**
133 * Render one accessible selectable Resource card.
134 *
135 * @param array<string,mixed> $resource Public Resource DTO array.
136 * @param array<string,mixed> $args Presentation arguments.
137 *
138 * @return string Escaped card markup.
139 */
140 private function render_selectable_card( $resource, $args ) {
141 $resource_id = absint( $resource['resource_id'] );
142 $resource_type = ! empty( $args['show_hierarchy'] ) && ! empty( $resource['resource_type'] )
143 ? sanitize_key( $resource['resource_type'] )
144 : 'single';
145 $input_id = sanitize_html_class( $args['input_id_prefix'] ) . '_' . $resource_id . '_' . wp_rand( 1000, 9999 );
146 $is_selected = $resource_id === absint( $args['selected_resource_id'] );
147 $search_text = wp_strip_all_tags( (string) $resource['title'] . ' ' . (string) $resource['description'] . ' ' . (string) $resource['parent_title'] );
148 $classes = array( 'wpbc_booking_resource_selector__choice', 'wpbc_booking_resource_catalog__card', 'is-' . $resource_type );
149 if ( $is_selected ) {
150 $classes[] = 'is-selected';
151 }
152
153 $html = '<label class="' . esc_attr( implode( ' ', array_map( 'sanitize_html_class', $classes ) ) ) . '" for="' . esc_attr( $input_id ) . '" data-resource-id="' . $resource_id . '" data-resource-type="' . esc_attr( $resource_type ) . '" data-resource-search="' . esc_attr( $search_text ) . '">';
154 $html .= '<input id="' . esc_attr( $input_id ) . '" type="radio" name="' . esc_attr( $args['input_name'] ) . '" value="' . $resource_id . '"' . checked( $is_selected, true, false ) . ' required>';
155 if ( ! empty( $args['show_image'] ) ) {
156 $html .= '<span class="wpbc_booking_resource_selector__resource_icon wpbc_booking_resource_catalog__image" aria-hidden="true">';
157 if ( ! empty( $resource['image_url'] ) ) {
158 $html .= '<img src="' . esc_url( $resource['image_url'] ) . '" alt="" loading="lazy" decoding="async">';
159 } else {
160 $html .= esc_html( wpbc_booking_resource_selector_get_resource_initials( $resource['title'] ) );
161 }
162 $html .= '</span>';
163 }
164 $html .= '<span class="wpbc_booking_resource_selector__choice_body wpbc_booking_resource_catalog__body"><span class="wpbc_booking_resource_catalog__title_line">';
165 $html .= ! empty( $args['show_title'] )
166 ? '<strong>' . esc_html( $resource['title'] ) . '</strong>'
167 : '<span class="screen-reader-text">' . esc_html( $resource['title'] ) . '</span>';
168 $html .= $this->render_hierarchy_badge( $resource, $args ) . '</span>';
169 if ( ! empty( $args['show_description'] ) && ! empty( $resource['description'] ) ) {
170 $html .= '<span class="wpbc_booking_resource_selector__choice_description wpbc_booking_resource_catalog__description">' . esc_html( $resource['description'] ) . '</span>';
171 }
172 $html .= $this->render_summaries( $resource, $args );
173 $html .= '</span><span class="wpbc_booking_resource_selector__choice_mark" aria-hidden="true"></span></label>';
174
175 return $html;
176 }
177
178 /**
179 * Render a concise hierarchy badge for one Resource.
180 *
181 * @param array<string,mixed> $resource Public Resource DTO array.
182 * @param array<string,mixed> $args Presentation arguments.
183 *
184 * @return string Escaped hierarchy markup or an empty string.
185 */
186 private function render_hierarchy_badge( $resource, $args ) {
187 if ( empty( $args['show_hierarchy'] ) ) {
188 return '';
189 }
190 $resource_type = isset( $resource['resource_type'] ) ? sanitize_key( $resource['resource_type'] ) : 'single';
191 if ( 'parent' === $resource_type ) {
192 $capacity = max( 1, absint( $resource['capacity'] ) );
193 /* translators: %s: Number of bookable units. */
194 $label = sprintf( _n( '%s unit', '%s units', $capacity, 'booking' ), number_format_i18n( $capacity ) );
195 } elseif ( 'child' === $resource_type ) {
196 $label = ! empty( $resource['parent_title'] )
197 ? sprintf( __( 'Unit of %s', 'booking' ), $resource['parent_title'] )
198 : __( 'Booking unit', 'booking' );
199 } else {
200 return '';
201 }
202
203 return '<span class="wpbc_booking_resource_catalog__hierarchy">' . esc_html( $label ) . '</span>';
204 }
205
206 /**
207 * Render availability and price summaries owned by the public DTO.
208 *
209 * @param array<string,mixed> $resource Public Resource DTO array.
210 * @param array<string,mixed> $args Presentation arguments.
211 *
212 * @return string Escaped summary markup.
213 */
214 private function render_summaries( $resource, $args ) {
215 $summary_items = array();
216 if ( ! empty( $args['show_availability'] ) && ! empty( $resource['availability_summary']['label'] ) ) {
217 $availability_description = isset( $resource['availability_summary']['description'] )
218 ? (string) $resource['availability_summary']['description']
219 : '';
220 $summary_items[] = '<span class="wpbc_booking_resource_catalog__summary wpbc_booking_resource_catalog__summary--availability" title="' . esc_attr( $availability_description ) . '">' . esc_html( $resource['availability_summary']['label'] ) . ( '' !== $availability_description ? '<span class="screen-reader-text"> ' . esc_html( $availability_description ) . '</span>' : '' ) . '</span>';
221 }
222 if ( ! empty( $args['show_price'] ) && ! empty( $resource['price_summary']['label'] ) ) {
223 $price_label = (string) $resource['price_summary']['label'];
224 if ( ! empty( $resource['price_summary']['period_label'] ) ) {
225 $price_label .= ' ' . (string) $resource['price_summary']['period_label'];
226 }
227 $summary_items[] = '<span class="wpbc_booking_resource_catalog__summary wpbc_booking_resource_catalog__summary--price">' . esc_html( $price_label ) . '</span>';
228 }
229
230 return empty( $summary_items ) ? '' : '<span class="wpbc_booking_resource_catalog__summaries">' . implode( '', $summary_items ) . '</span>';
231 }
232 }
233
234 /**
235 * Return the shared frontend Resource catalog presenter.
236 *
237 * @return WPBC_Booking_Resource_Catalog_Presenter Presenter singleton.
238 */
239 function wpbc_booking_resource_catalog_presenter() {
240 static $presenter = null;
241
242 if ( null === $presenter ) {
243 $presenter = new WPBC_Booking_Resource_Catalog_Presenter();
244 }
245
246 return $presenter;
247 }
248