PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
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 / page-form-builder / field-packs / calendar / calendar.php

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

973 lines 38.5 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 <a href="javascript:void(0);"
281 class="button button-primary"
282 onclick="wpbc_save_option_from_element(this);"
283 data-wpbc-u-save-name="<?php echo esc_attr( $booking_action ); ?>"
284 data-wpbc-u-save-value-from="#<?php echo esc_attr( $el_id ); ?>"
285 data-wpbc-u-autosave-on-form-save="1"
286 data-wpbc-u-busy-text="<?php esc_attr_e( 'Saving', 'booking' ); ?>...">
287 <?php esc_html_e( 'Save Calendar Skin', 'booking' ); ?>
288 </a>
289 </div>
290 </div>
291 </section>
292 <?php
293 }
294
295 /**
296 * Check if current user can manage global calendar options from Builder inspector.
297 *
298 * @return bool
299 */
300 protected static function can_manage_global_calendar_options() {
301 return ( ! function_exists( 'wpbc_is_mu_user_can_be_here' ) || wpbc_is_mu_user_can_be_here( 'only_super_admin' ) );
302 }
303
304 /**
305 * Calendar legend item definitions shared by inspector UI and boot data.
306 *
307 * @return array
308 */
309 protected static function get_calendar_legend_items() {
310 return array(
311 'available' => array(
312 'label' => __( 'Available item', 'booking' ),
313 'placeholder' => __( 'Available', 'booking' ),
314 'show' => 'booking_legend_is_show_item_available',
315 'text' => 'booking_legend_text_for_item_available',
316 ),
317 'pending' => array(
318 'label' => __( 'Pending item', 'booking' ),
319 'placeholder' => __( 'Pending', 'booking' ),
320 'show' => 'booking_legend_is_show_item_pending',
321 'text' => 'booking_legend_text_for_item_pending',
322 ),
323 'approved' => array(
324 'label' => __( 'Approved item', 'booking' ),
325 'placeholder' => __( 'Booked', 'booking' ),
326 'show' => 'booking_legend_is_show_item_approved',
327 'text' => 'booking_legend_text_for_item_approved',
328 ),
329 'partially' => array(
330 'label' => __( 'Partially booked item', 'booking' ),
331 'placeholder' => __( 'Partially booked', 'booking' ),
332 'show' => 'booking_legend_is_show_item_partially',
333 'text' => 'booking_legend_text_for_item_partially',
334 ),
335 'unavailable' => array(
336 'label' => __( 'Unavailable item', 'booking' ),
337 'placeholder' => __( 'Unavailable', 'booking' ),
338 'show' => 'booking_legend_is_show_item_unavailable',
339 'text' => 'booking_legend_text_for_item_unavailable',
340 ),
341 );
342 }
343
344 /**
345 * Get option names saved by the Calendar Legend inspector group.
346 *
347 * @return array
348 */
349 protected static function get_calendar_legend_option_names() {
350 $option_names = array(
351 'booking_is_show_legend',
352 'booking_legend_is_show_numbers',
353 'booking_legend_is_vertical',
354 );
355
356 foreach ( self::get_calendar_legend_items() as $item ) {
357 $option_names[] = $item['show'];
358 $option_names[] = $item['text'];
359 }
360
361 return $option_names;
362 }
363
364 /**
365 * Get normalized On/Off option value.
366 *
367 * @param string $option_name Option name.
368 *
369 * @return string
370 */
371 protected static function get_on_off_option( $option_name ) {
372 return ( 'On' === get_bk_option( $option_name ) ) ? 'On' : 'Off';
373 }
374
375 /**
376 * Render checkbox control for a Calendar Legend option.
377 *
378 * @param string $option_name Option name.
379 * @param string $class Additional classes.
380 *
381 * @return void
382 */
383 protected static function print_calendar_legend_checkbox( $option_name, $class = '' ) {
384 ?>
385 <input type="checkbox"
386 id="<?php echo esc_attr( $option_name ); ?>"
387 name="<?php echo esc_attr( $option_name ); ?>"
388 class="wpbc_bfb__calendar_legend_fields js-wpbc-bfb-calendar-legend-control <?php echo esc_attr( $class ); ?>"
389 value="On"
390 <?php checked( 'On', self::get_on_off_option( $option_name ) ); ?>>
391 <?php
392 }
393
394 /**
395 * Get Calendar Legend data for the Builder canvas live preview.
396 *
397 * @return array
398 */
399 protected static function get_calendar_legend_boot_data() {
400 $items = array();
401 $day_num = gmdate( 'd' );
402 $item_defs = self::get_calendar_legend_items();
403
404 foreach ( $item_defs as $item_key => $item ) {
405 $items[ $item_key ] = array(
406 'show' => self::get_on_off_option( $item['show'] ),
407 'title' => get_bk_option( $item['text'] ),
408 'placeholder' => $item['placeholder'],
409 'template_number' => self::get_calendar_legend_item_template_html( $item_key, $day_num ),
410 'template_blank' => self::get_calendar_legend_item_template_html( $item_key, '&nbsp;' ),
411 );
412 }
413
414 return array(
415 'show_legend' => self::get_on_off_option( 'booking_is_show_legend' ),
416 'show_numbers' => self::get_on_off_option( 'booking_legend_is_show_numbers' ),
417 'is_vertical' => self::get_on_off_option( 'booking_legend_is_vertical' ),
418 'day_number' => $day_num,
419 'items_order' => array_keys( $item_defs ),
420 'items' => $items,
421 );
422 }
423
424 /**
425 * Build one legend item HTML template with a replaceable title marker.
426 *
427 * @param string $item_key Legend item key.
428 * @param string $text_for_day_cell Day-cell text.
429 *
430 * @return string
431 */
432 protected static function get_calendar_legend_item_template_html( $item_key, $text_for_day_cell ) {
433 if ( ! function_exists( 'wpbc_get_calendar_legend__content_html' ) ) {
434 return '';
435 }
436
437 $html = wpbc_get_calendar_legend__content_html(
438 array(
439 'is_vertical' => false,
440 'text_for_day_cell' => $text_for_day_cell,
441 'items' => array( $item_key ),
442 'titles' => array( $item_key => '__WPBC_LEGEND_TITLE__' ),
443 )
444 );
445
446 return $html;
447 }
448
449 /**
450 * Print global calendar legend controls in the Calendar field Inspector.
451 *
452 * @return void
453 */
454 protected static function print_calendar_legend_inspector_group() {
455
456 if ( ! self::can_manage_global_calendar_options() ) {
457 return;
458 }
459
460 $booking_action = 'wpbc_calendar_legend_options';
461 $save_fields = array();
462
463 foreach ( self::get_calendar_legend_option_names() as $option_name ) {
464 $save_fields[] = '#' . $option_name;
465 }
466
467 ?>
468 <section class="wpbc_bfb__inspector__group wpbc_ui__collapsible_group is-open wpbc_bfb__inspector_calendar_legend" data-group="calendar-legend">
469 <button type="button" class="group__header">
470 <h3><?php echo esc_html__( 'Calendar Legend', 'booking' ); ?></h3>
471 <i class="wpbc_ui_el__vert_menu_root_section_icon menu_icon icon-1x wpbc-bi-chevron-right"></i>
472 </button>
473 <div class="group__fields">
474 <div class="inspector__row row__bordered">
475 <label class="inspector__label" for="booking_is_show_legend"><?php echo esc_html__( 'Show legend below calendar', 'booking' ); ?></label>
476 <div class="inspector__control">
477 <?php self::print_calendar_legend_checkbox( 'booking_is_show_legend', 'js-wpbc-bfb-calendar-legend-main' ); ?>
478 <p class="wpbc_bfb__help"><?php echo esc_html__( 'This is a global calendar option.', 'booking' ); ?></p>
479 </div>
480 </div>
481
482 <div class="js-wpbc-bfb-calendar-legend-options">
483 <?php foreach ( self::get_calendar_legend_items() as $item_key => $item ) : ?>
484 <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;">
485 <label class="inspector__label" for="<?php echo esc_attr( $item['show'] ); ?>"><?php echo esc_html( $item['label'] ); ?></label>
486 <div class="inspector__control">
487 <div style="display:flex;gap:8px;align-items:center;">
488 <?php self::print_calendar_legend_checkbox( $item['show'], 'js-wpbc-bfb-calendar-legend-item-toggle' ); ?>
489 <input type="text"
490 id="<?php echo esc_attr( $item['text'] ); ?>"
491 name="<?php echo esc_attr( $item['text'] ); ?>"
492 class="inspector__input wpbc_bfb__calendar_legend_fields js-wpbc-bfb-calendar-legend-control js-wpbc-bfb-calendar-legend-item-title"
493 value="<?php echo esc_attr( get_bk_option( $item['text'] ) ); ?>"
494 placeholder="<?php echo esc_attr( $item['placeholder'] ); ?>">
495 </div>
496 </div>
497 </div>
498 <?php endforeach; ?>
499
500 <div class="inspector__row row__bordered">
501 <label class="inspector__label" for="booking_legend_is_show_numbers"><?php echo esc_html__( 'Show date number in legend', 'booking' ); ?></label>
502 <div class="inspector__control">
503 <?php self::print_calendar_legend_checkbox( 'booking_legend_is_show_numbers' ); ?>
504 </div>
505 </div>
506
507 <div class="inspector__row row__bordered">
508 <label class="inspector__label" for="booking_legend_is_vertical"><?php echo esc_html__( 'Show legend items in a column', 'booking' ); ?></label>
509 <div class="inspector__control">
510 <?php self::print_calendar_legend_checkbox( 'booking_legend_is_vertical' ); ?>
511 </div>
512 </div>
513 </div>
514
515 <div class="inspector__row" style="justify-content:flex-end;">
516 <a href="javascript:void(0);"
517 class="button button-primary"
518 onclick="wpbc_save_option_from_element(this);"
519 data-wpbc-u-save-name="<?php echo esc_attr( $booking_action ); ?>"
520 data-wpbc-u-save-fields="<?php echo esc_attr( implode( ',', $save_fields ) ); ?>"
521 data-wpbc-u-autosave-watch=".wpbc_bfb__calendar_legend_fields"
522 data-wpbc-u-autosave-on-form-save="1"
523 data-wpbc-u-busy-text="<?php esc_attr_e( 'Saving', 'booking' ); ?>...">
524 <?php esc_html_e( 'Save Calendar Legend', 'booking' ); ?>
525 </a>
526 </div>
527 </div>
528 </section>
529 <?php
530 }
531
532 /**
533 * Enqueue the field JS (calendar renderer) and pass calendar boot options.
534 *
535 * @param string $page Current admin page slug used by the Builder.
536 *
537 * @return void
538 */
539 public static function enqueue_js( $page ) {
540
541 // Ensure we load only on the Builder UI.
542 if ( defined( 'WPBC_BFB_BUILDER_PAGE_SLUG' ) && WPBC_BFB_BUILDER_PAGE_SLUG !== $page ) {
543 return;
544 }
545
546 wp_enqueue_script(
547 'wpbc-bfb_field_calendar_wptpl',
548 wpbc_plugin_url( '/includes/page-form-builder/field-packs/calendar/_out/calendar.js' ),
549 array( 'wp-util', 'wpbc-bfb' ),
550 WP_BK_VERSION_NUM,
551 true
552 );
553
554 // Current request URI (used for AJAX routing/diagnostics).
555 $request_uri = '';
556 if ( isset( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
557 $request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
558 }
559
560 // Parallelism for calendar loader (defaults to 1).
561 $balancer_max_threads = intval( get_bk_option( 'booking_load_balancer_max_threads' ) );
562 $balancer_max_threads = ( empty( $balancer_max_threads ) ) ? 1 : $balancer_max_threads;
563
564 // Day-selection defaults (allow override from core helper if present).
565 $days_selection = array(
566 'days_select_mode' => 'multiple',
567 'fixed__days_num' => 0,
568 'fixed__week_days__start' => '-1',
569 'dynamic__days_min' => 0,
570 'dynamic__days_max' => 0,
571 'dynamic__days_specific' => '',
572 'dynamic__week_days__start' => '-1',
573 );
574 if ( function_exists( 'wpbc__calendar__js_params__get_days_selection_arr' ) ) {
575 $days_selection = wpbc__calendar__js_params__get_days_selection_arr();
576 }
577
578 /**
579 * Enterprise/business options — kept for compatibility.
580 * Note: We intentionally keep legacy option names (typos included) because related
581 * client code expects them. Do not rename without updating JS consumer logic.
582 */
583 $is_parent_resource = 0; // Placeholder: 0 means "single resource mode" preview.
584 $booking_capacity_field = '';
585 $booking_is_different_subres_disabled = '';
586 if ( class_exists( 'wpdev_bk_biz_l' ) ) {
587 $booking_capacity_field = function_exists( 'wpbc_get__booking_capacity_field__name' ) ? wpbc_get__booking_capacity_field__name() : '';
588 $booking_is_different_subres_disabled = get_bk_option( 'booking_is_dissbale_booking_for_different_sub_resources' );
589 }
590
591 $booking_recurrent_time = '';
592 if ( class_exists( 'wpdev_bk_biz_s' ) ) {
593 // No esc_js() — wp_localize_script() JSON-encodes safely.
594 $booking_recurrent_time = (string) get_bk_option( 'booking_recurrent_time' );
595 }
596
597 // Boot payload for real booking resources (used by the Inspector select).
598 $booking_resources = self::get_booking_resources_boot_data();
599 $is_free_version = ( class_exists( 'wpdev_bk_personal' ) ) ? 0 : 1;
600
601 wp_localize_script(
602 'wpbc-bfb_field_calendar_wptpl',
603 'WPBC_BFB_CalendarBoot',
604 array(
605 'nonce' => wp_create_nonce( 'wpbc_calendar_load_ajx' . '_wpbcnonce' ),
606 'user_id' => (int) wpbc_get_current_user_id(),
607 'locale' => (string) get_user_locale(),
608 'request_uri' => (string) $request_uri,
609
610 // Core calendar env.
611 'balancer_max_threads' => (int) $balancer_max_threads,
612 'booking_max_monthes_in_calendar' => (string) get_bk_option( 'booking_max_monthes_in_calendar' ),
613 'booking_start_day_weeek' => (string) get_bk_option( 'booking_start_day_weeek' ),
614 'booking_date_format' => (string) get_bk_option( 'booking_date_format' ),
615 'booking_time_format' => (string) get_bk_option( 'booking_time_format' ),
616
617 'default_preview_resource_id' => (int) self::get_preview_resource_id(),
618
619 // Day-selection policy.
620 'days_selection' => array(
621 'days_select_mode' => (string) $days_selection['days_select_mode'],
622 'fixed__days_num' => (int) $days_selection['fixed__days_num'],
623 'fixed__week_days__start' => (string) $days_selection['fixed__week_days__start'],
624 'dynamic__days_min' => (int) $days_selection['dynamic__days_min'],
625 'dynamic__days_max' => (int) $days_selection['dynamic__days_max'],
626 'dynamic__days_specific' => (string) $days_selection['dynamic__days_specific'],
627 'dynamic__week_days__start' => (string) $days_selection['dynamic__week_days__start'],
628 ),
629
630 // Enterprise/business flags.
631 'is_parent_resource' => (int) $is_parent_resource,
632 'booking_capacity_field' => (string) $booking_capacity_field,
633 'booking_is_dissbale_booking_for_different_sub_resources' => (string) $booking_is_different_subres_disabled,
634 'booking_recurrent_time' => (string) $booking_recurrent_time,
635
636 // Real booking resources for Inspector select.
637 'booking_resources' => $booking_resources,
638 'is_free_version' => (int) $is_free_version,
639 'calendar_legend' => self::get_calendar_legend_boot_data(),
640 )
641 );
642 }
643
644 /**
645 * Print Preview & Inspector templates (Underscore.js) on the Builder page.
646 *
647 * @param string $page Current admin page slug used by the Builder.
648 *
649 * @return void
650 */
651 public static function print_templates( $page ) {
652
653 if ( defined( 'WPBC_BFB_BUILDER_PAGE_SLUG' ) && WPBC_BFB_BUILDER_PAGE_SLUG !== $page ) {
654 return;
655 }
656
657 // == CANVAS PREVIEW ==
658 ?>
659 <script type="text/html" id="tmpl-wpbc-bfb-field-calendar">
660 <#
661 // Normalize booleans and numbers defensively.
662 var __boot_resources = ( window.WPBC_BFB_CalendarBoot && Array.isArray( window.WPBC_BFB_CalendarBoot.booking_resources ) ) ? window.WPBC_BFB_CalendarBoot.booking_resources : [];
663 var __configured_rid = Number( ( window.WPBC_BFB_CalendarBoot && window.WPBC_BFB_CalendarBoot.default_preview_resource_id ) || 1 );
664 var __data_rid = Number( data.resource_id || 1 );
665
666 function __resource_exists( id ) {
667 id = Number( id || 1 );
668 if ( ! isFinite( id ) || id <= 0 ) {
669 return false;
670 }
671 for ( var i = 0; i < __boot_resources.length; i++ ) {
672 if ( Number( __boot_resources[i].booking_type_id ) === id ) {
673 return true;
674 }
675 }
676 return false;
677 }
678 function __first_existing_resource_id() {
679 for ( var i = 0; i < __boot_resources.length; i++ ) {
680 var id = Number( __boot_resources[i].booking_type_id );
681 if ( isFinite( id ) && id > 0 ) {
682 return id;
683 }
684 }
685 return 1;
686 }
687 var rid = __resource_exists( __configured_rid ) ? __configured_rid : ( __resource_exists( __data_rid ) ? __data_rid : __first_existing_resource_id() );
688
689
690 var months = Math.max(1, Math.min(12, Number(data.months || 1)));
691
692 // Compute textarea ID/name: calendar requires 'date_booking{rid}' by convention.
693 var input_id = (data.html_id && String(data.html_id).trim()) ? data.html_id : ('date_booking' + rid);
694 var input_name = (data.name && String(data.name).trim()) ? data.name : ('date_booking' + rid);
695
696 // Guard CSS length: allow px, %, rem, em; fallback to 250px.
697 var minw = (function(v){
698 var s = String(v || '').trim();
699 var m = s.match(/^(-?\d+(?:\.\d+)?)(px|%|rem|em)$/i);
700 return m ? m[0] : '250px';
701 })( data.min_width );
702 #>
703
704 <span class="wpbc_bfb__noaction wpbc_bfb__no-drag-zone {{ data.cssclass || '' }}" inert=""
705 style="display:block; min-width: {{ minw }};">
706 <# if ( data.label && data.label !== '' ) { #>
707 <label class="wpbc_bfb__field-label" for="{{ input_id }}">
708 {{ data.label }}
709 </label>
710 <# } #>
711
712 <div class="wpbc_calendar_wraper wpbc_change_over_triangle">
713 <div class="wpbc_cal_container bk_calendar_frame wpbc_no_custom_width months_num_in_row_ cal_month_num_{{ months }}">
714 <div id="calendar_booking{{ rid }}"><?php esc_html_e( 'Calendar is loading', 'booking' ); ?>...</div>
715 </div>
716 </div>
717
718 <textarea rows="3" cols="50" id="{{ input_id }}" name="{{ input_name }}" autocomplete="off" style="display:none;" tabindex="-1" aria-hidden="true"></textarea>
719 <div class="js-wpbc-bfb-calendar-legend-preview">
720 <?php
721 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
722 echo wpbc_get_calendar_legend();
723 ?>
724 </div>
725 <# if ( data.help ) { #>
726 <div class="wpbc_bfb__help">{{ data.help }}</div>
727 <# } #>
728 </span>
729 </script>
730 <?php
731
732 // == INSPECTOR ==
733 ?>
734 <script type="text/html" id="tmpl-wpbc-bfb-inspector-calendar">
735 <div class="wpbc_bfb__inspector__head">
736 <div class="header_container">
737 <div class="header_title_content">
738 <h3 class="title"><?php echo esc_html__( 'Calendar', 'booking' ); ?></h3>
739 <div class="desc"><?php echo esc_html__( 'Configure the date picker calendar. Some options affect only the preview.', 'booking' ); ?></div>
740 </div>
741 <div class="actions wpbc_ajx_toolbar wpbc_no_borders">
742 <div class="ui_container ui_container_small">
743 <div class="ui_group">
744 <div class="ui_element">
745 <button type="button" class="button button-secondary wpbc_ui_control wpbc_ui_button"
746 data-action="deselect" aria-label="<?php echo esc_attr__( 'Deselect', 'booking' ); ?>">
747 <i class="menu_icon icon-1x wpbc_icn_rotate_270 wpbc_icn_near_me_disabled"></i>
748 </button>
749 <button type="button" class="button button-secondary wpbc_ui_control wpbc_ui_button"
750 data-action="scrollto" aria-label="<?php echo esc_attr__( 'Scroll to field', 'booking' ); ?>">
751 <i class="menu_icon icon-1x wpbc_icn_ads_click filter_center_focus"></i>
752 </button>
753 </div>
754 <div class="ui_element">
755 <button type="button" class="button button-secondary wpbc_ui_control wpbc_ui_button"
756 data-action="move-up" aria-label="<?php echo esc_attr__( 'Move up', 'booking' ); ?>">
757 <i class="menu_icon icon-1x wpbc_icn_arrow_upward"></i>
758 </button>
759 <button type="button" class="button button-secondary wpbc_ui_control wpbc_ui_button"
760 data-action="move-down" aria-label="<?php echo esc_attr__( 'Move down', 'booking' ); ?>">
761 <i class="menu_icon icon-1x wpbc_icn_arrow_downward"></i>
762 </button>
763 </div>
764 <div class="ui_element">
765 <button data-action="delete" aria-label="<?php echo esc_attr__( 'Delete', 'booking' ); ?>" type="button"
766 class="button button-secondary wpbc_ui_control wpbc_ui_button wpbc_ui_button_danger button-link-delete">
767 <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>
768 </button>
769 </div>
770 </div>
771 </div>
772 </div><!-- .actions -->
773 </div>
774 </div>
775
776 <div class="wpbc_bfb__inspector__body">
777
778 <section class="wpbc_bfb__inspector__group wpbc_ui__collapsible_group is-open" data-group="basic">
779 <button type="button" class="group__header">
780 <h3><?php echo esc_html__( 'Basic', 'booking' ); ?></h3>
781 <i class="wpbc_ui_el__vert_menu_root_section_icon menu_icon icon-1x wpbc-bi-chevron-right"></i>
782 </button>
783 <div class="group__fields">
784 <div class="inspector__row">
785 <label class="inspector__label"><?php echo esc_html__( 'Label', 'booking' ); ?></label>
786 <div class="inspector__control">
787 <input type="text" class="inspector__input" data-inspector-key="label"
788 value="{{ data.label || '<?php echo esc_js( __( 'Date', 'booking' ) ); ?>' }}">
789 </div>
790 </div>
791
792 <div class="inspector__row">
793 <label class="inspector__label"><?php echo esc_html__( 'Help text', 'booking' ); ?></label>
794 <div class="inspector__control">
795 <textarea class="inspector__textarea" rows="3" data-inspector-key="help">{{ data.help || '' }}</textarea>
796 </div>
797 </div>
798 </div>
799 </section>
800 <?php self::print_calendar_skin_inspector_group(); ?>
801 <?php self::print_calendar_legend_inspector_group(); ?>
802 <?php /* ?>
803 <section class="wpbc_bfb__inspector__group wpbc_ui__collapsible_group is-open" data-group="calendar">
804 <button type="button" class="group__header">
805 <h3><?php echo esc_html__( 'Calendar', 'booking' ); ?></h3>
806 <i class="wpbc_ui_el__vert_menu_root_section_icon menu_icon icon-1x wpbc-bi-chevron-right"></i>
807 </button>
808 <div class="group__fields">
809
810 <!-- Resource selector with Free-version guard -->
811 <div class="inspector__row">
812 <label class="inspector__label"><?php echo esc_html__( 'Booking resource', 'booking' ); ?></label>
813 <div class="inspector__control">
814 <#
815 var __boot = window.WPBC_BFB_CalendarBoot || {};
816 var __free = (__boot.is_free_version==='1');
817 var __res = (__boot.booking_resources) ? __boot.booking_resources : [];
818 var __has_list = (__res && __res.length > 0);
819 var __rid = String( data.resource_id || '1' );
820
821 // If Free -> enforce ID=1 and persist to data.
822 if ( __free ) {
823 __rid = '1';
824 data.resource_id = __rid;
825 }
826 #>
827 <# if ( __free ) { #>
828 <input type="hidden" class="inspector__input" data-inspector-key="resource_id" value="1">
829 <p class="wpbc_bfb__help">
830 <?php
831 echo esc_html__(
832 'The Free version has one default booking resource. To have multiple resources, please upgrade to a premium version.',
833 'booking'
834 );
835 ?>
836 </p>
837 <# } else if ( __has_list ) { #>
838 <select class="inspector__input" data-inspector-key="resource_id">
839 <#
840 // Coerce current value to an existing one if missing.
841 var __exists = _.some( __res, function(r){ return String(r.booking_type_id) === __rid; } );
842 if ( ! __exists ) { __rid = String( __res[0].booking_type_id ); data.resource_id = __rid; }
843
844 _.each( __res, function( r ){
845 var is_parent = ( String(r.parent||'0') === '0' );
846 var selected = ( __rid === String(r.booking_type_id) );
847 #>
848 <option value="{{ r.booking_type_id }}"
849 <# if ( selected ) { #>selected="selected"<# } #>
850 style="<# if ( is_parent ) { #>font-weight:600;<# } else { #>font-size:0.95em;padding-left:20px;<# } #>">
851 <# if ( ! is_parent ) { #>&nbsp;&nbsp;&nbsp;<# } #>{{ r.title }}
852 </option>
853 <# }); #>
854 </select>
855 <p class="wpbc_bfb__help"><?php echo esc_html__( 'Choose booking resource for calendar preview ', 'booking' ); ?></p>
856 <# } else { #>
857 <input type="hidden" class="inspector__input inspector__w_30" data-inspector-key="resource_id" value="{{ data.resource_id || 1 }}">
858 <p class="wpbc_bfb__help"><?php echo esc_html__( 'No resources available. Create booking resources on WP Booking Calendar > Resources > Resources page.', 'booking' ); ?></p>
859 <# } #>
860 </div>
861 </div>
862
863 <div class="inspector__row">
864 <label class="inspector__label"><?php echo esc_html__( 'Months in row', 'booking' ); ?></label>
865 <div class="inspector__control">
866 <div class="wpbc_len_group wpbc_inline_inputs" data-len-group="calendar-months">
867 <input data-len-range type="range" class="inspector__input" min="1" max="12"
868 step="1" data-inspector-key="months" value="{{ data.months || 1 }}">
869 <input data-len-value type="number" class="inspector__input inspector__w_30"
870 data-inspector-key="months" min="1" max="12" value="{{ data.months || 1 }}">
871 </div>
872 <p class="wpbc_bfb__help"><?php echo esc_html__( 'Preview hint: multiple months may require full calendar init in preview.', 'booking' ); ?></p>
873 </div>
874 </div>
875
876 </div>
877 </section>
878
879 <section class="wpbc_bfb__inspector__group wpbc_ui__collapsible_group" data-group="advanced">
880 <button type="button" class="group__header">
881 <h3><?php echo esc_html__( 'Advanced', 'booking' ); ?></h3>
882 <i class="wpbc_ui_el__vert_menu_root_section_icon menu_icon icon-1x wpbc-bi-chevron-right"></i>
883 </button>
884 <div class="group__fields">
885
886 <div class="inspector__row">
887 <label class="inspector__label"><?php echo esc_html__( 'CSS class', 'booking' ); ?></label>
888 <div class="inspector__control">
889 <input type="text" class="inspector__input" data-inspector-key="cssclass"
890 value="{{ data.cssclass || '' }}">
891 </div>
892 </div>
893
894 <?php
895 // HTML ID must be 'date_booking{rid}' (rid = resource id) to bind with the textarea under the calendar.
896 // This field is intentionally non-editable in Inspector to avoid breaking loader contracts.
897 // If you must expose it, ensure builder-side enforcement keeps the 'date_booking{rid}' pattern.
898 ?>
899
900 </div>
901 </section>
902 <?php */ ?>
903 </div>
904 </script>
905 <?php
906 }
907
908 /**
909 * Register the palette item (draggable field entry).
910 *
911 * @param string $group Palette group slug.
912 * @param string $position Position within group.
913 *
914 * @return void
915 */
916 public static function palette_register_item( $group, $position ) {
917 if ( 'essentials' !== $group || 'top' !== $position ) {
918 return;
919 }
920 ?>
921 <li class="wpbc_bfb__field"
922 data-id="calendar"
923 data-type="calendar"
924 data-usage_key="calendar"
925 data-usagenumber="1"
926 data-resource_id="<?php echo intval( self::get_preview_resource_id() ); ?>"
927 data-months="1"
928 data-label="<?php echo esc_attr( __( 'Calendar', 'booking' ) ); ?>"
929 data-min_width="250px">
930 <i class="menu_icon icon-1x wpbc-bi-calendar3"></i>
931 <span class="wpbc_bfb__field-label"><?php echo esc_html( __( 'Calendar', 'booking' ) ); ?></span>
932 <span class="wpbc_bfb__field-type">calendar</span>
933 </li>
934 <?php
935 }
936
937 /**
938 * Get preview resource ID for Builder calendar field.
939 *
940 * Uses the configured default resource if it exists in available resources.
941 * Falls back to the first existing resource ID.
942 *
943 * @return int
944 */
945 protected static function get_preview_resource_id() {
946 $resources_boot = self::get_booking_resources_boot_data();
947 $existing_ids = array();
948
949 foreach ( $resources_boot as $resource_item ) {
950 if ( ! empty( $resource_item['booking_type_id'] ) ) {
951 $existing_ids[] = intval( $resource_item['booking_type_id'] );
952 }
953 }
954
955 $existing_ids = array_values( array_filter( array_unique( $existing_ids ) ) );
956
957 $default_rid = intval( wpbc_get_default_resource() );
958
959 if ( in_array( $default_rid, $existing_ids, true ) ) {
960 return $default_rid;
961 }
962
963 if ( ! empty( $existing_ids ) ) {
964 return intval( $existing_ids[0] );
965 }
966
967 return 1;
968 }
969 }
970
971 // Bootstrap.
972 WPBC_BFB_Field_Calendar_WPTPL_Pack::init();
973