PluginProbe
Booking Calendar / 11.3
Booking Calendar v11.3
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 10.11 All 204 releases
booking / includes / page-form-builder / field-packs / calendar / calendar.php

calendar.php in Booking Calendar 11.3, at includes/page-form-builder/field-packs/calendar/calendar.php

982 lines 39.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WPBC BFB Pack: Calendar Field (WP-template–driven with Inspector)
4 *
5 * Purpose
6 * - Registers a "calendar" field pack with schema defaults and usage guard (once per form).
7 * - Prints wp.template() blocks for Preview and Inspector.
8 * - Enqueues the companion JS renderer and passes boot-time options via wp_localize_script().
9 *
10 * Contracts Used
11 * - Filter: wpbc_bfb_register_field_packs
12 * - Action: wpbc_enqueue_js_field_pack
13 * - Action: wpbc_bfb_palette_register_items
14 *
15 * Implementation Notes
16 * - Label "for" now always targets the computed textarea ID (accessibility fix).
17 * - Enqueue is limited to the Builder page slug.
18 * - Calendar boot params keep legacy option keys (historic spellings) for compatibility.
19 * - Minimal inline guard normalizes min_width to px/%/rem/em in the preview template.
20 *
21 * @package Booking Calendar
22 * @author wpdevelop, oplugins
23 * @since 11.0.0
24 * @modified 2025-10-10
25 * @version 1.2.3
26 * @file ../includes/page-form-builder/field-packs/calendar/calendar.php
27 */
28
29 if ( ! defined( 'ABSPATH' ) ) {
30 exit;
31 }
32
33 /**
34 * Class-like wrapper with static methods for better modularity/extendability.
35 * All names use snake_case per project convention.
36 */
37 class WPBC_BFB_Field_Calendar_WPTPL_Pack {
38
39 /**
40 * Bootstrap all hooks.
41 *
42 * @return void
43 */
44 public static function init() {
45 add_filter( 'wpbc_bfb_register_field_packs', array( __CLASS__, 'register_field_packs' ) );
46 add_action( 'wpbc_enqueue_js_field_pack', array( __CLASS__, 'enqueue_js' ), 10, 1 );
47 add_action( 'wpbc_bfb_palette_register_items', array( __CLASS__, 'palette_register_item' ), 10, 2 );
48 }
49
50 /**
51 * Register the "calendar" field pack and its schema.
52 *
53 * @param array $packs Accumulated packs from other modules.
54 *
55 * @return array Modified packs with "calendar".
56 */
57 public static function register_field_packs( $packs ) {
58 $packs['calendar'] = array(
59 'kind' => 'field',
60 'type' => 'calendar',
61 'label' => __( 'Calendar', 'booking' ),
62 'icon' => 'wpbc-bi-calendar3',
63 'usage_key' => 'calendar', // once-per-form guard lives on the client via data-usagenumber="1".
64 'schema' => array(
65 'props' => array(
66 'label' => array( 'type' => 'string', 'default' => __( 'Date', 'booking' ) ),
67 'name' => array( 'type' => 'string', 'default' => '' ),
68 'html_id' => array( 'type' => 'string', 'default' => '' ),
69 'cssclass' => array( 'type' => 'string', 'default' => '' ),
70 'help' => array( 'type' => 'string', 'default' => '' ),
71 'resource_id' => array( 'type' => 'number', 'default' => self::get_preview_resource_id(), 'min' => 1 ),
72 'months' => array( 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 12 ),
73 'min_width' => array( 'type' => 'string', 'default' => '250px' ),
74 ),
75 ),
76 'templates_printer' => array( __CLASS__, 'print_templates' ),
77 );
78
79 return $packs;
80 }
81
82 /**
83 * Build a minimal, sanitized resources list for client templates.
84 *
85 * Each item: array( 'booking_type_id' => int, 'title' => string, 'parent' => string '0'|id )
86 * Titles are stripped of HTML. Only essential keys are exported to reduce payload size.
87 *
88 * @return array
89 */
90 protected static function get_booking_resources_boot_data() {
91 $resources_boot = array();
92
93 // Booking plugin must be active. Keep hard guard, same as other toolbars.
94 if ( ! class_exists( 'wpdev_bk_personal' ) ) {
95 return $resources_boot;
96 }
97
98 if ( ! function_exists( 'wpbc_ajx_get_all_booking_resources_arr' ) || ! function_exists( 'wpbc_ajx_get_sorted_booking_resources_arr' ) ) {
99 return $resources_boot;
100 }
101
102 $resources_arr = wpbc_ajx_get_all_booking_resources_arr();
103 $resources_arr_sorted = wpbc_ajx_get_sorted_booking_resources_arr( $resources_arr );
104
105 foreach ( $resources_arr_sorted as $resource_item ) {
106 $id = isset( $resource_item['booking_type_id'] ) ? intval( $resource_item['booking_type_id'] ) : 0;
107 if ( $id <= 0 ) {
108 continue;
109 }
110
111 $title_raw = isset( $resource_item['title'] ) ? $resource_item['title'] : '';
112 $title = wp_strip_all_tags( $title_raw, true );
113
114 $parent = isset( $resource_item['parent'] ) ? (string) intval( $resource_item['parent'] ) : '0';
115
116 $resources_boot[] = array(
117 'booking_type_id' => $id,
118 'title' => $title,
119 'parent' => $parent,
120 );
121 }
122
123 /**
124 * Filter the resources list provided to the Calendar Inspector select.
125 *
126 * @param array $resources_boot List of arrays: booking_type_id, title, parent.
127 */
128 return apply_filters( 'wpbc_bfb_calendar_resources_boot', $resources_boot );
129 }
130
131 /**
132 * Normalize calendar skin option value to the DB-friendly relative path.
133 *
134 * @param string $skin_value Calendar skin path or URL.
135 *
136 * @return string
137 */
138 protected static function normalize_calendar_skin_value( $skin_value ) {
139
140 $skin_value = is_scalar( $skin_value ) ? (string) $skin_value : '';
141
142 $replace = array( WPBC_PLUGIN_DIR, WPBC_PLUGIN_URL );
143
144 $upload_dir = wp_upload_dir();
145 if ( ! empty( $upload_dir['basedir'] ) ) {
146 $replace[] = $upload_dir['basedir'];
147 }
148 if ( ! empty( $upload_dir['baseurl'] ) ) {
149 $replace[] = $upload_dir['baseurl'];
150 }
151
152 return str_replace( $replace, '', $skin_value );
153 }
154
155 /**
156 * Get URL for a relative calendar skin path.
157 *
158 * @param string $relative_skin Relative skin path.
159 *
160 * @return string
161 */
162 protected static function get_calendar_skin_url( $relative_skin ) {
163
164 $relative_skin = self::normalize_calendar_skin_value( $relative_skin );
165
166 $upload_dir = wp_upload_dir();
167 $upload_url = ( ! empty( $upload_dir['baseurl'] ) ) ? $upload_dir['baseurl'] : '';
168
169 if ( 0 === strpos( $relative_skin, '/wpbc_skins/' ) && ! empty( $upload_url ) ) {
170 return $upload_url . $relative_skin;
171 }
172
173 return WPBC_PLUGIN_URL . $relative_skin;
174 }
175
176 /**
177 * Build calendar skin options for the Calendar field Inspector.
178 *
179 * Keeps the same option groups as Setup Wizard, stores relative paths,
180 * and adds the resolved URL as option metadata for live preview.
181 *
182 * @return array
183 */
184 protected static function get_calendar_skin_options_for_select() {
185
186 $options = wpbc_get_calendar_skin_options();
187
188 foreach ( $options as $option_value => $option_label ) {
189
190 if ( is_array( $option_label ) && ! empty( $option_label['optgroup'] ) ) {
191 continue;
192 }
193
194 $relative_skin = self::normalize_calendar_skin_value( $option_value );
195 $skin_url = self::get_calendar_skin_url( $relative_skin );
196
197 if ( is_array( $option_label ) ) {
198 $option_label['attr'] = ( isset( $option_label['attr'] ) && is_array( $option_label['attr'] ) ) ? $option_label['attr'] : array();
199 $option_label['attr']['data-wpbc-calendar-skin-url'] = $skin_url;
200 $options[ $relative_skin ] = $option_label;
201
202 if ( $relative_skin !== $option_value ) {
203 unset( $options[ $option_value ] );
204 }
205 } else {
206 $options[ $relative_skin ] = array(
207 'title' => $option_label,
208 'attr' => array(
209 'data-wpbc-calendar-skin-url' => $skin_url,
210 ),
211 );
212
213 if ( $relative_skin !== $option_value ) {
214 unset( $options[ $option_value ] );
215 }
216 }
217 }
218
219 return $options;
220 }
221
222 /**
223 * Print global calendar skin control in the Calendar field Inspector.
224 *
225 * @return void
226 */
227 protected static function print_calendar_skin_inspector_group() {
228
229 if ( ! self::can_manage_global_calendar_options() ) {
230 return;
231 }
232
233 $booking_action = 'booking_skin';
234 $el_id = 'wpbc_bfb__calendar_field__booking_skin';
235 $current_skin = self::normalize_calendar_skin_value( get_bk_option( 'booking_skin' ) );
236
237 ?>
238 <section class="wpbc_bfb__inspector__group wpbc_ui__collapsible_group is-open wpbc_bfb__inspector_calendar_skin" data-group="calendar-skin">
239 <button type="button" class="group__header">
240 <h3><?php echo esc_html__( 'Calendar Skin', 'booking' ); ?></h3>
241 <i class="wpbc_ui_el__vert_menu_root_section_icon menu_icon icon-1x wpbc-bi-chevron-right"></i>
242 </button>
243 <div class="group__fields">
244 <div class="inspector__row row__bordered" style="align-items:flex-start;justify-content:space-between;">
245 <div class="inspector__control" style="flex:1 1 auto;">
246 <label for="<?php echo esc_attr( $el_id ); ?>" class="inspector__label" style="display:block;margin:0 0 6px;">
247 <strong><?php echo esc_html__( 'Calendar Skin', 'booking' ); ?></strong>
248 </label>
249 <div class="wpbc_ajx_toolbar wpbc_no_borders">
250 <div class="ui_container ui_container_small0">
251 <div class="ui_group">
252 <div class="ui_element ui_nowrap">
253 <?php
254 wpbc_flex_select(
255 array(
256 'id' => $el_id,
257 'name' => $booking_action,
258 'label' => '',
259 'class' => 'js-wpbc-bfb-calendar-skin wpbc_radio__set_days_customize_plugin',
260 'value' => $current_skin,
261 'onchange' => "if ( 'function' === typeof wpbc__calendar__change_skin ) { wpbc__calendar__change_skin( jQuery( this ).find( 'option:selected' ).attr( 'data-wpbc-calendar-skin-url' ) || jQuery( this ).val() ); }",
262 'disabled' => false,
263 'disabled_options' => array(),
264 'options' => self::get_calendar_skin_options_for_select(),
265 )
266 );
267
268 $is_apply_rotating_icon = false;
269 wpbc_smpl_form__ui__selectbox_prior_btn( $el_id, $is_apply_rotating_icon );
270 wpbc_smpl_form__ui__selectbox_next_btn( $el_id, $is_apply_rotating_icon );
271 ?>
272 </div>
273 </div>
274 </div>
275 </div>
276 <p class="wpbc_bfb__help" style="margin-top:6px;">
277 <?php echo esc_html__( 'This is a global calendar appearance option.', 'booking' ); ?>
278 </p>
279 </div>
280 <?php
281 $nonce_action = 'wpbc_nonce_' . $booking_action;
282 ?>
283 <a href="javascript:void(0);"
284 class="button button-primary"
285 onclick="wpbc_save_option_from_element(this);"
286 data-wpbc-u-save-name="<?php echo esc_attr( $booking_action ); ?>"
287 data-wpbc-u-save-nonce="<?php echo esc_attr( wp_create_nonce( $nonce_action ) ); ?>"
288 data-wpbc-u-save-action="<?php echo esc_attr( $nonce_action ); ?>"
289 data-wpbc-u-save-value-from="#<?php echo esc_attr( $el_id ); ?>"
290 data-wpbc-u-autosave-on-form-save="1"
291 data-wpbc-u-busy-text="<?php esc_attr_e( 'Saving', 'booking' ); ?>...">
292 <?php esc_html_e( 'Save Calendar Skin', 'booking' ); ?>
293 </a>
294 </div>
295 </div>
296 </section>
297 <?php
298 }
299
300 /**
301 * Check if current user can manage global calendar options from Builder inspector.
302 *
303 * @return bool
304 */
305 protected static function can_manage_global_calendar_options() {
306 return ( ! function_exists( 'wpbc_is_mu_user_can_be_here' ) || wpbc_is_mu_user_can_be_here( 'only_super_admin' ) );
307 }
308
309 /**
310 * Calendar legend item definitions shared by inspector UI and boot data.
311 *
312 * @return array
313 */
314 protected static function get_calendar_legend_items() {
315 return array(
316 'available' => array(
317 'label' => __( 'Available item', 'booking' ),
318 'placeholder' => __( 'Available', 'booking' ),
319 'show' => 'booking_legend_is_show_item_available',
320 'text' => 'booking_legend_text_for_item_available',
321 ),
322 'pending' => array(
323 'label' => __( 'Pending item', 'booking' ),
324 'placeholder' => __( 'Pending', 'booking' ),
325 'show' => 'booking_legend_is_show_item_pending',
326 'text' => 'booking_legend_text_for_item_pending',
327 ),
328 'approved' => array(
329 'label' => __( 'Approved item', 'booking' ),
330 'placeholder' => __( 'Booked', 'booking' ),
331 'show' => 'booking_legend_is_show_item_approved',
332 'text' => 'booking_legend_text_for_item_approved',
333 ),
334 'partially' => array(
335 'label' => __( 'Partially booked item', 'booking' ),
336 'placeholder' => __( 'Partially booked', 'booking' ),
337 'show' => 'booking_legend_is_show_item_partially',
338 'text' => 'booking_legend_text_for_item_partially',
339 ),
340 'unavailable' => array(
341 'label' => __( 'Unavailable item', 'booking' ),
342 'placeholder' => __( 'Unavailable', 'booking' ),
343 'show' => 'booking_legend_is_show_item_unavailable',
344 'text' => 'booking_legend_text_for_item_unavailable',
345 ),
346 );
347 }
348
349 /**
350 * Get option names saved by the Calendar Legend inspector group.
351 *
352 * @return array
353 */
354 protected static function get_calendar_legend_option_names() {
355 $option_names = array(
356 'booking_is_show_legend',
357 'booking_legend_is_show_numbers',
358 'booking_legend_is_vertical',
359 );
360
361 foreach ( self::get_calendar_legend_items() as $item ) {
362 $option_names[] = $item['show'];
363 $option_names[] = $item['text'];
364 }
365
366 return $option_names;
367 }
368
369 /**
370 * Get normalized On/Off option value.
371 *
372 * @param string $option_name Option name.
373 *
374 * @return string
375 */
376 protected static function get_on_off_option( $option_name ) {
377 return ( 'On' === get_bk_option( $option_name ) ) ? 'On' : 'Off';
378 }
379
380 /**
381 * Render checkbox control for a Calendar Legend option.
382 *
383 * @param string $option_name Option name.
384 * @param string $class Additional classes.
385 *
386 * @return void
387 */
388 protected static function print_calendar_legend_checkbox( $option_name, $class = '' ) {
389 ?>
390 <input type="checkbox"
391 id="<?php echo esc_attr( $option_name ); ?>"
392 name="<?php echo esc_attr( $option_name ); ?>"
393 class="wpbc_bfb__calendar_legend_fields js-wpbc-bfb-calendar-legend-control <?php echo esc_attr( $class ); ?>"
394 value="On"
395 <?php checked( 'On', self::get_on_off_option( $option_name ) ); ?>>
396 <?php
397 }
398
399 /**
400 * Get Calendar Legend data for the Builder canvas live preview.
401 *
402 * @return array
403 */
404 protected static function get_calendar_legend_boot_data() {
405 $items = array();
406 $day_num = gmdate( 'd' );
407 $item_defs = self::get_calendar_legend_items();
408
409 foreach ( $item_defs as $item_key => $item ) {
410 $items[ $item_key ] = array(
411 'show' => self::get_on_off_option( $item['show'] ),
412 'title' => get_bk_option( $item['text'] ),
413 'placeholder' => $item['placeholder'],
414 'template_number' => self::get_calendar_legend_item_template_html( $item_key, $day_num ),
415 'template_blank' => self::get_calendar_legend_item_template_html( $item_key, '&nbsp;' ),
416 );
417 }
418
419 return array(
420 'show_legend' => self::get_on_off_option( 'booking_is_show_legend' ),
421 'show_numbers' => self::get_on_off_option( 'booking_legend_is_show_numbers' ),
422 'is_vertical' => self::get_on_off_option( 'booking_legend_is_vertical' ),
423 'day_number' => $day_num,
424 'items_order' => array_keys( $item_defs ),
425 'items' => $items,
426 );
427 }
428
429 /**
430 * Build one legend item HTML template with a replaceable title marker.
431 *
432 * @param string $item_key Legend item key.
433 * @param string $text_for_day_cell Day-cell text.
434 *
435 * @return string
436 */
437 protected static function get_calendar_legend_item_template_html( $item_key, $text_for_day_cell ) {
438 if ( ! function_exists( 'wpbc_get_calendar_legend__content_html' ) ) {
439 return '';
440 }
441
442 $html = wpbc_get_calendar_legend__content_html(
443 array(
444 'is_vertical' => false,
445 'text_for_day_cell' => $text_for_day_cell,
446 'items' => array( $item_key ),
447 'titles' => array( $item_key => '__WPBC_LEGEND_TITLE__' ),
448 )
449 );
450
451 return $html;
452 }
453
454 /**
455 * Print global calendar legend controls in the Calendar field Inspector.
456 *
457 * @return void
458 */
459 protected static function print_calendar_legend_inspector_group() {
460
461 if ( ! self::can_manage_global_calendar_options() ) {
462 return;
463 }
464
465 $booking_action = 'wpbc_calendar_legend_options';
466 $nonce_action = 'wpbc_nonce_' . $booking_action;
467 $save_fields = array();
468
469 foreach ( self::get_calendar_legend_option_names() as $option_name ) {
470 $save_fields[] = '#' . $option_name;
471 }
472
473 ?>
474 <section class="wpbc_bfb__inspector__group wpbc_ui__collapsible_group is-open wpbc_bfb__inspector_calendar_legend" data-group="calendar-legend">
475 <button type="button" class="group__header">
476 <h3><?php echo esc_html__( 'Calendar Legend', 'booking' ); ?></h3>
477 <i class="wpbc_ui_el__vert_menu_root_section_icon menu_icon icon-1x wpbc-bi-chevron-right"></i>
478 </button>
479 <div class="group__fields">
480 <div class="inspector__row row__bordered">
481 <label class="inspector__label" for="booking_is_show_legend"><?php echo esc_html__( 'Show legend below calendar', 'booking' ); ?></label>
482 <div class="inspector__control">
483 <?php self::print_calendar_legend_checkbox( 'booking_is_show_legend', 'js-wpbc-bfb-calendar-legend-main' ); ?>
484 <p class="wpbc_bfb__help"><?php echo esc_html__( 'This is a global calendar option.', 'booking' ); ?></p>
485 </div>
486 </div>
487
488 <div class="js-wpbc-bfb-calendar-legend-options">
489 <?php foreach ( self::get_calendar_legend_items() as $item_key => $item ) : ?>
490 <div class="inspector__row row__bordered wpbc_bfb__calendar_legend_item" data-legend-item="<?php echo esc_attr( $item_key ); ?>" style="align-items:flex-start;">
491 <label class="inspector__label" for="<?php echo esc_attr( $item['show'] ); ?>"><?php echo esc_html( $item['label'] ); ?></label>
492 <div class="inspector__control">
493 <div style="display:flex;gap:8px;align-items:center;">
494 <?php self::print_calendar_legend_checkbox( $item['show'], 'js-wpbc-bfb-calendar-legend-item-toggle' ); ?>
495 <input type="text"
496 id="<?php echo esc_attr( $item['text'] ); ?>"
497 name="<?php echo esc_attr( $item['text'] ); ?>"
498 class="inspector__input wpbc_bfb__calendar_legend_fields js-wpbc-bfb-calendar-legend-control js-wpbc-bfb-calendar-legend-item-title"
499 value="<?php echo esc_attr( get_bk_option( $item['text'] ) ); ?>"
500 placeholder="<?php echo esc_attr( $item['placeholder'] ); ?>">
501 </div>
502 </div>
503 </div>
504 <?php endforeach; ?>
505
506 <div class="inspector__row row__bordered">
507 <label class="inspector__label" for="booking_legend_is_show_numbers"><?php echo esc_html__( 'Show date number in legend', 'booking' ); ?></label>
508 <div class="inspector__control">
509 <?php self::print_calendar_legend_checkbox( 'booking_legend_is_show_numbers' ); ?>
510 </div>
511 </div>
512
513 <div class="inspector__row row__bordered">
514 <label class="inspector__label" for="booking_legend_is_vertical"><?php echo esc_html__( 'Show legend items in a column', 'booking' ); ?></label>
515 <div class="inspector__control">
516 <?php self::print_calendar_legend_checkbox( 'booking_legend_is_vertical' ); ?>
517 </div>
518 </div>
519 </div>
520
521 <div class="inspector__row" style="justify-content:flex-end;">
522 <a href="javascript:void(0);"
523 class="button button-primary"
524 onclick="wpbc_save_option_from_element(this);"
525 data-wpbc-u-save-name="<?php echo esc_attr( $booking_action ); ?>"
526 data-wpbc-u-save-nonce="<?php echo esc_attr( wp_create_nonce( $nonce_action ) ); ?>"
527 data-wpbc-u-save-action="<?php echo esc_attr( $nonce_action ); ?>"
528 data-wpbc-u-save-mode="split"
529 data-wpbc-u-save-fields="<?php echo esc_attr( implode( ',', $save_fields ) ); ?>"
530 data-wpbc-u-autosave-watch=".wpbc_bfb__calendar_legend_fields"
531 data-wpbc-u-autosave-on-form-save="1"
532 data-wpbc-u-busy-text="<?php esc_attr_e( 'Saving', 'booking' ); ?>...">
533 <?php esc_html_e( 'Save Calendar Legend', 'booking' ); ?>
534 </a>
535 </div>
536 </div>
537 </section>
538 <?php
539 }
540
541 /**
542 * Enqueue the field JS (calendar renderer) and pass calendar boot options.
543 *
544 * @param string $page Current admin page slug used by the Builder.
545 *
546 * @return void
547 */
548 public static function enqueue_js( $page ) {
549
550 // Ensure we load only on the Builder UI.
551 if ( defined( 'WPBC_BFB_BUILDER_PAGE_SLUG' ) && WPBC_BFB_BUILDER_PAGE_SLUG !== $page ) {
552 return;
553 }
554
555 wp_enqueue_script(
556 'wpbc-bfb_field_calendar_wptpl',
557 wpbc_plugin_url( '/includes/page-form-builder/field-packs/calendar/_out/calendar.js' ),
558 array( 'wp-util', 'wpbc-bfb' ),
559 WP_BK_VERSION_NUM,
560 true
561 );
562
563 // Current request URI (used for AJAX routing/diagnostics).
564 $request_uri = '';
565 if ( isset( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
566 $request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
567 }
568
569 // Parallelism for calendar loader (defaults to 1).
570 $balancer_max_threads = intval( get_bk_option( 'booking_load_balancer_max_threads' ) );
571 $balancer_max_threads = ( empty( $balancer_max_threads ) ) ? 1 : $balancer_max_threads;
572
573 // Day-selection defaults (allow override from core helper if present).
574 $days_selection = array(
575 'days_select_mode' => 'multiple',
576 'fixed__days_num' => 0,
577 'fixed__week_days__start' => '-1',
578 'dynamic__days_min' => 0,
579 'dynamic__days_max' => 0,
580 'dynamic__days_specific' => '',
581 'dynamic__week_days__start' => '-1',
582 );
583 if ( function_exists( 'wpbc__calendar__js_params__get_days_selection_arr' ) ) {
584 $days_selection = wpbc__calendar__js_params__get_days_selection_arr();
585 }
586
587 /**
588 * Enterprise/business options — kept for compatibility.
589 * Note: We intentionally keep legacy option names (typos included) because related
590 * client code expects them. Do not rename without updating JS consumer logic.
591 */
592 $is_parent_resource = 0; // Placeholder: 0 means "single resource mode" preview.
593 $booking_capacity_field = '';
594 $booking_is_different_subres_disabled = '';
595 if ( class_exists( 'wpdev_bk_biz_l' ) ) {
596 $booking_capacity_field = function_exists( 'wpbc_get__booking_capacity_field__name' ) ? wpbc_get__booking_capacity_field__name() : '';
597 $booking_is_different_subres_disabled = get_bk_option( 'booking_is_dissbale_booking_for_different_sub_resources' );
598 }
599
600 $booking_recurrent_time = '';
601 if ( class_exists( 'wpdev_bk_biz_s' ) ) {
602 // No esc_js() — wp_localize_script() JSON-encodes safely.
603 $booking_recurrent_time = (string) get_bk_option( 'booking_recurrent_time' );
604 }
605
606 // Boot payload for real booking resources (used by the Inspector select).
607 $booking_resources = self::get_booking_resources_boot_data();
608 $is_free_version = ( class_exists( 'wpdev_bk_personal' ) ) ? 0 : 1;
609
610 wp_localize_script(
611 'wpbc-bfb_field_calendar_wptpl',
612 'WPBC_BFB_CalendarBoot',
613 array(
614 'nonce' => wp_create_nonce( 'wpbc_calendar_load_ajx' . '_wpbcnonce' ),
615 'user_id' => (int) wpbc_get_current_user_id(),
616 'locale' => (string) get_user_locale(),
617 'request_uri' => (string) $request_uri,
618
619 // Core calendar env.
620 'balancer_max_threads' => (int) $balancer_max_threads,
621 'booking_max_monthes_in_calendar' => (string) get_bk_option( 'booking_max_monthes_in_calendar' ),
622 'booking_start_day_weeek' => (string) get_bk_option( 'booking_start_day_weeek' ),
623 'booking_date_format' => (string) get_bk_option( 'booking_date_format' ),
624 'booking_time_format' => (string) get_bk_option( 'booking_time_format' ),
625
626 'default_preview_resource_id' => (int) self::get_preview_resource_id(),
627
628 // Day-selection policy.
629 'days_selection' => array(
630 'days_select_mode' => (string) $days_selection['days_select_mode'],
631 'fixed__days_num' => (int) $days_selection['fixed__days_num'],
632 'fixed__week_days__start' => (string) $days_selection['fixed__week_days__start'],
633 'dynamic__days_min' => (int) $days_selection['dynamic__days_min'],
634 'dynamic__days_max' => (int) $days_selection['dynamic__days_max'],
635 'dynamic__days_specific' => (string) $days_selection['dynamic__days_specific'],
636 'dynamic__week_days__start' => (string) $days_selection['dynamic__week_days__start'],
637 ),
638
639 // Enterprise/business flags.
640 'is_parent_resource' => (int) $is_parent_resource,
641 'booking_capacity_field' => (string) $booking_capacity_field,
642 'booking_is_dissbale_booking_for_different_sub_resources' => (string) $booking_is_different_subres_disabled,
643 'booking_recurrent_time' => (string) $booking_recurrent_time,
644
645 // Real booking resources for Inspector select.
646 'booking_resources' => $booking_resources,
647 'is_free_version' => (int) $is_free_version,
648 'calendar_legend' => self::get_calendar_legend_boot_data(),
649 )
650 );
651 }
652
653 /**
654 * Print Preview & Inspector templates (Underscore.js) on the Builder page.
655 *
656 * @param string $page Current admin page slug used by the Builder.
657 *
658 * @return void
659 */
660 public static function print_templates( $page ) {
661
662 if ( defined( 'WPBC_BFB_BUILDER_PAGE_SLUG' ) && WPBC_BFB_BUILDER_PAGE_SLUG !== $page ) {
663 return;
664 }
665
666 // == CANVAS PREVIEW ==
667 ?>
668 <script type="text/html" id="tmpl-wpbc-bfb-field-calendar">
669 <#
670 // Normalize booleans and numbers defensively.
671 var __boot_resources = ( window.WPBC_BFB_CalendarBoot && Array.isArray( window.WPBC_BFB_CalendarBoot.booking_resources ) ) ? window.WPBC_BFB_CalendarBoot.booking_resources : [];
672 var __configured_rid = Number( ( window.WPBC_BFB_CalendarBoot && window.WPBC_BFB_CalendarBoot.default_preview_resource_id ) || 1 );
673 var __data_rid = Number( data.resource_id || 1 );
674
675 function __resource_exists( id ) {
676 id = Number( id || 1 );
677 if ( ! isFinite( id ) || id <= 0 ) {
678 return false;
679 }
680 for ( var i = 0; i < __boot_resources.length; i++ ) {
681 if ( Number( __boot_resources[i].booking_type_id ) === id ) {
682 return true;
683 }
684 }
685 return false;
686 }
687 function __first_existing_resource_id() {
688 for ( var i = 0; i < __boot_resources.length; i++ ) {
689 var id = Number( __boot_resources[i].booking_type_id );
690 if ( isFinite( id ) && id > 0 ) {
691 return id;
692 }
693 }
694 return 1;
695 }
696 var rid = __resource_exists( __configured_rid ) ? __configured_rid : ( __resource_exists( __data_rid ) ? __data_rid : __first_existing_resource_id() );
697
698
699 var months = Math.max(1, Math.min(12, Number(data.months || 1)));
700
701 // Compute textarea ID/name: calendar requires 'date_booking{rid}' by convention.
702 var input_id = (data.html_id && String(data.html_id).trim()) ? data.html_id : ('date_booking' + rid);
703 var input_name = (data.name && String(data.name).trim()) ? data.name : ('date_booking' + rid);
704
705 // Guard CSS length: allow px, %, rem, em; fallback to 250px.
706 var minw = (function(v){
707 var s = String(v || '').trim();
708 var m = s.match(/^(-?\d+(?:\.\d+)?)(px|%|rem|em)$/i);
709 return m ? m[0] : '250px';
710 })( data.min_width );
711 #>
712
713 <span class="wpbc_bfb__noaction wpbc_bfb__no-drag-zone {{ data.cssclass || '' }}" inert=""
714 style="display:block; min-width: {{ minw }};">
715 <# if ( data.label && data.label !== '' ) { #>
716 <label class="wpbc_bfb__field-label" for="{{ input_id }}">
717 {{ data.label }}
718 </label>
719 <# } #>
720
721 <div class="wpbc_calendar_wraper wpbc_change_over_triangle">
722 <div class="wpbc_cal_container bk_calendar_frame wpbc_no_custom_width months_num_in_row_ cal_month_num_{{ months }}">
723 <div id="calendar_booking{{ rid }}"><?php esc_html_e( 'Calendar is loading', 'booking' ); ?>...</div>
724 </div>
725 </div>
726
727 <textarea rows="3" cols="50" id="{{ input_id }}" name="{{ input_name }}" autocomplete="off" style="display:none;" tabindex="-1" aria-hidden="true"></textarea>
728 <div class="js-wpbc-bfb-calendar-legend-preview">
729 <?php
730 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
731 echo wpbc_get_calendar_legend();
732 ?>
733 </div>
734 <# if ( data.help ) { #>
735 <div class="wpbc_bfb__help">{{ data.help }}</div>
736 <# } #>
737 </span>
738 </script>
739 <?php
740
741 // == INSPECTOR ==
742 ?>
743 <script type="text/html" id="tmpl-wpbc-bfb-inspector-calendar">
744 <div class="wpbc_bfb__inspector__head">
745 <div class="header_container">
746 <div class="header_title_content">
747 <h3 class="title"><?php echo esc_html__( 'Calendar', 'booking' ); ?></h3>
748 <div class="desc"><?php echo esc_html__( 'Configure the date picker calendar. Some options affect only the preview.', 'booking' ); ?></div>
749 </div>
750 <div class="actions wpbc_ajx_toolbar wpbc_no_borders">
751 <div class="ui_container ui_container_small">
752 <div class="ui_group">
753 <div class="ui_element">
754 <button type="button" class="button button-secondary wpbc_ui_control wpbc_ui_button"
755 data-action="deselect" aria-label="<?php echo esc_attr__( 'Deselect', 'booking' ); ?>">
756 <i class="menu_icon icon-1x wpbc_icn_remove_done"></i>
757 </button>
758 <button type="button" class="button button-secondary wpbc_ui_control wpbc_ui_button"
759 data-action="scrollto" aria-label="<?php echo esc_attr__( 'Scroll to field', 'booking' ); ?>">
760 <i class="menu_icon icon-1x wpbc_icn_ads_click filter_center_focus"></i>
761 </button>
762 </div>
763 <div class="ui_element">
764 <button type="button" class="button button-secondary wpbc_ui_control wpbc_ui_button"
765 data-action="move-up" aria-label="<?php echo esc_attr__( 'Move up', 'booking' ); ?>">
766 <i class="menu_icon icon-1x wpbc_icn_arrow_upward"></i>
767 </button>
768 <button type="button" class="button button-secondary wpbc_ui_control wpbc_ui_button"
769 data-action="move-down" aria-label="<?php echo esc_attr__( 'Move down', 'booking' ); ?>">
770 <i class="menu_icon icon-1x wpbc_icn_arrow_downward"></i>
771 </button>
772 </div>
773 <div class="ui_element">
774 <button data-action="delete" aria-label="<?php echo esc_attr__( 'Delete', 'booking' ); ?>" type="button"
775 class="button button-secondary wpbc_ui_control wpbc_ui_button wpbc_ui_button_danger button-link-delete">
776 <span class="in-button-text"><?php echo esc_html__( 'Delete', 'booking' ); ?></span>&nbsp;&nbsp;<i class="menu_icon icon-1x wpbc_icn_delete_outline"></i>
777 </button>
778 </div>
779 </div>
780 </div>
781 </div><!-- .actions -->
782 </div>
783 </div>
784
785 <div class="wpbc_bfb__inspector__body">
786
787 <section class="wpbc_bfb__inspector__group wpbc_ui__collapsible_group is-open" data-group="basic">
788 <button type="button" class="group__header">
789 <h3><?php echo esc_html__( 'Basic', 'booking' ); ?></h3>
790 <i class="wpbc_ui_el__vert_menu_root_section_icon menu_icon icon-1x wpbc-bi-chevron-right"></i>
791 </button>
792 <div class="group__fields">
793 <div class="inspector__row">
794 <label class="inspector__label"><?php echo esc_html__( 'Label', 'booking' ); ?></label>
795 <div class="inspector__control">
796 <input type="text" class="inspector__input" data-inspector-key="label"
797 value="{{ data.label || '<?php echo esc_js( __( 'Date', 'booking' ) ); ?>' }}">
798 </div>
799 </div>
800
801 <div class="inspector__row">
802 <label class="inspector__label"><?php echo esc_html__( 'Help text', 'booking' ); ?></label>
803 <div class="inspector__control">
804 <textarea class="inspector__textarea" rows="3" data-inspector-key="help">{{ data.help || '' }}</textarea>
805 </div>
806 </div>
807 </div>
808 </section>
809 <?php self::print_calendar_skin_inspector_group(); ?>
810 <?php self::print_calendar_legend_inspector_group(); ?>
811 <?php /* ?>
812 <section class="wpbc_bfb__inspector__group wpbc_ui__collapsible_group is-open" data-group="calendar">
813 <button type="button" class="group__header">
814 <h3><?php echo esc_html__( 'Calendar', 'booking' ); ?></h3>
815 <i class="wpbc_ui_el__vert_menu_root_section_icon menu_icon icon-1x wpbc-bi-chevron-right"></i>
816 </button>
817 <div class="group__fields">
818
819 <!-- Resource selector with Free-version guard -->
820 <div class="inspector__row">
821 <label class="inspector__label"><?php echo esc_html__( 'Booking resource', 'booking' ); ?></label>
822 <div class="inspector__control">
823 <#
824 var __boot = window.WPBC_BFB_CalendarBoot || {};
825 var __free = (__boot.is_free_version==='1');
826 var __res = (__boot.booking_resources) ? __boot.booking_resources : [];
827 var __has_list = (__res && __res.length > 0);
828 var __rid = String( data.resource_id || '1' );
829
830 // If Free -> enforce ID=1 and persist to data.
831 if ( __free ) {
832 __rid = '1';
833 data.resource_id = __rid;
834 }
835 #>
836 <# if ( __free ) { #>
837 <input type="hidden" class="inspector__input" data-inspector-key="resource_id" value="1">
838 <p class="wpbc_bfb__help">
839 <?php
840 echo esc_html__(
841 'The Free version has one default booking resource. To have multiple resources, please upgrade to a premium version.',
842 'booking'
843 );
844 ?>
845 </p>
846 <# } else if ( __has_list ) { #>
847 <select class="inspector__input" data-inspector-key="resource_id">
848 <#
849 // Coerce current value to an existing one if missing.
850 var __exists = _.some( __res, function(r){ return String(r.booking_type_id) === __rid; } );
851 if ( ! __exists ) { __rid = String( __res[0].booking_type_id ); data.resource_id = __rid; }
852
853 _.each( __res, function( r ){
854 var is_parent = ( String(r.parent||'0') === '0' );
855 var selected = ( __rid === String(r.booking_type_id) );
856 #>
857 <option value="{{ r.booking_type_id }}"
858 <# if ( selected ) { #>selected="selected"<# } #>
859 style="<# if ( is_parent ) { #>font-weight:600;<# } else { #>font-size:0.95em;padding-left:20px;<# } #>">
860 <# if ( ! is_parent ) { #>&nbsp;&nbsp;&nbsp;<# } #>{{ r.title }}
861 </option>
862 <# }); #>
863 </select>
864 <p class="wpbc_bfb__help"><?php echo esc_html__( 'Choose booking resource for calendar preview ', 'booking' ); ?></p>
865 <# } else { #>
866 <input type="hidden" class="inspector__input inspector__w_30" data-inspector-key="resource_id" value="{{ data.resource_id || 1 }}">
867 <p class="wpbc_bfb__help"><?php echo esc_html__( 'No resources available. Create booking resources on WP Booking Calendar > Resources > Resources page.', 'booking' ); ?></p>
868 <# } #>
869 </div>
870 </div>
871
872 <div class="inspector__row">
873 <label class="inspector__label"><?php echo esc_html__( 'Months in row', 'booking' ); ?></label>
874 <div class="inspector__control">
875 <div class="wpbc_len_group wpbc_inline_inputs" data-len-group="calendar-months">
876 <input data-len-range type="range" class="inspector__input" min="1" max="12"
877 step="1" data-inspector-key="months" value="{{ data.months || 1 }}">
878 <input data-len-value type="number" class="inspector__input inspector__w_30"
879 data-inspector-key="months" min="1" max="12" value="{{ data.months || 1 }}">
880 </div>
881 <p class="wpbc_bfb__help"><?php echo esc_html__( 'Preview hint: multiple months may require full calendar init in preview.', 'booking' ); ?></p>
882 </div>
883 </div>
884
885 </div>
886 </section>
887
888 <section class="wpbc_bfb__inspector__group wpbc_ui__collapsible_group" data-group="advanced">
889 <button type="button" class="group__header">
890 <h3><?php echo esc_html__( 'Advanced', 'booking' ); ?></h3>
891 <i class="wpbc_ui_el__vert_menu_root_section_icon menu_icon icon-1x wpbc-bi-chevron-right"></i>
892 </button>
893 <div class="group__fields">
894
895 <div class="inspector__row">
896 <label class="inspector__label"><?php echo esc_html__( 'CSS class', 'booking' ); ?></label>
897 <div class="inspector__control">
898 <input type="text" class="inspector__input" data-inspector-key="cssclass"
899 value="{{ data.cssclass || '' }}">
900 </div>
901 </div>
902
903 <?php
904 // HTML ID must be 'date_booking{rid}' (rid = resource id) to bind with the textarea under the calendar.
905 // This field is intentionally non-editable in Inspector to avoid breaking loader contracts.
906 // If you must expose it, ensure builder-side enforcement keeps the 'date_booking{rid}' pattern.
907 ?>
908
909 </div>
910 </section>
911 <?php */ ?>
912 </div>
913 </script>
914 <?php
915 }
916
917 /**
918 * Register the palette item (draggable field entry).
919 *
920 * @param string $group Palette group slug.
921 * @param string $position Position within group.
922 *
923 * @return void
924 */
925 public static function palette_register_item( $group, $position ) {
926 if ( 'essentials' !== $group || 'top' !== $position ) {
927 return;
928 }
929 ?>
930 <li class="wpbc_bfb__field"
931 data-id="calendar"
932 data-type="calendar"
933 data-usage_key="calendar"
934 data-usagenumber="1"
935 data-resource_id="<?php echo intval( self::get_preview_resource_id() ); ?>"
936 data-months="1"
937 data-label="<?php echo esc_attr( __( 'Calendar', 'booking' ) ); ?>"
938 data-min_width="250px">
939 <i class="menu_icon icon-1x wpbc-bi-calendar3"></i>
940 <span class="wpbc_bfb__field-label"><?php echo esc_html( __( 'Calendar', 'booking' ) ); ?></span>
941 <span class="wpbc_bfb__field-type">calendar</span>
942 </li>
943 <?php
944 }
945
946 /**
947 * Get preview resource ID for Builder calendar field.
948 *
949 * Uses the configured default resource if it exists in available resources.
950 * Falls back to the first existing resource ID.
951 *
952 * @return int
953 */
954 protected static function get_preview_resource_id() {
955 $resources_boot = self::get_booking_resources_boot_data();
956 $existing_ids = array();
957
958 foreach ( $resources_boot as $resource_item ) {
959 if ( ! empty( $resource_item['booking_type_id'] ) ) {
960 $existing_ids[] = intval( $resource_item['booking_type_id'] );
961 }
962 }
963
964 $existing_ids = array_values( array_filter( array_unique( $existing_ids ) ) );
965
966 $default_rid = intval( wpbc_get_default_resource() );
967
968 if ( in_array( $default_rid, $existing_ids, true ) ) {
969 return $default_rid;
970 }
971
972 if ( ! empty( $existing_ids ) ) {
973 return intval( $existing_ids[0] );
974 }
975
976 return 1;
977 }
978 }
979
980 // Bootstrap.
981 WPBC_BFB_Field_Calendar_WPTPL_Pack::init();
982