PluginProbe
Booking Calendar / 11.8.3
Booking Calendar v11.8.3
11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 10.11 All 204 releases
booking / includes / _shared-ui-catalog / _src / wpbc_ui_catalog.js

wpbc_ui_catalog.js in Booking Calendar 11.8.3, at includes/_shared-ui-catalog/_src/wpbc_ui_catalog.js

2,096 lines 75.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Control shared request sequences and render normalized catalog responses.
3 *
4 * Domain scripts provide configuration and domain-specific interactions. This
5 * controller owns only allow-listed WP templates, shared response validation,
6 * loading, empty, populated, error, and stale-response mechanics.
7 *
8 * @since 11.6.0
9 */
10 ( function ( window, document ) {
11 'use strict';
12
13 var catalog_states = {};
14
15 /**
16 * Return a normalized non-negative request sequence.
17 *
18 * @param {*} sequence Candidate request sequence.
19 * @return {number|null} Sequence or null when malformed.
20 */
21 function normalize_sequence( sequence ) {
22 var normalized_sequence;
23
24 if ( 'number' === typeof sequence && isFinite( sequence ) && Math.floor( sequence ) === sequence ) {
25 normalized_sequence = sequence;
26 } else if ( 'string' === typeof sequence && /^\d+$/.test( sequence ) ) {
27 normalized_sequence = parseInt( sequence, 10 );
28 } else {
29 return null;
30 }
31
32 return 0 <= normalized_sequence ? normalized_sequence : null;
33 }
34
35 /**
36 * Return a supported positive response schema version.
37 *
38 * WordPress localizes top-level scalar values as strings, so the registered
39 * configuration may contain "1" while the nested response retains number 1.
40 *
41 * @param {*} schema_version Candidate schema version.
42 * @return {number|null} Supported version or null.
43 */
44 function normalize_schema_version( schema_version ) {
45 var normalized_version = normalize_sequence( schema_version );
46
47 return 1 === normalized_version ? normalized_version : null;
48 }
49
50 /**
51 * Return one catalog's request state.
52 *
53 * @param {string} catalog_id Registered catalog identifier.
54 * @return {Object|null} Mutable catalog state or null.
55 */
56 function get_catalog_state( catalog_id ) {
57 if ( ! catalog_id || 'string' !== typeof catalog_id ) {
58 return null;
59 }
60
61 if ( ! catalog_states[ catalog_id ] ) {
62 catalog_states[ catalog_id ] = {
63 actions_controller: null,
64 abort_controller: null,
65 config: null,
66 content_element: null,
67 latest_sequence: 0,
68 preference_abort_controller: null,
69 preference_revision: 0,
70 request_values: {},
71 search_timer: 0,
72 selection_controller: null,
73 sortable: null
74 };
75 }
76
77 return catalog_states[ catalog_id ];
78 }
79
80 /**
81 * Return the registered, bounded delay for an incremental search request.
82 *
83 * Search timing is a domain-neutral interaction mechanic. Catalogs may tune
84 * the delay through their server-normalized configuration without replacing
85 * the shared request controller.
86 *
87 * @param {Object} config Registered browser configuration.
88 * @return {number} Delay in milliseconds between zero and 2000.
89 */
90 function get_search_debounce_delay( config ) {
91 var search_config = config && config.search && 'object' === typeof config.search ? config.search : {};
92 var debounce_delay = Number( search_config.debounce_delay_ms );
93
94 if ( ! isFinite( debounce_delay ) || debounce_delay < 0 ) {
95 return 300;
96 }
97
98 return Math.min( 2000, Math.floor( debounce_delay ) );
99 }
100
101 /**
102 * Determine whether clearing search bypasses the incremental-search delay.
103 *
104 * Immediate clear remains the compatibility default. A catalog may disable
105 * it only through server-normalized, domain-neutral search configuration.
106 *
107 * @param {Object} config Registered browser configuration.
108 * @return {boolean} True when Clear must request unfiltered results now.
109 */
110 function is_immediate_search_clear_enabled( config ) {
111 return ! config || ! config.search || false !== config.search.immediate_clear;
112 }
113
114 /**
115 * Start a new request sequence for one catalog.
116 *
117 * @param {string} catalog_id Registered catalog identifier.
118 * @return {number} New sequence, or zero for an invalid catalog.
119 */
120 function next_request_sequence( catalog_id ) {
121 var catalog_state = get_catalog_state( catalog_id );
122
123 if ( ! catalog_state ) {
124 return 0;
125 }
126
127 catalog_state.latest_sequence += 1;
128
129 return catalog_state.latest_sequence;
130 }
131
132 /**
133 * Determine whether a response belongs to an older request.
134 *
135 * @param {string} catalog_id Registered catalog identifier.
136 * @param {*} sequence Response request sequence.
137 * @return {boolean} True when the response must not render.
138 */
139 function is_stale_response( catalog_id, sequence ) {
140 var catalog_state = get_catalog_state( catalog_id );
141 var normalized_sequence = normalize_sequence( sequence );
142
143 return ! catalog_state || null === normalized_sequence || normalized_sequence < catalog_state.latest_sequence;
144 }
145
146 /**
147 * Resolve one allow-listed template identifier from the configuration.
148 *
149 * @param {Object} config Registered browser configuration.
150 * @param {string} template_role Template role such as empty or error.
151 * @return {string} Template identifier or an empty string.
152 */
153 function get_template_id( config, template_role ) {
154 var catalog_state;
155 var initial_request;
156 var template_id = '';
157 var template_pack;
158 var template_pack_id;
159
160 if ( ! config || ! config.templates || 'string' !== typeof template_role ) {
161 return '';
162 }
163
164 if ( 'string' === typeof config.templates[ template_role ] ) {
165 template_id = config.templates[ template_role ];
166 }
167
168 catalog_state = ( config.catalog_id || config.id ) ? get_catalog_state( config.catalog_id || config.id ) : null;
169 initial_request = config.initial_request || {};
170 template_pack_id = catalog_state && catalog_state.request_values.template_pack
171 ? catalog_state.request_values.template_pack
172 : initial_request.template_pack;
173 template_pack = config.template_packs && config.template_packs[ template_pack_id ];
174
175 if ( template_pack && 'string' === typeof template_pack[ template_role ] ) {
176 template_id = template_pack[ template_role ];
177 }
178
179 return /^[a-z0-9_-]+$/.test( template_id ) ? template_id : '';
180 }
181
182 /**
183 * Synchronize one server-authoritative allow-listed presentation pack.
184 *
185 * The active pack is shared presentation state only. Updating it before an
186 * items template is resolved allows an AJAX response to switch markup while
187 * leaving the provider, DTO, authorization, and mutation paths unchanged.
188 *
189 * @param {Object} config Registered browser configuration.
190 * @param {*} template_pack_id Candidate pack identifier from a response.
191 * @return {string} Active allow-listed pack identifier.
192 */
193 function set_active_template_pack( config, template_pack_id ) {
194 var catalog_root;
195 var catalog_state = config && config.catalog_id ? get_catalog_state( config.catalog_id ) : null;
196 var normalized_pack_id = 'string' === typeof template_pack_id ? template_pack_id : '';
197
198 if ( ! catalog_state || ! config.template_packs || ! config.template_packs[ normalized_pack_id ] ) {
199 normalized_pack_id = config && config.default_template_pack && config.template_packs
200 && config.template_packs[ config.default_template_pack ]
201 ? config.default_template_pack
202 : '';
203 }
204 if ( ! normalized_pack_id ) {
205 return '';
206 }
207
208 catalog_state.request_values.template_pack = normalized_pack_id;
209 catalog_root = catalog_state.content_element
210 ? catalog_state.content_element.closest( '[data-wpbc-catalog-id]' )
211 : null;
212 if ( catalog_root ) {
213 catalog_root.setAttribute( 'data-wpbc-template-pack', normalized_pack_id );
214 }
215
216 return normalized_pack_id;
217 }
218
219 /**
220 * Compile one allow-listed WordPress template.
221 *
222 * @param {Object} config Registered browser configuration.
223 * @param {string} template_role Template role.
224 * @return {Function|null} Compiled template or null.
225 */
226 function load_template( config, template_role ) {
227 var template_id = get_template_id( config, template_role );
228
229 if ( ! template_id || ! window.wp || 'function' !== typeof window.wp.template ) {
230 return null;
231 }
232
233 try {
234 return window.wp.template( template_id );
235 } catch ( error ) {
236 return null;
237 }
238 }
239
240 /**
241 * Replace one catalog's current presentation with rendered template output.
242 *
243 * @param {Object} config Registered browser configuration.
244 * @param {string} template_role Allow-listed template role.
245 * @param {Object} template_data Normalized template data.
246 * @return {boolean} True when rendered.
247 */
248 function render_template( config, template_role, template_data ) {
249 var catalog_root;
250 var catalog_state = get_catalog_state( config.catalog_id );
251 var render_target;
252 var rendered_html;
253 var template = load_template( config, template_role );
254
255 if ( ! catalog_state || ! catalog_state.content_element || ! template ) {
256 return false;
257 }
258
259 try {
260 rendered_html = template( template_data || {} );
261 } catch ( error ) {
262 return false;
263 }
264
265 render_target = catalog_state.response_element || catalog_state.content_element;
266 dispatch_catalog_event( config, 'wpbc:ui-catalog-before-render', {
267 catalog_id: config.catalog_id,
268 template_role: template_role
269 } );
270 render_target.innerHTML = rendered_html;
271 catalog_root = catalog_state.content_element.parentNode;
272 if ( catalog_root && 'function' === typeof catalog_root.setAttribute ) {
273 catalog_root.setAttribute( 'aria-busy', 'shell' === template_role ? 'true' : 'false' );
274 }
275 if ( 'shell' !== template_role ) {
276 set_catalog_loading_state( config, false );
277 }
278
279 return true;
280 }
281
282 /**
283 * Toggle a persistent catalog overlay without removing the current rows.
284 *
285 * Catalogs with a dedicated overlay keep their existing table visible beneath
286 * the Booking Calendar spinner. Generic catalogs retain the shell-template
287 * fallback when no overlay is declared.
288 *
289 * @param {Object} config Registered browser configuration.
290 * @param {boolean} is_loading Whether a request is active.
291 * @return {boolean} True when a persistent overlay was updated.
292 */
293 function set_catalog_loading_state( config, is_loading ) {
294 var catalog_state = config && config.catalog_id ? get_catalog_state( config.catalog_id ) : null;
295 var loading_element = catalog_state ? catalog_state.loading_element : null;
296
297 if ( catalog_state && catalog_state.content_element ) {
298 catalog_state.content_element.setAttribute( 'aria-busy', is_loading ? 'true' : 'false' );
299 }
300 if ( ! loading_element ) {
301 return false;
302 }
303
304 loading_element.classList.toggle( 'is-visible', !! is_loading );
305 return true;
306 }
307
308 /**
309 * Set the table minimum width from currently visible header contracts.
310 *
311 * Domain styles declare `--wpbc-listing-column-min-width` per column. The
312 * shared controller sums only rendered headers so wide/custom views scroll
313 * horizontally while short presets continue filling the available panel.
314 *
315 * @param {Object} config Registered browser configuration.
316 * @return {void}
317 */
318 function sync_catalog_table_min_width( config ) {
319 var mount_element = config && config.mount_id ? document.getElementById( config.mount_id ) : null;
320 var table = mount_element ? mount_element.querySelector( '.wpbc_ui_listing__table--catalog' ) : null;
321 var header_cells;
322 var table_min_width = 0;
323
324 if ( ! table || 'function' !== typeof window.getComputedStyle ) {
325 return;
326 }
327 header_cells = Array.prototype.filter.call( table.querySelectorAll( 'thead > tr > th' ), function ( header_cell ) {
328 return ! header_cell.hidden;
329 } );
330 header_cells.forEach( function ( header_cell ) {
331 var column_min_width = parseFloat(
332 window.getComputedStyle( header_cell ).getPropertyValue( '--wpbc-listing-column-min-width' )
333 );
334 if ( isFinite( column_min_width ) && 0 < column_min_width ) {
335 table_min_width += column_min_width;
336 }
337 } );
338 if ( 0 < table_min_width ) {
339 table.style.setProperty( '--wpbc-listing-table-min-width', Math.ceil( table_min_width ) + 'px' );
340 }
341 }
342
343 /**
344 * Keep the open column customizer inside the usable browser viewport.
345 *
346 * @param {HTMLDetailsElement} customizer Column customizer details element.
347 * @return {void}
348 */
349 function position_display_panel( customizer ) {
350 var panel = customizer ? customizer.querySelector( '.wpbc_ui_listing__display_panel' ) : null;
351 var summary = customizer ? customizer.querySelector( 'summary' ) : null;
352 var field_list = customizer ? customizer.querySelector( '[data-wpbc-ui-catalog-column-list]' ) : null;
353 var summary_rect;
354 var panel_rect;
355 var viewport_width;
356 var viewport_height;
357 var margin = 12;
358 var gap = 6;
359 var space_above;
360 var space_below;
361 var natural_height;
362 var open_above;
363 var available_height;
364 var rendered_height;
365 var panel_left;
366 var panel_top;
367
368 if ( ! customizer || ! customizer.open || ! panel || ! summary ) {
369 return;
370 }
371
372 customizer.classList.remove( 'is-positioned' );
373 panel.style.removeProperty( '--wpbc-listing-display-panel-max-height' );
374 panel.style.removeProperty( 'left' );
375 panel.style.removeProperty( 'top' );
376 summary_rect = summary.getBoundingClientRect();
377 panel_rect = panel.getBoundingClientRect();
378 viewport_width = document.documentElement.clientWidth || window.innerWidth || 0;
379 viewport_height = window.innerHeight || document.documentElement.clientHeight || 0;
380 space_above = Math.max( 0, summary_rect.top - margin - gap );
381 space_below = Math.max( 0, viewport_height - summary_rect.bottom - margin - gap );
382 natural_height = panel.scrollHeight;
383 if ( field_list ) {
384 natural_height += Math.max( 0, field_list.scrollHeight - field_list.clientHeight );
385 }
386 open_above = space_below < natural_height && space_above > space_below;
387 available_height = open_above ? space_above : space_below;
388 customizer.classList.toggle( 'is-open-above', open_above );
389 panel.style.setProperty( '--wpbc-listing-display-panel-max-height', Math.floor( available_height ) + 'px' );
390 rendered_height = panel.getBoundingClientRect().height;
391 panel_left = Math.max( margin, Math.min( summary_rect.right - panel_rect.width, viewport_width - panel_rect.width - margin ) );
392 panel_top = open_above ? summary_rect.top - gap - rendered_height : summary_rect.bottom + gap;
393 panel_top = Math.max( margin, Math.min( panel_top, viewport_height - rendered_height - margin ) );
394 panel.style.setProperty( 'left', Math.round( panel_left ) + 'px' );
395 panel.style.setProperty( 'top', Math.round( panel_top ) + 'px' );
396 customizer.classList.add( 'is-positioned' );
397 }
398
399 /**
400 * Clear fixed column-panel coordinates after the customizer closes.
401 *
402 * @param {HTMLDetailsElement} customizer Column customizer details element.
403 * @return {void}
404 */
405 function reset_display_panel_position( customizer ) {
406 var panel = customizer ? customizer.querySelector( '.wpbc_ui_listing__display_panel' ) : null;
407
408 if ( ! customizer || ! panel ) {
409 return;
410 }
411 customizer.classList.remove( 'is-open-above', 'is-positioned' );
412 panel.style.removeProperty( '--wpbc-listing-display-panel-max-height' );
413 panel.style.removeProperty( 'left' );
414 panel.style.removeProperty( 'top' );
415 }
416
417 /**
418 * Close one column customizer and optionally return focus to its summary.
419 *
420 * Keyboard and explicit Close-button dismissal restore focus to the control
421 * that opened the panel. Pointer dismissal keeps the pointer's natural focus
422 * destination while sharing the same details-toggle cleanup path.
423 *
424 * @param {HTMLDetailsElement|null} customizer Column customizer details element.
425 * @param {boolean} restore_focus Whether summary focus is restored.
426 * @return {void}
427 */
428 function close_display_customizer( customizer, restore_focus ) {
429 var summary;
430
431 if ( ! customizer || ! customizer.open ) {
432 return;
433 }
434 customizer.open = false;
435 if ( ! restore_focus ) {
436 return;
437 }
438 summary = customizer.querySelector( 'summary' );
439 if ( summary && 'function' === typeof summary.focus ) {
440 summary.focus();
441 }
442 }
443
444 /**
445 * Render a generic safe browser error.
446 *
447 * @param {Object} config Registered browser configuration.
448 * @param {string} message Safe localized error message.
449 * @return {boolean} True when rendered.
450 */
451 function render_error( config, message ) {
452 var i18n = config.i18n || {};
453
454 return render_template( config, 'error', {
455 title: i18n.error_title || '',
456 message: message || i18n.error_message || ''
457 } );
458 }
459
460 /**
461 * Dispatch one shared catalog lifecycle event from the current mount.
462 *
463 * @param {Object} config Registered browser configuration.
464 * @param {string} event_name Stable shared event name.
465 * @param {Object} detail JSON-safe event detail.
466 * @return {boolean} True when the event was dispatched.
467 */
468 function dispatch_catalog_event( config, event_name, detail ) {
469 var catalog_event;
470 var catalog_state = get_catalog_state( config.catalog_id );
471
472 if ( ! catalog_state || ! catalog_state.content_element || 'string' !== typeof event_name ) {
473 return false;
474 }
475
476 if ( 'function' === typeof window.CustomEvent ) {
477 catalog_event = new window.CustomEvent( event_name, {
478 bubbles: true,
479 detail: detail || {}
480 } );
481 } else {
482 catalog_event = document.createEvent( 'CustomEvent' );
483 catalog_event.initCustomEvent( event_name, true, false, detail || {} );
484 }
485
486 catalog_state.content_element.dispatchEvent( catalog_event );
487
488 return true;
489 }
490
491 /**
492 * Append one normalized request value to a URL-encoded AJAX body.
493 *
494 * @param {URLSearchParams} request_body Request body receiving values.
495 * @param {string} request_key Normalized request key.
496 * @param {*} request_value Scalar or scalar-array value.
497 * @return {void}
498 */
499 function append_request_value( request_body, request_key, request_value ) {
500 if ( Array.isArray( request_value ) ) {
501 request_value.forEach( function ( array_value ) {
502 if ( null !== array_value && 'object' !== typeof array_value ) {
503 request_body.append( request_key + '[]', String( array_value ) );
504 }
505 } );
506 return;
507 }
508
509 if ( null !== request_value && 'undefined' !== typeof request_value && 'object' !== typeof request_value ) {
510 request_body.append( request_key, String( request_value ) );
511 }
512 }
513
514 /**
515 * Return ordered column IDs from the current display controls.
516 *
517 * @param {HTMLElement} mount_element Catalog mount element.
518 * @return {string[]} Current column order.
519 */
520 function get_column_order( mount_element ) {
521 return Array.prototype.slice.call( mount_element.querySelectorAll( '[data-wpbc-ui-catalog-column-item]' ) ).map( function ( column_item ) {
522 return column_item.getAttribute( 'data-wpbc-ui-catalog-column-item' ) || '';
523 } ).filter( function ( column_id ) {
524 return !! column_id;
525 } );
526 }
527
528 /**
529 * Return visible column IDs from the current display controls.
530 *
531 * @param {HTMLElement} mount_element Catalog mount element.
532 * @return {string[]} Current visible columns.
533 */
534 function get_visible_columns( mount_element ) {
535 return Array.prototype.slice.call( mount_element.querySelectorAll( '[data-wpbc-ui-catalog-column-visible]' ) ).filter( function ( column_control ) {
536 return column_control.checked;
537 } ).map( function ( column_control ) {
538 return column_control.value;
539 } );
540 }
541
542 /**
543 * Request the current column controls and persist the validated result.
544 *
545 * @param {Object} config Registered browser configuration.
546 * @param {HTMLElement} mount_element Catalog mount element.
547 * @return {Promise<boolean>} Shared request result.
548 */
549 function save_column_controls( config, mount_element ) {
550 var view_control = mount_element.querySelector( '[data-wpbc-ui-catalog-view]' );
551
552 if ( view_control ) {
553 view_control.value = 'custom';
554 }
555
556 return request_catalog( config, {
557 column_order: get_column_order( mount_element ),
558 page_number: 1,
559 preference_action: 'save',
560 visible_columns: get_visible_columns( mount_element )
561 } );
562 }
563
564 /**
565 * Announce a completed column-order change to assistive technology.
566 *
567 * @param {Object} config Registered browser configuration.
568 * @param {HTMLElement} mount_element Catalog mount element.
569 * @return {void}
570 */
571 function announce_column_moved( config, mount_element ) {
572 var status_element = mount_element.querySelector( '[data-wpbc-ui-catalog-column-status]' );
573
574 if ( ! status_element ) {
575 return;
576 }
577 status_element.textContent = '';
578 window.setTimeout( function () {
579 status_element.textContent = config.i18n && config.i18n.column_moved ? config.i18n.column_moved : '';
580 }, 0 );
581 }
582
583 /**
584 * Synchronize the current catalog state into the initial URL aliases.
585 *
586 * Search and page number remain request-local but survive a normal refresh
587 * through the URL. Persisted settings are also reflected for shareable state.
588 *
589 * @param {Object} config Registered browser configuration.
590 * @param {Object} response Normalized successful response.
591 * @return {void}
592 */
593 function update_url_state( config, response ) {
594 var filters = response.filters || {};
595 var parameters = config.url_parameters || {};
596 var state_values = {
597 page_number: response.pagination.page_number,
598 items_per_page: response.pagination.items_per_page,
599 sort_by: response.sorting.sort_by,
600 sort_order: response.sorting.sort_order,
601 search: filters.search || '',
602 visible_columns: response.display.visible_columns || [],
603 column_order: response.display.column_order || [],
604 template_pack: response.display.template_pack || ''
605 };
606 var page_url;
607
608 if ( ! window.history || 'function' !== typeof window.history.replaceState || 'function' !== typeof window.URL ) {
609 return;
610 }
611
612 page_url = new window.URL( window.location.href );
613 Object.keys( filters ).forEach( function ( filter_key ) {
614 state_values[ filter_key ] = filters[ filter_key ];
615 } );
616 Object.keys( parameters ).forEach( function ( state_key ) {
617 var parameter_name = parameters[ state_key ];
618 var state_value = state_values[ state_key ];
619 if ( ! parameter_name ) {
620 return;
621 }
622 if ( Array.isArray( state_value ) ) {
623 state_value = state_value.join( ',' );
624 }
625 if ( '' === state_value || null === state_value || 'undefined' === typeof state_value ) {
626 page_url.searchParams.delete( parameter_name );
627 } else {
628 page_url.searchParams.set( parameter_name, String( state_value ) );
629 }
630 } );
631 window.history.replaceState( {}, document.title, page_url.toString() );
632 }
633
634 /**
635 * Bind domain-neutral delegated catalog controls once per mount.
636 *
637 * @param {Object} config Registered browser configuration.
638 * @param {HTMLElement} mount_element Catalog mount element.
639 * @return {void}
640 */
641 function bind_catalog_controls( config, mount_element ) {
642 var catalog_state = get_catalog_state( config.catalog_id );
643
644 if ( ! catalog_state || mount_element._wpbc_ui_catalog_controls_bound ) {
645 return;
646 }
647 mount_element._wpbc_ui_catalog_controls_bound = true;
648
649 mount_element.addEventListener( 'submit', function ( event ) {
650 var search_control;
651 if ( ! event.target.matches( '[data-wpbc-ui-catalog-filters]' ) ) {
652 return;
653 }
654 event.preventDefault();
655 search_control = mount_element.querySelector( '[data-wpbc-ui-catalog-search]' );
656 request_catalog( config, { page_number: 1, search: search_control ? search_control.value : '' } );
657 } );
658
659 mount_element.addEventListener( 'input', function ( event ) {
660 var clear_control;
661 if ( ! event.target.matches( '[data-wpbc-ui-catalog-search]' ) ) {
662 return;
663 }
664 clear_control = mount_element.querySelector( '[data-wpbc-ui-catalog-search-clear]' );
665 if ( clear_control ) {
666 clear_control.hidden = ! event.target.value;
667 }
668 window.clearTimeout( catalog_state.search_timer );
669 catalog_state.search_timer = window.setTimeout( function () {
670 request_catalog( config, { page_number: 1, search: event.target.value || '' } );
671 }, get_search_debounce_delay( config ) );
672 } );
673
674 mount_element.addEventListener( 'change', function ( event ) {
675 var default_request = config.default_request || {};
676 var filter_key;
677 if ( event.target.matches( '[data-wpbc-ui-catalog-items-per-page]' ) ) {
678 request_catalog( config, { items_per_page: Number( event.target.value ), page_number: 1, preference_action: 'save' } );
679 } else if ( event.target.matches( '[data-wpbc-ui-catalog-page-number]' ) ) {
680 request_catalog( config, { page_number: Number( event.target.value ) || 1 } );
681 } else if ( event.target.matches( '[data-wpbc-ui-catalog-template-pack]' ) ) {
682 if ( config.template_packs && config.template_packs[ event.target.value ] ) {
683 request_catalog( config, {
684 page_number: 1,
685 preference_action: 'save',
686 template_pack: event.target.value
687 } );
688 }
689 } else if ( event.target.matches( '[data-wpbc-ui-catalog-filter]' ) ) {
690 filter_key = event.target.getAttribute( 'data-wpbc-ui-catalog-filter' ) || '';
691 if ( /^[a-z0-9_]+$/.test( filter_key ) ) {
692 var filter_request = { page_number: 1, preference_action: 'save' };
693 filter_request[ filter_key ] = event.target.value;
694 request_catalog( config, filter_request );
695 }
696 } else if ( event.target.matches( '[data-wpbc-ui-catalog-column-visible]' ) ) {
697 save_column_controls( config, mount_element );
698 } else if ( event.target.matches( '[data-wpbc-ui-catalog-view]' ) && 'custom' !== event.target.value ) {
699 var view_definition = config.views && config.views.definitions ? config.views.definitions[ event.target.value ] : null;
700 if ( view_definition && Array.isArray( view_definition.fields ) ) {
701 request_catalog( config, {
702 page_number: 1,
703 preference_action: 'save',
704 visible_columns: view_definition.fields
705 } );
706 }
707 }
708 } );
709
710 mount_element.addEventListener( 'click', function ( event ) {
711 var close_control = event.target.closest( '[data-wpbc-ui-catalog-display-close]' );
712 var default_request = config.default_request || {};
713 var page_control = event.target.closest( '[data-wpbc-ui-catalog-page]' );
714 var reset_control = event.target.closest( '[data-wpbc-ui-catalog-preferences-reset]' );
715 var reset_order_control = event.target.closest( '[data-wpbc-ui-catalog-column-order-reset]' );
716 var search_clear = event.target.closest( '[data-wpbc-ui-catalog-search-clear]' );
717 var sort_control = event.target.closest( '[data-wpbc-ui-catalog-sort]' );
718 var sort_key;
719
720 if ( search_clear ) {
721 event.preventDefault();
722 var search_control = mount_element.querySelector( '[data-wpbc-ui-catalog-search]' );
723 window.clearTimeout( catalog_state.search_timer );
724 if ( search_control ) {
725 search_control.value = '';
726 search_control.focus();
727 }
728 search_clear.hidden = true;
729 if ( is_immediate_search_clear_enabled( config ) ) {
730 request_catalog( config, { page_number: 1, search: '' } );
731 } else {
732 catalog_state.search_timer = window.setTimeout( function () {
733 request_catalog( config, { page_number: 1, search: '' } );
734 }, get_search_debounce_delay( config ) );
735 }
736 } else if ( sort_control ) {
737 event.preventDefault();
738 sort_key = sort_control.getAttribute( 'data-wpbc-ui-catalog-sort' ) || '';
739 request_catalog( config, {
740 page_number: 1,
741 preference_action: 'save',
742 sort_by: sort_key,
743 sort_order: sort_key === catalog_state.request_values.sort_by && 'asc' === catalog_state.request_values.sort_order ? 'desc' : 'asc'
744 } );
745 } else if ( page_control && ! page_control.disabled ) {
746 event.preventDefault();
747 request_catalog( config, { page_number: Number( page_control.getAttribute( 'data-wpbc-ui-catalog-page' ) ) || 1 } );
748 } else if ( reset_order_control ) {
749 event.preventDefault();
750 request_catalog( config, { column_order: default_request.column_order || [], page_number: 1, preference_action: 'save' } );
751 } else if ( reset_control ) {
752 event.preventDefault();
753 request_catalog( config, Object.assign( {}, default_request, { preference_action: 'reset' } ) );
754 } else if ( close_control ) {
755 event.preventDefault();
756 var customizer = close_control.closest( '[data-wpbc-ui-catalog-display-customizer]' );
757 close_display_customizer( customizer, true );
758 }
759 } );
760
761 mount_element.addEventListener( 'keydown', function ( event ) {
762 var customizer = event.target && 'function' === typeof event.target.closest
763 ? event.target.closest( '[data-wpbc-ui-catalog-display-customizer]' )
764 : null;
765 if ( 'Escape' !== event.key || ! customizer || ! customizer.open ) {
766 return;
767 }
768 event.preventDefault();
769 close_display_customizer( customizer, true );
770 } );
771
772 mount_element.addEventListener( 'toggle', function ( event ) {
773 var customizer = event.target.closest( '[data-wpbc-ui-catalog-display-customizer]' );
774 if ( ! customizer ) {
775 return;
776 }
777 if ( customizer.open ) {
778 window.requestAnimationFrame( function () {
779 position_display_panel( customizer );
780 } );
781 } else {
782 reset_display_panel_position( customizer );
783 }
784 }, true );
785
786 document.addEventListener( 'click', function ( event ) {
787 var customizer = mount_element.querySelector( '[data-wpbc-ui-catalog-display-customizer]' );
788 if ( customizer && customizer.open && ! customizer.contains( event.target ) ) {
789 close_display_customizer( customizer, false );
790 }
791 } );
792 window.addEventListener( 'resize', function () {
793 position_display_panel( mount_element.querySelector( '[data-wpbc-ui-catalog-display-customizer]' ) );
794 sync_catalog_table_min_width( config );
795 } );
796 window.addEventListener( 'scroll', function ( event ) {
797 var customizer = mount_element.querySelector( '[data-wpbc-ui-catalog-display-customizer]' );
798 if (
799 customizer
800 && customizer.open
801 && (
802 ! event.target
803 || 'function' !== typeof event.target.closest
804 || ! event.target.closest( '[data-wpbc-ui-catalog-display-customizer]' )
805 )
806 ) {
807 position_display_panel( customizer );
808 }
809 }, true );
810 }
811
812 /**
813 * Initialize pointer and keyboard column ordering after toolbar rendering.
814 *
815 * @param {Object} config Registered browser configuration.
816 * @return {void}
817 */
818 function refresh_catalog_controls( config ) {
819 var catalog_state = get_catalog_state( config.catalog_id );
820 var mount_element = document.getElementById( config.mount_id );
821 var column_list = mount_element ? mount_element.querySelector( '[data-wpbc-ui-catalog-column-list]' ) : null;
822
823 if ( ! catalog_state || ! column_list || column_list._wpbc_ui_catalog_initialized ) {
824 return;
825 }
826 column_list._wpbc_ui_catalog_initialized = true;
827 column_list.addEventListener( 'keydown', function ( event ) {
828 var handle = event.target.closest( '[data-wpbc-ui-catalog-column-drag-handle]' );
829 var item;
830 var sibling;
831 if ( ! handle || ( 'ArrowUp' !== event.key && 'ArrowDown' !== event.key ) ) {
832 return;
833 }
834 item = handle.closest( '[data-wpbc-ui-catalog-column-item]' );
835 sibling = 'ArrowUp' === event.key ? item.previousElementSibling : item.nextElementSibling;
836 while ( sibling && '1' !== sibling.getAttribute( 'data-wpbc-ui-catalog-column-reorderable' ) ) {
837 sibling = 'ArrowUp' === event.key ? sibling.previousElementSibling : sibling.nextElementSibling;
838 }
839 if ( ! sibling ) {
840 return;
841 }
842 event.preventDefault();
843 if ( 'ArrowUp' === event.key ) {
844 column_list.insertBefore( item, sibling );
845 } else {
846 column_list.insertBefore( sibling, item );
847 }
848 save_column_controls( config, mount_element );
849 announce_column_moved( config, mount_element );
850 handle.focus();
851 } );
852
853 if ( 'function' === typeof window.Sortable ) {
854 catalog_state.sortable = new window.Sortable( column_list, {
855 animation: 150,
856 chosenClass: 'is-dragging',
857 draggable: '[data-wpbc-ui-catalog-column-reorderable="1"]',
858 ghostClass: 'is-drag-placeholder',
859 handle: '[data-wpbc-ui-catalog-column-drag-handle]',
860 onEnd: function ( sort_event ) {
861 if ( sort_event.oldIndex !== sort_event.newIndex ) {
862 save_column_controls( config, mount_element );
863 announce_column_moved( config, mount_element );
864 }
865 }
866 } );
867 }
868 }
869
870 /**
871 * Validate a normalized server response before rendering.
872 *
873 * @param {Object} config Registered browser configuration.
874 * @param {*} response Candidate response.
875 * @return {boolean} True when the response contract is supported.
876 */
877 function validate_response( config, response ) {
878 var configured_schema_version = config ? normalize_schema_version( config.schema_version ) : null;
879 var response_schema_version = response ? normalize_schema_version( response.schema_version ) : null;
880
881 if (
882 ! config
883 || ! response
884 || 'object' !== typeof response
885 || response.catalog_id !== config.catalog_id
886 || null === configured_schema_version
887 || response_schema_version !== configured_schema_version
888 || 'boolean' !== typeof response.success
889 || null === normalize_sequence( response.request_id )
890 ) {
891 return false;
892 }
893
894 if ( false === response.success ) {
895 return !! response.error
896 && 'object' === typeof response.error
897 && 'string' === typeof response.error.code
898 && 'string' === typeof response.error.message
899 && 'boolean' === typeof response.error.retryable;
900 }
901
902 return Array.isArray( response.items )
903 && !! response.pagination
904 && 'object' === typeof response.pagination
905 && !! response.sorting
906 && 'object' === typeof response.sorting
907 && !! response.filters
908 && 'object' === typeof response.filters
909 && !! response.display
910 && 'object' === typeof response.display
911 && !! response.hierarchy
912 && 'object' === typeof response.hierarchy
913 && !! response.capabilities
914 && 'object' === typeof response.capabilities
915 && Array.isArray( response.messages );
916 }
917
918 /**
919 * Refresh optional shared hierarchy mechanics after domain rows are mounted.
920 *
921 * The rendered lifecycle event runs synchronously first so a domain adapter
922 * can compose its WP row templates before the controller indexes node DOM.
923 *
924 * @param {Object} config Registered browser configuration.
925 * @param {Object} response Normalized current response.
926 * @return {boolean} Whether hierarchy behavior is active.
927 */
928 function refresh_catalog_hierarchy( config, response ) {
929 var catalog_state = config && config.catalog_id ? get_catalog_state( config.catalog_id ) : null;
930
931 return !! (
932 catalog_state
933 && catalog_state.hierarchy_controller
934 && 'function' === typeof catalog_state.hierarchy_controller.refresh
935 && catalog_state.hierarchy_controller.refresh( response && response.hierarchy ? response.hierarchy : {} )
936 );
937 }
938
939 /**
940 * Render a current normalized response and ignore stale sequences.
941 *
942 * @param {Object} config Registered browser configuration.
943 * @param {*} response Candidate normalized response.
944 * @param {*} request_sequence Sequence assigned when the request began.
945 * @return {boolean} True when the response changed the catalog.
946 */
947 function render_response( config, response, request_sequence ) {
948 var catalog_state;
949 var i18n;
950 var items_template_data;
951 var response_sequence = response && normalize_sequence( response.request_id );
952 var normalized_sequence = normalize_sequence( request_sequence );
953
954 if ( ! config || ! config.catalog_id ) {
955 return false;
956 }
957
958 catalog_state = get_catalog_state( config.catalog_id );
959 if (
960 ! catalog_state
961 || null === normalized_sequence
962 || null === response_sequence
963 || response_sequence !== normalized_sequence
964 || is_stale_response( config.catalog_id, normalized_sequence )
965 ) {
966 return false;
967 }
968
969 if ( ! validate_response( config, response ) ) {
970 return render_error( config, config.i18n && config.i18n.error_message ? config.i18n.error_message : '' );
971 }
972
973 if ( false === response.success ) {
974 return render_error( config, response.error.message );
975 }
976
977 set_active_template_pack( config, response.display.template_pack );
978
979 i18n = config.i18n || {};
980 if ( 0 === response.items.length ) {
981 var is_empty_rendered = render_template( config, 'empty', {
982 title: i18n.empty_title || '',
983 message: i18n.empty_message || ''
984 } );
985 if ( is_empty_rendered ) {
986 dispatch_catalog_event( config, 'wpbc:ui-catalog-rendered', {
987 catalog_id: config.catalog_id,
988 request_sequence: normalized_sequence,
989 response: response
990 } );
991 refresh_catalog_hierarchy( config, response );
992 }
993 return is_empty_rendered;
994 }
995
996 items_template_data = Object.assign( {}, response, { i18n: i18n } );
997 if ( ! render_template( config, 'items', items_template_data ) ) {
998 return render_error( config, i18n.error_message || '' );
999 }
1000 dispatch_catalog_event( config, 'wpbc:ui-catalog-rendered', {
1001 catalog_id: config.catalog_id,
1002 request_sequence: normalized_sequence,
1003 response: response
1004 } );
1005 refresh_catalog_hierarchy( config, response );
1006 sync_catalog_table_min_width( config );
1007
1008 return true;
1009 }
1010
1011 /**
1012 * Request and render one normalized catalog response.
1013 *
1014 * Request cancellation and sequence checks are shared mechanics. Catalog
1015 * scripts supply only normalized request values and respond to lifecycle
1016 * events after the allow-listed items template is mounted.
1017 *
1018 * @param {Object} config Registered browser configuration.
1019 * @param {Object} request_values Normalized request overrides.
1020 * @return {Promise<boolean>} Whether a current response was rendered.
1021 */
1022 function request_catalog( config, request_values ) {
1023 var catalog_state;
1024 var persistent_request_values;
1025 var preference_action;
1026 var request_body;
1027 var request_sequence;
1028 var request_url;
1029
1030 if (
1031 ! config
1032 || ! config.catalog_id
1033 || ! config.ajax_url
1034 || ! config.action
1035 || ! config.nonce
1036 || 'function' !== typeof window.fetch
1037 ) {
1038 return Promise.resolve( render_error( config || {}, config && config.i18n ? config.i18n.error_message : '' ) );
1039 }
1040
1041 catalog_state = get_catalog_state( config.catalog_id );
1042 if ( ! catalog_state ) {
1043 return Promise.resolve( false );
1044 }
1045
1046 if ( catalog_state.abort_controller && 'function' === typeof catalog_state.abort_controller.abort ) {
1047 catalog_state.abort_controller.abort();
1048 }
1049 catalog_state.abort_controller = 'function' === typeof window.AbortController ? new window.AbortController() : null;
1050 persistent_request_values = Object.assign( {}, request_values || {} );
1051 preference_action = persistent_request_values.preference_action || '';
1052 delete persistent_request_values.preference_action;
1053 catalog_state.request_values = Object.assign( {}, config.initial_request || {}, catalog_state.request_values || {}, persistent_request_values );
1054 request_sequence = next_request_sequence( config.catalog_id );
1055 catalog_state.request_values.request_id = request_sequence;
1056
1057 if ( ! set_catalog_loading_state( config, true ) ) {
1058 render_template( config, 'shell', {
1059 catalog_id: config.catalog_id,
1060 aria_label: config.i18n && config.i18n.catalog_label ? config.i18n.catalog_label : '',
1061 loading_message: config.i18n && config.i18n.loading ? config.i18n.loading : ''
1062 } );
1063 }
1064 dispatch_catalog_event( config, 'wpbc:ui-catalog-loading', {
1065 catalog_id: config.catalog_id,
1066 request_sequence: request_sequence
1067 } );
1068
1069 request_body = new window.URLSearchParams();
1070 request_body.append( 'action', config.action );
1071 request_body.append( 'nonce', config.nonce );
1072 if ( preference_action ) {
1073 catalog_state.preference_revision = Math.max( Date.now(), catalog_state.preference_revision + 1 );
1074 request_body.append( 'preference_action', preference_action );
1075 request_body.append( 'preference_revision', String( catalog_state.preference_revision ) );
1076 }
1077 Object.keys( catalog_state.request_values ).forEach( function ( request_key ) {
1078 append_request_value( request_body, request_key, catalog_state.request_values[ request_key ] );
1079 } );
1080 request_url = String( config.ajax_url );
1081
1082 return window.fetch( request_url, {
1083 method: 'POST',
1084 credentials: 'same-origin',
1085 headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
1086 body: request_body.toString(),
1087 signal: catalog_state.abort_controller ? catalog_state.abort_controller.signal : undefined
1088 } ).then( function ( response ) {
1089 return response.text().then( function ( response_text ) {
1090 var response_payload = null;
1091
1092 try {
1093 response_payload = JSON.parse( response_text );
1094 } catch ( error ) {
1095 response_payload = null;
1096 }
1097
1098 if ( is_stale_response( config.catalog_id, request_sequence ) ) {
1099 return false;
1100 }
1101 if ( ! response_payload ) {
1102 return render_error( config, config.i18n && config.i18n.error_message ? config.i18n.error_message : '' );
1103 }
1104
1105 var is_rendered = render_response( config, response_payload, request_sequence );
1106 if ( is_rendered && response_payload.success ) {
1107 catalog_state.request_values = Object.assign( {}, catalog_state.request_values, {
1108 page_number: response_payload.pagination.page_number,
1109 items_per_page: response_payload.pagination.items_per_page,
1110 sort_by: response_payload.sorting.sort_by,
1111 sort_order: response_payload.sorting.sort_order,
1112 search: response_payload.filters.search || '',
1113 visible_columns: response_payload.display.visible_columns || [],
1114 column_order: response_payload.display.column_order || [],
1115 template_pack: response_payload.display.template_pack || ''
1116 } );
1117 Object.keys( response_payload.filters || {} ).forEach( function ( filter_key ) {
1118 catalog_state.request_values[ filter_key ] = response_payload.filters[ filter_key ];
1119 } );
1120 update_url_state( config, response_payload );
1121 }
1122
1123 return is_rendered;
1124 } );
1125 } ).catch( function ( error ) {
1126 if ( error && 'AbortError' === error.name ) {
1127 return false;
1128 }
1129 if ( is_stale_response( config.catalog_id, request_sequence ) ) {
1130 return false;
1131 }
1132
1133 return render_error( config, config.i18n && config.i18n.error_message ? config.i18n.error_message : '' );
1134 } );
1135 }
1136
1137 /**
1138 * Persist validated presentation preferences without rebuilding catalog rows.
1139 *
1140 * Domain catalogs may add their own scalar preference values to the shared
1141 * request state. The endpoint remains responsible for validation and
1142 * authorization. A separate abort slot prevents a disclosure-state save from
1143 * cancelling an active list request or showing the catalog loading overlay.
1144 *
1145 * @param {Object} config Registered browser configuration.
1146 * @param {Object} preference_values Shared or domain-owned request values.
1147 * @return {Promise<boolean>} Whether the current preference request succeeded.
1148 */
1149 function save_catalog_preferences( config, preference_values ) {
1150 var catalog_state;
1151 var request_body;
1152 var request_revision;
1153
1154 if ( ! config || ! config.catalog_id || ! config.ajax_url || ! config.action || ! config.nonce || 'function' !== typeof window.fetch ) {
1155 return Promise.resolve( false );
1156 }
1157 catalog_state = get_catalog_state( config.catalog_id );
1158 if ( ! catalog_state ) {
1159 return Promise.resolve( false );
1160 }
1161 if ( catalog_state.preference_abort_controller && 'function' === typeof catalog_state.preference_abort_controller.abort ) {
1162 catalog_state.preference_abort_controller.abort();
1163 }
1164 catalog_state.preference_abort_controller = 'function' === typeof window.AbortController ? new window.AbortController() : null;
1165 catalog_state.request_values = Object.assign( {}, config.initial_request || {}, catalog_state.request_values || {}, preference_values || {} );
1166 catalog_state.preference_revision = Math.max( Date.now(), catalog_state.preference_revision + 1 );
1167 request_revision = catalog_state.preference_revision;
1168
1169 request_body = new window.URLSearchParams();
1170 request_body.append( 'action', config.action );
1171 request_body.append( 'nonce', config.nonce );
1172 request_body.append( 'preference_action', 'save' );
1173 request_body.append( 'preference_revision', String( request_revision ) );
1174 request_body.append( 'preferences_only', '1' );
1175 Object.keys( catalog_state.request_values ).forEach( function ( request_key ) {
1176 append_request_value( request_body, request_key, catalog_state.request_values[ request_key ] );
1177 } );
1178
1179 return window.fetch( String( config.ajax_url ), {
1180 method: 'POST',
1181 credentials: 'same-origin',
1182 headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
1183 body: request_body.toString(),
1184 signal: catalog_state.preference_abort_controller ? catalog_state.preference_abort_controller.signal : undefined
1185 } ).then( function ( response ) {
1186 return response.text().then( function ( response_text ) {
1187 var response_payload = null;
1188 try {
1189 response_payload = JSON.parse( response_text );
1190 } catch ( error ) {
1191 response_payload = null;
1192 }
1193 return request_revision === catalog_state.preference_revision
1194 && response.ok
1195 && !! response_payload
1196 && true === response_payload.success;
1197 } );
1198 } ).catch( function ( error ) {
1199 return false;
1200 } );
1201 }
1202
1203 /**
1204 * Add full-text tooltips only to catalog text that is visually clipped.
1205 *
1206 * The helper owns the domain-neutral overflow measurement, keyboard focus,
1207 * Booking Calendar tooltip initialization, and native-title fallback. Domain
1208 * templates opt in by providing authorized plain text through the
1209 * `data-wpbc-ui-catalog-overflow-tooltip` attribute.
1210 *
1211 * @param {HTMLElement} catalog_mount Catalog mount element.
1212 * @return {void}
1213 */
1214 function synchronize_overflow_tooltips( catalog_mount ) {
1215 var has_overflowing_text = false;
1216 var tooltip_selector;
1217
1218 if ( ! catalog_mount ) {
1219 return;
1220 }
1221 catalog_mount.querySelectorAll( '[data-wpbc-ui-catalog-overflow-tooltip]' ).forEach( function ( text_element ) {
1222 var full_text = text_element.getAttribute( 'data-wpbc-ui-catalog-overflow-tooltip' ) || '';
1223 var static_title = text_element.getAttribute( 'data-wpbc-ui-catalog-static-title' ) || '';
1224 var is_overflowing = text_element.scrollWidth > text_element.clientWidth + 1
1225 || text_element.scrollHeight > text_element.clientHeight + 1;
1226
1227 if ( text_element._tippy && 'function' === typeof text_element._tippy.destroy ) {
1228 text_element._tippy.destroy();
1229 }
1230 text_element.classList.remove( 'tooltip_top', 'wpbc_ui_listing__overflow_tooltip' );
1231 text_element.removeAttribute( 'title' );
1232 text_element.removeAttribute( 'data-original-title' );
1233 if ( '1' === text_element.getAttribute( 'data-wpbc-ui-catalog-tooltip-tabindex' ) ) {
1234 text_element.removeAttribute( 'tabindex' );
1235 text_element.removeAttribute( 'data-wpbc-ui-catalog-tooltip-tabindex' );
1236 }
1237
1238 if ( full_text && is_overflowing ) {
1239 text_element.setAttribute( 'data-original-title', full_text );
1240 text_element.classList.add( 'tooltip_top', 'wpbc_ui_listing__overflow_tooltip' );
1241 if ( ! text_element.hasAttribute( 'tabindex' ) ) {
1242 text_element.setAttribute( 'tabindex', '0' );
1243 text_element.setAttribute( 'data-wpbc-ui-catalog-tooltip-tabindex', '1' );
1244 }
1245 has_overflowing_text = true;
1246 } else if ( static_title ) {
1247 text_element.setAttribute( 'title', static_title );
1248 }
1249 } );
1250
1251 tooltip_selector = catalog_mount.id ? '#' + catalog_mount.id + ' .wpbc_ui_listing__overflow_tooltip' : '';
1252 if ( has_overflowing_text && tooltip_selector && 'function' === typeof window.wpbc_define_tippy_tooltips && window.wpbc_define_tippy_tooltips( tooltip_selector ) ) {
1253 return;
1254 }
1255 catalog_mount.querySelectorAll( '.wpbc_ui_listing__overflow_tooltip' ).forEach( function ( text_element ) {
1256 text_element.setAttribute( 'title', text_element.getAttribute( 'data-original-title' ) || '' );
1257 } );
1258 }
1259
1260 /**
1261 * Create a domain-neutral native inspector state workflow.
1262 *
1263 * Domains supply their allow-listed shell renderer, host/footer boundaries,
1264 * localized shell data, and sidebar expansion callback. The shared workflow
1265 * owns only shell mounting and the empty, loading, error, and form states.
1266 *
1267 * @param {Object} settings Inspector boundary callbacks and shell data.
1268 * @return {Object|false} Inspector workflow controller or false when invalid.
1269 */
1270 function create_inspector_workflow( settings ) {
1271 var options = Object.assign( {
1272 expand: null,
1273 get_footer: null,
1274 get_host: null,
1275 render_shell: null,
1276 shell_data: {}
1277 }, settings || {} );
1278
1279 if ( 'function' !== typeof options.get_host || 'function' !== typeof options.render_shell ) {
1280 return false;
1281 }
1282
1283 /**
1284 * Return the current domain-owned inspector host.
1285 *
1286 * @return {Element|null} Inspector host or null when it is unavailable.
1287 */
1288 function get_host() {
1289 var host = options.get_host();
1290
1291 return host && host.querySelector ? host : null;
1292 }
1293
1294 /**
1295 * Return the current domain-owned sticky footer when configured.
1296 *
1297 * @return {Element|null} Inspector footer or null when it is unavailable.
1298 */
1299 function get_footer() {
1300 var footer = 'function' === typeof options.get_footer ? options.get_footer() : null;
1301
1302 return footer && footer.querySelector ? footer : null;
1303 }
1304
1305 /**
1306 * Mount the allow-listed shared shell inside the domain host once.
1307 *
1308 * @return {boolean} True when the inspector shell is available.
1309 */
1310 function mount() {
1311 var host = get_host();
1312 var rendered_shell;
1313
1314 if ( ! host ) {
1315 return false;
1316 }
1317 if ( ! host.querySelector( '[data-wpbc-ui-catalog-inspector]' ) ) {
1318 try {
1319 rendered_shell = options.render_shell( Object.assign( {}, options.shell_data || {} ) );
1320 } catch ( error ) {
1321 return false;
1322 }
1323 if ( 'string' !== typeof rendered_shell || ! rendered_shell ) {
1324 return false;
1325 }
1326 host.innerHTML = rendered_shell;
1327 }
1328
1329 return !! host.querySelector( '[data-wpbc-ui-catalog-inspector]' );
1330 }
1331
1332 /**
1333 * Synchronize one allow-listed inspector presentation state.
1334 *
1335 * @param {string} state Empty, loading, error, or form.
1336 * @param {string} message Optional safe error message.
1337 * @return {boolean} True when the mounted shell was updated.
1338 */
1339 function set_state( state, message ) {
1340 var error;
1341 var error_text;
1342 var footer;
1343 var form_target;
1344 var host;
1345 var loading;
1346 var empty;
1347
1348 if ( [ 'empty', 'loading', 'error', 'form' ].indexOf( state ) < 0 || ! mount() ) {
1349 return false;
1350 }
1351
1352 host = get_host();
1353 footer = get_footer();
1354 empty = host.querySelector( '[data-wpbc-ui-catalog-inspector-empty]' );
1355 loading = host.querySelector( '[data-wpbc-ui-catalog-inspector-loading]' );
1356 error = host.querySelector( '[data-wpbc-ui-catalog-inspector-error]' );
1357 form_target = host.querySelector( '[data-wpbc-ui-catalog-inspector-form]' );
1358
1359 if ( empty ) { empty.hidden = 'empty' !== state; }
1360 if ( loading ) { loading.hidden = 'loading' !== state; }
1361 if ( error ) {
1362 error.hidden = 'error' !== state;
1363 error_text = error.querySelector( 'p' );
1364 if ( error_text ) { error_text.textContent = String( message || '' ); }
1365 }
1366 if ( form_target && 'form' !== state ) { form_target.innerHTML = ''; }
1367 if ( footer ) { footer.hidden = 'form' !== state; }
1368
1369 return true;
1370 }
1371
1372 /**
1373 * Expand the configured native sidebar boundary.
1374 *
1375 * @return {void}
1376 */
1377 function expand() {
1378 if ( 'function' === typeof options.expand ) {
1379 options.expand();
1380 }
1381 }
1382
1383 /**
1384 * Mount, reveal loading state, and immediately expand the inspector.
1385 *
1386 * @return {boolean} True when the loading state was opened.
1387 */
1388 function open_loading() {
1389 if ( ! set_state( 'loading', '' ) ) {
1390 return false;
1391 }
1392 expand();
1393
1394 return true;
1395 }
1396
1397 /**
1398 * Return the shell form target used by domain-owned templates.
1399 *
1400 * @return {Element|null} Form target or null when mounting failed.
1401 */
1402 function get_form_target() {
1403 var host = mount() ? get_host() : null;
1404
1405 return host ? host.querySelector( '[data-wpbc-ui-catalog-inspector-form]' ) : null;
1406 }
1407
1408 return {
1409 expand: expand,
1410 get_form_target: get_form_target,
1411 mount: mount,
1412 open_loading: open_loading,
1413 set_state: set_state
1414 };
1415 }
1416
1417 /**
1418 * Create a domain-neutral inline-editing workflow controller.
1419 *
1420 * Domains retain ownership of editable fields, draft values, authorization,
1421 * review payloads, and mutations. This controller only synchronizes the
1422 * repeated catalog mechanics around an active inline workflow: sticky-bar
1423 * registration, busy controls, navigation locking, changed-row presentation,
1424 * and the shared active-state classes.
1425 *
1426 * @param {HTMLElement|string} catalog_mount Catalog mount element or its ID.
1427 * @param {Object} settings Domain selectors and page element.
1428 * @return {Object|false} Inline workflow controller or false when unavailable.
1429 */
1430 function create_inline_editing_workflow( catalog_mount, settings ) {
1431 var options;
1432 var mount_element = 'string' === typeof catalog_mount ? document.getElementById( catalog_mount ) : catalog_mount;
1433 var default_protected_selector = [
1434 '[data-wpbc-ui-catalog-view]',
1435 '[data-wpbc-ui-catalog-template-pack]',
1436 '[data-wpbc-ui-catalog-display-customizer] summary',
1437 '[data-wpbc-ui-catalog-search]',
1438 '[data-wpbc-ui-catalog-filter]',
1439 '[data-wpbc-ui-catalog-select-item]',
1440 '[data-wpbc-ui-catalog-select-all]',
1441 '[data-wpbc-ui-catalog-sort]',
1442 '[data-wpbc-ui-catalog-page]',
1443 '[data-wpbc-ui-catalog-page-number]',
1444 '[data-wpbc-ui-catalog-items-per-page]',
1445 '[data-wpbc-ui-catalog-column-visible]',
1446 '[data-wpbc-ui-catalog-column-order-reset]',
1447 '[data-wpbc-ui-catalog-preferences-reset]'
1448 ].join( ', ' );
1449
1450 if ( ! mount_element || ! mount_element.querySelector ) {
1451 return false;
1452 }
1453
1454 options = Object.assign( {
1455 bar_selector: '[data-wpbc-ui-catalog-inline-bar]',
1456 cancel_selector: '[data-wpbc-ui-catalog-inline-cancel]',
1457 controls_root: mount_element,
1458 count_selector: '[data-wpbc-ui-catalog-inline-count]',
1459 page_element: mount_element,
1460 protected_selector: '',
1461 review_selector: '[data-wpbc-ui-catalog-inline-review]',
1462 toggle_label_selector: '[data-wpbc-ui-catalog-inline-toggle-label]',
1463 toggle_selector: '[data-wpbc-ui-catalog-inline-toggle]'
1464 }, settings || {} );
1465
1466 /**
1467 * Return the configured page element without escaping the catalog mount.
1468 *
1469 * @return {HTMLElement|null} Configured page root, mount, or null.
1470 */
1471 function get_page_element() {
1472 if ( options.page_element && options.page_element.nodeType ) {
1473 return options.page_element;
1474 }
1475
1476 return 'string' === typeof options.page_element
1477 ? mount_element.querySelector( options.page_element )
1478 : mount_element;
1479 }
1480
1481 /**
1482 * Return the complete selector for controls locked by active drafts.
1483 *
1484 * @return {string} Shared selectors plus the trusted domain extension.
1485 */
1486 function get_protected_selector() {
1487 return options.protected_selector
1488 ? default_protected_selector + ', ' + options.protected_selector
1489 : default_protected_selector;
1490 }
1491
1492 /**
1493 * Preserve and restore a control's pre-workflow disabled state.
1494 *
1495 * @param {HTMLElement} control Catalog control to synchronize.
1496 * @param {boolean} controls_locked Whether inline navigation is locked.
1497 * @return {void}
1498 */
1499 function synchronize_protected_control( control, controls_locked ) {
1500 var prior_disabled;
1501
1502 if ( controls_locked ) {
1503 if ( ! control.hasAttribute( 'data-wpbc-ui-catalog-inline-was-disabled' ) ) {
1504 control.setAttribute( 'data-wpbc-ui-catalog-inline-was-disabled', control.disabled ? '1' : '0' );
1505 }
1506 control.disabled = true;
1507 control.setAttribute( 'aria-disabled', 'true' );
1508 return;
1509 }
1510
1511 if ( ! control.hasAttribute( 'data-wpbc-ui-catalog-inline-was-disabled' ) ) {
1512 return;
1513 }
1514 prior_disabled = '1' === control.getAttribute( 'data-wpbc-ui-catalog-inline-was-disabled' );
1515 control.disabled = prior_disabled;
1516 control.removeAttribute( 'data-wpbc-ui-catalog-inline-was-disabled' );
1517 if ( ! prior_disabled ) {
1518 control.removeAttribute( 'aria-disabled' );
1519 }
1520 }
1521
1522 /**
1523 * Register the current inline bar with the shared viewport controller.
1524 *
1525 * @return {void}
1526 */
1527 function register_sticky_bar() {
1528 var inline_bar = mount_element.querySelector( options.bar_selector );
1529 var selection_controller = mount_element._wpbc_ui_catalog_selection_controller;
1530
1531 if ( inline_bar && selection_controller && 'function' === typeof selection_controller.register_viewport_sticky ) {
1532 selection_controller.register_viewport_sticky( inline_bar );
1533 }
1534 }
1535
1536 /**
1537 * Remove shared changed-row presentation after inline mode ends.
1538 *
1539 * Domain drafts and values remain domain-owned. This cleanup removes only
1540 * the shared class and badge that this controller previously applied.
1541 *
1542 * @return {void}
1543 */
1544 function clear_changed_rows() {
1545 mount_element.querySelectorAll( '.wpbc_ui_catalog_inline_row.is-inline-changed' ).forEach( function ( row_element ) {
1546 set_row_changed( row_element, false, null, '' );
1547 } );
1548 }
1549
1550 /**
1551 * Synchronize shared inline workflow presentation from domain-owned state.
1552 *
1553 * @param {Object} workflow_state Normalized active, busy, count, and labels.
1554 * @return {void}
1555 */
1556 function synchronize( workflow_state ) {
1557 var active;
1558 var busy;
1559 var controls_root;
1560 var controls_locked;
1561 var inline_bar;
1562 var page_element;
1563 var toggle_button;
1564 var toggle_label;
1565
1566 workflow_state = workflow_state || {};
1567 active = true === workflow_state.active;
1568 busy = true === workflow_state.busy;
1569 controls_root = options.controls_root && options.controls_root.querySelectorAll ? options.controls_root : mount_element;
1570 controls_locked = active || true === workflow_state.lock_controls;
1571 inline_bar = mount_element.querySelector( options.bar_selector );
1572 page_element = get_page_element();
1573 toggle_button = mount_element.querySelector( options.toggle_selector );
1574
1575 if ( inline_bar ) {
1576 inline_bar.hidden = ! active;
1577 inline_bar.setAttribute( 'aria-busy', busy ? 'true' : 'false' );
1578 if ( inline_bar.querySelector( options.count_selector ) ) {
1579 inline_bar.querySelector( options.count_selector ).textContent = String( workflow_state.count_text || '' );
1580 }
1581 if ( inline_bar.querySelector( options.review_selector ) ) {
1582 inline_bar.querySelector( options.review_selector ).disabled = busy || ! Number( workflow_state.changed_count || 0 );
1583 }
1584 if ( inline_bar.querySelector( options.cancel_selector ) ) {
1585 inline_bar.querySelector( options.cancel_selector ).disabled = busy;
1586 }
1587 }
1588
1589 if ( toggle_button ) {
1590 toggle_button.classList.toggle( 'is-active', active );
1591 toggle_button.classList.toggle( 'is-busy', busy );
1592 toggle_button.disabled = busy
1593 || true === workflow_state.toggle_disabled
1594 || ( ! active && false === workflow_state.has_items );
1595 toggle_button.setAttribute( 'aria-pressed', active ? 'true' : 'false' );
1596 toggle_button.setAttribute( 'aria-busy', busy ? 'true' : 'false' );
1597 toggle_label = toggle_button.querySelector( options.toggle_label_selector );
1598 if ( toggle_label ) {
1599 toggle_label.textContent = active
1600 ? String( workflow_state.active_toggle_text || '' )
1601 : String( workflow_state.inactive_toggle_text || '' );
1602 }
1603 }
1604
1605 if ( page_element ) {
1606 page_element.classList.toggle( 'is-inline-editing', active );
1607 }
1608 if ( ! active ) {
1609 clear_changed_rows();
1610 }
1611 controls_root.querySelectorAll( get_protected_selector() ).forEach( function ( control ) {
1612 synchronize_protected_control( control, controls_locked );
1613 } );
1614 register_sticky_bar();
1615 if (
1616 mount_element._wpbc_ui_catalog_selection_controller
1617 && 'function' === typeof mount_element._wpbc_ui_catalog_selection_controller.refresh_viewport_sticky
1618 ) {
1619 mount_element._wpbc_ui_catalog_selection_controller.refresh_viewport_sticky();
1620 }
1621 }
1622
1623 /**
1624 * Block a captured event that targets a control protected by active drafts.
1625 *
1626 * @param {Event} event Captured browser event.
1627 * @param {boolean} controls_locked Whether the domain workflow is active.
1628 * @return {boolean} True when the event was blocked.
1629 */
1630 function protect_event( event, controls_locked ) {
1631 if ( ! controls_locked || ! event.target || ! event.target.closest ) {
1632 return false;
1633 }
1634 if ( ! event.target.closest( get_protected_selector() ) ) {
1635 return false;
1636 }
1637
1638 event.preventDefault();
1639 event.stopImmediatePropagation();
1640 return true;
1641 }
1642
1643 /**
1644 * Synchronize one changed row and its accessible text badge.
1645 *
1646 * @param {HTMLElement} row_element Domain row or card element.
1647 * @param {boolean} changed Whether its draft differs.
1648 * @param {HTMLElement} indicator_element Backward-compatible fallback badge host.
1649 * @param {string} changed_label Localized badge text.
1650 * @return {void}
1651 */
1652 function set_row_changed( row_element, changed, indicator_element, changed_label ) {
1653 var indicator;
1654 var preferred_indicator_host;
1655
1656 if ( ! row_element ) {
1657 return;
1658 }
1659 row_element.classList.add( 'wpbc_ui_catalog_inline_row' );
1660 row_element.classList.toggle( 'is-inline-changed', !! changed );
1661 preferred_indicator_host = row_element.querySelector( '[data-wpbc-ui-catalog-inline-changed-host]' );
1662 indicator = row_element.querySelector( '[data-wpbc-ui-catalog-inline-changed-label]' );
1663 if ( ! changed ) {
1664 if ( indicator ) {
1665 indicator.remove();
1666 }
1667 return;
1668 }
1669 if ( indicator && preferred_indicator_host && indicator.parentElement !== preferred_indicator_host ) {
1670 preferred_indicator_host.insertBefore( indicator, preferred_indicator_host.firstChild );
1671 }
1672 indicator_element = preferred_indicator_host || indicator_element;
1673 if ( ! indicator && indicator_element ) {
1674 indicator = document.createElement( 'span' );
1675 indicator.className = 'wpbc_ui_catalog_inline_changed_label';
1676 indicator.setAttribute( 'data-wpbc-ui-catalog-inline-changed-label', '' );
1677 if ( preferred_indicator_host ) {
1678 preferred_indicator_host.insertBefore( indicator, preferred_indicator_host.firstChild );
1679 } else {
1680 indicator_element.appendChild( indicator );
1681 }
1682 }
1683 if ( indicator ) {
1684 indicator.textContent = String( changed_label || '' );
1685 }
1686 }
1687
1688 return {
1689 protect_event: protect_event,
1690 register_sticky_bar: register_sticky_bar,
1691 set_row_changed: set_row_changed,
1692 synchronize: synchronize
1693 };
1694 }
1695
1696 /**
1697 * Create a domain-neutral signed-review presentation controller.
1698 *
1699 * Domains own preview and apply requests, signed plans, permissions, field
1700 * validation, and mutations. This controller accepts only the normalized
1701 * review DTO and owns the repeated model preparation and busy-state locking.
1702 *
1703 * @param {Object} settings DOM roots and domain button selectors.
1704 * @return {Object} Review presentation controller.
1705 */
1706 function create_inline_review_workflow( settings ) {
1707 var options = Object.assign( {
1708 apply_selector: '[data-wpbc-ui-catalog-inline-review-apply]',
1709 cancel_selector: '[data-wpbc-ui-catalog-inline-review-cancel]',
1710 root: document
1711 }, settings || {} );
1712
1713 /**
1714 * Normalize one server-authoritative review DTO for a domain template.
1715 *
1716 * @param {Object} review Server review with rows and field changes.
1717 * @param {Object} presentation Localized headings and explanatory text.
1718 * @return {Object} Executable-free template model.
1719 */
1720 function prepare( review, presentation ) {
1721 var normalized_rows = [];
1722
1723 review = review && 'object' === typeof review ? review : {};
1724 presentation = presentation && 'object' === typeof presentation ? presentation : {};
1725 ( Array.isArray( review.rows ) ? review.rows : [] ).forEach( function ( row ) {
1726 var normalized_fields = [];
1727 var normalized_notes = [];
1728
1729 if ( ! row || 'object' !== typeof row ) {
1730 return;
1731 }
1732 ( Array.isArray( row.fields ) ? row.fields : [] ).forEach( function ( field ) {
1733 if ( ! field || 'object' !== typeof field ) {
1734 return;
1735 }
1736 normalized_fields.push( {
1737 after: String( undefined === field.after ? '' : field.after ),
1738 before: String( undefined === field.before ? '' : field.before ),
1739 key: String( field.key || '' ),
1740 label: String( field.label || field.key || '' )
1741 } );
1742 } );
1743 ( Array.isArray( row.notes ) ? row.notes : [] ).forEach( function ( note ) {
1744 if ( 'string' === typeof note || 'number' === typeof note ) {
1745 normalized_notes.push( String( note ) );
1746 }
1747 } );
1748 if ( normalized_fields.length ) {
1749 normalized_rows.push( {
1750 fields: normalized_fields,
1751 id: Number( row.id || 0 ),
1752 notes: normalized_notes,
1753 title: String( row.title || '' )
1754 } );
1755 }
1756 } );
1757
1758 return {
1759 changed_label: String( presentation.changed_label || '' ),
1760 description: String( presentation.description || '' ),
1761 form_id: String( presentation.form_id || '' ),
1762 mode: String( presentation.mode || 'inline_review' ),
1763 pending_message: String( presentation.pending_message || '' ),
1764 rows: normalized_rows,
1765 title: String( presentation.title || '' ),
1766 warning: String( review.warning || presentation.warning || '' )
1767 };
1768 }
1769
1770 /**
1771 * Lock or unlock review actions while a domain request is in flight.
1772 *
1773 * @param {Object} review_state Busy and apply-ready flags.
1774 * @return {void}
1775 */
1776 function synchronize( review_state ) {
1777 var busy;
1778 var can_apply;
1779 var root = options.root && options.root.querySelectorAll ? options.root : document;
1780
1781 review_state = review_state || {};
1782 busy = true === review_state.busy;
1783 can_apply = true === review_state.can_apply;
1784 root.querySelectorAll( options.apply_selector ).forEach( function ( control ) {
1785 control.disabled = busy || ! can_apply;
1786 control.classList.toggle( 'is-busy', busy );
1787 control.setAttribute( 'aria-busy', busy ? 'true' : 'false' );
1788 } );
1789 root.querySelectorAll( options.cancel_selector ).forEach( function ( control ) {
1790 control.disabled = busy;
1791 } );
1792 root.querySelectorAll( '[data-wpbc-ui-catalog-inline-review-form]' ).forEach( function ( form ) {
1793 form.setAttribute( 'aria-busy', busy ? 'true' : 'false' );
1794 } );
1795 }
1796
1797 return {
1798 prepare: prepare,
1799 synchronize: synchronize
1800 };
1801 }
1802
1803 /**
1804 * Create a domain-neutral permanent-deletion review controller.
1805 *
1806 * Domains remain responsible for deciding whether deletion is allowed,
1807 * producing the signed impact review, rendering their allow-listed template,
1808 * and applying the mutation. This controller owns only the repeated browser
1809 * mechanics for explicit acknowledgement, destructive footer presentation,
1810 * busy locking, and reduced-motion-safe attention feedback.
1811 *
1812 * @param {Object} settings DOM roots and domain selectors.
1813 * @return {Object} Deletion-review presentation controller.
1814 */
1815 function create_delete_review_workflow( settings ) {
1816 var options = Object.assign( {
1817 acknowledgement_selector: '[data-wpbc-ui-catalog-delete-acknowledgement]',
1818 apply_selector: '[data-wpbc-ui-catalog-delete-apply], [data-wpbc-ui-catalog-inspector-save]',
1819 cancel_selector: '[data-wpbc-ui-catalog-delete-cancel], [data-wpbc-ui-catalog-inspector-cancel]',
1820 root: document
1821 }, settings || {} );
1822 var review_state = {
1823 busy: false,
1824 can_apply: false
1825 };
1826
1827 /**
1828 * Return the configured query root.
1829 *
1830 * @return {Document|Element} Query-capable root.
1831 */
1832 function get_root() {
1833 return options.root && options.root.querySelectorAll ? options.root : document;
1834 }
1835
1836 /**
1837 * Return the active acknowledgement checkbox.
1838 *
1839 * @return {HTMLInputElement|null} Checkbox or null when the review is blocked.
1840 */
1841 function get_acknowledgement() {
1842 return get_root().querySelector( options.acknowledgement_selector );
1843 }
1844
1845 /**
1846 * Restart the finite acknowledgement attention animation.
1847 *
1848 * @return {void}
1849 */
1850 function pulse_acknowledgement() {
1851 var acknowledgement = get_acknowledgement();
1852 var container = acknowledgement ? acknowledgement.closest( '.wpbc_ui_catalog_delete_review__acknowledgement' ) : null;
1853
1854 if ( ! container ) {
1855 return;
1856 }
1857 container.classList.remove( 'is-attention' );
1858 void container.offsetWidth;
1859 container.classList.add( 'is-attention' );
1860 }
1861
1862 /**
1863 * Synchronize destructive review actions with server and user state.
1864 *
1865 * @param {Object} next_state Busy and server-authoritative apply flags.
1866 * @return {void}
1867 */
1868 function synchronize( next_state ) {
1869 var acknowledgement;
1870 var acknowledged;
1871 var root = get_root();
1872
1873 next_state = next_state || {};
1874 if ( 'boolean' === typeof next_state.busy ) {
1875 review_state.busy = next_state.busy;
1876 }
1877 if ( 'boolean' === typeof next_state.can_apply ) {
1878 review_state.can_apply = next_state.can_apply;
1879 }
1880 acknowledgement = get_acknowledgement();
1881 acknowledged = !! acknowledgement && acknowledgement.checked;
1882 root.querySelectorAll( options.apply_selector ).forEach( function ( control ) {
1883 control.disabled = review_state.busy || ! review_state.can_apply || ! acknowledged;
1884 control.classList.toggle( 'is-busy', review_state.busy );
1885 control.setAttribute( 'aria-busy', review_state.busy ? 'true' : 'false' );
1886 } );
1887 root.querySelectorAll( options.cancel_selector ).forEach( function ( control ) {
1888 control.disabled = review_state.busy;
1889 } );
1890 root.querySelectorAll( '[data-wpbc-ui-catalog-delete-review-form]' ).forEach( function ( form ) {
1891 form.setAttribute( 'aria-busy', review_state.busy ? 'true' : 'false' );
1892 } );
1893 }
1894
1895 /**
1896 * Apply the standard destructive footer contract to domain-owned controls.
1897 *
1898 * @param {Object} footer_settings Footer element, form ID, and label.
1899 * @return {void}
1900 */
1901 function configure_footer( footer_settings ) {
1902 var footer_options = footer_settings || {};
1903 var footer = footer_options.footer && footer_options.footer.querySelector ? footer_options.footer : null;
1904 var apply_button = footer ? footer.querySelector( options.apply_selector ) : null;
1905
1906 if ( ! apply_button ) {
1907 return;
1908 }
1909 apply_button.classList.remove( 'button-primary', 'button-link-delete' );
1910 apply_button.classList.add( 'button-secondary', 'wpbc_ui_catalog_delete_review__apply' );
1911 apply_button.textContent = String( footer_options.label || '' );
1912 if ( footer_options.form_id ) {
1913 apply_button.setAttribute( 'form', String( footer_options.form_id ) );
1914 }
1915 review_state.can_apply = true === footer_options.can_apply;
1916 review_state.busy = false;
1917 synchronize();
1918 }
1919
1920 /**
1921 * Handle a delegated acknowledgement change.
1922 *
1923 * @param {Event} event Browser change event.
1924 * @return {boolean} True when the event belonged to this workflow.
1925 */
1926 function handle_change( event ) {
1927 var target = event && event.target;
1928
1929 if ( ! target || ! target.matches || ! target.matches( options.acknowledgement_selector ) ) {
1930 return false;
1931 }
1932 if ( target.checked ) {
1933 var container = target.closest( '.wpbc_ui_catalog_delete_review__acknowledgement' );
1934 if ( container ) {
1935 container.classList.remove( 'is-attention' );
1936 }
1937 } else {
1938 pulse_acknowledgement();
1939 }
1940 synchronize();
1941
1942 return true;
1943 }
1944
1945 return {
1946 configure_footer: configure_footer,
1947 handle_change: handle_change,
1948 pulse_acknowledgement: pulse_acknowledgement,
1949 synchronize: synchronize
1950 };
1951 }
1952
1953 /**
1954 * Mount one registered catalog and render its initial response.
1955 *
1956 * @param {Object} config Registered browser configuration.
1957 * @return {Object|false} Catalog controller or false when mounting fails.
1958 */
1959 function mount_catalog( config ) {
1960 var catalog_state;
1961 var catalog_template;
1962 var content_element;
1963 var initial_sequence;
1964 var mount_element;
1965
1966 if ( ! config || ! config.id || ! config.mount_id || ! config.templates || ! config.templates.catalog || ! config.templates.shell ) {
1967 return false;
1968 }
1969
1970 config.catalog_id = config.id;
1971 mount_element = document.getElementById( config.mount_id );
1972 catalog_template = load_template( config, 'catalog' );
1973
1974 if ( ! mount_element || ! catalog_template ) {
1975 return false;
1976 }
1977
1978 mount_element.innerHTML = catalog_template( Object.assign( {}, config, { catalog_id: config.catalog_id } ) );
1979 content_element = mount_element.querySelector( '[data-wpbc-catalog-content]' );
1980 if ( ! content_element ) {
1981 return false;
1982 }
1983 if ( config.i18n && config.i18n.catalog_label ) {
1984 content_element.parentNode.setAttribute( 'aria-label', config.i18n.catalog_label );
1985 }
1986
1987 catalog_state = get_catalog_state( config.catalog_id );
1988 catalog_state.config = config;
1989 catalog_state.content_element = content_element;
1990 catalog_state.response_element = content_element.querySelector( '[data-wpbc-ui-catalog-response]' ) || content_element;
1991 catalog_state.loading_element = content_element.querySelector( '[data-wpbc-ui-catalog-loading]' );
1992 catalog_state.latest_sequence = 0;
1993 catalog_state.request_values = Object.assign( {}, config.initial_request || {} );
1994 bind_catalog_controls( config, mount_element );
1995 if ( window.wpbc_ui_catalog_actions && 'function' === typeof window.wpbc_ui_catalog_actions.initialize ) {
1996 catalog_state.actions_controller = window.wpbc_ui_catalog_actions.initialize( mount_element, config );
1997 }
1998 if (
1999 config.features
2000 && config.features.hierarchy
2001 && window.wpbc_ui_catalog_hierarchy
2002 && 'function' === typeof window.wpbc_ui_catalog_hierarchy.initialize
2003 ) {
2004 catalog_state.hierarchy_controller = window.wpbc_ui_catalog_hierarchy.initialize( mount_element, config, function ( hierarchy_state ) {
2005 var hierarchy_configuration = config.hierarchy || {};
2006 var preference_key = String( hierarchy_configuration.preference_key || '' );
2007 var preference_values = {};
2008
2009 if ( 'global' !== hierarchy_configuration.persistence || ! preference_key ) {
2010 return Promise.resolve( false );
2011 }
2012 preference_values[ preference_key ] = JSON.stringify( hierarchy_state || {} );
2013
2014 return save_catalog_preferences( config, preference_values );
2015 } );
2016 }
2017 if (
2018 config.features
2019 && config.features.selection
2020 && window.wpbc_ui_catalog_selection
2021 && 'function' === typeof window.wpbc_ui_catalog_selection.initialize
2022 ) {
2023 catalog_state.selection_controller = window.wpbc_ui_catalog_selection.initialize( mount_element, config );
2024 }
2025
2026 if ( ! set_catalog_loading_state( config, true ) ) {
2027 render_template( config, 'shell', {
2028 catalog_id: config.catalog_id,
2029 aria_label: config.i18n && config.i18n.catalog_label ? config.i18n.catalog_label : '',
2030 loading_message: config.i18n && config.i18n.loading ? config.i18n.loading : ''
2031 } );
2032 }
2033
2034 if ( config.auto_load ) {
2035 request_catalog( config, config.initial_request || {} );
2036 initial_sequence = catalog_state.latest_sequence;
2037 } else {
2038 initial_sequence = next_request_sequence( config.catalog_id );
2039 if ( config.initial_response ) {
2040 render_response( config, config.initial_response, initial_sequence );
2041 }
2042 }
2043
2044 return {
2045 catalog_id: config.catalog_id,
2046 clear_selection: function () {
2047 if ( catalog_state.selection_controller && 'function' === typeof catalog_state.selection_controller.clear ) {
2048 catalog_state.selection_controller.clear();
2049 }
2050 },
2051 get_selected_ids: function () {
2052 return catalog_state.selection_controller && 'function' === typeof catalog_state.selection_controller.get_selected_ids
2053 ? catalog_state.selection_controller.get_selected_ids()
2054 : [];
2055 },
2056 get_hierarchy_controller: function () {
2057 return catalog_state.hierarchy_controller || false;
2058 },
2059 sequence: initial_sequence,
2060 load: function ( request_values ) {
2061 return request_catalog( config, request_values || {} );
2062 },
2063 save_preferences: function ( preference_values ) {
2064 return save_catalog_preferences( config, preference_values || {} );
2065 },
2066 refresh_controls: function () {
2067 refresh_catalog_controls( config );
2068 },
2069 sync_table_min_width: function () {
2070 sync_catalog_table_min_width( config );
2071 },
2072 next_sequence: function () {
2073 return next_request_sequence( config.catalog_id );
2074 },
2075 render_response: function ( response, request_sequence ) {
2076 return render_response( config, response, request_sequence );
2077 }
2078 };
2079 }
2080
2081 window.wpbc_ui_catalog = window.wpbc_ui_catalog || {};
2082 window.wpbc_ui_catalog.create_inspector_workflow = create_inspector_workflow;
2083 window.wpbc_ui_catalog.create_inline_editing_workflow = create_inline_editing_workflow;
2084 window.wpbc_ui_catalog.create_inline_review_workflow = create_inline_review_workflow;
2085 window.wpbc_ui_catalog.create_delete_review_workflow = create_delete_review_workflow;
2086 window.wpbc_ui_catalog.is_stale_response = is_stale_response;
2087 window.wpbc_ui_catalog.load_template = load_template;
2088 window.wpbc_ui_catalog.mount = mount_catalog;
2089 window.wpbc_ui_catalog.next_request_sequence = next_request_sequence;
2090 window.wpbc_ui_catalog.render_response = render_response;
2091 window.wpbc_ui_catalog.request = request_catalog;
2092 window.wpbc_ui_catalog.sync_table_min_width = sync_catalog_table_min_width;
2093 window.wpbc_ui_catalog.synchronize_overflow_tooltips = synchronize_overflow_tooltips;
2094 window.wpbc_ui_catalog.validate_response = validate_response;
2095 }( window, document ) );
2096