PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
booking / includes / page-catalog-booking-resources / _src / booking_resources_catalog.js

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

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