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 / page-catalog-booking-resources / _src / booking_resources_catalog.js

booking_resources_catalog.js in Booking Calendar 11.7, at includes/page-catalog-booking-resources/_src/booking_resources_catalog.js

3,726 lines 149.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Render normalized Booking Resource DTOs through identifiable WP templates.
3 *
4 * @since 11.6.0
5 */
6 ( function ( window, document ) {
7 'use strict';
8
9 var catalog_controller = null;
10 var inline_workflow_controller = null;
11 var inline_review_workflow_controller = null;
12 var delete_review_workflow_controller = null;
13 var inspector_workflow_controller = null;
14 var catalog_response = null;
15 var details_abort_controller = null;
16 var details_request_sequence = 0;
17 var details_resource_id = 0;
18 var details_toggle_button = null;
19 var pending_focus_direction = '';
20 var inspector_dirty = false;
21 var inspector_focus_target = null;
22 var inspector_mode = '';
23 var inspector_mutation_in_progress = false;
24 var inspector_mutation_request_sequence = 0;
25 var inspector_original_fields = '';
26 var inspector_request_sequence = 0;
27 var inspector_resource_id = 0;
28 var inspector_resource_ids = [];
29 var inspector_bulk_operations = {};
30 var inspector_review_token = '';
31 var inspector_selection_stale = false;
32 var inspector_tracks_selection = false;
33 var inspector_capacity_context = null;
34 var inspector_capacity_detach_ids = [];
35 var inspector_capacity_decrease_action = 'detach';
36 var inspector_capacity_target = 0;
37 var pending_highlight_ids = [];
38 var inline_state = {
39 active: false,
40 changed_rows: [],
41 loading: false,
42 request_sequence: 0,
43 review_token: ''
44 };
45
46 /**
47 * Normalize a localized WordPress flag to a strict boolean.
48 *
49 * @param {*} flag_value Localized flag value.
50 * @return {boolean} True only for an explicitly enabled flag.
51 */
52 function is_true_flag( flag_value ) {
53 return true === flag_value || 1 === flag_value || '1' === flag_value || 'true' === String( flag_value ).toLowerCase();
54 }
55
56 /**
57 * Format a localized positional-placeholder string.
58 *
59 * @param {string} template Localized string containing `%1$s` placeholders.
60 * @param {Array<*>} values Scalar replacement values.
61 * @return {string} Formatted plain text.
62 */
63 function format_message( template, values ) {
64 var message = String( template || '' );
65
66 values.forEach( function ( replacement, replacement_index ) {
67 var placeholder = new RegExp( '%' + ( replacement_index + 1 ) + '\\$s', 'g' );
68 message = message.replace( placeholder, String( replacement ) );
69 } );
70
71 return message;
72 }
73
74 /**
75 * Return the shared signed-review presentation controller.
76 *
77 * @return {Object|false} Shared review controller or false when unavailable.
78 */
79 function get_inline_review_workflow() {
80 if ( inline_review_workflow_controller ) {
81 return inline_review_workflow_controller;
82 }
83 if ( ! window.wpbc_ui_catalog || 'function' !== typeof window.wpbc_ui_catalog.create_inline_review_workflow ) {
84 return false;
85 }
86 inline_review_workflow_controller = window.wpbc_ui_catalog.create_inline_review_workflow( {
87 apply_selector: '[data-wpbc-ui-catalog-inspector-save]',
88 cancel_selector: '[data-wpbc-ui-catalog-inspector-cancel]',
89 root: document
90 } );
91
92 return inline_review_workflow_controller;
93 }
94
95 /**
96 * Return the shared permanent-deletion presentation controller.
97 *
98 * @return {Object|false} Shared deletion controller or false when unavailable.
99 */
100 function get_delete_review_workflow() {
101 if ( delete_review_workflow_controller ) {
102 return delete_review_workflow_controller;
103 }
104 if ( ! window.wpbc_ui_catalog || 'function' !== typeof window.wpbc_ui_catalog.create_delete_review_workflow ) {
105 return false;
106 }
107 delete_review_workflow_controller = window.wpbc_ui_catalog.create_delete_review_workflow( {
108 acknowledgement_selector: '[data-wpbc-catalog-resource-delete-acknowledgement]',
109 apply_selector: '[data-wpbc-ui-catalog-inspector-save]',
110 cancel_selector: '[data-wpbc-ui-catalog-inspector-cancel]',
111 root: document
112 } );
113
114 return delete_review_workflow_controller;
115 }
116
117 /**
118 * Return the localized count shown in a collapsed child-group summary.
119 *
120 * The DTO label remains authoritative because PHP applies WordPress locale
121 * plural rules. The numeric fallback consumes the shared direct-child count
122 * when an older or custom domain DTO does not include the prepared label.
123 *
124 * @param {Object} parent_resource Parent Booking Resource DTO.
125 * @param {Object} i18n Localized catalog strings.
126 * @return {string} Localized child-count label.
127 */
128 function get_children_summary_label( parent_resource, i18n ) {
129 var hierarchy = parent_resource && parent_resource.hierarchy ? parent_resource.hierarchy : {};
130 var server_label = String( hierarchy.children_label || '' ).trim();
131 var child_count = Math.max( 0, Number( hierarchy.rendered_children_count ) || 0 );
132 var label_template;
133
134 if ( server_label ) {
135 return server_label;
136 }
137
138 label_template = 1 === child_count
139 ? i18n.child_count_singular || '%1$s child resource'
140 : i18n.child_count_plural || '%1$s child resources';
141
142 return format_message( label_template, [ child_count ] );
143 }
144
145 /**
146 * Render one allow-listed Resource presentation template.
147 *
148 * @param {Object} config Registered catalog configuration.
149 * @param {string} template_role Registered template role.
150 * @param {Object} template_data Normalized DTO or presentation data.
151 * @return {string} Escaped template HTML or an empty string.
152 */
153 function render_component( config, template_role, template_data ) {
154 var component_template = window.wpbc_ui_catalog.load_template( config, template_role );
155
156 if ( ! component_template ) {
157 return '';
158 }
159
160 try {
161 return component_template( template_data || {} );
162 } catch ( error ) {
163 return '';
164 }
165 }
166
167 /**
168 * Return complete column presentation records in the active order.
169 *
170 * @param {Object} config Registered catalog configuration.
171 * @param {Object} display_state Normalized display request or response.
172 * @param {boolean} visible_only Whether hidden columns must be omitted.
173 * @param {Object} sorting_state Normalized sorting response.
174 * @return {Array<Object>} Ordered presentation-only column records.
175 */
176 function get_columns( config, display_state, visible_only, sorting_state ) {
177 var column_config = config.columns || {};
178 var definitions = column_config.definitions || {};
179 var default_order = Array.isArray( column_config.default_order ) ? column_config.default_order : [];
180 var order = display_state && Array.isArray( display_state.column_order ) ? display_state.column_order.slice() : default_order.slice();
181 var visible_columns = display_state && Array.isArray( display_state.visible_columns ) ? display_state.visible_columns : column_config.default_visible || [];
182
183 default_order.forEach( function ( column_id ) {
184 if ( -1 === order.indexOf( column_id ) ) {
185 order.push( column_id );
186 }
187 } );
188
189 return order.filter( function ( column_id ) {
190 return definitions[ column_id ] && ( ! visible_only || -1 !== visible_columns.indexOf( column_id ) );
191 } ).map( function ( column_id, column_index ) {
192 var definition = definitions[ column_id ];
193 var is_sorted = !! definition.sort_key && sorting_state && definition.sort_key === sorting_state.sort_by;
194 return {
195 aria_sort: is_sorted ? ( 'desc' === sorting_state.sort_order ? 'descending' : 'ascending' ) : 'none',
196 class_name: definition.class || 'column-' + column_id,
197 default_index: default_order.indexOf( column_id ),
198 id: column_id,
199 is_sorted: is_sorted,
200 label: definition.label || column_id,
201 move_label: format_message( config.i18n.move_column || '', [ definition.label || column_id ] ),
202 reorderable: false !== definition.reorderable,
203 required: !! definition.required,
204 sort_icon: is_sorted ? ( 'desc' === sorting_state.sort_order ? 'wpbc-bi-arrow-down' : 'wpbc-bi-arrow-up' ) : 'wpbc_icn_import_export',
205 sort_key: definition.sort_key || '',
206 visible: -1 !== visible_columns.indexOf( column_id )
207 };
208 } );
209 }
210
211 /**
212 * Determine whether display values match the Overview defaults.
213 *
214 * @param {Object} config Registered catalog configuration.
215 * @param {Object} display_state Current normalized display state.
216 * @return {string} overview or custom.
217 */
218 function get_active_view( config, display_state ) {
219 var view_definitions = config.views && config.views.definitions ? config.views.definitions : {};
220 var current_visible = display_state && Array.isArray( display_state.visible_columns ) ? display_state.visible_columns : [];
221 var matching_view = '';
222
223 Object.keys( view_definitions ).some( function ( view_id ) {
224 var view_fields = Array.isArray( view_definitions[ view_id ].fields ) ? view_definitions[ view_id ].fields : [];
225 if ( JSON.stringify( current_visible ) === JSON.stringify( view_fields ) ) {
226 matching_view = view_id;
227 return true;
228 }
229 return false;
230 } );
231
232 return matching_view || 'custom';
233 }
234
235 /**
236 * Return ordered view presets declared by the independent PHP configuration.
237 *
238 * @param {Object} config Registered catalog configuration.
239 * @return {Array<Object>} Browser-safe view definitions.
240 */
241 function get_view_definitions( config ) {
242 var definitions = config.views && config.views.definitions ? config.views.definitions : {};
243
244 return Object.keys( definitions ).map( function ( view_id ) {
245 return definitions[ view_id ];
246 } );
247 }
248
249 /**
250 * Return the allow-listed presentation packs declared by PHP.
251 *
252 * Labels remain domain-owned while the shared controller validates and
253 * persists only pack identifiers registered in the catalog configuration.
254 *
255 * @param {Object} config Registered catalog configuration.
256 * @return {Array<Object>} Ordered browser-safe pack options.
257 */
258 function get_template_pack_definitions( config ) {
259 var labels = {
260 cards: config.i18n.layout_cards || '',
261 compact: config.i18n.layout_compact || '',
262 table: config.i18n.layout_table || ''
263 };
264 var template_packs = config.template_packs || {};
265
266 return Object.keys( template_packs ).map( function ( template_pack_id ) {
267 return {
268 id: template_pack_id,
269 label: labels[ template_pack_id ] || template_pack_id
270 };
271 } );
272 }
273
274 /**
275 * Render the established Resource filters above the bordered listing.
276 *
277 * Free editions intentionally render no filter form, matching the existing
278 * page where a single default Resource makes these controls unnecessary.
279 *
280 * @param {Object} config Registered catalog configuration.
281 * @return {boolean} True when the filters target was found.
282 */
283 function render_booking_resources_filters( config ) {
284 var initial_request = config.initial_request || {};
285 var mount_element = document.getElementById( config.mount_id );
286 var filters_target = mount_element ? mount_element.querySelector( '[data-wpbc-booking-resources-filters]' ) : null;
287
288 if ( ! filters_target ) {
289 return false;
290 }
291 filters_target.innerHTML = render_component( config, 'filters', {
292 i18n: config.i18n || {},
293 resource_type: initial_request.resource_type || 'all',
294 search: initial_request.search || '',
295 show_filters: !! ( config.features && config.features.resource_filters ),
296 show_resource_type_filter: !! ( config.features && config.features.resource_type_filter )
297 } );
298
299 return true;
300 }
301
302 /**
303 * Render persistent filters and display controls outside response content.
304 *
305 * @param {Object} config Registered catalog configuration.
306 * @return {boolean} True when the toolbar target was populated.
307 */
308 function render_booking_resources_toolbar( config ) {
309 var initial_request = config.initial_request || {};
310 var mount_element = document.getElementById( config.mount_id );
311 var toolbar_target = mount_element ? mount_element.querySelector( '[data-wpbc-booking-resources-toolbar]' ) : null;
312
313 if ( ! toolbar_target ) {
314 return false;
315 }
316 toolbar_target.innerHTML = render_component( config, 'toolbar', {
317 active_template_pack: initial_request.template_pack || config.default_template_pack || 'table',
318 active_view: get_active_view( config, initial_request ),
319 columns: get_columns( config, initial_request, false, initial_request ),
320 i18n: config.i18n || {},
321 template_packs: get_template_pack_definitions( config ),
322 views: get_view_definitions( config )
323 } );
324 if ( catalog_controller && 'function' === typeof catalog_controller.refresh_controls ) {
325 catalog_controller.refresh_controls();
326 }
327
328 return !! toolbar_target.firstElementChild;
329 }
330
331 /**
332 * Add full-text tooltips only to elements whose rendered text is clipped.
333 *
334 * Native title text remains available without a JavaScript tooltip library;
335 * the established tooltip attributes are also supplied for Booking Calendar
336 * admin themes that initialize them globally.
337 *
338 * @param {HTMLElement} catalog_mount Catalog mount element.
339 * @return {void}
340 */
341 function synchronize_overflow_tooltips( catalog_mount ) {
342 if ( ! window.wpbc_ui_catalog || 'function' !== typeof window.wpbc_ui_catalog.synchronize_overflow_tooltips ) {
343 return;
344 }
345
346 window.wpbc_ui_catalog.synchronize_overflow_tooltips( catalog_mount );
347 }
348
349 /**
350 * Initialize tooltips for compact controls inserted by a lazy details render.
351 *
352 * The native title remains as a fallback when the established Booking
353 * Calendar tooltip helper is unavailable.
354 *
355 * @param {HTMLElement} catalog_mount Catalog mount element.
356 * @return {void}
357 */
358 function initialize_details_tooltips( catalog_mount ) {
359 var tooltip_selector;
360
361 if ( ! catalog_mount || ! catalog_mount.id || 'function' !== typeof window.wpbc_define_tippy_tooltips ) {
362 return;
363 }
364 tooltip_selector = '#' + catalog_mount.id + ' [data-wpbc-ui-catalog-details-tooltip]';
365 window.wpbc_define_tippy_tooltips( tooltip_selector );
366 }
367
368 /**
369 * Synchronize persistent controls with server-authoritative response state.
370 *
371 * @param {Object} config Registered catalog configuration.
372 * @param {Object} response Normalized catalog response.
373 * @return {void}
374 */
375 function synchronize_booking_resources_toolbar( config, response ) {
376 var columns = get_columns( config, response.display || {}, false, response.sorting || {} );
377 var mount_element = document.getElementById( config.mount_id );
378 var column_list = mount_element ? mount_element.querySelector( '[data-wpbc-ui-catalog-column-list]' ) : null;
379 var search_control = mount_element ? mount_element.querySelector( '[data-wpbc-ui-catalog-search]' ) : null;
380 var template_pack_control = mount_element ? mount_element.querySelector( '[data-wpbc-ui-catalog-template-pack]' ) : null;
381 var type_control = mount_element ? mount_element.querySelector( '[data-wpbc-ui-catalog-filter="resource_type"]' ) : null;
382 var view_control = mount_element ? mount_element.querySelector( '[data-wpbc-ui-catalog-view]' ) : null;
383
384 if ( search_control && document.activeElement !== search_control ) {
385 search_control.value = response.filters.search || '';
386 }
387 if ( type_control ) {
388 type_control.value = response.filters.resource_type || 'all';
389 }
390 if ( template_pack_control && response.display && response.display.template_pack ) {
391 template_pack_control.value = response.display.template_pack;
392 }
393 columns.forEach( function ( column ) {
394 var column_control = mount_element.querySelector( '[data-wpbc-ui-catalog-column-visible][value="' + column.id + '"]' );
395 var column_item = mount_element.querySelector( '[data-wpbc-ui-catalog-column-item="' + column.id + '"]' );
396 if ( column_control ) {
397 column_control.checked = column.visible;
398 }
399 if ( column_list && column_item ) {
400 column_list.appendChild( column_item );
401 }
402 } );
403 if ( view_control ) {
404 view_control.value = get_active_view( config, response.display || {} );
405 }
406 }
407
408 /**
409 * Render the persistent inline-edit status bar from its registered template.
410 *
411 * @param {Object} config Catalog configuration.
412 * @return {void}
413 */
414 function render_inline_bar( config ) {
415 var mount_element = document.getElementById( config.mount_id );
416 var inline_host = mount_element ? mount_element.querySelector( '[data-wpbc-catalog-inline-bar-host]' ) : null;
417
418 if ( inline_host && ! inline_host.firstElementChild ) {
419 inline_host.innerHTML = render_component( config, 'inline_bar', { i18n: config.i18n || {} } );
420 }
421 if ( inline_workflow_controller ) {
422 inline_workflow_controller.register_sticky_bar();
423 }
424 }
425
426 /**
427 * Synchronize inline activation, changed count, and disabled navigation.
428 *
429 * @param {Object} config Catalog configuration.
430 * @return {void}
431 */
432 function synchronize_inline_controls( config ) {
433 var mount_element = document.getElementById( config.mount_id );
434 var changed_count = inline_state.changed_rows.length;
435 var count_label = 1 === changed_count ? config.i18n.inline_changed_row : config.i18n.inline_changed_rows;
436
437 if ( ! mount_element || ! inline_workflow_controller ) {
438 return;
439 }
440
441 inline_workflow_controller.synchronize( {
442 active: inline_state.active,
443 active_toggle_text: config.i18n.inline_editing_rows || '',
444 busy: inline_state.loading,
445 changed_count: changed_count,
446 count_text: inline_state.loading
447 ? config.i18n.inline_loading || ''
448 : format_message( count_label || '%1$s changed rows', [ changed_count ] ),
449 has_items: !! mount_element.querySelector( '[data-wpbc-booking-resource-id]' ),
450 inactive_toggle_text: config.i18n.edit_rows || ''
451 } );
452 }
453
454 /**
455 * Block page-changing catalog controls before shared handlers can discard drafts.
456 *
457 * Native summary elements do not honor a disabled property, so this capture
458 * guard complements the visual disabled state while inline editing is active.
459 *
460 * @param {Event} event Captured catalog event.
461 * @return {void}
462 */
463 function protect_inline_drafts_from_catalog_controls( event ) {
464 if ( inline_workflow_controller ) {
465 inline_workflow_controller.protect_event( event, inline_state.active );
466 }
467 }
468
469 /**
470 * Show or clear an inline workflow error.
471 *
472 * @param {Object} config Catalog configuration.
473 * @param {string} message Safe message or empty string.
474 * @return {void}
475 */
476 function show_inline_message( config, message ) {
477 var mount_element = document.getElementById( config.mount_id );
478 var notice = mount_element ? mount_element.querySelector( '[data-wpbc-catalog-inline-message]' ) : null;
479
480 if ( notice ) {
481 notice.hidden = ! message;
482 var text = notice.querySelector( 'p' );
483 if ( text ) {
484 text.textContent = message || '';
485 }
486 }
487 }
488
489 /**
490 * Render one server-declared field through the inline WP template.
491 *
492 * @param {Object} config Catalog configuration.
493 * @param {Object} row_schema Row schema.
494 * @return {void}
495 */
496 function render_inline_row( config, row_schema ) {
497 var mount_element = document.getElementById( config.mount_id );
498 var resource_id = Number( row_schema.resource_id ) || 0;
499 var row = mount_element ? mount_element.querySelector( '[data-wpbc-booking-resource-id="' + resource_id + '"]' ) : null;
500 var resource_fields = [];
501
502 if ( ! row ) {
503 return;
504 }
505 ( row_schema.fields || [] ).forEach( function ( field ) {
506 var cell = row.querySelector( '[data-wpbc-ui-catalog-field="' + String( field.column || '' ) + '"]' );
507 if ( ! cell || cell.hidden ) {
508 return;
509 }
510 if ( 'resource' === field.column ) {
511 resource_fields.push( field );
512 return;
513 }
514 cell.innerHTML = render_component( config, 'inline_field', { field: field, resource_id: resource_id } );
515 } );
516 if ( resource_fields.length ) {
517 var copy = row.querySelector( '[data-wpbc-ui-catalog-field="resource"] .wpbc_ui_listing__item_copy' );
518 if ( copy ) {
519 var wrapper = document.createElement( 'span' );
520 wrapper.className = 'wpbc_booking_resources__inline_identity_fields';
521 resource_fields.forEach( function ( field ) {
522 wrapper.insertAdjacentHTML( 'beforeend', render_component( config, 'inline_field', { field: field, resource_id: resource_id } ) );
523 } );
524 copy.replaceWith( wrapper );
525 }
526 }
527 }
528
529 /**
530 * Collect only changed row fields while preserving visible catalog order.
531 *
532 * @param {Object} config Catalog configuration.
533 * @return {Array<Object>} Changed row envelopes.
534 */
535 function collect_inline_drafts( config ) {
536 var mount_element = document.getElementById( config.mount_id );
537 var changed_rows = [];
538
539 if ( ! mount_element ) {
540 return changed_rows;
541 }
542 mount_element.querySelectorAll( '.wpbc_booking_resources__item[data-wpbc-booking-resource-id]' ).forEach( function ( row ) {
543 var fields = {};
544 var has_changes;
545 var indicator_host;
546
547 row.querySelectorAll( '[data-wpbc-catalog-inline-field]' ).forEach( function ( control ) {
548 var field_key = control.getAttribute( 'data-wpbc-catalog-inline-field' ) || '';
549 if ( field_key && String( control.value || '' ) !== String( control.getAttribute( 'data-wpbc-catalog-inline-original' ) || '' ) ) {
550 fields[ field_key ] = control.value;
551 }
552 } );
553 has_changes = 0 < Object.keys( fields ).length;
554 indicator_host = row.querySelector( '[data-wpbc-ui-catalog-field="resource"]' );
555 row.classList.toggle( 'is-inline-dirty', has_changes );
556 if ( inline_workflow_controller ) {
557 inline_workflow_controller.set_row_changed( row, has_changes, indicator_host, config.i18n.inline_changed || '' );
558 }
559 if ( has_changes ) {
560 changed_rows.push( { resource_id: Number( row.getAttribute( 'data-wpbc-booking-resource-id' ) ), fields: fields } );
561 }
562 } );
563
564 return changed_rows;
565 }
566
567 /**
568 * Invalidate a prior review and synchronize draft state.
569 *
570 * @param {Object} config Catalog configuration.
571 * @return {void}
572 */
573 function synchronize_inline_drafts( config ) {
574 inline_state.changed_rows = collect_inline_drafts( config );
575 inline_state.review_token = '';
576 show_inline_message( config, '' );
577 synchronize_inline_controls( config );
578 }
579
580 /**
581 * Exit inline mode and optionally reload canonical rows.
582 *
583 * @param {Object} config Catalog configuration.
584 * @param {boolean} reload Whether to reload the catalog.
585 * @param {string} message Optional success message.
586 * @return {void}
587 */
588 function leave_inline_mode( config, reload, message ) {
589 inline_state.request_sequence += 1;
590 inline_state.active = false;
591 inline_state.loading = false;
592 inline_state.changed_rows = [];
593 inline_state.review_token = '';
594 if ( 'inline_review' === inspector_mode ) {
595 close_inspector( config, false );
596 }
597 synchronize_inline_controls( config );
598 if ( message ) {
599 show_admin_message( message, 'success', 4000 );
600 }
601 if ( reload && catalog_controller ) {
602 catalog_controller.load();
603 }
604 }
605
606 /**
607 * Start row editing for the current visible Resource page.
608 *
609 * @param {Object} config Catalog configuration.
610 * @return {void}
611 */
612 function start_inline_mode( config ) {
613 var mount_element = document.getElementById( config.mount_id );
614 var resource_ids = [];
615 var request_sequence;
616
617 if ( inline_state.active ) {
618 synchronize_inline_drafts( config );
619 if ( ! inline_state.changed_rows.length || window.confirm( config.i18n.inline_discard || '' ) ) {
620 leave_inline_mode( config, true, '' );
621 }
622 return;
623 }
624 mount_element.querySelectorAll( '.wpbc_booking_resources__item[data-wpbc-booking-resource-id]' ).forEach( function ( row ) {
625 resource_ids.push( Number( row.getAttribute( 'data-wpbc-booking-resource-id' ) ) );
626 } );
627 if ( ! resource_ids.length ) {
628 return;
629 }
630 if ( ! can_discard_inspector( config ) ) {
631 return;
632 }
633 close_inspector( config, false );
634 mount_element.querySelectorAll( '[data-wpbc-ui-catalog-display-customizer][open]' ).forEach( function ( customizer ) {
635 customizer.removeAttribute( 'open' );
636 } );
637 inline_state.active = true;
638 inline_state.loading = true;
639 inline_state.changed_rows = [];
640 request_sequence = ++inline_state.request_sequence;
641 close_details_row( false );
642 synchronize_inline_controls( config );
643 request_inspector( config, config.inline_schema_action, { resource_ids: JSON.stringify( resource_ids ) } ).then( function ( response ) {
644 if ( request_sequence !== inline_state.request_sequence || ! inline_state.active || ! response || ! response.success || ! response.data || ! response.data.schema ) {
645 throw new Error( get_inspector_response_message( response, config.i18n.inline_load_failed ) );
646 }
647 ( response.data.schema.rows || [] ).forEach( function ( row_schema ) {
648 render_inline_row( config, row_schema );
649 } );
650 var first_field = mount_element.querySelector( '[data-wpbc-catalog-inline-field]' );
651 if ( first_field ) {
652 first_field.focus();
653 }
654 } ).catch( function ( error ) {
655 if ( request_sequence === inline_state.request_sequence ) {
656 show_admin_message( error.message || config.i18n.inline_load_failed || '', 'error', 5000 );
657 inline_state.active = false;
658 if ( catalog_controller ) {
659 catalog_controller.load();
660 }
661 }
662 } ).then( function () {
663 if ( request_sequence === inline_state.request_sequence ) {
664 inline_state.loading = false;
665 synchronize_inline_controls( config );
666 }
667 } );
668 }
669
670 /**
671 * Preview current inline drafts and open their signed review inspector.
672 *
673 * @param {Object} config Catalog configuration.
674 * @param {HTMLElement} focus_target Review trigger for focus restoration.
675 * @return {void}
676 */
677 function preview_inline_changes( config, focus_target ) {
678 var inspector_workflow;
679 var request_sequence;
680
681 synchronize_inline_drafts( config );
682 inspector_workflow = get_inspector_workflow( config );
683 if ( ! inline_state.changed_rows.length || inline_state.loading || ! inspector_workflow || ! inspector_workflow.mount() ) {
684 return;
685 }
686 inline_state.loading = true;
687 request_sequence = ++inline_state.request_sequence;
688 inspector_focus_target = focus_target;
689 inspector_mode = 'inline_review';
690 inspector_dirty = true;
691 if ( ! inspector_workflow.open_loading() ) {
692 inline_state.loading = false;
693 return;
694 }
695 synchronize_inline_controls( config );
696 request_inspector( config, config.inline_preview_action, { rows: JSON.stringify( inline_state.changed_rows ) } ).then( function ( response ) {
697 var review_workflow;
698 var review_model;
699 var target;
700 if ( request_sequence !== inline_state.request_sequence ) {
701 return;
702 }
703 if ( ! response || ! response.success || ! response.data || ! response.data.preview ) {
704 throw new Error( get_inspector_response_message( response, config.i18n.inline_review_failed ) );
705 }
706 inline_state.review_token = String( response.data.preview.review_token || '' );
707 target = get_inspector_host().querySelector( '[data-wpbc-ui-catalog-inspector-form]' );
708 review_workflow = get_inline_review_workflow();
709 review_model = review_workflow ? review_workflow.prepare( response.data.preview.review || {}, {
710 changed_label: format_message( 1 === inline_state.changed_rows.length ? config.i18n.inline_changed_row : config.i18n.inline_changed_rows, [ inline_state.changed_rows.length ] ),
711 description: config.i18n.inline_review_description || '',
712 form_id: 'wpbc_catalog_booking_resources_inline_review_form',
713 mode: 'inline_review',
714 pending_message: config.i18n.review_changes_help || '',
715 title: config.i18n.inline_review_title || ''
716 } ) : {};
717 target.innerHTML = render_component( config, 'inspector_inline_review', review_model );
718 set_inspector_state( 'form', '' );
719 configure_inspector_footer( 'wpbc_catalog_booking_resources_inline_review_form', config.i18n.apply_changes || '', false, ! inline_state.review_token );
720 if ( review_workflow ) {
721 review_workflow.synchronize( { busy: false, can_apply: !! inline_state.review_token } );
722 }
723 focus_inspector_heading( target.querySelector( '[data-wpbc-catalog-inline-review-form]' ) );
724 } ).catch( function ( error ) {
725 if ( request_sequence !== inline_state.request_sequence ) {
726 return;
727 }
728 inline_state.review_token = '';
729 inspector_dirty = false;
730 show_admin_message( error.message || config.i18n.inline_review_failed || '', 'error', 5000 );
731 close_inspector( config, false );
732 } ).then( function () {
733 if ( request_sequence === inline_state.request_sequence ) {
734 inline_state.loading = false;
735 synchronize_inline_controls( config );
736 }
737 } );
738 }
739
740 /**
741 * Apply the reviewed inline plan and retain the catalog selection.
742 *
743 * @param {SubmitEvent} event Review form submit event.
744 * @param {Object} config Catalog configuration.
745 * @return {void}
746 */
747 function apply_inline_changes( event, config ) {
748 var form = event.target;
749 var save_button = document.querySelector( '[data-wpbc-ui-catalog-inspector-save]' );
750 var request_sequence;
751
752 event.preventDefault();
753 if ( inline_state.loading || ! inline_state.review_token || ( save_button && save_button.disabled ) ) {
754 return;
755 }
756 inline_state.loading = true;
757 inspector_mutation_in_progress = true;
758 if ( get_inline_review_workflow() ) {
759 get_inline_review_workflow().synchronize( { busy: true, can_apply: true } );
760 }
761 request_sequence = ++inspector_mutation_request_sequence;
762 if ( save_button ) {
763 save_button.disabled = true;
764 save_button.classList.add( 'is-busy' );
765 }
766 request_inspector( config, config.inline_apply_action, { rows: JSON.stringify( inline_state.changed_rows ), review_token: inline_state.review_token } ).then( function ( response ) {
767 if ( request_sequence !== inspector_mutation_request_sequence || ! response || ! response.success || ! response.data ) {
768 throw new Error( get_inspector_response_message( response, config.i18n.inline_apply_failed ) );
769 }
770 pending_highlight_ids = ( response.data.updated_ids || [] ).map( String );
771 inspector_mutation_in_progress = false;
772 leave_inline_mode( config, true, get_inspector_response_message( response, '' ) );
773 } ).catch( function ( error ) {
774 inspector_mutation_in_progress = false;
775 inline_state.review_token = '';
776 if ( get_inline_review_workflow() ) {
777 get_inline_review_workflow().synchronize( { busy: false, can_apply: false } );
778 }
779 if ( document.documentElement.contains( form ) ) {
780 show_inspector_message( form, error.message || config.i18n.inline_apply_failed || '', true );
781 } else {
782 show_admin_message( error.message || config.i18n.inline_apply_failed || '', 'error', 5000 );
783 }
784 if ( save_button ) {
785 save_button.disabled = true;
786 save_button.classList.remove( 'is-busy' );
787 }
788 } ).then( function () {
789 inline_state.loading = false;
790 synchronize_inline_controls( config );
791 } );
792 }
793
794 /**
795 * Render the header, complete Resource rows, partials, and pagination.
796 *
797 * @param {Object} config Registered catalog configuration.
798 * @param {Object} response Normalized catalog response.
799 * @return {boolean} True when every required presentation target exists.
800 */
801 function render_booking_resources_response( config, response ) {
802 var catalog_heading;
803 var catalog_mount = document.getElementById( config.mount_id );
804 var card_groups = {};
805 var children_by_parent = {};
806 var columns;
807 var header_element;
808 var hierarchy_enabled;
809 var hierarchy_is_expanded;
810 var is_cards_pack;
811 var parent_resources = {};
812 var pagination;
813 var pagination_element;
814 var rows_element;
815
816 if ( ! catalog_mount || ! response || ! Array.isArray( response.items ) ) {
817 return false;
818 }
819
820 header_element = catalog_mount.querySelector( '[data-wpbc-booking-resources-header]' );
821 rows_element = catalog_mount.querySelector( '[data-wpbc-booking-resources-rows]' );
822 pagination_element = catalog_mount.querySelector( '[data-wpbc-booking-resources-pagination]' );
823 if ( ! header_element || ! rows_element || ! pagination_element ) {
824 return false;
825 }
826
827 columns = get_columns( config, response.display || {}, true, response.sorting || {} );
828 is_cards_pack = 'cards' === String( response.display && response.display.template_pack || '' );
829 hierarchy_enabled = !! ( response.hierarchy && response.hierarchy.enabled );
830 hierarchy_is_expanded = hierarchy_enabled
831 && window.wpbc_ui_catalog_hierarchy
832 && 'function' === typeof window.wpbc_ui_catalog_hierarchy.get_initial_expanded
833 && window.wpbc_ui_catalog_hierarchy.get_initial_expanded( response.hierarchy || {} );
834 header_element.innerHTML = render_component( config, 'header', {
835 all_expanded: hierarchy_is_expanded,
836 columns: columns,
837 hierarchy_enabled: hierarchy_enabled,
838 i18n: config.i18n || {},
839 selection_enabled: !! ( config.features && config.features.selection )
840 } );
841 rows_element.innerHTML = '';
842 if ( hierarchy_enabled ) {
843 response.items.forEach( function ( resource ) {
844 if ( resource.hierarchy && 'parent' === resource.hierarchy.type ) {
845 parent_resources[ String( resource.id ) ] = resource;
846 } else if ( resource.hierarchy && 'child' === resource.hierarchy.type ) {
847 var parent_id = String( resource.hierarchy.parent_id || '' );
848 children_by_parent[ parent_id ] = children_by_parent[ parent_id ] || [];
849 children_by_parent[ parent_id ].push( resource );
850 }
851 } );
852 }
853 response.items.forEach( function ( resource ) {
854 var action_target;
855 var classic_label_classes = {
856 child: 'wpbc_label_resource_child',
857 cost: 'wpbc_label_cost',
858 'default-form': 'wpbc_label_resource_default_form',
859 owner: 'wpbc_label_user_owner',
860 parent: 'wpbc_label_resource_parent',
861 single: 'wpbc_label_resource_single'
862 };
863 var label_target;
864 var price_target;
865 var description = resource.description || config.i18n.no_description || '';
866 var hierarchy = Object.assign( {}, resource.hierarchy || {} );
867 var parent_context_label = '';
868 var row_variant = hierarchy_enabled && ( 'parent' === hierarchy.type || 'child' === hierarchy.type ) ? hierarchy.type : 'single';
869 var row_template_role = 'parent' === row_variant ? 'parent_row' : ( 'child' === row_variant ? 'child_row' : 'row' );
870 var type_badge_label = config.i18n.independent_label || '';
871 hierarchy.expandable = !! ( hierarchy_enabled && hierarchy.expandable );
872 if ( hierarchy.parent_title ) {
873 parent_context_label = format_message( config.i18n.child_of || '', [ hierarchy.parent_title ] );
874 }
875 if ( 'parent' === row_variant ) {
876 var rendered_child_count = Math.max( 0, Number( hierarchy.rendered_children_count ) || 0 );
877 var parent_children_template = 1 === rendered_child_count
878 ? config.i18n.parent_child_label || '%1$s · %2$s child'
879 : config.i18n.parent_children_label || '%1$s · %2$s children';
880 type_badge_label = format_message( parent_children_template, [
881 config.i18n.parent_label || '',
882 rendered_child_count
883 ] );
884 } else if ( 'child' === row_variant ) {
885 type_badge_label = config.i18n.child_label || '';
886 }
887 var resource_row_data = Object.assign( {}, resource, {
888 parent_context_label: parent_context_label,
889 collapse_label: format_message( config.i18n.collapse_children_for || config.i18n.collapse_children || '', [ resource.title || '' ] ),
890 columns: columns,
891 expand_label: format_message( config.i18n.expand_children_for || config.i18n.expand_children || '', [ resource.title || '' ] ),
892 hierarchy: hierarchy,
893 i18n: config.i18n || {},
894 is_expanded: hierarchy_is_expanded,
895 parent_label: config.i18n.parent_label || '',
896 row_variant: row_variant,
897 selection_label: format_message( config.i18n.select_resource || '', [ resource.title || '' ] ),
898 selection_enabled: !! ( config.features && config.features.selection ),
899 thumbnail_label: format_message( config.i18n.thumbnail_tooltip || '', [ resource.title || '', description ] ),
900 type_badge_label: type_badge_label
901 } );
902 var resource_row_html = render_component( config, row_template_role, resource_row_data );
903 var resource_row;
904
905 if ( ! resource_row_html ) {
906 return;
907 }
908
909 if ( is_cards_pack && 'parent' === row_variant ) {
910 var child_resources = children_by_parent[ String( resource.id ) ] || [];
911 var child_count = Math.max( child_resources.length, Number( hierarchy.rendered_children_count ) || 0 );
912 var card_group_html = render_component( config, 'card_group', {
913 children_description: format_message( config.i18n.children_belong_to || '', [ resource.title || '' ] ),
914 children_heading: format_message( config.i18n.children_of_count || '', [ resource.title || '', child_count ] ),
915 collapse_label: resource_row_data.collapse_label,
916 expand_label: resource_row_data.expand_label,
917 is_expanded: hierarchy_is_expanded,
918 parent_id: resource.id,
919 parent_node_id: hierarchy.node_id,
920 stack_items: child_resources.slice( 0, 3 )
921 } );
922 if ( ! card_group_html ) {
923 return;
924 }
925 rows_element.insertAdjacentHTML( 'beforeend', card_group_html );
926 card_groups[ String( resource.id ) ] = rows_element.lastElementChild;
927 var parent_slot = card_groups[ String( resource.id ) ].querySelector( '[data-wpbc-booking-resource-card-parent-slot]' );
928 parent_slot.insertAdjacentHTML( 'beforeend', resource_row_html );
929 resource_row = parent_slot.lastElementChild;
930 } else if ( is_cards_pack && 'child' === row_variant && card_groups[ String( hierarchy.parent_id ) ] ) {
931 var children_slot = card_groups[ String( hierarchy.parent_id ) ].querySelector( '[data-wpbc-booking-resource-card-children-slot]' );
932 children_slot.insertAdjacentHTML( 'beforeend', resource_row_html );
933 resource_row = children_slot.lastElementChild;
934 } else {
935 rows_element.insertAdjacentHTML( 'beforeend', resource_row_html );
936 resource_row = rows_element.lastElementChild;
937 }
938 if ( ! resource_row ) {
939 return;
940 }
941
942 label_target = resource_row.querySelector( '[data-wpbc-booking-resource-labels]' );
943 price_target = resource_row.querySelector( '[data-wpbc-booking-resource-price]' );
944 action_target = resource_row.querySelector( '[data-wpbc-booking-resource-actions]' );
945 if ( label_target ) {
946 label_target.innerHTML = render_component( config, 'labels', {
947 aria_label: config.i18n.column_labels || '',
948 empty_label: config.i18n.no_labels || '',
949 labels: Array.isArray( resource.labels ) ? resource.labels.map( function ( label ) {
950 return Object.assign( {}, label, { class_name: classic_label_classes[ label.kind ] || '' } );
951 } ) : []
952 } );
953 }
954 if ( price_target ) {
955 price_target.innerHTML = render_component( config, 'price', {
956 empty_label: config.i18n.price_unavailable || '',
957 price: resource.price || {}
958 } );
959 }
960 if ( action_target ) {
961 action_target.innerHTML = render_component( config, 'action_menu', {
962 actions: Array.isArray( resource.action_items ) ? resource.action_items.map( function ( action ) {
963 var action_classes = { adjust_capacity: 'capacity', delete_resource: 'delete', edit_resource: 'edit', publish_resource: 'publish' };
964 var action_id = String( action.id || '' );
965 return Object.assign( {}, action, { class_name: 'wpbc_booking_resources__action_' + ( action_classes[ action_id ] || action_id ) } );
966 } ) : [],
967 aria_label: format_message( config.i18n.actions_for || '', [ resource.title || '' ] ),
968 empty_label: config.i18n.no_actions || '',
969 menu_id: 'wpbc_' + config.id + '_actions_' + String( resource.id ),
970 resource_id: resource.id
971 } );
972 }
973 if ( hierarchy_enabled && 'child' === row_variant && hierarchy.is_last_sibling ) {
974 var parent_resource = parent_resources[ String( hierarchy.parent_id ) ];
975 if ( parent_resource ) {
976 var summary_target = is_cards_pack && card_groups[ String( hierarchy.parent_id ) ]
977 ? card_groups[ String( hierarchy.parent_id ) ].querySelector( '[data-wpbc-booking-resource-card-parent-slot]' )
978 : rows_element;
979 summary_target.insertAdjacentHTML( 'beforeend', render_component( config, 'child_summary', {
980 children_label: get_children_summary_label( parent_resource, config.i18n || {} ),
981 collapse_label: format_message( config.i18n.collapse_children_for || config.i18n.collapse_children || '', [ parent_resource.title || '' ] ),
982 columns: columns,
983 expand_label: format_message( config.i18n.expand_children_for || config.i18n.expand_children || '', [ parent_resource.title || '' ] ),
984 is_expanded: hierarchy_is_expanded,
985 parent_id: hierarchy.parent_id,
986 parent_node_id: parent_resource.hierarchy.node_id,
987 selection_enabled: !! ( config.features && config.features.selection )
988 } ) );
989 }
990 }
991 } );
992 synchronize_overflow_tooltips( catalog_mount );
993
994 pagination = response.pagination || {};
995 pagination_element.innerHTML = render_component( config, 'pagination', {
996 aria_label: config.i18n.pagination_label || '',
997 has_next: Number( pagination.page_number ) < Number( pagination.total_pages ),
998 has_previous: 1 < Number( pagination.page_number ),
999 items_per_page: Number( pagination.items_per_page ),
1000 items_per_page_options: config.items_per_page && Array.isArray( config.items_per_page.options ) ? config.items_per_page.options : [],
1001 next_label: config.i18n.next_page || '',
1002 next_page: Math.min( Number( pagination.total_pages ), Number( pagination.page_number ) + 1 ),
1003 page_number: Number( pagination.page_number ),
1004 page_number_label: config.i18n.page_number || '',
1005 per_page_label: config.i18n.per_page || '',
1006 previous_label: config.i18n.previous_page || '',
1007 previous_page: Math.max( 1, Number( pagination.page_number ) - 1 ),
1008 results_status: format_message( config.i18n.results_status || '', [ pagination.items_from, pagination.items_to, pagination.total_items ] ),
1009 show_label: config.i18n.show || '',
1010 total_pages: Math.max( 1, Number( pagination.total_pages ) )
1011 } );
1012 synchronize_booking_resources_toolbar( config, response );
1013
1014 if ( pending_focus_direction ) {
1015 catalog_heading = catalog_mount.querySelector( '[data-wpbc-catalog-heading]' );
1016 pending_focus_direction = '';
1017 if ( catalog_heading && 'function' === typeof catalog_heading.focus ) {
1018 catalog_heading.focus();
1019 }
1020 }
1021
1022 return rows_element.querySelectorAll( '[data-wpbc-ui-catalog-selectable-row][data-wpbc-booking-resource-id]' ).length === response.items.length;
1023 }
1024
1025 /**
1026 * Return the number of currently visible cells in one Resource row.
1027 *
1028 * @param {HTMLElement} resource_row Rendered Resource table row.
1029 * @return {number} Safe details-row colspan.
1030 */
1031 function get_details_colspan( resource_row ) {
1032 var visible_cells = 0;
1033
1034 if ( resource_row && 'TR' === resource_row.tagName ) {
1035 Array.prototype.forEach.call( resource_row.cells || [], function ( cell ) {
1036 if ( ! cell.hidden && 'none' !== window.getComputedStyle( cell ).display ) {
1037 visible_cells += 1;
1038 }
1039 } );
1040 }
1041
1042 return Math.max( 1, visible_cells );
1043 }
1044
1045 /**
1046 * Resolve the rendered Resource container that owns an interactive control.
1047 *
1048 * Resource controls repeat the Resource ID for event dispatch. Limiting this
1049 * lookup to selectable row/card containers prevents lazy templates from being
1050 * inserted beside the control itself.
1051 *
1052 * @param {Element|null} source_element Element inside a Resource row or card.
1053 * @return {HTMLElement|null} Owning Resource row/card, or null when unavailable.
1054 */
1055 function get_resource_item_container( source_element ) {
1056 if ( ! source_element || 'function' !== typeof source_element.closest ) {
1057 return null;
1058 }
1059
1060 return source_element.closest( '[data-wpbc-ui-catalog-selectable-row][data-wpbc-booking-resource-id]' );
1061 }
1062
1063 /**
1064 * Synchronize Cards-pack parent stages with shared hierarchy visibility.
1065 *
1066 * Child cards remain normal shared hierarchy nodes. This adapter only mirrors
1067 * their computed visibility onto the presentation-only tray wrapper.
1068 *
1069 * @param {HTMLElement} catalog_mount Mounted Booking Resources catalog.
1070 * @return {void}
1071 */
1072 function synchronize_card_group_panels( catalog_mount ) {
1073 var catalog_root;
1074
1075 if ( ! catalog_mount ) {
1076 return;
1077 }
1078
1079 catalog_root = catalog_mount.hasAttribute( 'data-wpbc-catalog-id' )
1080 ? catalog_mount
1081 : catalog_mount.querySelector( '[data-wpbc-catalog-id]' );
1082
1083 // The shared controller owns the active-pack attribute on the inner catalog root.
1084 if ( ! catalog_root || 'cards' !== catalog_root.getAttribute( 'data-wpbc-template-pack' ) ) {
1085 return;
1086 }
1087
1088 catalog_root.querySelectorAll( '[data-wpbc-booking-resource-card-group]' ).forEach( function ( card_group ) {
1089 var children_panel = card_group.querySelector( '[data-wpbc-booking-resource-card-children-panel]' );
1090 var visible_child = card_group.querySelector( '[data-wpbc-booking-resource-card-children-slot] [data-wpbc-ui-catalog-parent-node-id]:not([hidden])' );
1091
1092 if ( children_panel ) {
1093 children_panel.hidden = ! visible_child;
1094 card_group.classList.toggle( 'is-expanded', !! visible_child );
1095 }
1096 } );
1097 }
1098
1099 /**
1100 * Synchronize one details disclosure button.
1101 *
1102 * @param {HTMLElement|null} toggle_button Disclosure button.
1103 * @param {boolean} is_expanded Whether its details row is open.
1104 * @return {void}
1105 */
1106 function set_details_toggle_state( toggle_button, is_expanded ) {
1107 var icon;
1108 var label;
1109
1110 if ( ! toggle_button ) {
1111 return;
1112 }
1113
1114 label = toggle_button.getAttribute( is_expanded ? 'data-hide-label' : 'data-show-label' ) || '';
1115 toggle_button.setAttribute( 'aria-expanded', is_expanded ? 'true' : 'false' );
1116 toggle_button.setAttribute( 'aria-label', label );
1117 toggle_button.setAttribute( 'title', label );
1118 icon = toggle_button.querySelector( 'span[aria-hidden="true"]' );
1119 if ( icon ) {
1120 icon.className = is_expanded ? 'wpbc-bi-chevron-up' : 'wpbc-bi-chevron-down';
1121 }
1122 }
1123
1124 /**
1125 * Close the active details row and cancel its lazy request.
1126 *
1127 * @param {boolean} restore_focus Whether to return focus to the disclosure.
1128 * @return {void}
1129 */
1130 function close_details_row( restore_focus ) {
1131 var active_row = document.querySelector( '[data-wpbc-booking-resource-details-row]' );
1132 var focus_target = details_toggle_button;
1133
1134 details_request_sequence += 1;
1135 if ( details_abort_controller && 'function' === typeof details_abort_controller.abort ) {
1136 details_abort_controller.abort();
1137 }
1138 details_abort_controller = null;
1139 if ( active_row && active_row.parentNode ) {
1140 active_row.parentNode.removeChild( active_row );
1141 }
1142 set_details_toggle_state( details_toggle_button, false );
1143 if ( details_toggle_button ) {
1144 var source_row = get_resource_item_container( details_toggle_button );
1145 if ( source_row ) {
1146 source_row.classList.remove( 'is-details-expanded' );
1147 }
1148 }
1149 details_resource_id = 0;
1150 details_toggle_button = null;
1151
1152 if ( restore_focus && focus_target && document.documentElement.contains( focus_target ) && 'function' === typeof focus_target.focus ) {
1153 focus_target.focus();
1154 }
1155 }
1156
1157 /**
1158 * Replace the active details row through the registered WP template.
1159 *
1160 * @param {Object} config Catalog configuration.
1161 * @param {HTMLElement} resource_row Source Resource row.
1162 * @param {Object} template_data Presentation-only template state.
1163 * @return {HTMLElement|null} Rendered details row.
1164 */
1165 function render_details_row( config, resource_row, template_data ) {
1166 var active_row = document.querySelector( '[data-wpbc-booking-resource-details-row]' );
1167 var card_group = resource_row ? resource_row.closest( '[data-wpbc-booking-resource-card-group]' ) : null;
1168 var details_html = render_component( config, 'details', template_data );
1169 var insertion_target = card_group || resource_row;
1170
1171 if ( ! details_html || ! insertion_target || ! insertion_target.parentNode ) {
1172 return null;
1173 }
1174 if ( active_row && active_row.parentNode ) {
1175 active_row.parentNode.removeChild( active_row );
1176 }
1177 insertion_target.insertAdjacentHTML( 'afterend', details_html );
1178
1179 return insertion_target.nextElementSibling;
1180 }
1181
1182 /**
1183 * Return a safe message from a normalized details error response.
1184 *
1185 * @param {Object} response Normalized endpoint response.
1186 * @param {string} fallback Localized fallback message.
1187 * @return {string} Safe plain-text message.
1188 */
1189 function get_details_error_message( response, fallback ) {
1190 return response && response.error && response.error.message
1191 ? String( response.error.message )
1192 : String( fallback || '' );
1193 }
1194
1195 /**
1196 * Open one details row and lazily request its authorized DTO.
1197 *
1198 * @param {Object} config Catalog configuration.
1199 * @param {HTMLElement} toggle_button Details disclosure button.
1200 * @param {HTMLElement} resource_row Source Resource row.
1201 * @param {number} resource_id Booking Resource ID.
1202 * @return {void}
1203 */
1204 function open_details_row( config, toggle_button, resource_row, resource_id ) {
1205 var details_request_id;
1206 var request_body;
1207 var resource_title_element = resource_row.querySelector( '.wpbc_ui_listing__item_title' );
1208 var resource_title = resource_title_element ? resource_title_element.textContent.trim() : '';
1209 var template_base;
1210
1211 close_details_row( false );
1212 details_resource_id = resource_id;
1213 details_toggle_button = toggle_button;
1214 details_request_id = ++details_request_sequence;
1215 set_details_toggle_state( toggle_button, true );
1216 resource_row.classList.add( 'is-details-expanded' );
1217 template_base = {
1218 colspan: get_details_colspan( resource_row ),
1219 resource_id: resource_id,
1220 title: resource_title
1221 };
1222 render_details_row( config, resource_row, Object.assign( {}, template_base, {
1223 loading_label: config.i18n.details_loading || config.i18n.loading || '',
1224 state: 'loading'
1225 } ) );
1226
1227 request_body = new window.URLSearchParams();
1228 request_body.append( 'action', config.details_action );
1229 request_body.append( 'nonce', config.nonce || '' );
1230 request_body.append( 'request_id', String( details_request_id ) );
1231 request_body.append( 'resource_id', String( resource_id ) );
1232 details_abort_controller = 'function' === typeof window.AbortController ? new window.AbortController() : null;
1233
1234 window.fetch( config.ajax_url, {
1235 body: request_body.toString(),
1236 credentials: 'same-origin',
1237 headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
1238 method: 'POST',
1239 signal: details_abort_controller ? details_abort_controller.signal : undefined
1240 } ).then( function ( response ) {
1241 return response.json();
1242 } ).then( function ( response ) {
1243 if ( details_request_id !== details_request_sequence || resource_id !== details_resource_id ) {
1244 return;
1245 }
1246 if ( ! response || true !== response.success || Number( response.request_id ) !== details_request_id || Number( response.resource_id ) !== resource_id || ! response.details || ! Array.isArray( response.details.sections ) ) {
1247 render_details_row( config, resource_row, Object.assign( {}, template_base, {
1248 error_message: get_details_error_message( response, config.i18n.details_load_failed ),
1249 state: 'error'
1250 } ) );
1251 return;
1252 }
1253 render_details_row( config, resource_row, Object.assign( {}, template_base, response.details, {
1254 colspan: get_details_colspan( resource_row ),
1255 state: 'ready'
1256 } ) );
1257 var catalog_mount = document.getElementById( config.mount_id );
1258 synchronize_overflow_tooltips( catalog_mount );
1259 initialize_details_tooltips( catalog_mount );
1260 } ).catch( function ( error ) {
1261 if ( error && 'AbortError' === error.name ) {
1262 return;
1263 }
1264 if ( details_request_id === details_request_sequence && resource_id === details_resource_id ) {
1265 render_details_row( config, resource_row, Object.assign( {}, template_base, {
1266 error_message: config.i18n.details_load_failed || '',
1267 state: 'error'
1268 } ) );
1269 }
1270 } ).then( function () {
1271 if ( details_request_id === details_request_sequence ) {
1272 details_abort_controller = null;
1273 }
1274 } );
1275 }
1276
1277 /**
1278 * Copy one details value without navigating or mutating Resource state.
1279 *
1280 * @param {string} copy_value Plain text to copy.
1281 * @param {HTMLElement} action_button Copy button used to locate status text.
1282 * @param {Object} config Catalog configuration.
1283 * @return {void}
1284 */
1285 function copy_details_value( copy_value, action_button, config ) {
1286 var details_row = action_button.closest( '[data-wpbc-booking-resource-details-row]' );
1287 var resource_id = Number( action_button.getAttribute( 'data-wpbc-booking-resource-id' ) || 0 );
1288 var status_element = details_row
1289 ? details_row.querySelector( '[data-wpbc-booking-resource-copy-status]' )
1290 : document.querySelector( '[data-wpbc-booking-resource-copy-status="' + String( resource_id ) + '"]' );
1291 var copy_promise;
1292
1293 if ( window.navigator.clipboard && 'function' === typeof window.navigator.clipboard.writeText ) {
1294 copy_promise = window.navigator.clipboard.writeText( copy_value );
1295 } else {
1296 copy_promise = new window.Promise( function ( resolve, reject ) {
1297 var copy_input = document.createElement( 'textarea' );
1298 copy_input.value = copy_value;
1299 copy_input.setAttribute( 'readonly', 'readonly' );
1300 copy_input.style.position = 'fixed';
1301 copy_input.style.opacity = '0';
1302 document.body.appendChild( copy_input );
1303 copy_input.select();
1304 if ( document.execCommand( 'copy' ) ) {
1305 resolve();
1306 } else {
1307 reject();
1308 }
1309 document.body.removeChild( copy_input );
1310 } );
1311 }
1312 copy_promise.then( function () {
1313 if ( status_element ) {
1314 status_element.classList.remove( 'has-error' );
1315 status_element.textContent = config.i18n.shortcode_copied || '';
1316 }
1317 } ).catch( function () {
1318 if ( status_element ) {
1319 status_element.classList.add( 'has-error' );
1320 status_element.textContent = config.i18n.shortcode_copy_failed || '';
1321 }
1322 } );
1323 }
1324
1325 /**
1326 * Return the current effective shortcode for one Resource.
1327 *
1328 * The active inspector wins so unsaved customizer changes are used by Copy
1329 * and Publish. A hidden compatibility input is the fallback required by the
1330 * shared Booking Calendar publish wizard.
1331 *
1332 * @param {number} resource_id Booking Resource ID.
1333 * @param {HTMLElement} action_button Optional initiating action.
1334 * @return {string} Current effective shortcode.
1335 */
1336 function get_booking_resource_shortcode( resource_id, action_button ) {
1337 var inspector_shortcode = resource_id === inspector_resource_id
1338 ? document.querySelector( '[data-wpbc-catalog-resource-inspector-form] .wpbc_catalog_booking_resources__editor_code' )
1339 : null;
1340 var hidden_shortcode = document.getElementById( 'booking_resource_shortcode_' + String( resource_id ) );
1341
1342 if ( inspector_shortcode ) {
1343 return String( inspector_shortcode.value || '' );
1344 }
1345 if ( action_button && action_button.getAttribute( 'data-wpbc-booking-resource-shortcode' ) ) {
1346 return String( action_button.getAttribute( 'data-wpbc-booking-resource-shortcode' ) || '' );
1347 }
1348
1349 return hidden_shortcode ? String( hidden_shortcode.value || '' ) : '';
1350 }
1351
1352 /**
1353 * Create or update the hidden input consumed by the shared publish wizard.
1354 *
1355 * @param {number} resource_id Booking Resource ID.
1356 * @param {string} shortcode Effective Booking shortcode.
1357 * @return {HTMLInputElement|null} Compatibility input or null.
1358 */
1359 function synchronize_booking_resource_shortcode_input( resource_id, shortcode ) {
1360 var input;
1361
1362 if ( ! resource_id ) {
1363 return null;
1364 }
1365 input = document.getElementById( 'booking_resource_shortcode_' + String( resource_id ) );
1366 if ( ! input ) {
1367 input = document.createElement( 'input' );
1368 input.type = 'hidden';
1369 input.id = 'booking_resource_shortcode_' + String( resource_id );
1370 input.setAttribute( 'data-wpbc-catalog-shortcode-compatibility', String( resource_id ) );
1371 document.body.appendChild( input );
1372 }
1373 input.value = String( shortcode || '' );
1374
1375 return input;
1376 }
1377
1378 /**
1379 * Open the shared Booking Calendar shortcode customizer for one Resource.
1380 *
1381 * @param {number} resource_id Booking Resource ID.
1382 * @param {string} shortcode Current shortcode.
1383 * @return {void}
1384 */
1385 function customize_booking_resource_shortcode( resource_id, shortcode ) {
1386 synchronize_booking_resource_shortcode_input( resource_id, shortcode );
1387 if ( 'function' === typeof window.wpbc_resource_page_btn_click ) {
1388 window.wpbc_resource_page_btn_click( resource_id, shortcode );
1389 }
1390 }
1391
1392 /**
1393 * Open the shared Booking Calendar embed/create-page wizard.
1394 *
1395 * @param {number} resource_id Booking Resource ID.
1396 * @param {string} shortcode Current shortcode.
1397 * @return {void}
1398 */
1399 function publish_booking_resource_shortcode( resource_id, shortcode, trigger_button ) {
1400 synchronize_booking_resource_shortcode_input( resource_id, shortcode );
1401 if ( 'function' === typeof window.wpbc_publish_booking_form__open ) {
1402 window.wpbc_publish_booking_form__open( resource_id, shortcode, trigger_button );
1403 }
1404 }
1405
1406 /**
1407 * Open the informational Free-edition Booking Resource upgrade dialog.
1408 *
1409 * @param {HTMLElement} trigger_button Button that opened the dialog.
1410 * @return {void}
1411 */
1412 function open_booking_resource_upgrade_dialog( trigger_button ) {
1413 var modal_element = document.getElementById( 'wpbc_catalog_booking_resources__upgrade_modal' );
1414 var upgrade_url = trigger_button ? trigger_button.getAttribute( 'data-wpbc-catalog-booking-resource-upgrade-url' ) : '';
1415
1416 if ( modal_element && window.jQuery && 'function' === typeof window.jQuery( modal_element ).wpbc_my_modal ) {
1417 window.jQuery( modal_element )
1418 .off( 'hidden.wpbc.modal.wpbcCatalogResourceUpgrade hidden.bs.modal.wpbcCatalogResourceUpgrade' )
1419 .one( 'hidden.wpbc.modal.wpbcCatalogResourceUpgrade hidden.bs.modal.wpbcCatalogResourceUpgrade', function () {
1420 if ( trigger_button && document.contains( trigger_button ) ) {
1421 trigger_button.focus();
1422 }
1423 } )
1424 .wpbc_my_modal( 'show' );
1425 return;
1426 }
1427
1428 if ( upgrade_url ) {
1429 window.open( upgrade_url, '_blank', 'noopener' );
1430 }
1431 }
1432
1433 /**
1434 * Open the reusable native catalog message dialog.
1435 *
1436 * Message content is assigned with textContent so translated or server-provided
1437 * text cannot become dialog markup. The browser alert remains a resilience
1438 * fallback when the Booking Calendar modal runtime is unavailable.
1439 *
1440 * @param {string} message Message shown in the dialog body.
1441 * @param {string} title Optional dialog heading.
1442 * @param {HTMLElement} trigger_button Control that opened the dialog.
1443 * @return {boolean} True when the native dialog opened; otherwise false.
1444 */
1445 function open_booking_resource_message_dialog( message, title, trigger_button ) {
1446 var modal_element = document.getElementById( 'wpbc_catalog_booking_resources__message_modal' );
1447 var title_element = document.getElementById( 'wpbc_catalog_booking_resources__message_modal_title' );
1448 var description_element = document.getElementById( 'wpbc_catalog_booking_resources__message_modal_description' );
1449
1450 if ( message && modal_element && description_element && window.jQuery && 'function' === typeof window.jQuery( modal_element ).wpbc_my_modal ) {
1451 description_element.textContent = message;
1452 if ( title_element ) {
1453 title_element.textContent = title || title_element.getAttribute( 'data-wpbc-default-title' ) || '';
1454 }
1455 window.jQuery( modal_element )
1456 .off( 'hidden.wpbc.modal.wpbcCatalogResourceMessage hidden.bs.modal.wpbcCatalogResourceMessage' )
1457 .one( 'hidden.wpbc.modal.wpbcCatalogResourceMessage hidden.bs.modal.wpbcCatalogResourceMessage', function () {
1458 if ( trigger_button && document.contains( trigger_button ) ) {
1459 trigger_button.focus();
1460 }
1461 } )
1462 .wpbc_my_modal( 'show' );
1463 return true;
1464 }
1465
1466 if ( message && 'function' === typeof window.alert ) {
1467 window.alert( message );
1468 }
1469 return false;
1470 }
1471
1472 /**
1473 * Return the template-driven inspector host.
1474 *
1475 * @return {HTMLElement|null} Inspector host or null.
1476 */
1477 function get_inspector_host() {
1478 return document.querySelector( '[data-wpbc-catalog-booking-resources-inspector-host]' );
1479 }
1480
1481 /**
1482 * Return the sticky native-sidebar footer.
1483 *
1484 * @return {HTMLElement|null} Footer element or null.
1485 */
1486 function get_inspector_footer() {
1487 return document.querySelector( '[data-wpbc-ui-catalog-inspector-footer]' );
1488 }
1489
1490 /**
1491 * Return the shared native inspector state workflow.
1492 *
1493 * @param {Object} config Catalog configuration.
1494 * @return {Object|false} Shared inspector workflow or false.
1495 */
1496 function get_inspector_workflow( config ) {
1497 if ( inspector_workflow_controller ) {
1498 return inspector_workflow_controller;
1499 }
1500 if ( ! window.wpbc_ui_catalog || 'function' !== typeof window.wpbc_ui_catalog.create_inspector_workflow ) {
1501 return false;
1502 }
1503
1504 inspector_workflow_controller = window.wpbc_ui_catalog.create_inspector_workflow( {
1505 expand: expand_inspector_sidebar,
1506 get_footer: get_inspector_footer,
1507 get_host: get_inspector_host,
1508 render_shell: function ( shell_data ) { return render_component( config, 'inspector', shell_data ); },
1509 shell_data: {
1510 catalog_id: config.id,
1511 empty_icon: 'wpbc-bi-pencil-square',
1512 empty_message: config.i18n.inspector_empty_message || '',
1513 empty_title: config.i18n.inspector_empty_title || '',
1514 loading_label: config.i18n.inspector_loading || config.i18n.loading || ''
1515 }
1516 } );
1517
1518 return inspector_workflow_controller;
1519 }
1520
1521 /**
1522 * Render the shared inspector fallback-state shell once.
1523 *
1524 * @param {Object} config Catalog configuration.
1525 * @return {boolean} True when the shell is available.
1526 */
1527 function mount_inspector_shell( config ) {
1528 var inspector_workflow = get_inspector_workflow( config );
1529
1530 return !! inspector_workflow && inspector_workflow.mount();
1531 }
1532
1533 /**
1534 * Expand the native right sidebar after an explicit editor action.
1535 *
1536 * @return {void}
1537 */
1538 function expand_inspector_sidebar() {
1539 synchronize_inspector_width();
1540 if ( 'function' === typeof window.wpbc_admin_ui__sidebar_right__do_max ) {
1541 window.wpbc_admin_ui__sidebar_right__do_max();
1542 }
1543 document.dispatchEvent( new CustomEvent( 'wpbc_setup_wizard_layout_changed' ) );
1544 }
1545
1546 /**
1547 * Apply the established wider sidebar only while creating Resources.
1548 *
1549 * The new catalog owns its class and styling so it does not depend on the old
1550 * listing assets while retaining the same native-sidebar width contract.
1551 *
1552 * @return {void}
1553 */
1554 function synchronize_inspector_width() {
1555 var host = get_inspector_host();
1556 var sidebar = host ? host.closest( '.wpbc_ui_el__vert_right_bar__wrapper' ) : null;
1557
1558 if ( sidebar ) {
1559 sidebar.classList.toggle( 'wpbc_catalog_booking_resources__inspector_width--wide', 'create' === inspector_mode );
1560 }
1561 }
1562
1563 /**
1564 * Mark only the Resource currently owned by the inspector.
1565 *
1566 * @param {number} resource_id Resource ID or zero to clear highlighting.
1567 * @return {void}
1568 */
1569 function mark_inspector_resource_row( resource_id ) {
1570 document.querySelectorAll( '[data-wpbc-booking-resource-id].is-editor-active' ).forEach( function ( row ) {
1571 row.classList.remove( 'is-editor-active' );
1572 } );
1573 if ( resource_id ) {
1574 var row = document.querySelector( '[data-wpbc-booking-resource-id="' + String( resource_id ) + '"]' );
1575 if ( row ) {
1576 row.classList.add( 'is-editor-active' );
1577 row.scrollIntoView( { block: 'nearest', behavior: 'smooth' } );
1578 }
1579 }
1580 }
1581
1582 /**
1583 * Synchronize shared empty, loading, error, form, and footer states.
1584 *
1585 * @param {string} state Empty, loading, error, or form.
1586 * @param {string} message Optional safe error message.
1587 * @return {void}
1588 */
1589 function set_inspector_state( state, message ) {
1590 if ( inspector_workflow_controller ) {
1591 inspector_workflow_controller.set_state( state, message );
1592 }
1593 }
1594
1595 /**
1596 * Serialize current editable field values for dirty-state comparison.
1597 *
1598 * @return {string} Stable JSON field snapshot.
1599 */
1600 function serialize_inspector_fields() {
1601 var fields = {};
1602 document.querySelectorAll( '[data-wpbc-catalog-resource-inspector-form] [data-wpbc-catalog-resource-radio-field]:checked' ).forEach( function ( field ) {
1603 fields[ field.getAttribute( 'data-wpbc-catalog-resource-radio-field' ) || '' ] = field.value;
1604 } );
1605 document.querySelectorAll( '[data-wpbc-catalog-resource-inspector-form] [data-wpbc-catalog-resource-field]' ).forEach( function ( field ) {
1606 fields[ field.getAttribute( 'data-wpbc-catalog-resource-field' ) || '' ] = field.value;
1607 } );
1608
1609 return JSON.stringify( fields );
1610 }
1611
1612 /**
1613 * Return the currently selected Resource creation mode.
1614 *
1615 * @return {string} Independent or children.
1616 */
1617 function get_inspector_creation_mode() {
1618 var selected_mode = document.querySelector( '[data-wpbc-catalog-resource-radio-field="creation_mode"]:checked' );
1619 var hidden_mode = document.querySelector( '[data-wpbc-catalog-resource-field="creation_mode"]' );
1620
1621 return String( selected_mode ? selected_mode.value : hidden_mode ? hidden_mode.value : 'independent' );
1622 }
1623
1624 /**
1625 * Synchronize create-only conditional fields and radio-card presentation.
1626 *
1627 * These controls improve clarity only. The create service independently
1628 * authorizes the selected parent and derives all inherited values.
1629 *
1630 * @return {void}
1631 */
1632 function synchronize_create_inspector_controls() {
1633 if ( 'create' !== inspector_mode ) {
1634 return;
1635 }
1636
1637 var creation_mode = get_inspector_creation_mode();
1638 var parent_wrap = document.querySelector( '[data-wpbc-catalog-resource-field-wrap="parent_id"]' );
1639
1640 document.querySelectorAll( '[data-wpbc-catalog-resource-radio-field="creation_mode"]' ).forEach( function ( radio ) {
1641 var choice = radio.closest( 'label' );
1642 if ( choice ) {
1643 choice.classList.toggle( 'is-selected', radio.checked );
1644 }
1645 } );
1646 if ( parent_wrap ) {
1647 parent_wrap.hidden = 'children' !== creation_mode;
1648 parent_wrap.classList.toggle( 'is-conditionally-hidden', 'children' !== creation_mode );
1649 }
1650 [ 'base_cost', 'default_form', 'owner_user_id' ].forEach( function ( field_key ) {
1651 var field_wrap = document.querySelector( '[data-wpbc-catalog-resource-field-wrap="' + field_key + '"]' );
1652 if ( field_wrap ) {
1653 field_wrap.hidden = 'children' === creation_mode;
1654 field_wrap.classList.toggle( 'is-conditionally-hidden', 'children' === creation_mode );
1655 }
1656 } );
1657 }
1658
1659 /**
1660 * Synchronize dirty state and the sticky primary action.
1661 *
1662 * @return {void}
1663 */
1664 function synchronize_inspector_dirty_state() {
1665 var save_button = document.querySelector( '[data-wpbc-ui-catalog-inspector-save]' );
1666 var form = document.querySelector( '[data-wpbc-catalog-resource-inspector-form]' );
1667 var save_is_busy = !! save_button && save_button.classList.contains( 'is-busy' );
1668
1669 inspector_dirty = !! form && serialize_inspector_fields() !== inspector_original_fields;
1670 if ( save_button ) {
1671 var title_field = form ? form.querySelector( '[data-wpbc-catalog-resource-field="title"]' ) : null;
1672 var parent_field = form ? form.querySelector( '[data-wpbc-catalog-resource-field="parent_id"]' ) : null;
1673 var create_is_valid = 'create' !== inspector_mode
1674 || ( form
1675 && 'true' === form.getAttribute( 'data-can-create' )
1676 && title_field
1677 && '' !== String( title_field.value || '' ).trim()
1678 && ( 'children' !== get_inspector_creation_mode() || ( parent_field && Number( parent_field.value ) > 0 ) ) );
1679
1680 save_button.disabled = save_is_busy || ! form || ! inspector_dirty || ! create_is_valid;
1681 }
1682 }
1683
1684 /**
1685 * Confirm whether the active inspector may be replaced or closed.
1686 *
1687 * @param {Object} config Catalog configuration.
1688 * @return {boolean} True when navigation may continue.
1689 */
1690 function can_discard_inspector( config ) {
1691 if ( inspector_mutation_in_progress ) {
1692 return false;
1693 }
1694
1695 return ! inspector_dirty || window.confirm( config.i18n.inspector_discard || '' );
1696 }
1697
1698 /**
1699 * Close the inspector without changing catalog checkbox selection.
1700 *
1701 * @param {Object} config Catalog configuration.
1702 * @param {boolean} confirm_discard Whether dirty state needs confirmation.
1703 * @return {boolean} True when closed.
1704 */
1705 function close_inspector( config, confirm_discard ) {
1706 if ( confirm_discard && ! can_discard_inspector( config ) ) {
1707 return false;
1708 }
1709
1710 inspector_request_sequence += 1;
1711 inspector_dirty = false;
1712 inspector_mode = '';
1713 inspector_original_fields = '';
1714 inspector_resource_id = 0;
1715 inspector_resource_ids = [];
1716 inspector_bulk_operations = {};
1717 inspector_review_token = '';
1718 inspector_selection_stale = false;
1719 inspector_tracks_selection = false;
1720 inspector_capacity_context = null;
1721 inspector_capacity_detach_ids = [];
1722 inspector_capacity_decrease_action = 'detach';
1723 inspector_capacity_target = 0;
1724 synchronize_inspector_width();
1725 set_inspector_state( 'empty', '' );
1726 mark_inspector_resource_row( 0 );
1727 if ( 'function' === typeof window.wpbc_admin_ui__sidebar_right__do_hide ) {
1728 window.wpbc_admin_ui__sidebar_right__do_hide();
1729 }
1730 document.dispatchEvent( new CustomEvent( 'wpbc_setup_wizard_layout_changed' ) );
1731 if ( inspector_focus_target && document.documentElement.contains( inspector_focus_target ) && 'function' === typeof inspector_focus_target.focus ) {
1732 inspector_focus_target.focus();
1733 }
1734 inspector_focus_target = null;
1735
1736 return true;
1737 }
1738
1739 /**
1740 * Post one authenticated inspector request.
1741 *
1742 * @param {Object} config Catalog configuration.
1743 * @param {string} action AJAX action.
1744 * @param {Object} values Request values.
1745 * @return {Promise<Object>} Parsed WordPress response.
1746 */
1747 function request_inspector( config, action, values ) {
1748 var body = new window.URLSearchParams();
1749
1750 body.append( 'action', action );
1751 body.append( 'nonce', config.nonce || '' );
1752 Object.keys( values || {} ).forEach( function ( key ) {
1753 body.append( key, String( values[ key ] ) );
1754 } );
1755
1756 return window.fetch( config.ajax_url, {
1757 body: body.toString(),
1758 credentials: 'same-origin',
1759 headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
1760 method: 'POST'
1761 } ).then( function ( response ) {
1762 return response.json();
1763 } );
1764 }
1765
1766 /**
1767 * Return the explicit selection owned by the shared catalog controller.
1768 *
1769 * @param {Object} config Catalog configuration.
1770 * @return {Array<number>} Selected positive Resource IDs.
1771 */
1772 function get_selected_resource_ids( config ) {
1773 var mount = document.getElementById( config.mount_id );
1774 var selection = mount && mount._wpbc_ui_catalog_selection_controller;
1775 var selected_ids = selection && 'function' === typeof selection.get_selected_ids ? selection.get_selected_ids() : [];
1776
1777 return selected_ids.map( Number ).filter( function ( resource_id ) {
1778 return resource_id > 0;
1779 } );
1780 }
1781
1782 /**
1783 * Compare two Resource-ID selections without relying on event ordering.
1784 *
1785 * @param {Array<number|string>} first_ids First ID list.
1786 * @param {Array<number|string>} second_ids Second ID list.
1787 * @return {boolean} True when both lists contain the same Resource IDs.
1788 */
1789 function resource_id_lists_match( first_ids, second_ids ) {
1790 var normalize_ids = function ( resource_ids ) {
1791 return ( resource_ids || [] ).map( Number ).filter( function ( resource_id ) {
1792 return resource_id > 0;
1793 } ).sort( function ( first_id, second_id ) {
1794 return first_id - second_id;
1795 } );
1796 };
1797
1798 return JSON.stringify( normalize_ids( first_ids ) ) === JSON.stringify( normalize_ids( second_ids ) );
1799 }
1800
1801 /**
1802 * Return a localized selection-count label.
1803 *
1804 * @param {Object} config Catalog configuration.
1805 * @param {number} count Number of selected Resources.
1806 * @return {string} Count and localized noun.
1807 */
1808 function get_selection_count_label( config, count ) {
1809 return String( count ) + ' ' + ( 1 === Number( count ) ? config.i18n.resource_selected || '' : config.i18n.resources_selected || '' );
1810 }
1811
1812 /**
1813 * Configure the native sticky footer for the active inspector workflow.
1814 *
1815 * @param {string} form_id Form receiving the submit action.
1816 * @param {string} button_label Localized primary label.
1817 * @param {boolean} destructive Whether the action permanently deletes data.
1818 * @param {boolean} disabled Whether submission starts disabled.
1819 * @return {void}
1820 */
1821 function configure_inspector_footer( form_id, button_label, destructive, disabled ) {
1822 var footer = get_inspector_footer();
1823 var save_button = footer ? footer.querySelector( '[data-wpbc-ui-catalog-inspector-save]' ) : null;
1824 var cancel_button = footer ? footer.querySelector( '[data-wpbc-ui-catalog-inspector-cancel]' ) : null;
1825 var delete_workflow;
1826
1827 if ( save_button ) {
1828 save_button.classList.remove( 'is-busy', 'button-link-delete', 'wpbc_catalog_booking_resources__delete_submit', 'wpbc_ui_listing__inspector_action--destructive', 'wpbc_booking_resources__delete_confirm_button', 'wpbc_ui_catalog_delete_review__apply' );
1829 save_button.classList.toggle( 'button-primary', ! destructive );
1830 save_button.classList.toggle( 'button-secondary', !! destructive );
1831 save_button.classList.toggle( 'wpbc_ui_listing__inspector_action--destructive', !! destructive );
1832 save_button.classList.toggle( 'wpbc_booking_resources__delete_confirm_button', !! destructive );
1833 save_button.textContent = button_label || '';
1834 save_button.setAttribute( 'form', form_id );
1835 save_button.disabled = !! disabled;
1836 }
1837 if ( cancel_button ) {
1838 cancel_button.textContent = window.wpbc_catalog_booking_resources_config && window.wpbc_catalog_booking_resources_config.i18n
1839 ? window.wpbc_catalog_booking_resources_config.i18n.cancel || cancel_button.textContent
1840 : cancel_button.textContent;
1841 cancel_button.disabled = false;
1842 }
1843 if ( destructive && 'wpbc_catalog_booking_resources_delete_form' === form_id ) {
1844 delete_workflow = get_delete_review_workflow();
1845 if ( delete_workflow ) {
1846 delete_workflow.configure_footer( {
1847 can_apply: true,
1848 footer: footer,
1849 form_id: form_id,
1850 label: button_label
1851 } );
1852 }
1853 }
1854 }
1855
1856 /**
1857 * Emphasize the permanent-deletion acknowledgement.
1858 *
1859 * Restarting the finite animation mirrors the established Booking Resource
1860 * editor behavior when a deletion review opens or acknowledgement is cleared.
1861 *
1862 * @param {HTMLElement|null} acknowledgement Deletion acknowledgement label.
1863 * @return {void}
1864 */
1865 function pulse_delete_acknowledgement( acknowledgement ) {
1866 var delete_workflow = get_delete_review_workflow();
1867
1868 if ( acknowledgement && acknowledgement.matches( '.wpbc_ui_catalog_delete_review__acknowledgement' ) && delete_workflow ) {
1869 delete_workflow.pulse_acknowledgement();
1870 return;
1871 }
1872 if ( ! acknowledgement ) {
1873 return;
1874 }
1875
1876 acknowledgement.classList.remove( 'wpbc_booking_resources__delete_acknowledgement--attention' );
1877 void acknowledgement.offsetWidth;
1878 acknowledgement.classList.add( 'wpbc_booking_resources__delete_acknowledgement--attention' );
1879 }
1880
1881 /**
1882 * Return a safe message from a WordPress inspector response.
1883 *
1884 * @param {Object} response Response payload.
1885 * @param {string} fallback Fallback message.
1886 * @return {string} Plain message.
1887 */
1888 function get_inspector_response_message( response, fallback ) {
1889 return response && response.data && response.data.message ? String( response.data.message ) : String( fallback || '' );
1890 }
1891
1892 /**
1893 * Show a success or error notice in the active inspector.
1894 *
1895 * @param {HTMLFormElement} form Inspector form.
1896 * @param {string} message Safe server or localized message.
1897 * @param {boolean} is_error Whether the notice represents an error.
1898 * @return {void}
1899 */
1900 function show_inspector_message( form, message, is_error ) {
1901 var notice = form ? form.querySelector( '[data-wpbc-catalog-resource-inspector-message]' ) : null;
1902
1903 if ( ! notice ) {
1904 return;
1905 }
1906 notice.classList.toggle( 'notice-error', !! is_error );
1907 notice.classList.toggle( 'notice-success', ! is_error );
1908 notice.hidden = ! message;
1909 var notice_text = notice.querySelector( 'p' );
1910 if ( notice_text ) {
1911 notice_text.textContent = message || '';
1912 }
1913 }
1914
1915 /**
1916 * Move keyboard focus to the heading of a newly rendered reviewed inspector.
1917 *
1918 * @param {HTMLFormElement|null} form Rendered inspector form.
1919 * @return {void}
1920 */
1921 function focus_inspector_heading( form ) {
1922 var heading = form ? form.querySelector( '[data-wpbc-catalog-resource-inspector-heading]' ) : null;
1923
1924 if ( ! heading ) {
1925 return;
1926 }
1927 window.setTimeout( function () {
1928 if ( document.documentElement.contains( heading ) && 'function' === typeof heading.focus ) {
1929 heading.focus();
1930 }
1931 }, 0 );
1932 }
1933
1934 /**
1935 * Show a standard Booking Calendar administration notice.
1936 *
1937 * Successful mutations use the shared top-right notice area used by the
1938 * Booking Calendar settings pages. The boolean return lets callers retain
1939 * an inline fallback when that shared helper is unavailable.
1940 *
1941 * @param {string} message Safe server or localized message.
1942 * @param {string} message_type Notice type accepted by the shared helper.
1943 * @param {number} delay Notice visibility duration in milliseconds.
1944 * @return {boolean} True when the message was displayed.
1945 */
1946 function show_admin_message( message, message_type, delay ) {
1947 var config;
1948 var mount_element;
1949 var notice;
1950 var notice_text;
1951
1952 if ( ! message ) {
1953 return false;
1954 }
1955 if ( 'function' === typeof window.wpbc_admin_show_message ) {
1956 window.wpbc_admin_show_message( message, message_type || 'info', delay || 4000, false );
1957 return true;
1958 }
1959
1960 config = window.wpbc_catalog_booking_resources_config || {};
1961 mount_element = config.mount_id ? document.getElementById( config.mount_id ) : null;
1962 mount_element = mount_element && mount_element.parentNode ? mount_element.parentNode : document.getElementById( 'wpbody-content' ) || document.body;
1963 if ( ! mount_element ) {
1964 return false;
1965 }
1966
1967 notice = document.createElement( 'div' );
1968 notice.className = 'notice notice-' + ( 'error' === message_type ? 'error' : 'success' ) + ' wpbc_catalog_booking_resources__mutation_notice';
1969 notice.setAttribute( 'role', 'error' === message_type ? 'alert' : 'status' );
1970 notice.setAttribute( 'aria-live', 'error' === message_type ? 'assertive' : 'polite' );
1971 notice_text = document.createElement( 'p' );
1972 notice_text.textContent = message;
1973 notice.appendChild( notice_text );
1974 mount_element.insertBefore( notice, mount_element.firstChild );
1975 window.setTimeout( function () {
1976 if ( notice.parentNode ) {
1977 notice.parentNode.removeChild( notice );
1978 }
1979 }, delay || 4000 );
1980
1981 return true;
1982 }
1983
1984 /**
1985 * Open and focus one server-defined edit-inspector section.
1986 *
1987 * The shared collapsible controller keeps exclusive group state and ARIA
1988 * attributes synchronized. The header click is retained as a compatibility
1989 * fallback for older administration bundles.
1990 *
1991 * @param {string} section_id Inspector section identifier.
1992 * @return {boolean} True when the requested section exists.
1993 */
1994 function activate_inspector_section( section_id ) {
1995 var form;
1996 var group;
1997 var root;
1998 var controller;
1999 var header;
2000
2001 section_id = String( section_id || '' );
2002 if ( ! /^[a-z0-9_]+$/.test( section_id ) ) {
2003 return false;
2004 }
2005
2006 form = document.querySelector( '[data-wpbc-catalog-resource-inspector-form][data-mode="edit"]' );
2007 group = form ? form.querySelector( '[data-group="catalog-booking-resource-' + section_id + '"]' ) : null;
2008 header = group ? group.querySelector( '.group__header' ) : null;
2009 if ( ! group || ! header ) {
2010 return false;
2011 }
2012
2013 root = group.closest( '.wpbc_collapsible' );
2014 controller = root && root.__wpbc_collapsible_instance ? root.__wpbc_collapsible_instance : null;
2015 if ( controller && 'function' === typeof controller.expand ) {
2016 controller.expand( group );
2017 } else if ( ! group.classList.contains( 'is-open' ) ) {
2018 header.click();
2019 }
2020
2021 window.setTimeout( function () {
2022 group.scrollIntoView( { behavior: 'smooth', block: 'start' } );
2023 header.focus();
2024 }, 120 );
2025
2026 return true;
2027 }
2028
2029 /**
2030 * Render one server-authoritative create or edit schema.
2031 *
2032 * @param {Object} config Catalog configuration.
2033 * @param {Object} schema Inspector schema.
2034 * @param {boolean} focus_title Whether the title control receives focus.
2035 * @return {boolean} True when rendered.
2036 */
2037 function render_inspector_schema( config, schema, focus_title ) {
2038 var host = get_inspector_host();
2039 var form_target = host ? host.querySelector( '[data-wpbc-ui-catalog-inspector-form]' ) : null;
2040 var footer = get_inspector_footer();
2041 var save_button = footer ? footer.querySelector( '[data-wpbc-ui-catalog-inspector-save]' ) : null;
2042 var template_role = 'create' === schema.mode ? 'inspector_create' : 'inspector_edit';
2043
2044 if ( ! form_target ) {
2045 return false;
2046 }
2047 form_target.innerHTML = render_component( config, template_role, { i18n: config.i18n || {}, schema: schema } );
2048 var form = form_target.querySelector( '[data-wpbc-catalog-resource-inspector-form]' );
2049 if ( ! form ) {
2050 return false;
2051 }
2052 if ( 'create' === schema.mode ) {
2053 form.setAttribute( 'data-can-create', schema.can_create ? 'true' : 'false' );
2054 }
2055 inspector_mode = schema.mode;
2056 inspector_resource_id = Number( schema.resource_id ) || 0;
2057 if ( inspector_resource_id ) {
2058 var shortcode_field = form.querySelector( '.wpbc_catalog_booking_resources__editor_code' );
2059 if ( shortcode_field ) {
2060 synchronize_booking_resource_shortcode_input( inspector_resource_id, shortcode_field.value );
2061 }
2062 }
2063 set_inspector_state( 'form', '' );
2064 if ( save_button ) {
2065 configure_inspector_footer( 'wpbc_catalog_booking_resource_inspector_form', 'create' === inspector_mode ? config.i18n.add_resource || '' : config.i18n.save_changes || '', false, true );
2066 }
2067 var cancel_button = footer ? footer.querySelector( '[data-wpbc-ui-catalog-inspector-cancel]' ) : null;
2068 if ( cancel_button ) {
2069 cancel_button.disabled = false;
2070 }
2071 if ( 'function' === typeof window.WPBC_Collapsible_AutoInit ) {
2072 window.WPBC_Collapsible_AutoInit();
2073 }
2074 synchronize_all_inspector_numeric_ranges();
2075 synchronize_create_inspector_controls();
2076 inspector_original_fields = serialize_inspector_fields();
2077 inspector_dirty = false;
2078 synchronize_inspector_dirty_state();
2079 mark_inspector_resource_row( inspector_resource_id );
2080 if ( false !== focus_title ) {
2081 window.setTimeout( function () {
2082 var title_field = form.querySelector( '[data-wpbc-catalog-resource-field="title"]' );
2083 if ( title_field && 'function' === typeof title_field.focus ) {
2084 title_field.focus();
2085 }
2086 }, 120 );
2087 }
2088
2089 return true;
2090 }
2091
2092 /**
2093 * Load and open one create or edit inspector.
2094 *
2095 * @param {Object} config Catalog configuration.
2096 * @param {string} mode Create or edit.
2097 * @param {number} resource_id Resource ID for edit.
2098 * @param {HTMLElement} focus_target Initiating control.
2099 * @param {string} section_id Optional edit section to open and focus.
2100 * @return {void}
2101 */
2102 function open_inspector( config, mode, resource_id, focus_target, section_id ) {
2103 var request_sequence;
2104 var action;
2105
2106 resource_id = Number( resource_id ) || 0;
2107 if ( 'edit' === mode && resource_id === inspector_resource_id && document.querySelector( '[data-wpbc-catalog-resource-inspector-form][data-mode="edit"]' ) ) {
2108 expand_inspector_sidebar();
2109 mark_inspector_resource_row( resource_id );
2110 if ( section_id ) {
2111 activate_inspector_section( section_id );
2112 }
2113 return;
2114 }
2115 if ( ! can_discard_inspector( config ) || ! mount_inspector_shell( config ) ) {
2116 return;
2117 }
2118 close_details_row( false );
2119 inspector_focus_target = focus_target || document.activeElement;
2120 request_sequence = ++inspector_request_sequence;
2121 inspector_dirty = false;
2122 inspector_mode = mode;
2123 inspector_resource_id = resource_id;
2124 synchronize_inspector_width();
2125 action = 'create' === mode ? config.inspector_create_schema_action : config.inspector_edit_schema_action;
2126 set_inspector_state( 'loading', '' );
2127 mark_inspector_resource_row( inspector_resource_id );
2128 expand_inspector_sidebar();
2129
2130 request_inspector( config, action, 'edit' === mode ? { resource_id: inspector_resource_id } : {} ).then( function ( response ) {
2131 if ( request_sequence !== inspector_request_sequence ) {
2132 return;
2133 }
2134 if ( ! response || true !== response.success || ! response.data || ! response.data.schema || ! render_inspector_schema( config, response.data.schema, ! section_id ) ) {
2135 set_inspector_state( 'error', get_inspector_response_message( response, config.i18n.inspector_load_failed ) );
2136 } else if ( section_id ) {
2137 activate_inspector_section( section_id );
2138 }
2139 } ).catch( function () {
2140 if ( request_sequence === inspector_request_sequence ) {
2141 set_inspector_state( 'error', config.i18n.inspector_load_failed || '' );
2142 }
2143 } );
2144 }
2145
2146 /**
2147 * Render the server-generated common-field bulk editor.
2148 *
2149 * @param {Object} config Catalog configuration.
2150 * @param {Object} schema Authorized bulk schema.
2151 * @return {boolean} True when the form rendered.
2152 */
2153 function render_bulk_editor( config, schema ) {
2154 var host = get_inspector_host();
2155 var target = host ? host.querySelector( '[data-wpbc-ui-catalog-inspector-form]' ) : null;
2156
2157 if ( ! target ) {
2158 return false;
2159 }
2160 target.innerHTML = render_component( config, 'inspector_bulk_edit', { i18n: config.i18n || {}, schema: schema, selection_label: get_selection_count_label( config, schema.selection_count ) } );
2161 if ( ! target.querySelector( '[data-wpbc-catalog-resource-bulk-form]' ) ) {
2162 return false;
2163 }
2164 inspector_mode = 'bulk_edit';
2165 inspector_resource_id = 0;
2166 inspector_resource_ids = ( schema.resource_ids || [] ).map( Number );
2167 inspector_bulk_operations = {};
2168 inspector_review_token = '';
2169 inspector_selection_stale = false;
2170 inspector_tracks_selection = true;
2171 inspector_dirty = false;
2172 set_inspector_state( 'form', '' );
2173 configure_inspector_footer( 'wpbc_catalog_booking_resources_bulk_form', config.i18n.review_changes_button || '', false, true );
2174 if ( 'function' === typeof window.WPBC_Collapsible_AutoInit ) {
2175 window.WPBC_Collapsible_AutoInit();
2176 }
2177 mark_inspector_resource_row( 0 );
2178 focus_inspector_heading( target.querySelector( '[data-wpbc-catalog-resource-bulk-form]' ) );
2179
2180 return true;
2181 }
2182
2183 /**
2184 * Open a bulk editor for the current explicit selection.
2185 *
2186 * @param {Object} config Catalog configuration.
2187 * @param {HTMLElement} focus_target Initiating control.
2188 * @return {void}
2189 */
2190 function open_bulk_editor( config, focus_target ) {
2191 var resource_ids = get_selected_resource_ids( config );
2192 var request_sequence;
2193
2194 if ( ! resource_ids.length || ! can_discard_inspector( config ) || ! mount_inspector_shell( config ) ) {
2195 return;
2196 }
2197 close_details_row( false );
2198 inspector_focus_target = focus_target || document.activeElement;
2199 request_sequence = ++inspector_request_sequence;
2200 inspector_mode = 'bulk_edit';
2201 inspector_resource_ids = resource_ids.slice();
2202 inspector_dirty = false;
2203 inspector_tracks_selection = true;
2204 synchronize_inspector_width();
2205 set_inspector_state( 'loading', '' );
2206 expand_inspector_sidebar();
2207
2208 request_inspector( config, config.bulk_schema_action, { resource_ids: JSON.stringify( resource_ids ) } ).then( function ( response ) {
2209 if ( request_sequence !== inspector_request_sequence ) {
2210 return;
2211 }
2212 if ( ! response || true !== response.success || ! response.data || ! response.data.schema || ! render_bulk_editor( config, response.data.schema ) ) {
2213 set_inspector_state( 'error', get_inspector_response_message( response, config.i18n.bulk_load_failed ) );
2214 } else if ( ! resource_id_lists_match( inspector_resource_ids, get_selected_resource_ids( config ) ) ) {
2215 handle_inspector_selection_change( null, config );
2216 }
2217 } ).catch( function () {
2218 if ( request_sequence === inspector_request_sequence ) {
2219 set_inspector_state( 'error', config.i18n.bulk_load_failed || '' );
2220 }
2221 } );
2222 }
2223
2224 /**
2225 * Return only explicitly enabled bulk operations.
2226 *
2227 * @return {Object} Operation envelope keyed by field.
2228 */
2229 function collect_bulk_operations() {
2230 var operations = {};
2231
2232 document.querySelectorAll( '[data-wpbc-catalog-resource-bulk-enable]:checked' ).forEach( function ( enabled_control ) {
2233 var field_key = enabled_control.getAttribute( 'data-wpbc-catalog-resource-bulk-enable' ) || '';
2234 var operation = document.querySelector( '[data-wpbc-catalog-resource-bulk-operation="' + field_key + '"]' );
2235 var field_value = document.querySelector( '[data-wpbc-catalog-resource-bulk-value="' + field_key + '"]' );
2236
2237 if ( field_key && operation && field_value ) {
2238 operations[ field_key ] = { operation: operation.value, value: field_value.value };
2239 }
2240 } );
2241
2242 return operations;
2243 }
2244
2245 /**
2246 * Synchronize one optional bulk field and the review action.
2247 *
2248 * @param {HTMLElement|null} changed_control Control that changed, when available.
2249 * @return {void}
2250 */
2251 function synchronize_bulk_editor( changed_control ) {
2252 var field_wrap = changed_control ? changed_control.closest( '[data-wpbc-catalog-resource-bulk-field]' ) : null;
2253 var save_button = document.querySelector( '[data-wpbc-ui-catalog-inspector-save]' );
2254
2255 if ( field_wrap ) {
2256 var enabled_control = field_wrap.querySelector( '[data-wpbc-catalog-resource-bulk-enable]' );
2257 var operation_control = field_wrap.querySelector( '[data-wpbc-catalog-resource-bulk-operation]' );
2258 var prefix_element = field_wrap.querySelector( '[data-wpbc-catalog-resource-bulk-prefix]' );
2259 var suffix_element = field_wrap.querySelector( '[data-wpbc-catalog-resource-bulk-suffix]' );
2260 var enabled = !! enabled_control && enabled_control.checked;
2261 var operation_id = operation_control ? String( operation_control.value || '' ) : '';
2262 var is_percent = -1 !== operation_id.indexOf( 'percent' );
2263 field_wrap.classList.toggle( 'is-enabled', enabled );
2264 field_wrap.querySelectorAll( '[data-wpbc-catalog-resource-bulk-operation], [data-wpbc-catalog-resource-bulk-value], [data-wpbc-catalog-resource-bulk-range]' ).forEach( function ( control ) {
2265 control.disabled = ! enabled;
2266 } );
2267 if ( prefix_element ) {
2268 prefix_element.textContent = is_percent ? '' : field_wrap.getAttribute( 'data-wpbc-catalog-resource-bulk-prefix' ) || '';
2269 }
2270 if ( suffix_element ) {
2271 suffix_element.textContent = is_percent ? '%' : field_wrap.getAttribute( 'data-wpbc-catalog-resource-bulk-suffix' ) || '';
2272 }
2273 if ( changed_control && changed_control.matches( '[data-wpbc-catalog-resource-bulk-range]' ) ) {
2274 var number_control = field_wrap.querySelector( '[data-wpbc-catalog-resource-bulk-value]' );
2275 if ( number_control ) {
2276 number_control.value = changed_control.value;
2277 }
2278 } else {
2279 var range_control = field_wrap.querySelector( '[data-wpbc-catalog-resource-bulk-range]' );
2280 var field_value_control = field_wrap.querySelector( '[data-wpbc-catalog-resource-bulk-value]' );
2281 if ( range_control && field_value_control && '' !== field_value_control.value ) {
2282 range_control.value = field_value_control.value;
2283 }
2284 }
2285 }
2286 inspector_bulk_operations = collect_bulk_operations();
2287 inspector_dirty = Object.keys( inspector_bulk_operations ).length > 0;
2288 if ( save_button ) {
2289 save_button.disabled = inspector_selection_stale || ! inspector_dirty;
2290 }
2291 }
2292
2293 /**
2294 * Render a signed bulk-edit review without performing a mutation.
2295 *
2296 * @param {Object} config Catalog configuration.
2297 * @param {Object} preview Server-authoritative preview.
2298 * @return {boolean} True when rendered.
2299 */
2300 function render_bulk_review( config, preview ) {
2301 var host = get_inspector_host();
2302 var review_workflow = get_inline_review_workflow();
2303 var review_model;
2304 var target = host ? host.querySelector( '[data-wpbc-ui-catalog-inspector-form]' ) : null;
2305
2306 if ( ! target ) {
2307 return false;
2308 }
2309 review_model = review_workflow ? review_workflow.prepare( preview.review || {}, {
2310 changed_label: get_selection_count_label( config, preview.schema.resource_ids.length ),
2311 description: config.i18n.inline_review_description || '',
2312 form_id: 'wpbc_catalog_booking_resources_bulk_review_form',
2313 mode: 'bulk_review',
2314 pending_message: config.i18n.review_changes_help || '',
2315 title: config.i18n.review_changes || config.i18n.edit_booking_resources || ''
2316 } ) : {};
2317 target.innerHTML = render_component( config, 'inspector_bulk_review', review_model );
2318 if ( ! target.querySelector( '[data-wpbc-catalog-resource-bulk-review-form]' ) ) {
2319 return false;
2320 }
2321 inspector_mode = 'bulk_review';
2322 inspector_review_token = String( preview.review_token || '' );
2323 inspector_dirty = true;
2324 set_inspector_state( 'form', '' );
2325 configure_inspector_footer( 'wpbc_catalog_booking_resources_bulk_review_form', config.i18n.apply_changes || '', false, inspector_selection_stale );
2326 if ( review_workflow ) {
2327 review_workflow.synchronize( { busy: false, can_apply: ! inspector_selection_stale && !! inspector_review_token } );
2328 }
2329 focus_inspector_heading( target.querySelector( '[data-wpbc-catalog-resource-bulk-review-form]' ) );
2330
2331 return true;
2332 }
2333
2334 /**
2335 * Render the signed deletion impact and explicit acknowledgement.
2336 *
2337 * @param {Object} config Catalog configuration.
2338 * @param {Object} preview Server-authoritative deletion preview.
2339 * @return {boolean} True when rendered.
2340 */
2341 function render_delete_review( config, preview ) {
2342 var host = get_inspector_host();
2343 var target = host ? host.querySelector( '[data-wpbc-ui-catalog-inspector-form]' ) : null;
2344 var acknowledgement;
2345
2346 if ( ! target ) {
2347 return false;
2348 }
2349 var delete_i18n = preview.i18n || {};
2350
2351 target.innerHTML = render_component( config, 'inspector_delete', {
2352 delete_i18n: {
2353 acknowledgement: delete_i18n.acknowledgement || config.i18n.delete_acknowledgement || '',
2354 actions_heading: delete_i18n.actions_heading || '',
2355 bookings_retained_warning: delete_i18n.bookings_retained_warning || config.i18n.bookings_retained_warning || '',
2356 resources_to_delete: delete_i18n.resources_to_delete || config.i18n.resources_to_delete || '',
2357 review_help: delete_i18n.review_help || config.i18n.delete_review_help || '',
2358 title: delete_i18n.title || config.i18n.delete_booking_resources || '',
2359 warning: delete_i18n.warning || config.i18n.delete_warning || ''
2360 },
2361 i18n: config.i18n || {},
2362 preview: preview,
2363 selection_label: delete_i18n.selection_label || get_selection_count_label( config, preview.selection_count )
2364 } );
2365 if ( ! target.querySelector( '[data-wpbc-catalog-resource-delete-form]' ) ) {
2366 return false;
2367 }
2368 inspector_mode = 'delete_review';
2369 inspector_resource_ids = ( preview.resources || [] ).map( function ( resource ) { return Number( resource.id ); } );
2370 inspector_resource_id = ! inspector_tracks_selection && 1 === inspector_resource_ids.length ? inspector_resource_ids[0] : 0;
2371 inspector_review_token = String( preview.review_token || '' );
2372 inspector_selection_stale = false;
2373 inspector_dirty = false;
2374 set_inspector_state( 'form', '' );
2375 configure_inspector_footer(
2376 'wpbc_catalog_booking_resources_delete_form',
2377 delete_i18n.delete_button || format_message( 1 === Number( preview.selection_count ) ? config.i18n.delete_resource || '' : config.i18n.delete_resources || '', [ preview.selection_count ] ),
2378 true,
2379 true
2380 );
2381 acknowledgement = target.querySelector( '.wpbc_booking_resources__delete_acknowledgement' );
2382 pulse_delete_acknowledgement( acknowledgement );
2383 mark_inspector_resource_row( inspector_resource_id );
2384 focus_inspector_heading( target.querySelector( '[data-wpbc-catalog-resource-delete-form]' ) );
2385
2386 return true;
2387 }
2388
2389 /**
2390 * Open the independent deletion review for explicit Resource IDs.
2391 *
2392 * @param {Object} config Catalog configuration.
2393 * @param {Array<number>} resource_ids Resource IDs selected for deletion.
2394 * @param {HTMLElement} focus_target Initiating control.
2395 * @param {boolean} track_selection Whether deletion owns the checkbox selection.
2396 * @return {void}
2397 */
2398 function open_delete_review( config, resource_ids, focus_target, track_selection ) {
2399 var request_sequence;
2400
2401 resource_ids = ( resource_ids || [] ).map( Number ).filter( function ( resource_id ) { return resource_id > 0; } );
2402 if ( ! resource_ids.length || ! can_discard_inspector( config ) || ! mount_inspector_shell( config ) ) {
2403 return;
2404 }
2405 close_details_row( false );
2406 inspector_focus_target = focus_target || document.activeElement;
2407 request_sequence = ++inspector_request_sequence;
2408 inspector_mode = 'delete_review';
2409 inspector_resource_ids = resource_ids.slice();
2410 inspector_resource_id = ! track_selection && 1 === inspector_resource_ids.length ? inspector_resource_ids[0] : 0;
2411 inspector_dirty = false;
2412 inspector_tracks_selection = !! track_selection;
2413 synchronize_inspector_width();
2414 set_inspector_state( 'loading', '' );
2415 mark_inspector_resource_row( inspector_resource_id );
2416 expand_inspector_sidebar();
2417
2418 request_inspector( config, config.delete_preview_action, { resource_ids: JSON.stringify( resource_ids ) } ).then( function ( response ) {
2419 if ( request_sequence !== inspector_request_sequence ) {
2420 return;
2421 }
2422 if ( ! response || true !== response.success || ! response.data || ! response.data.preview || ! render_delete_review( config, response.data.preview ) ) {
2423 set_inspector_state( 'error', get_inspector_response_message( response, config.i18n.delete_load_failed ) );
2424 } else if ( inspector_tracks_selection && ! resource_id_lists_match( inspector_resource_ids, get_selected_resource_ids( config ) ) ) {
2425 handle_inspector_selection_change( null, config );
2426 }
2427 } ).catch( function () {
2428 if ( request_sequence === inspector_request_sequence ) {
2429 set_inspector_state( 'error', config.i18n.delete_load_failed || '' );
2430 }
2431 } );
2432 }
2433
2434 /**
2435 * Return one localized capacity count label.
2436 *
2437 * @param {Object} config Catalog configuration.
2438 * @param {string} singular_key Singular translation key.
2439 * @param {string} plural_key Plural translation key.
2440 * @param {number} count Non-negative count.
2441 * @return {string} Localized label.
2442 */
2443 function get_capacity_count_label( config, singular_key, plural_key, count ) {
2444 var template = 1 === Number( count ) ? config.i18n[ singular_key ] : config.i18n[ plural_key ];
2445
2446 return format_message( template || '', [ count ] );
2447 }
2448
2449 /**
2450 * Build presentation-only data for the capacity WP template.
2451 *
2452 * @param {Object} config Catalog configuration.
2453 * @param {Object} context Server-authoritative capacity context.
2454 * @return {Object} Template view data.
2455 */
2456 function get_capacity_editor_view( config, context ) {
2457 var current_capacity = Number( context.current_capacity ) || 1;
2458 var target_capacity = inspector_capacity_target || current_capacity;
2459 var operation = target_capacity > current_capacity ? 'increase' : ( target_capacity < current_capacity ? 'decrease' : 'unchanged' );
2460 var keep_count = 'decrease' === operation ? target_capacity : current_capacity;
2461 var create_count = Math.max( 0, target_capacity - current_capacity );
2462 var decrease_count = Math.max( 0, current_capacity - target_capacity );
2463 var delete_action = 'decrease' === operation && 'delete' === inspector_capacity_decrease_action;
2464
2465 return {
2466 children: ( context.children || [] ).map( function ( child ) {
2467 child = Object.assign( {}, child );
2468 child.selected = -1 !== inspector_capacity_detach_ids.indexOf( Number( child.id ) );
2469 return child;
2470 } ),
2471 context_label: ( config.i18n.resource_id || 'ID' ) + ': ' + String( context.resource_id ),
2472 current_capacity: current_capacity,
2473 decrease_action: inspector_capacity_decrease_action,
2474 decrease_heading: get_capacity_count_label( config, delete_action ? 'choose_delete_unit' : 'choose_detach_unit', delete_action ? 'choose_delete_units' : 'choose_detach_units', decrease_count ),
2475 decrease_help: delete_action ? config.i18n.delete_units_help || '' : config.i18n.select_detach_help || '',
2476 decrease_outcome_label: delete_action ? config.i18n.will_be_deleted || '' : config.i18n.make_independent || '',
2477 description: config.i18n.capacity_description || '',
2478 create_label: get_capacity_count_label( config, 'create_new_unit', 'create_new_units', create_count ),
2479 keep_label: get_capacity_count_label( config, 'keep_existing_unit', 'keep_existing_units', keep_count ),
2480 maximum_capacity: Number( context.maximum_capacity ) || current_capacity,
2481 minimum_capacity: Number( context.minimum_capacity ) || 1,
2482 mode: 'capacity',
2483 operation: operation,
2484 target_capacity: target_capacity,
2485 title: config.i18n.adjust_capacity || ''
2486 };
2487 }
2488
2489 /**
2490 * Synchronize the live capacity plan without replacing focused controls.
2491 *
2492 * @param {Object} config Catalog configuration.
2493 * @return {void}
2494 */
2495 function synchronize_capacity_editor( config ) {
2496 var form = document.querySelector( '[data-wpbc-catalog-resource-capacity-form][data-mode="capacity"]' );
2497 var save_button = document.querySelector( '[data-wpbc-ui-catalog-inspector-save]' );
2498 var context = inspector_capacity_context || {};
2499 var current_capacity = Number( context.current_capacity ) || 1;
2500 var target_capacity = inspector_capacity_target || current_capacity;
2501 var operation = target_capacity > current_capacity ? 'increase' : ( target_capacity < current_capacity ? 'decrease' : 'unchanged' );
2502 var required_detach_count = Math.max( 0, current_capacity - target_capacity );
2503 var target_number = form ? form.querySelector( '[data-wpbc-catalog-capacity-target]' ) : null;
2504 var target_range = form ? form.querySelector( '[data-wpbc-catalog-capacity-range]' ) : null;
2505
2506 if ( ! form ) {
2507 return;
2508 }
2509 if ( 'decrease' !== operation ) {
2510 inspector_capacity_detach_ids = [];
2511 inspector_capacity_decrease_action = 'detach';
2512 } else if ( inspector_capacity_detach_ids.length > required_detach_count ) {
2513 inspector_capacity_detach_ids = inspector_capacity_detach_ids.slice( 0, required_detach_count );
2514 }
2515 if ( target_number ) {
2516 target_number.value = String( target_capacity );
2517 }
2518 if ( target_range ) {
2519 target_range.value = String( target_capacity );
2520 }
2521 var after_value = form.querySelector( '[data-wpbc-catalog-capacity-after]' );
2522 var keep_label = form.querySelector( '[data-wpbc-catalog-capacity-keep-label]' );
2523 var create_label = form.querySelector( '[data-wpbc-catalog-capacity-create-label]' );
2524 var increase_row = form.querySelector( '[data-wpbc-catalog-capacity-increase-row]' );
2525 var decrease_panel = form.querySelector( '[data-wpbc-catalog-capacity-decrease]' );
2526 var decrease_heading = form.querySelector( '[data-wpbc-catalog-capacity-decrease-heading]' );
2527 var decrease_help = form.querySelector( '[data-wpbc-catalog-capacity-decrease-help]' );
2528 var delete_action = 'decrease' === operation && 'delete' === inspector_capacity_decrease_action;
2529 if ( after_value ) {
2530 after_value.textContent = String( target_capacity );
2531 }
2532 if ( keep_label ) {
2533 keep_label.textContent = get_capacity_count_label( config, 'keep_existing_unit', 'keep_existing_units', 'decrease' === operation ? target_capacity : current_capacity );
2534 }
2535 if ( create_label ) {
2536 create_label.textContent = get_capacity_count_label( config, 'create_new_unit', 'create_new_units', Math.max( 0, target_capacity - current_capacity ) );
2537 }
2538 if ( increase_row ) {
2539 increase_row.hidden = 'increase' !== operation;
2540 }
2541 if ( decrease_panel ) {
2542 decrease_panel.hidden = 'decrease' !== operation;
2543 }
2544 if ( decrease_heading ) {
2545 decrease_heading.textContent = get_capacity_count_label( config, delete_action ? 'choose_delete_unit' : 'choose_detach_unit', delete_action ? 'choose_delete_units' : 'choose_detach_units', required_detach_count );
2546 }
2547 if ( decrease_help ) {
2548 decrease_help.textContent = delete_action ? config.i18n.delete_units_help || '' : config.i18n.select_detach_help || '';
2549 }
2550 form.querySelectorAll( '[data-wpbc-catalog-capacity-decrease-action]' ).forEach( function ( action_control ) {
2551 var action_selected = action_control.value === inspector_capacity_decrease_action;
2552 action_control.checked = action_selected;
2553 if ( action_control.closest( 'label' ) ) {
2554 action_control.closest( 'label' ).classList.toggle( 'is-selected', action_selected );
2555 }
2556 } );
2557 form.querySelectorAll( '[data-wpbc-catalog-capacity-detach]' ).forEach( function ( checkbox ) {
2558 var selected = -1 !== inspector_capacity_detach_ids.indexOf( Number( checkbox.value ) );
2559 var unit = checkbox.closest( '.wpbc_booking_resources__capacity_unit' );
2560 var outcome = unit ? unit.querySelector( '.wpbc_booking_resources__capacity_unit_outcome' ) : null;
2561 checkbox.checked = selected;
2562 checkbox.disabled = ! selected && inspector_capacity_detach_ids.length >= required_detach_count;
2563 if ( unit ) {
2564 unit.classList.toggle( 'is-selected', selected );
2565 }
2566 if ( outcome ) {
2567 outcome.hidden = ! selected;
2568 outcome.textContent = delete_action ? config.i18n.will_be_deleted || '' : config.i18n.make_independent || '';
2569 outcome.classList.toggle( 'is-destructive', delete_action );
2570 }
2571 } );
2572 inspector_dirty = target_capacity !== current_capacity;
2573 if ( save_button ) {
2574 save_button.disabled = 'unchanged' === operation || ( 'decrease' === operation && inspector_capacity_detach_ids.length !== required_detach_count );
2575 }
2576 }
2577
2578 /**
2579 * Render an authorized capacity editor context.
2580 *
2581 * @param {Object} config Catalog configuration.
2582 * @param {Object} context Server capacity context.
2583 * @return {boolean} True when the template rendered.
2584 */
2585 function render_capacity_editor( config, context ) {
2586 var host = get_inspector_host();
2587 var target = host ? host.querySelector( '[data-wpbc-ui-catalog-inspector-form]' ) : null;
2588
2589 if ( ! target ) {
2590 return false;
2591 }
2592 inspector_capacity_context = context;
2593 inspector_capacity_target = Number( context.current_capacity ) || 1;
2594 inspector_capacity_detach_ids = [];
2595 inspector_capacity_decrease_action = 'detach';
2596 target.innerHTML = render_component( config, 'inspector_capacity', {
2597 i18n: config.i18n || {},
2598 view: get_capacity_editor_view( config, context )
2599 } );
2600 if ( ! target.querySelector( '[data-wpbc-catalog-resource-capacity-form]' ) ) {
2601 return false;
2602 }
2603 inspector_mode = 'capacity';
2604 inspector_resource_id = Number( context.resource_id ) || 0;
2605 inspector_review_token = '';
2606 inspector_dirty = false;
2607 set_inspector_state( 'form', '' );
2608 configure_inspector_footer( 'wpbc_catalog_booking_resource_capacity_form', config.i18n.review_capacity_change || '', false, true );
2609 mark_inspector_resource_row( inspector_resource_id );
2610 focus_inspector_heading( target.querySelector( '[data-wpbc-catalog-resource-capacity-form]' ) );
2611
2612 return true;
2613 }
2614
2615 /**
2616 * Open capacity context from either row action entry point.
2617 *
2618 * @param {Object} config Catalog configuration.
2619 * @param {number} resource_id Root or child Resource ID.
2620 * @param {HTMLElement} focus_target Initiating control.
2621 * @return {void}
2622 */
2623 function open_capacity_editor( config, resource_id, focus_target ) {
2624 var request_sequence;
2625
2626 if ( ! resource_id || ! can_discard_inspector( config ) || ! mount_inspector_shell( config ) ) {
2627 return;
2628 }
2629 close_details_row( false );
2630 inspector_focus_target = focus_target || document.activeElement;
2631 request_sequence = ++inspector_request_sequence;
2632 inspector_mode = 'capacity';
2633 inspector_resource_id = resource_id;
2634 inspector_dirty = false;
2635 inspector_tracks_selection = false;
2636 synchronize_inspector_width();
2637 set_inspector_state( 'loading', '' );
2638 mark_inspector_resource_row( resource_id );
2639 expand_inspector_sidebar();
2640
2641 request_inspector( config, config.capacity_context_action, { resource_id: resource_id } ).then( function ( response ) {
2642 if ( request_sequence !== inspector_request_sequence ) {
2643 return;
2644 }
2645 if ( ! response || true !== response.success || ! response.data || ! response.data.context || ! render_capacity_editor( config, response.data.context ) ) {
2646 set_inspector_state( 'error', get_inspector_response_message( response, config.i18n.capacity_load_failed ) );
2647 }
2648 } ).catch( function () {
2649 if ( request_sequence === inspector_request_sequence ) {
2650 set_inspector_state( 'error', config.i18n.capacity_load_failed || '' );
2651 }
2652 } );
2653 }
2654
2655 /**
2656 * Render a signed capacity review returned by the domain service.
2657 *
2658 * @param {Object} config Catalog configuration.
2659 * @param {Object} preview Signed preview.
2660 * @return {boolean} True when rendered.
2661 */
2662 function render_capacity_review( config, preview ) {
2663 var host = get_inspector_host();
2664 var target = host ? host.querySelector( '[data-wpbc-ui-catalog-inspector-form]' ) : null;
2665 var increase = 'increase' === preview.operation;
2666 var delete_action = 'delete' === preview.decrease_action;
2667 var view;
2668
2669 if ( ! target ) {
2670 return false;
2671 }
2672 view = {
2673 context_label: ( config.i18n.resource_id || 'ID' ) + ': ' + String( preview.resource_id ),
2674 current_capacity: Number( preview.current_capacity ),
2675 decrease_action: preview.decrease_action || 'detach',
2676 delete_has_bookings: true === preview.delete_has_bookings,
2677 description: config.i18n.review_capacity_help || '',
2678 mode: 'capacity_review',
2679 operation: preview.operation,
2680 operation_help: increase ? config.i18n.create_units_help || '' : ( delete_action ? config.i18n.delete_units_help || '' : config.i18n.select_detach_help || '' ),
2681 operation_label: increase
2682 ? get_capacity_count_label( config, 'create_new_unit', 'create_new_units', Number( preview.create_count ) )
2683 : get_capacity_count_label( config, 'keep_existing_unit', 'keep_existing_units', Number( preview.target_capacity ) ),
2684 resources: increase ? preview.create_resources || [] : preview.detach_resources || [],
2685 target_capacity: Number( preview.target_capacity ),
2686 title: config.i18n.review_capacity_title || ''
2687 };
2688 target.innerHTML = render_component( config, 'inspector_capacity', { i18n: config.i18n || {}, view: view } );
2689 if ( ! target.querySelector( '[data-wpbc-catalog-resource-capacity-form]' ) ) {
2690 return false;
2691 }
2692 inspector_mode = 'capacity_review';
2693 inspector_resource_id = Number( preview.resource_id ) || 0;
2694 inspector_review_token = String( preview.review_token || '' );
2695 inspector_capacity_decrease_action = preview.decrease_action || 'detach';
2696 inspector_dirty = true;
2697 set_inspector_state( 'form', '' );
2698 configure_inspector_footer( 'wpbc_catalog_booking_resource_capacity_form', config.i18n.apply_capacity_change || '', delete_action, delete_action );
2699 if ( delete_action ) {
2700 var acknowledgement = target.querySelector( '[data-wpbc-catalog-capacity-delete-acknowledgement]' );
2701 pulse_delete_acknowledgement( acknowledgement ? acknowledgement.closest( '.wpbc_booking_resources__delete_acknowledgement' ) : null );
2702 }
2703 var cancel_button = document.querySelector( '[data-wpbc-ui-catalog-inspector-cancel]' );
2704 if ( cancel_button ) {
2705 cancel_button.textContent = config.i18n.back || '';
2706 }
2707 focus_inspector_heading( target.querySelector( '[data-wpbc-catalog-resource-capacity-form]' ) );
2708
2709 return true;
2710 }
2711
2712 /**
2713 * Synchronize the Resource image preview after Media Library changes.
2714 *
2715 * @param {HTMLElement} field Picture URL field.
2716 * @return {void}
2717 */
2718 function synchronize_inspector_image( field ) {
2719 var field_wrap = field ? field.closest( '[data-wpbc-catalog-resource-field-wrap]' ) : null;
2720 var preview = field_wrap ? field_wrap.querySelector( '[data-wpbc-catalog-resource-image-preview]' ) : null;
2721 var placeholder = field_wrap ? field_wrap.querySelector( '[data-wpbc-catalog-resource-image-placeholder]' ) : null;
2722 var remove_button = field_wrap ? field_wrap.querySelector( '[data-wpbc-catalog-resource-remove-image]' ) : null;
2723 var picture_url = field ? String( field.value || '' ).trim() : '';
2724
2725 if ( preview ) {
2726 preview.src = picture_url;
2727 preview.hidden = ! picture_url;
2728 }
2729 if ( placeholder ) {
2730 placeholder.hidden = !! picture_url;
2731 }
2732 if ( remove_button ) {
2733 remove_button.disabled = ! picture_url;
2734 }
2735 }
2736
2737 /**
2738 * Synchronize one numeric slider with its precise number field.
2739 *
2740 * Suggested slider bounds remain convenient for ordinary values. Price keeps
2741 * its product-defined 0-1000 slider while the authoritative number field can
2742 * still preserve a legacy price above 1000. Other numeric controls can expand
2743 * to represent stored values outside their suggested range.
2744 *
2745 * @param {string} field_key Numeric inspector field key.
2746 * @return {void}
2747 */
2748 function synchronize_inspector_numeric_range( field_key ) {
2749 var number_field = document.querySelector( '[data-wpbc-catalog-resource-field="' + field_key + '"][type="number"]' );
2750 var range = document.querySelector( '[data-wpbc-catalog-resource-range="' + field_key + '"]' );
2751 var number_value;
2752 var default_min;
2753 var default_max;
2754 var hard_min;
2755 var hard_max;
2756 var range_min;
2757 var range_max;
2758
2759 if ( ! number_field || ! range ) {
2760 return;
2761 }
2762
2763 number_value = Number( number_field.value );
2764 if ( ! isFinite( number_value ) ) {
2765 return;
2766 }
2767
2768 default_min = Number( range.getAttribute( 'data-wpbc-catalog-resource-range-default-min' ) );
2769 default_max = Number( range.getAttribute( 'data-wpbc-catalog-resource-range-default-max' ) );
2770 hard_min = '' === String( number_field.getAttribute( 'min' ) || '' ) ? null : Number( number_field.getAttribute( 'min' ) );
2771 hard_max = '' === String( number_field.getAttribute( 'max' ) || '' ) ? null : Number( number_field.getAttribute( 'max' ) );
2772 if ( 'base_cost' === field_key ) {
2773 range_min = isFinite( default_min ) ? default_min : 0;
2774 range_max = isFinite( default_max ) ? default_max : 1000;
2775 } else {
2776 range_min = null !== hard_min && isFinite( hard_min )
2777 ? hard_min
2778 : Math.min( isFinite( default_min ) ? default_min : number_value, number_value );
2779 range_max = null !== hard_max && isFinite( hard_max )
2780 ? hard_max
2781 : Math.max( isFinite( default_max ) ? default_max : number_value, number_value );
2782 }
2783
2784 range.min = String( range_min );
2785 range.max = String( range_max );
2786 range.value = String( number_value );
2787 }
2788
2789 /**
2790 * Synchronize every rendered inspector numeric slider.
2791 *
2792 * @return {void}
2793 */
2794 function synchronize_all_inspector_numeric_ranges() {
2795 document.querySelectorAll( '[data-wpbc-catalog-resource-range]' ).forEach( function ( range ) {
2796 synchronize_inspector_numeric_range( range.getAttribute( 'data-wpbc-catalog-resource-range' ) || '' );
2797 } );
2798 }
2799
2800 /**
2801 * Copy a slider value into its authoritative number field.
2802 *
2803 * The number field remains the only serialized control and emits one native
2804 * input event so validation and dirty-state behavior stay centralized.
2805 *
2806 * @param {HTMLElement} range Numeric range control.
2807 * @return {void}
2808 */
2809 function synchronize_inspector_number_from_range( range ) {
2810 var field_key = range ? range.getAttribute( 'data-wpbc-catalog-resource-range' ) || '' : '';
2811 var number_field = field_key ? document.querySelector( '[data-wpbc-catalog-resource-field="' + field_key + '"][type="number"]' ) : null;
2812
2813 if ( ! number_field ) {
2814 return;
2815 }
2816 number_field.value = range.value;
2817 number_field.dispatchEvent( new Event( 'input', { bubbles: true } ) );
2818 }
2819
2820 /**
2821 * Confirm that a form submit originated from the inspector save action.
2822 *
2823 * WordPress Media Library controls may temporarily coexist with the sidebar
2824 * form. Rejecting their submitters prevents an image insertion from starting
2825 * a Resource mutation. The active inspector field fallback preserves native
2826 * Enter-key submission in browsers without SubmitEvent.submitter support.
2827 *
2828 * @param {SubmitEvent} event Form submission.
2829 * @param {HTMLElement} form Active inspector form.
2830 * @return {boolean} True only for an intentional inspector submission.
2831 */
2832 function is_expected_inspector_submit( event, form ) {
2833 var submitter = event.submitter || document.activeElement;
2834
2835 if ( submitter && submitter.matches && submitter.matches( '[data-wpbc-ui-catalog-inspector-save]' ) ) {
2836 return true;
2837 }
2838
2839 return ! event.submitter
2840 && submitter
2841 && form.contains( submitter )
2842 && submitter.matches
2843 && submitter.matches( 'input:not([type="button"]):not([type="submit"]), select' );
2844 }
2845
2846 /**
2847 * Report inspector validity without rejecting an existing decimal price.
2848 *
2849 * Price controls intentionally use a one-unit spinner step. Existing prices
2850 * may still contain decimals, so their step constraint is relaxed only while
2851 * native form validity is evaluated. Server-side price validation remains the
2852 * authoritative boundary.
2853 *
2854 * @param {HTMLFormElement} form Inspector form.
2855 * @return {boolean} True when the form passes native validity checks.
2856 */
2857 function report_inspector_validity( form ) {
2858 var price_fields = form.querySelectorAll( '[data-wpbc-catalog-resource-field="base_cost"], [data-wpbc-catalog-resource-bulk-value="base_cost"]' );
2859 var price_steps = [];
2860 var is_valid;
2861
2862 price_fields.forEach( function ( price_field ) {
2863 price_steps.push( {
2864 field: price_field,
2865 step: price_field.getAttribute( 'step' )
2866 } );
2867 price_field.setAttribute( 'step', 'any' );
2868 } );
2869
2870 is_valid = form.reportValidity();
2871 price_steps.forEach( function ( price_step ) {
2872 if ( null === price_step.step ) {
2873 price_step.field.removeAttribute( 'step' );
2874 } else {
2875 price_step.field.setAttribute( 'step', price_step.step );
2876 }
2877 } );
2878
2879 return is_valid;
2880 }
2881
2882 /**
2883 * Save the active inspector through its independent mutation endpoint.
2884 *
2885 * @param {SubmitEvent} event Form submission.
2886 * @param {Object} config Catalog configuration.
2887 * @return {void}
2888 */
2889 function submit_inspector( event, config ) {
2890 var form = event.target;
2891 var save_button = document.querySelector( '[data-wpbc-ui-catalog-inspector-save]' );
2892 var cancel_button = document.querySelector( '[data-wpbc-ui-catalog-inspector-cancel]' );
2893 var mutation_request_sequence;
2894 var fields;
2895 var action;
2896 var submitted_mode;
2897 var request_values;
2898 var control_disabled_states = [];
2899 var success_message;
2900 var success_message_is_global;
2901 var submitted_form_is_active;
2902
2903 event.preventDefault();
2904 if ( ! is_expected_inspector_submit( event, form ) || ( save_button && save_button.classList.contains( 'is-busy' ) ) || ! report_inspector_validity( form ) ) {
2905 return;
2906 }
2907 mutation_request_sequence = ++inspector_mutation_request_sequence;
2908 inspector_mutation_in_progress = true;
2909 fields = JSON.parse( serialize_inspector_fields() || '{}' );
2910 action = 'create' === inspector_mode ? config.inspector_create_action : config.inspector_update_action;
2911 submitted_mode = inspector_mode;
2912 request_values = { fields: JSON.stringify( fields ) };
2913 if ( 'edit' === inspector_mode ) {
2914 request_values.resource_id = inspector_resource_id;
2915 }
2916 if ( save_button ) {
2917 save_button.disabled = true;
2918 save_button.classList.add( 'is-busy' );
2919 }
2920 if ( cancel_button ) {
2921 cancel_button.disabled = true;
2922 }
2923 form.classList.add( 'is-saving' );
2924 form.setAttribute( 'aria-busy', 'true' );
2925 form.querySelectorAll( 'input, select, textarea, button' ).forEach( function ( control ) {
2926 control_disabled_states.push( { control: control, disabled: control.disabled } );
2927 control.disabled = true;
2928 } );
2929
2930 request_inspector( config, action, request_values ).then( function ( response ) {
2931 if ( mutation_request_sequence !== inspector_mutation_request_sequence ) {
2932 return;
2933 }
2934 if ( ! response || true !== response.success || ! response.data ) {
2935 throw new Error( get_inspector_response_message( response, config.i18n.inspector_save_failed ) );
2936 }
2937 pending_highlight_ids = Array.isArray( response.data.resource_ids ) ? response.data.resource_ids.map( String ) : [];
2938 inspector_dirty = false;
2939 success_message = get_inspector_response_message( response, '' );
2940 success_message_is_global = show_admin_message( success_message, 'success', 3000 );
2941 submitted_form_is_active = document.documentElement.contains( form );
2942 if ( 'create' === submitted_mode ) {
2943 inspector_mutation_in_progress = false;
2944 if ( submitted_form_is_active ) {
2945 close_inspector( config, false );
2946 }
2947 if ( catalog_controller ) {
2948 catalog_controller.load( { page_number: 1 } );
2949 }
2950 return;
2951 }
2952 if ( ! submitted_form_is_active ) {
2953 if ( catalog_controller ) {
2954 catalog_controller.load();
2955 }
2956 inspector_mutation_in_progress = false;
2957 return;
2958 }
2959 if ( response.data.schema && render_inspector_schema( config, response.data.schema, false ) ) {
2960 form = document.querySelector( '[data-wpbc-catalog-resource-inspector-form]' );
2961 show_inspector_message( form, success_message_is_global ? '' : success_message, false );
2962 } else {
2963 form.classList.remove( 'is-saving' );
2964 form.removeAttribute( 'aria-busy' );
2965 control_disabled_states.forEach( function ( control_state ) {
2966 if ( document.documentElement.contains( control_state.control ) ) {
2967 control_state.control.disabled = control_state.disabled;
2968 }
2969 } );
2970 if ( save_button ) {
2971 save_button.classList.remove( 'is-busy' );
2972 }
2973 if ( cancel_button ) {
2974 cancel_button.disabled = false;
2975 }
2976 inspector_original_fields = serialize_inspector_fields();
2977 synchronize_inspector_dirty_state();
2978 show_inspector_message( form, success_message_is_global ? '' : success_message, false );
2979 }
2980 if ( catalog_controller ) {
2981 catalog_controller.load();
2982 }
2983 inspector_mutation_in_progress = false;
2984 } ).catch( function ( error ) {
2985 if ( mutation_request_sequence !== inspector_mutation_request_sequence ) {
2986 return;
2987 }
2988 inspector_mutation_in_progress = false;
2989 var message = error && error.message ? error.message : config.i18n.inspector_save_failed || '';
2990 if ( ! document.documentElement.contains( form ) ) {
2991 show_admin_message( message, 'error', 5000 );
2992 return;
2993 }
2994 form.classList.remove( 'is-saving' );
2995 form.removeAttribute( 'aria-busy' );
2996 show_inspector_message( form, message, true );
2997 control_disabled_states.forEach( function ( control_state ) {
2998 if ( document.documentElement.contains( control_state.control ) ) {
2999 control_state.control.disabled = control_state.disabled;
3000 }
3001 } );
3002 if ( save_button ) {
3003 save_button.classList.remove( 'is-busy' );
3004 }
3005 if ( cancel_button ) {
3006 cancel_button.disabled = false;
3007 }
3008 synchronize_inspector_dirty_state();
3009 } );
3010 }
3011
3012 /**
3013 * Submit bulk, permanent-delete, and capacity review inspector states.
3014 *
3015 * Mutations remain impossible from selection alone: bulk editing requires a
3016 * signed preview, and deletion additionally requires explicit acknowledgement.
3017 *
3018 * @param {SubmitEvent} event Inspector form submission.
3019 * @param {Object} config Catalog configuration.
3020 * @return {void}
3021 */
3022 function submit_reviewed_inspector( event, config ) {
3023 var form = event.target;
3024 var save_button = document.querySelector( '[data-wpbc-ui-catalog-inspector-save]' );
3025 var cancel_button = document.querySelector( '[data-wpbc-ui-catalog-inspector-cancel]' );
3026 var request_sequence;
3027 var action;
3028 var values;
3029 var fallback;
3030 var is_mutation;
3031 var submitted_mode;
3032 var submitted_resource_ids;
3033 var submitted_tracks_selection;
3034
3035 event.preventDefault();
3036 if ( inspector_selection_stale || ! is_expected_inspector_submit( event, form ) || ( save_button && ( save_button.disabled || save_button.classList.contains( 'is-busy' ) ) ) || ! report_inspector_validity( form ) ) {
3037 return;
3038 }
3039 submitted_mode = inspector_mode;
3040 submitted_resource_ids = inspector_resource_ids.slice();
3041 submitted_tracks_selection = inspector_tracks_selection;
3042 if ( 'bulk_edit' === submitted_mode ) {
3043 inspector_bulk_operations = collect_bulk_operations();
3044 action = config.bulk_preview_action;
3045 values = { resource_ids: JSON.stringify( inspector_resource_ids ), operations: JSON.stringify( inspector_bulk_operations ) };
3046 fallback = config.i18n.bulk_review_failed;
3047 } else if ( 'bulk_review' === submitted_mode ) {
3048 action = config.bulk_apply_action;
3049 values = { resource_ids: JSON.stringify( inspector_resource_ids ), operations: JSON.stringify( inspector_bulk_operations ), review_token: inspector_review_token };
3050 fallback = config.i18n.bulk_apply_failed;
3051 } else if ( 'delete_review' === submitted_mode ) {
3052 action = config.delete_apply_action;
3053 values = { acknowledged: '1', resource_ids: JSON.stringify( inspector_resource_ids ), review_token: inspector_review_token };
3054 fallback = config.i18n.delete_apply_failed;
3055 } else if ( 'capacity' === submitted_mode ) {
3056 action = config.capacity_preview_action;
3057 values = { resource_id: inspector_resource_id, target_capacity: inspector_capacity_target, detach_resource_ids: JSON.stringify( inspector_capacity_detach_ids ), decrease_action: inspector_capacity_decrease_action };
3058 fallback = config.i18n.capacity_review_failed;
3059 } else if ( 'capacity_review' === submitted_mode ) {
3060 action = config.capacity_apply_action;
3061 var capacity_acknowledgement = form.querySelector( '[data-wpbc-catalog-capacity-delete-acknowledgement]' );
3062 values = { resource_id: inspector_resource_id, target_capacity: inspector_capacity_target, detach_resource_ids: JSON.stringify( inspector_capacity_detach_ids ), decrease_action: inspector_capacity_decrease_action, acknowledged: capacity_acknowledgement && capacity_acknowledgement.checked ? '1' : '0', review_token: inspector_review_token };
3063 fallback = config.i18n.capacity_apply_failed;
3064 } else {
3065 return;
3066 }
3067
3068 is_mutation = 'bulk_review' === submitted_mode || 'delete_review' === submitted_mode || 'capacity_review' === submitted_mode;
3069 request_sequence = is_mutation ? ++inspector_mutation_request_sequence : ++inspector_request_sequence;
3070 if ( is_mutation ) {
3071 inspector_mutation_in_progress = true;
3072 }
3073 if ( 'bulk_review' === submitted_mode && get_inline_review_workflow() ) {
3074 get_inline_review_workflow().synchronize( { busy: true, can_apply: true } );
3075 }
3076 if ( 'delete_review' === submitted_mode && get_delete_review_workflow() ) {
3077 get_delete_review_workflow().synchronize( { busy: true, can_apply: true } );
3078 }
3079 if ( save_button ) {
3080 save_button.disabled = true;
3081 save_button.classList.add( 'is-busy' );
3082 }
3083 if ( cancel_button ) {
3084 cancel_button.disabled = true;
3085 }
3086 form.classList.add( 'is-saving' );
3087 form.setAttribute( 'aria-busy', 'true' );
3088
3089 request_inspector( config, action, values ).then( function ( response ) {
3090 if ( request_sequence !== ( is_mutation ? inspector_mutation_request_sequence : inspector_request_sequence ) ) {
3091 return;
3092 }
3093 if ( ! response || true !== response.success || ! response.data ) {
3094 throw new Error( get_inspector_response_message( response, fallback ) );
3095 }
3096 if ( 'bulk_edit' === submitted_mode ) {
3097 if ( ! response.data.preview || ! render_bulk_review( config, response.data.preview ) ) {
3098 throw new Error( fallback || '' );
3099 }
3100 return;
3101 }
3102 if ( 'capacity' === submitted_mode ) {
3103 if ( ! response.data.preview || ! render_capacity_review( config, response.data.preview ) ) {
3104 throw new Error( fallback || '' );
3105 }
3106 return;
3107 }
3108 if ( 'bulk_review' === submitted_mode ) {
3109 pending_highlight_ids = Array.isArray( response.data.updated_ids ) ? response.data.updated_ids.map( String ) : [];
3110 } else if ( 'capacity_review' === submitted_mode ) {
3111 pending_highlight_ids = Array.isArray( response.data.affected_ids ) ? response.data.affected_ids.map( String ) : [];
3112 } else {
3113 var mount = document.getElementById( config.mount_id );
3114 var selection = mount && mount._wpbc_ui_catalog_selection_controller;
3115 var selected_resource_ids = get_selected_resource_ids( config );
3116 var deleted_selected_resource = submitted_resource_ids.some( function ( resource_id ) {
3117 return -1 !== selected_resource_ids.indexOf( Number( resource_id ) );
3118 } );
3119 if ( selection && 'function' === typeof selection.clear && ( submitted_tracks_selection || deleted_selected_resource ) ) {
3120 selection.clear();
3121 }
3122 }
3123 show_admin_message( get_inspector_response_message( response, '' ), 'success', 4000 );
3124 inspector_dirty = false;
3125 inspector_mutation_in_progress = false;
3126 if ( document.documentElement.contains( form ) ) {
3127 close_inspector( config, false );
3128 }
3129 if ( catalog_controller ) {
3130 catalog_controller.load();
3131 }
3132 } ).catch( function ( error ) {
3133 if ( request_sequence !== ( is_mutation ? inspector_mutation_request_sequence : inspector_request_sequence ) ) {
3134 return;
3135 }
3136 if ( is_mutation ) {
3137 inspector_mutation_in_progress = false;
3138 }
3139 if ( ! document.documentElement.contains( form ) ) {
3140 show_admin_message( error && error.message ? error.message : fallback || '', 'error', 5000 );
3141 return;
3142 }
3143 form.classList.remove( 'is-saving' );
3144 form.removeAttribute( 'aria-busy' );
3145 show_inspector_message( form, error && error.message ? error.message : fallback || '', true );
3146 if ( save_button ) {
3147 save_button.classList.remove( 'is-busy' );
3148 save_button.disabled = inspector_selection_stale || ( 'delete_review' === inspector_mode && ! form.querySelector( '[data-wpbc-catalog-resource-delete-acknowledgement]:checked' ) );
3149 }
3150 if ( cancel_button ) {
3151 cancel_button.disabled = false;
3152 }
3153 if ( 'bulk_review' === submitted_mode && get_inline_review_workflow() ) {
3154 get_inline_review_workflow().synchronize( { busy: false, can_apply: ! inspector_selection_stale } );
3155 }
3156 if ( 'delete_review' === submitted_mode && get_delete_review_workflow() ) {
3157 get_delete_review_workflow().synchronize( { busy: false, can_apply: ! inspector_selection_stale } );
3158 }
3159 } );
3160 }
3161
3162 /**
3163 * Invalidate an open selection-owned inspector when its selection changes.
3164 *
3165 * @param {CustomEvent} event Shared selection lifecycle event.
3166 * @param {Object} config Catalog configuration.
3167 * @return {void}
3168 */
3169 function handle_inspector_selection_change( event, config ) {
3170 var selected_ids = event && event.detail && Array.isArray( event.detail.selected_ids ) ? event.detail.selected_ids : get_selected_resource_ids( config );
3171 var form;
3172 var save_button;
3173
3174 if ( ! inspector_tracks_selection || -1 === [ 'bulk_edit', 'bulk_review', 'delete_review' ].indexOf( inspector_mode ) ) {
3175 return;
3176 }
3177 form = document.querySelector( '[data-wpbc-catalog-resource-bulk-form], [data-wpbc-catalog-resource-bulk-review-form], [data-wpbc-catalog-resource-delete-form]' );
3178 save_button = document.querySelector( '[data-wpbc-ui-catalog-inspector-save]' );
3179 if ( resource_id_lists_match( inspector_resource_ids, selected_ids ) ) {
3180 if ( ! inspector_selection_stale ) {
3181 return;
3182 }
3183 inspector_selection_stale = false;
3184 show_inspector_message( form, '', false );
3185 if ( 'delete_review' === inspector_mode && get_delete_review_workflow() ) {
3186 get_delete_review_workflow().synchronize( { busy: false, can_apply: true } );
3187 }
3188 if ( 'bulk_edit' === inspector_mode ) {
3189 synchronize_bulk_editor( null );
3190 } else if ( save_button ) {
3191 var acknowledgement = form ? form.querySelector( '[data-wpbc-catalog-resource-delete-acknowledgement]' ) : null;
3192 save_button.disabled = 'delete_review' === inspector_mode && ( ! acknowledgement || ! acknowledgement.checked );
3193 }
3194 return;
3195 }
3196 inspector_selection_stale = true;
3197 show_inspector_message( form, config.i18n.selection_changed || '', true );
3198 if ( 'delete_review' === inspector_mode && get_delete_review_workflow() ) {
3199 get_delete_review_workflow().synchronize( { busy: false, can_apply: false } );
3200 }
3201 if ( save_button ) {
3202 save_button.disabled = true;
3203 }
3204 }
3205
3206 /**
3207 * Apply the post-mutation highlight after an AJAX catalog refresh.
3208 *
3209 * @return {void}
3210 */
3211 function apply_pending_highlights() {
3212 var first_row = null;
3213
3214 pending_highlight_ids.forEach( function ( resource_id ) {
3215 var row = document.querySelector( '[data-wpbc-booking-resource-id="' + resource_id + '"]' );
3216 if ( row ) {
3217 row.classList.add( 'is-recently-saved' );
3218 first_row = first_row || row;
3219 }
3220 } );
3221 if ( first_row ) {
3222 first_row.scrollIntoView( { block: 'nearest', behavior: 'smooth' } );
3223 }
3224 window.setTimeout( function () {
3225 document.querySelectorAll( '.wpbc_booking_resources__item.is-recently-saved' ).forEach( function ( row ) {
3226 row.classList.remove( 'is-recently-saved' );
3227 } );
3228 }, 5000 );
3229 pending_highlight_ids = [];
3230 }
3231
3232 /**
3233 * Handle completed shared renders for this Resource catalog only.
3234 *
3235 * @param {CustomEvent} event Shared catalog lifecycle event.
3236 * @return {void}
3237 */
3238 function handle_catalog_rendered( event ) {
3239 var config = window.wpbc_catalog_booking_resources_config;
3240 var event_detail = event && event.detail ? event.detail : {};
3241
3242 if ( ! config || event_detail.catalog_id !== config.id || ! event_detail.response ) {
3243 return;
3244 }
3245
3246 catalog_response = event_detail.response;
3247 synchronize_booking_resources_toolbar( config, catalog_response );
3248 render_booking_resources_response( config, catalog_response );
3249 render_inline_bar( config );
3250 synchronize_inline_controls( config );
3251 if ( inspector_resource_id ) {
3252 mark_inspector_resource_row( inspector_resource_id );
3253 }
3254 apply_pending_highlights();
3255 }
3256
3257 /**
3258 * Request a selected pagination page through the shared controller.
3259 *
3260 * @param {MouseEvent} event Catalog click event.
3261 * @return {void}
3262 */
3263 function handle_catalog_click( event ) {
3264 var action_button = event.target.closest( '[data-wpbc-booking-resource-action]' );
3265 var action_details;
3266 var catalog_mount;
3267 var config = window.wpbc_catalog_booking_resources_config;
3268 var page_button = event.target.closest( '[data-wpbc-ui-catalog-page]' );
3269 var resource_action_event;
3270
3271 if ( inline_state.active && action_button ) {
3272 event.preventDefault();
3273 return;
3274 }
3275
3276 if ( page_button && ! page_button.disabled ) {
3277 pending_focus_direction = page_button.getAttribute( 'data-wpbc-ui-catalog-page-direction' ) || 'page';
3278 }
3279
3280 if ( ! action_button || ! config ) {
3281 return;
3282 }
3283 var action_id = action_button.getAttribute( 'data-wpbc-booking-resource-action' ) || '';
3284 if ( 'toggle_details' === action_id ) {
3285 var resource_id = Number( action_button.getAttribute( 'data-wpbc-booking-resource-id' ) || 0 );
3286 var resource_row = get_resource_item_container( action_button );
3287 event.preventDefault();
3288 if ( resource_id && resource_row && config.details_action ) {
3289 if ( resource_id === details_resource_id ) {
3290 close_details_row( true );
3291 } else {
3292 open_details_row( config, action_button, resource_row, resource_id );
3293 }
3294 }
3295 return;
3296 }
3297 if ( 'copy_details_value' === action_id ) {
3298 event.preventDefault();
3299 copy_details_value( action_button.getAttribute( 'data-wpbc-booking-resource-copy-value' ) || '', action_button, config );
3300 return;
3301 }
3302 action_details = action_button.closest( 'details' );
3303 if ( action_details ) {
3304 action_details.removeAttribute( 'open' );
3305 }
3306
3307 catalog_mount = document.getElementById( config.mount_id );
3308 if ( catalog_mount ) {
3309 if ( 'function' === typeof window.CustomEvent ) {
3310 resource_action_event = new window.CustomEvent( 'wpbc:booking-resource-action', {
3311 bubbles: true,
3312 detail: {
3313 action: action_button.getAttribute( 'data-wpbc-booking-resource-action' ) || '',
3314 resource_id: Number( action_button.getAttribute( 'data-wpbc-booking-resource-id' ) || 0 )
3315 }
3316 } );
3317 } else {
3318 resource_action_event = document.createEvent( 'CustomEvent' );
3319 resource_action_event.initCustomEvent( 'wpbc:booking-resource-action', true, false, {
3320 action: action_button.getAttribute( 'data-wpbc-booking-resource-action' ) || '',
3321 resource_id: Number( action_button.getAttribute( 'data-wpbc-booking-resource-id' ) || 0 )
3322 } );
3323 }
3324 catalog_mount.dispatchEvent( resource_action_event );
3325 }
3326 }
3327
3328 /**
3329 * Close expanded details with Escape and restore disclosure focus.
3330 *
3331 * @param {KeyboardEvent} event Catalog keyboard event.
3332 * @return {void}
3333 */
3334 function handle_catalog_keydown( event ) {
3335 var config = window.wpbc_catalog_booking_resources_config;
3336
3337 if ( 'Escape' === event.key && inline_state.active && 'inline_review' !== inspector_mode ) {
3338 synchronize_inline_drafts( config );
3339 if ( ! inline_state.changed_rows.length || window.confirm( config.i18n.inline_discard || '' ) ) {
3340 event.preventDefault();
3341 leave_inline_mode( config, true, '' );
3342 }
3343 return;
3344 }
3345 if ( 'Escape' === event.key && event.target.closest( '[data-wpbc-booking-resource-details-row]' ) ) {
3346 event.preventDefault();
3347 close_details_row( true );
3348 }
3349 }
3350
3351 /**
3352 * Block Booking Resource image changes in public demo installations.
3353 *
3354 * This capture-phase guard runs before the shared delegated media-uploader
3355 * handler, preventing the WordPress media modal from opening. Server-side
3356 * create validation independently rejects a forged picture URL.
3357 *
3358 * @param {MouseEvent} event Browser click event.
3359 * @param {Object} config Catalog configuration.
3360 * @return {void}
3361 */
3362 function protect_demo_resource_image_change( event, config ) {
3363 var media_button;
3364 var inspector_form;
3365 var message;
3366 var message_title;
3367
3368 if ( ! config || ! is_true_flag( config.is_demo ) || ! event.target || 'function' !== typeof event.target.closest ) {
3369 return;
3370 }
3371
3372 media_button = event.target.closest( '.wpbc_media_upload_button, [data-wpbc-catalog-resource-remove-image]' );
3373 inspector_form = media_button ? media_button.closest( '[data-wpbc-catalog-resource-inspector-form]' ) : null;
3374 if ( ! media_button || ! inspector_form ) {
3375 return;
3376 }
3377
3378 event.preventDefault();
3379 event.stopPropagation();
3380 if ( 'function' === typeof event.stopImmediatePropagation ) {
3381 event.stopImmediatePropagation();
3382 }
3383 message = config.i18n && config.i18n.demo_image_change_unavailable ? config.i18n.demo_image_change_unavailable : '';
3384 message_title = config.i18n && config.i18n.demo_image_change_unavailable_title ? config.i18n.demo_image_change_unavailable_title : '';
3385 open_booking_resource_message_dialog( message, message_title, media_button );
3386 }
3387
3388 /**
3389 * Mount the localized catalog configuration after the document is ready.
3390 *
3391 * @return {void}
3392 */
3393 function mount_booking_resources_catalog() {
3394 var config = window.wpbc_catalog_booking_resources_config;
3395 var mount_element;
3396
3397 if ( ! config || ! window.wpbc_ui_catalog || 'function' !== typeof window.wpbc_ui_catalog.mount ) {
3398 return;
3399 }
3400
3401 mount_element = document.getElementById( config.mount_id );
3402 if ( ! mount_element ) {
3403 return;
3404 }
3405
3406 mount_element.addEventListener( 'wpbc:ui-catalog-rendered', handle_catalog_rendered );
3407 mount_element.addEventListener( 'wpbc:ui-catalog-before-render', function () {
3408 close_details_row( false );
3409 } );
3410 mount_element.addEventListener( 'wpbc:ui-catalog-hierarchy-change', function () {
3411 close_details_row( false );
3412 synchronize_card_group_panels( mount_element );
3413 } );
3414 mount_element.addEventListener( 'wpbc:ui-catalog-selection-change', function ( event ) {
3415 handle_inspector_selection_change( event, config );
3416 } );
3417 mount_element.addEventListener( 'wpbc:ui-catalog-selection-restored', function ( event ) {
3418 handle_inspector_selection_change( event, config );
3419 } );
3420 mount_element.addEventListener( 'click', protect_inline_drafts_from_catalog_controls, true );
3421 mount_element.addEventListener( 'change', protect_inline_drafts_from_catalog_controls, true );
3422 mount_element.addEventListener( 'input', protect_inline_drafts_from_catalog_controls, true );
3423 mount_element.addEventListener( 'click', handle_catalog_click );
3424 mount_element.addEventListener( 'keydown', handle_catalog_keydown );
3425 window.addEventListener( 'resize', function () {
3426 synchronize_overflow_tooltips( mount_element );
3427 } );
3428 catalog_controller = window.wpbc_ui_catalog.mount( config );
3429 if ( catalog_controller ) {
3430 if ( 'function' === typeof window.wpbc_ui_catalog.create_inline_editing_workflow ) {
3431 inline_workflow_controller = window.wpbc_ui_catalog.create_inline_editing_workflow( mount_element, {
3432 bar_selector: '[data-wpbc-catalog-inline-bar]',
3433 cancel_selector: '[data-wpbc-catalog-inline-cancel]',
3434 controls_root: document,
3435 count_selector: '[data-wpbc-catalog-inline-count]',
3436 page_element: mount_element.matches( '.wpbc_booking_resources_page' ) ? mount_element : mount_element.querySelector( '.wpbc_booking_resources_page' ),
3437 protected_selector: '[data-wpbc-catalog-booking-resource-create]',
3438 review_selector: '[data-wpbc-catalog-inline-review]',
3439 toggle_label_selector: '[data-wpbc-catalog-inline-toggle-label]',
3440 toggle_selector: '[data-wpbc-catalog-inline-toggle]'
3441 } );
3442 }
3443 render_booking_resources_filters( config );
3444 render_booking_resources_toolbar( config );
3445 render_inline_bar( config );
3446 synchronize_inline_controls( config );
3447 mount_inspector_shell( config );
3448 }
3449 if ( 'function' === typeof window.wpbc_define_tippy_tooltips ) {
3450 window.wpbc_define_tippy_tooltips( '[data-wpbc-catalog-booking-resource-upgrade]' );
3451 }
3452
3453 document.addEventListener( 'wpbc:booking-resource-action', function ( event ) {
3454 var detail = event && event.detail ? event.detail : {};
3455 if ( 'edit_resource' === detail.action ) {
3456 open_inspector( config, 'edit', Number( detail.resource_id ) || 0, document.activeElement );
3457 } else if ( 'publish_resource' === detail.action ) {
3458 open_inspector( config, 'edit', Number( detail.resource_id ) || 0, document.activeElement, 'shortcode_publishing' );
3459 } else if ( 'adjust_capacity' === detail.action ) {
3460 open_capacity_editor( config, Number( detail.resource_id ) || 0, document.activeElement );
3461 } else if ( 'delete_resource' === detail.action ) {
3462 open_delete_review( config, [ Number( detail.resource_id ) || 0 ], document.activeElement, false );
3463 }
3464 } );
3465 document.addEventListener( 'click', function ( event ) {
3466 protect_demo_resource_image_change( event, config );
3467 }, true );
3468 document.addEventListener( 'click', function ( event ) {
3469 var inline_toggle = event.target.closest( '[data-wpbc-catalog-inline-toggle]' );
3470 var inline_cancel = event.target.closest( '[data-wpbc-catalog-inline-cancel]' );
3471 var inline_review = event.target.closest( '[data-wpbc-catalog-inline-review]' );
3472 var create_button = event.target.closest( '[data-wpbc-catalog-booking-resource-create]' );
3473 var upgrade_button = event.target.closest( '[data-wpbc-catalog-booking-resource-upgrade]' );
3474 var cancel_button = event.target.closest( '[data-wpbc-ui-catalog-inspector-cancel]' );
3475 var remove_image_button = event.target.closest( '[data-wpbc-catalog-resource-remove-image]' );
3476 var shortcode_button = event.target.closest( '[data-wpbc-booking-resource-shortcode-command]' );
3477 var resource_row = get_resource_item_container( event.target );
3478 var selection_action = event.target.closest( '[data-wpbc-catalog-selection-action]' );
3479 if ( inline_toggle ) {
3480 event.preventDefault();
3481 start_inline_mode( config );
3482 return;
3483 }
3484 if ( inline_cancel ) {
3485 event.preventDefault();
3486 synchronize_inline_drafts( config );
3487 if ( ! inline_state.changed_rows.length || window.confirm( config.i18n.inline_discard || '' ) ) {
3488 leave_inline_mode( config, true, '' );
3489 }
3490 return;
3491 }
3492 if ( inline_review ) {
3493 event.preventDefault();
3494 preview_inline_changes( config, inline_review );
3495 return;
3496 }
3497 if ( selection_action ) {
3498 event.preventDefault();
3499 if ( 'bulk_edit' === selection_action.getAttribute( 'data-wpbc-catalog-selection-action' ) ) {
3500 open_bulk_editor( config, selection_action );
3501 } else {
3502 open_delete_review( config, get_selected_resource_ids( config ), selection_action, true );
3503 }
3504 return;
3505 }
3506 if ( shortcode_button ) {
3507 event.preventDefault();
3508 var shortcode_resource_id = Number( shortcode_button.getAttribute( 'data-wpbc-booking-resource-id' ) || 0 );
3509 var shortcode_command = shortcode_button.getAttribute( 'data-wpbc-booking-resource-shortcode-command' ) || '';
3510 var shortcode_value = get_booking_resource_shortcode( shortcode_resource_id, shortcode_button );
3511 if ( 'copy' === shortcode_command ) {
3512 copy_details_value( shortcode_value, shortcode_button, config );
3513 } else if ( 'customize' === shortcode_command ) {
3514 customize_booking_resource_shortcode( shortcode_resource_id, shortcode_value );
3515 } else if ( 'publish' === shortcode_command ) {
3516 publish_booking_resource_shortcode( shortcode_resource_id, shortcode_value, shortcode_button );
3517 }
3518 return;
3519 }
3520 if ( create_button ) {
3521 event.preventDefault();
3522 open_inspector( config, 'create', 0, create_button );
3523 return;
3524 }
3525 if ( upgrade_button ) {
3526 event.preventDefault();
3527 open_booking_resource_upgrade_dialog( upgrade_button );
3528 return;
3529 }
3530 if ( cancel_button ) {
3531 event.preventDefault();
3532 if ( 'inline_review' === inspector_mode ) {
3533 inspector_dirty = false;
3534 close_inspector( config, false );
3535 inline_state.review_token = '';
3536 synchronize_inline_controls( config );
3537 } else if ( 'capacity_review' === inspector_mode && inspector_capacity_context ) {
3538 var reviewed_target_capacity = inspector_capacity_target;
3539 var reviewed_detach_ids = inspector_capacity_detach_ids.slice();
3540 var reviewed_decrease_action = inspector_capacity_decrease_action;
3541 inspector_dirty = false;
3542 render_capacity_editor( config, inspector_capacity_context );
3543 inspector_capacity_target = reviewed_target_capacity;
3544 inspector_capacity_detach_ids = reviewed_detach_ids;
3545 inspector_capacity_decrease_action = reviewed_decrease_action;
3546 synchronize_capacity_editor( config );
3547 } else {
3548 close_inspector( config, true );
3549 }
3550 return;
3551 }
3552 if ( remove_image_button ) {
3553 event.preventDefault();
3554 var image_field = remove_image_button.closest( '[data-wpbc-catalog-resource-field-wrap]' ).querySelector( '[data-wpbc-catalog-resource-field="picture_url"]' );
3555 if ( image_field ) {
3556 image_field.value = '';
3557 synchronize_inspector_image( image_field );
3558 synchronize_inspector_dirty_state();
3559 }
3560 return;
3561 }
3562 if ( ! inline_state.active && resource_row && ! event.target.closest( 'a, button, input, select, textarea, summary, details, label' ) ) {
3563 open_inspector( config, 'edit', Number( resource_row.getAttribute( 'data-wpbc-booking-resource-id' ) ) || 0, resource_row );
3564 }
3565 } );
3566 document.addEventListener( 'input', function ( event ) {
3567 if ( event.target.matches( '[data-wpbc-catalog-capacity-target], [data-wpbc-catalog-capacity-range]' ) ) {
3568 var context = inspector_capacity_context || {};
3569 var minimum = Number( context.minimum_capacity ) || 1;
3570 var maximum = Number( context.maximum_capacity ) || minimum;
3571 var requested_capacity = Math.round( Number( event.target.value ) || minimum );
3572
3573 inspector_capacity_target = Math.max( minimum, Math.min( maximum, requested_capacity ) );
3574 synchronize_capacity_editor( config );
3575 return;
3576 }
3577 if ( event.target.matches( '[data-wpbc-catalog-inline-field]' ) ) {
3578 synchronize_inline_drafts( config );
3579 return;
3580 }
3581 if ( event.target.matches( '[data-wpbc-catalog-resource-bulk-value], [data-wpbc-catalog-resource-bulk-range]' ) ) {
3582 synchronize_bulk_editor( event.target );
3583 return;
3584 }
3585 if ( event.target.matches( '[data-wpbc-catalog-resource-range]' ) ) {
3586 synchronize_inspector_number_from_range( event.target );
3587 return;
3588 }
3589 if ( event.target.matches( '[data-wpbc-catalog-resource-inspector-form] [data-wpbc-catalog-resource-field]' ) ) {
3590 if ( 'picture_url' === event.target.getAttribute( 'data-wpbc-catalog-resource-field' ) ) {
3591 synchronize_inspector_image( event.target );
3592 }
3593 if ( 'number' === event.target.type ) {
3594 synchronize_inspector_numeric_range( event.target.getAttribute( 'data-wpbc-catalog-resource-field' ) || '' );
3595 }
3596 synchronize_inspector_dirty_state();
3597 }
3598 } );
3599 document.addEventListener( 'change', function ( event ) {
3600 if ( get_delete_review_workflow() && get_delete_review_workflow().handle_change( event ) ) {
3601 return;
3602 }
3603 if ( event.target.matches( '[data-wpbc-catalog-capacity-decrease-action]' ) ) {
3604 inspector_capacity_decrease_action = 'delete' === event.target.value ? 'delete' : 'detach';
3605 synchronize_capacity_editor( config );
3606 return;
3607 }
3608 if ( event.target.matches( '[data-wpbc-catalog-capacity-detach]' ) ) {
3609 var detach_id = Number( event.target.value ) || 0;
3610 if ( event.target.checked ) {
3611 if ( -1 === inspector_capacity_detach_ids.indexOf( detach_id ) ) {
3612 inspector_capacity_detach_ids.push( detach_id );
3613 }
3614 } else {
3615 inspector_capacity_detach_ids = inspector_capacity_detach_ids.filter( function ( resource_id ) { return resource_id !== detach_id; } );
3616 }
3617 synchronize_capacity_editor( config );
3618 return;
3619 }
3620 if ( event.target.matches( '[data-wpbc-catalog-inline-field]' ) ) {
3621 synchronize_inline_drafts( config );
3622 return;
3623 }
3624 if ( event.target.matches( '[data-wpbc-catalog-resource-bulk-enable], [data-wpbc-catalog-resource-bulk-operation], [data-wpbc-catalog-resource-bulk-value], [data-wpbc-catalog-resource-bulk-range]' ) ) {
3625 synchronize_bulk_editor( event.target );
3626 return;
3627 }
3628 if ( event.target.matches( '[data-wpbc-catalog-capacity-delete-acknowledgement]' ) ) {
3629 var capacity_delete_button = document.querySelector( '[data-wpbc-ui-catalog-inspector-save]' );
3630 var capacity_acknowledgement = event.target.closest( '.wpbc_booking_resources__delete_acknowledgement' );
3631
3632 if ( event.target.checked && capacity_acknowledgement ) {
3633 capacity_acknowledgement.classList.remove( 'wpbc_booking_resources__delete_acknowledgement--attention' );
3634 } else {
3635 pulse_delete_acknowledgement( capacity_acknowledgement );
3636 }
3637 if ( capacity_delete_button ) {
3638 capacity_delete_button.disabled = ! event.target.checked;
3639 }
3640 return;
3641 }
3642 if ( event.target.matches( '[data-wpbc-catalog-resource-range]' ) ) {
3643 synchronize_inspector_number_from_range( event.target );
3644 return;
3645 }
3646 if ( event.target.matches( '[data-wpbc-catalog-resource-radio-field="creation_mode"]' ) ) {
3647 synchronize_create_inspector_controls();
3648 synchronize_inspector_dirty_state();
3649 return;
3650 }
3651 if ( event.target.matches( '[data-wpbc-catalog-resource-inspector-form] [data-wpbc-catalog-resource-field]' ) ) {
3652 if ( 'create' === inspector_mode ) {
3653 synchronize_create_inspector_controls();
3654 }
3655 synchronize_inspector_dirty_state();
3656 }
3657 } );
3658 if ( window.jQuery ) {
3659 window.jQuery( '.wpbc_settings_page_wrapper' ).on( 'wpbc:right-sidebar-before-content-collapse.wpbcCatalogBookingResources', function ( event ) {
3660 var closing_mode = inspector_mode;
3661
3662 if ( inspector_mode && ! close_inspector( config, true ) ) {
3663 event.preventDefault();
3664 return;
3665 }
3666 if ( 'inline_review' === closing_mode ) {
3667 inline_state.review_token = '';
3668 synchronize_inline_controls( config );
3669 }
3670 } );
3671 window.jQuery( document ).on( 'wpbc_media_upload_url_set', '[data-wpbc-catalog-resource-field="picture_url"]', function () {
3672 synchronize_inspector_image( this );
3673 synchronize_inspector_dirty_state();
3674 } );
3675 window.jQuery( document ).on( 'wpbc:resource-shortcode-selected', function ( event, selection ) {
3676 var selected_resource_id = Number( selection && selection.resource_id ? selection.resource_id : 0 );
3677 var selected_shortcode = String( selection && selection.shortcode ? selection.shortcode : '' );
3678 var inspector_shortcode;
3679
3680 if ( ! selected_resource_id ) {
3681 return;
3682 }
3683 synchronize_booking_resource_shortcode_input( selected_resource_id, selected_shortcode );
3684 document.querySelectorAll( '[data-wpbc-booking-resource-id="' + String( selected_resource_id ) + '"][data-wpbc-booking-resource-shortcode-command]' ).forEach( function ( action_button ) {
3685 action_button.setAttribute( 'data-wpbc-booking-resource-shortcode', selected_shortcode );
3686 } );
3687 var details_row = document.querySelector( '[data-wpbc-booking-resource-details-row="' + String( selected_resource_id ) + '"]' );
3688 var details_code = details_row ? details_row.querySelector( '[data-wpbc-booking-resource-details-section="booking_page"] code' ) : null;
3689 if ( details_code ) {
3690 details_code.textContent = selected_shortcode;
3691 details_code.setAttribute( 'data-wpbc-ui-catalog-overflow-tooltip', selected_shortcode );
3692 synchronize_overflow_tooltips( document.getElementById( config.mount_id ) );
3693 }
3694 if ( selected_resource_id === inspector_resource_id ) {
3695 inspector_shortcode = document.querySelector( '[data-wpbc-catalog-resource-inspector-form] .wpbc_catalog_booking_resources__editor_code' );
3696 if ( inspector_shortcode ) {
3697 inspector_shortcode.value = selected_shortcode;
3698 synchronize_inspector_dirty_state();
3699 }
3700 }
3701 } );
3702 }
3703 document.addEventListener( 'submit', function ( event ) {
3704 if ( event.target.matches( '[data-wpbc-catalog-inline-review-form]' ) ) {
3705 apply_inline_changes( event, config );
3706 } else if ( event.target.matches( '[data-wpbc-catalog-resource-inspector-form]' ) ) {
3707 submit_inspector( event, config );
3708 } else if ( event.target.matches( '[data-wpbc-catalog-resource-bulk-form], [data-wpbc-catalog-resource-bulk-review-form], [data-wpbc-catalog-resource-delete-form], [data-wpbc-catalog-resource-capacity-form]' ) ) {
3709 submit_reviewed_inspector( event, config );
3710 }
3711 } );
3712 window.addEventListener( 'beforeunload', function ( event ) {
3713 if ( inspector_dirty || inspector_mutation_in_progress || ( inline_state.active && inline_state.changed_rows.length ) ) {
3714 event.preventDefault();
3715 event.returnValue = '';
3716 }
3717 } );
3718 }
3719
3720 if ( 'loading' === document.readyState ) {
3721 document.addEventListener( 'DOMContentLoaded', mount_booking_resources_catalog );
3722 } else {
3723 mount_booking_resources_catalog();
3724 }
3725 }( window, document ) );
3726