PluginProbe
Booking Calendar / 11.6.1
Booking Calendar v11.6.1
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 10.11.2 All 203 releases
booking / includes / _shared-ui-catalog / _src / wpbc_ui_catalog.js

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

2,085 lines 74.6 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-items-per-page]',
1444 '[data-wpbc-ui-catalog-column-visible]',
1445 '[data-wpbc-ui-catalog-column-order-reset]',
1446 '[data-wpbc-ui-catalog-preferences-reset]'
1447 ].join( ', ' );
1448
1449 if ( ! mount_element || ! mount_element.querySelector ) {
1450 return false;
1451 }
1452
1453 options = Object.assign( {
1454 bar_selector: '[data-wpbc-ui-catalog-inline-bar]',
1455 cancel_selector: '[data-wpbc-ui-catalog-inline-cancel]',
1456 controls_root: mount_element,
1457 count_selector: '[data-wpbc-ui-catalog-inline-count]',
1458 page_element: mount_element,
1459 protected_selector: '',
1460 review_selector: '[data-wpbc-ui-catalog-inline-review]',
1461 toggle_label_selector: '[data-wpbc-ui-catalog-inline-toggle-label]',
1462 toggle_selector: '[data-wpbc-ui-catalog-inline-toggle]'
1463 }, settings || {} );
1464
1465 /**
1466 * Return the configured page element without escaping the catalog mount.
1467 *
1468 * @return {HTMLElement|null} Configured page root, mount, or null.
1469 */
1470 function get_page_element() {
1471 if ( options.page_element && options.page_element.nodeType ) {
1472 return options.page_element;
1473 }
1474
1475 return 'string' === typeof options.page_element
1476 ? mount_element.querySelector( options.page_element )
1477 : mount_element;
1478 }
1479
1480 /**
1481 * Return the complete selector for controls locked by active drafts.
1482 *
1483 * @return {string} Shared selectors plus the trusted domain extension.
1484 */
1485 function get_protected_selector() {
1486 return options.protected_selector
1487 ? default_protected_selector + ', ' + options.protected_selector
1488 : default_protected_selector;
1489 }
1490
1491 /**
1492 * Preserve and restore a control's pre-workflow disabled state.
1493 *
1494 * @param {HTMLElement} control Catalog control to synchronize.
1495 * @param {boolean} controls_locked Whether inline navigation is locked.
1496 * @return {void}
1497 */
1498 function synchronize_protected_control( control, controls_locked ) {
1499 var prior_disabled;
1500
1501 if ( controls_locked ) {
1502 if ( ! control.hasAttribute( 'data-wpbc-ui-catalog-inline-was-disabled' ) ) {
1503 control.setAttribute( 'data-wpbc-ui-catalog-inline-was-disabled', control.disabled ? '1' : '0' );
1504 }
1505 control.disabled = true;
1506 control.setAttribute( 'aria-disabled', 'true' );
1507 return;
1508 }
1509
1510 if ( ! control.hasAttribute( 'data-wpbc-ui-catalog-inline-was-disabled' ) ) {
1511 return;
1512 }
1513 prior_disabled = '1' === control.getAttribute( 'data-wpbc-ui-catalog-inline-was-disabled' );
1514 control.disabled = prior_disabled;
1515 control.removeAttribute( 'data-wpbc-ui-catalog-inline-was-disabled' );
1516 if ( ! prior_disabled ) {
1517 control.removeAttribute( 'aria-disabled' );
1518 }
1519 }
1520
1521 /**
1522 * Register the current inline bar with the shared viewport controller.
1523 *
1524 * @return {void}
1525 */
1526 function register_sticky_bar() {
1527 var inline_bar = mount_element.querySelector( options.bar_selector );
1528 var selection_controller = mount_element._wpbc_ui_catalog_selection_controller;
1529
1530 if ( inline_bar && selection_controller && 'function' === typeof selection_controller.register_viewport_sticky ) {
1531 selection_controller.register_viewport_sticky( inline_bar );
1532 }
1533 }
1534
1535 /**
1536 * Remove shared changed-row presentation after inline mode ends.
1537 *
1538 * Domain drafts and values remain domain-owned. This cleanup removes only
1539 * the shared class and badge that this controller previously applied.
1540 *
1541 * @return {void}
1542 */
1543 function clear_changed_rows() {
1544 mount_element.querySelectorAll( '.wpbc_ui_catalog_inline_row.is-inline-changed' ).forEach( function ( row_element ) {
1545 set_row_changed( row_element, false, null, '' );
1546 } );
1547 }
1548
1549 /**
1550 * Synchronize shared inline workflow presentation from domain-owned state.
1551 *
1552 * @param {Object} workflow_state Normalized active, busy, count, and labels.
1553 * @return {void}
1554 */
1555 function synchronize( workflow_state ) {
1556 var active;
1557 var busy;
1558 var controls_root;
1559 var controls_locked;
1560 var inline_bar;
1561 var page_element;
1562 var toggle_button;
1563 var toggle_label;
1564
1565 workflow_state = workflow_state || {};
1566 active = true === workflow_state.active;
1567 busy = true === workflow_state.busy;
1568 controls_root = options.controls_root && options.controls_root.querySelectorAll ? options.controls_root : mount_element;
1569 controls_locked = active || true === workflow_state.lock_controls;
1570 inline_bar = mount_element.querySelector( options.bar_selector );
1571 page_element = get_page_element();
1572 toggle_button = mount_element.querySelector( options.toggle_selector );
1573
1574 if ( inline_bar ) {
1575 inline_bar.hidden = ! active;
1576 inline_bar.setAttribute( 'aria-busy', busy ? 'true' : 'false' );
1577 if ( inline_bar.querySelector( options.count_selector ) ) {
1578 inline_bar.querySelector( options.count_selector ).textContent = String( workflow_state.count_text || '' );
1579 }
1580 if ( inline_bar.querySelector( options.review_selector ) ) {
1581 inline_bar.querySelector( options.review_selector ).disabled = busy || ! Number( workflow_state.changed_count || 0 );
1582 }
1583 if ( inline_bar.querySelector( options.cancel_selector ) ) {
1584 inline_bar.querySelector( options.cancel_selector ).disabled = busy;
1585 }
1586 }
1587
1588 if ( toggle_button ) {
1589 toggle_button.classList.toggle( 'is-active', active );
1590 toggle_button.classList.toggle( 'is-busy', busy );
1591 toggle_button.disabled = busy
1592 || true === workflow_state.toggle_disabled
1593 || ( ! active && false === workflow_state.has_items );
1594 toggle_button.setAttribute( 'aria-pressed', active ? 'true' : 'false' );
1595 toggle_button.setAttribute( 'aria-busy', busy ? 'true' : 'false' );
1596 toggle_label = toggle_button.querySelector( options.toggle_label_selector );
1597 if ( toggle_label ) {
1598 toggle_label.textContent = active
1599 ? String( workflow_state.active_toggle_text || '' )
1600 : String( workflow_state.inactive_toggle_text || '' );
1601 }
1602 }
1603
1604 if ( page_element ) {
1605 page_element.classList.toggle( 'is-inline-editing', active );
1606 }
1607 if ( ! active ) {
1608 clear_changed_rows();
1609 }
1610 controls_root.querySelectorAll( get_protected_selector() ).forEach( function ( control ) {
1611 synchronize_protected_control( control, controls_locked );
1612 } );
1613 register_sticky_bar();
1614 if (
1615 mount_element._wpbc_ui_catalog_selection_controller
1616 && 'function' === typeof mount_element._wpbc_ui_catalog_selection_controller.refresh_viewport_sticky
1617 ) {
1618 mount_element._wpbc_ui_catalog_selection_controller.refresh_viewport_sticky();
1619 }
1620 }
1621
1622 /**
1623 * Block a captured event that targets a control protected by active drafts.
1624 *
1625 * @param {Event} event Captured browser event.
1626 * @param {boolean} controls_locked Whether the domain workflow is active.
1627 * @return {boolean} True when the event was blocked.
1628 */
1629 function protect_event( event, controls_locked ) {
1630 if ( ! controls_locked || ! event.target || ! event.target.closest ) {
1631 return false;
1632 }
1633 if ( ! event.target.closest( get_protected_selector() ) ) {
1634 return false;
1635 }
1636
1637 event.preventDefault();
1638 event.stopImmediatePropagation();
1639 return true;
1640 }
1641
1642 /**
1643 * Synchronize one changed row and its accessible text badge.
1644 *
1645 * @param {HTMLElement} row_element Domain row or card element.
1646 * @param {boolean} changed Whether its draft differs.
1647 * @param {HTMLElement} indicator_element Element receiving the badge.
1648 * @param {string} changed_label Localized badge text.
1649 * @return {void}
1650 */
1651 function set_row_changed( row_element, changed, indicator_element, changed_label ) {
1652 var indicator;
1653
1654 if ( ! row_element ) {
1655 return;
1656 }
1657 row_element.classList.add( 'wpbc_ui_catalog_inline_row' );
1658 row_element.classList.toggle( 'is-inline-changed', !! changed );
1659 indicator = row_element.querySelector( '[data-wpbc-ui-catalog-inline-changed-label]' );
1660 if ( ! changed ) {
1661 if ( indicator ) {
1662 indicator.remove();
1663 }
1664 return;
1665 }
1666 if ( ! indicator && indicator_element ) {
1667 indicator = document.createElement( 'span' );
1668 indicator.className = 'wpbc_ui_catalog_inline_changed_label';
1669 indicator.setAttribute( 'data-wpbc-ui-catalog-inline-changed-label', '' );
1670 indicator_element.appendChild( indicator );
1671 }
1672 if ( indicator ) {
1673 indicator.textContent = String( changed_label || '' );
1674 }
1675 }
1676
1677 return {
1678 protect_event: protect_event,
1679 register_sticky_bar: register_sticky_bar,
1680 set_row_changed: set_row_changed,
1681 synchronize: synchronize
1682 };
1683 }
1684
1685 /**
1686 * Create a domain-neutral signed-review presentation controller.
1687 *
1688 * Domains own preview and apply requests, signed plans, permissions, field
1689 * validation, and mutations. This controller accepts only the normalized
1690 * review DTO and owns the repeated model preparation and busy-state locking.
1691 *
1692 * @param {Object} settings DOM roots and domain button selectors.
1693 * @return {Object} Review presentation controller.
1694 */
1695 function create_inline_review_workflow( settings ) {
1696 var options = Object.assign( {
1697 apply_selector: '[data-wpbc-ui-catalog-inline-review-apply]',
1698 cancel_selector: '[data-wpbc-ui-catalog-inline-review-cancel]',
1699 root: document
1700 }, settings || {} );
1701
1702 /**
1703 * Normalize one server-authoritative review DTO for a domain template.
1704 *
1705 * @param {Object} review Server review with rows and field changes.
1706 * @param {Object} presentation Localized headings and explanatory text.
1707 * @return {Object} Executable-free template model.
1708 */
1709 function prepare( review, presentation ) {
1710 var normalized_rows = [];
1711
1712 review = review && 'object' === typeof review ? review : {};
1713 presentation = presentation && 'object' === typeof presentation ? presentation : {};
1714 ( Array.isArray( review.rows ) ? review.rows : [] ).forEach( function ( row ) {
1715 var normalized_fields = [];
1716 var normalized_notes = [];
1717
1718 if ( ! row || 'object' !== typeof row ) {
1719 return;
1720 }
1721 ( Array.isArray( row.fields ) ? row.fields : [] ).forEach( function ( field ) {
1722 if ( ! field || 'object' !== typeof field ) {
1723 return;
1724 }
1725 normalized_fields.push( {
1726 after: String( undefined === field.after ? '' : field.after ),
1727 before: String( undefined === field.before ? '' : field.before ),
1728 key: String( field.key || '' ),
1729 label: String( field.label || field.key || '' )
1730 } );
1731 } );
1732 ( Array.isArray( row.notes ) ? row.notes : [] ).forEach( function ( note ) {
1733 if ( 'string' === typeof note || 'number' === typeof note ) {
1734 normalized_notes.push( String( note ) );
1735 }
1736 } );
1737 if ( normalized_fields.length ) {
1738 normalized_rows.push( {
1739 fields: normalized_fields,
1740 id: Number( row.id || 0 ),
1741 notes: normalized_notes,
1742 title: String( row.title || '' )
1743 } );
1744 }
1745 } );
1746
1747 return {
1748 changed_label: String( presentation.changed_label || '' ),
1749 description: String( presentation.description || '' ),
1750 form_id: String( presentation.form_id || '' ),
1751 mode: String( presentation.mode || 'inline_review' ),
1752 pending_message: String( presentation.pending_message || '' ),
1753 rows: normalized_rows,
1754 title: String( presentation.title || '' ),
1755 warning: String( review.warning || presentation.warning || '' )
1756 };
1757 }
1758
1759 /**
1760 * Lock or unlock review actions while a domain request is in flight.
1761 *
1762 * @param {Object} review_state Busy and apply-ready flags.
1763 * @return {void}
1764 */
1765 function synchronize( review_state ) {
1766 var busy;
1767 var can_apply;
1768 var root = options.root && options.root.querySelectorAll ? options.root : document;
1769
1770 review_state = review_state || {};
1771 busy = true === review_state.busy;
1772 can_apply = true === review_state.can_apply;
1773 root.querySelectorAll( options.apply_selector ).forEach( function ( control ) {
1774 control.disabled = busy || ! can_apply;
1775 control.classList.toggle( 'is-busy', busy );
1776 control.setAttribute( 'aria-busy', busy ? 'true' : 'false' );
1777 } );
1778 root.querySelectorAll( options.cancel_selector ).forEach( function ( control ) {
1779 control.disabled = busy;
1780 } );
1781 root.querySelectorAll( '[data-wpbc-ui-catalog-inline-review-form]' ).forEach( function ( form ) {
1782 form.setAttribute( 'aria-busy', busy ? 'true' : 'false' );
1783 } );
1784 }
1785
1786 return {
1787 prepare: prepare,
1788 synchronize: synchronize
1789 };
1790 }
1791
1792 /**
1793 * Create a domain-neutral permanent-deletion review controller.
1794 *
1795 * Domains remain responsible for deciding whether deletion is allowed,
1796 * producing the signed impact review, rendering their allow-listed template,
1797 * and applying the mutation. This controller owns only the repeated browser
1798 * mechanics for explicit acknowledgement, destructive footer presentation,
1799 * busy locking, and reduced-motion-safe attention feedback.
1800 *
1801 * @param {Object} settings DOM roots and domain selectors.
1802 * @return {Object} Deletion-review presentation controller.
1803 */
1804 function create_delete_review_workflow( settings ) {
1805 var options = Object.assign( {
1806 acknowledgement_selector: '[data-wpbc-ui-catalog-delete-acknowledgement]',
1807 apply_selector: '[data-wpbc-ui-catalog-delete-apply], [data-wpbc-ui-catalog-inspector-save]',
1808 cancel_selector: '[data-wpbc-ui-catalog-delete-cancel], [data-wpbc-ui-catalog-inspector-cancel]',
1809 root: document
1810 }, settings || {} );
1811 var review_state = {
1812 busy: false,
1813 can_apply: false
1814 };
1815
1816 /**
1817 * Return the configured query root.
1818 *
1819 * @return {Document|Element} Query-capable root.
1820 */
1821 function get_root() {
1822 return options.root && options.root.querySelectorAll ? options.root : document;
1823 }
1824
1825 /**
1826 * Return the active acknowledgement checkbox.
1827 *
1828 * @return {HTMLInputElement|null} Checkbox or null when the review is blocked.
1829 */
1830 function get_acknowledgement() {
1831 return get_root().querySelector( options.acknowledgement_selector );
1832 }
1833
1834 /**
1835 * Restart the finite acknowledgement attention animation.
1836 *
1837 * @return {void}
1838 */
1839 function pulse_acknowledgement() {
1840 var acknowledgement = get_acknowledgement();
1841 var container = acknowledgement ? acknowledgement.closest( '.wpbc_ui_catalog_delete_review__acknowledgement' ) : null;
1842
1843 if ( ! container ) {
1844 return;
1845 }
1846 container.classList.remove( 'is-attention' );
1847 void container.offsetWidth;
1848 container.classList.add( 'is-attention' );
1849 }
1850
1851 /**
1852 * Synchronize destructive review actions with server and user state.
1853 *
1854 * @param {Object} next_state Busy and server-authoritative apply flags.
1855 * @return {void}
1856 */
1857 function synchronize( next_state ) {
1858 var acknowledgement;
1859 var acknowledged;
1860 var root = get_root();
1861
1862 next_state = next_state || {};
1863 if ( 'boolean' === typeof next_state.busy ) {
1864 review_state.busy = next_state.busy;
1865 }
1866 if ( 'boolean' === typeof next_state.can_apply ) {
1867 review_state.can_apply = next_state.can_apply;
1868 }
1869 acknowledgement = get_acknowledgement();
1870 acknowledged = !! acknowledgement && acknowledgement.checked;
1871 root.querySelectorAll( options.apply_selector ).forEach( function ( control ) {
1872 control.disabled = review_state.busy || ! review_state.can_apply || ! acknowledged;
1873 control.classList.toggle( 'is-busy', review_state.busy );
1874 control.setAttribute( 'aria-busy', review_state.busy ? 'true' : 'false' );
1875 } );
1876 root.querySelectorAll( options.cancel_selector ).forEach( function ( control ) {
1877 control.disabled = review_state.busy;
1878 } );
1879 root.querySelectorAll( '[data-wpbc-ui-catalog-delete-review-form]' ).forEach( function ( form ) {
1880 form.setAttribute( 'aria-busy', review_state.busy ? 'true' : 'false' );
1881 } );
1882 }
1883
1884 /**
1885 * Apply the standard destructive footer contract to domain-owned controls.
1886 *
1887 * @param {Object} footer_settings Footer element, form ID, and label.
1888 * @return {void}
1889 */
1890 function configure_footer( footer_settings ) {
1891 var footer_options = footer_settings || {};
1892 var footer = footer_options.footer && footer_options.footer.querySelector ? footer_options.footer : null;
1893 var apply_button = footer ? footer.querySelector( options.apply_selector ) : null;
1894
1895 if ( ! apply_button ) {
1896 return;
1897 }
1898 apply_button.classList.remove( 'button-primary', 'button-link-delete' );
1899 apply_button.classList.add( 'button-secondary', 'wpbc_ui_catalog_delete_review__apply' );
1900 apply_button.textContent = String( footer_options.label || '' );
1901 if ( footer_options.form_id ) {
1902 apply_button.setAttribute( 'form', String( footer_options.form_id ) );
1903 }
1904 review_state.can_apply = true === footer_options.can_apply;
1905 review_state.busy = false;
1906 synchronize();
1907 }
1908
1909 /**
1910 * Handle a delegated acknowledgement change.
1911 *
1912 * @param {Event} event Browser change event.
1913 * @return {boolean} True when the event belonged to this workflow.
1914 */
1915 function handle_change( event ) {
1916 var target = event && event.target;
1917
1918 if ( ! target || ! target.matches || ! target.matches( options.acknowledgement_selector ) ) {
1919 return false;
1920 }
1921 if ( target.checked ) {
1922 var container = target.closest( '.wpbc_ui_catalog_delete_review__acknowledgement' );
1923 if ( container ) {
1924 container.classList.remove( 'is-attention' );
1925 }
1926 } else {
1927 pulse_acknowledgement();
1928 }
1929 synchronize();
1930
1931 return true;
1932 }
1933
1934 return {
1935 configure_footer: configure_footer,
1936 handle_change: handle_change,
1937 pulse_acknowledgement: pulse_acknowledgement,
1938 synchronize: synchronize
1939 };
1940 }
1941
1942 /**
1943 * Mount one registered catalog and render its initial response.
1944 *
1945 * @param {Object} config Registered browser configuration.
1946 * @return {Object|false} Catalog controller or false when mounting fails.
1947 */
1948 function mount_catalog( config ) {
1949 var catalog_state;
1950 var catalog_template;
1951 var content_element;
1952 var initial_sequence;
1953 var mount_element;
1954
1955 if ( ! config || ! config.id || ! config.mount_id || ! config.templates || ! config.templates.catalog || ! config.templates.shell ) {
1956 return false;
1957 }
1958
1959 config.catalog_id = config.id;
1960 mount_element = document.getElementById( config.mount_id );
1961 catalog_template = load_template( config, 'catalog' );
1962
1963 if ( ! mount_element || ! catalog_template ) {
1964 return false;
1965 }
1966
1967 mount_element.innerHTML = catalog_template( Object.assign( {}, config, { catalog_id: config.catalog_id } ) );
1968 content_element = mount_element.querySelector( '[data-wpbc-catalog-content]' );
1969 if ( ! content_element ) {
1970 return false;
1971 }
1972 if ( config.i18n && config.i18n.catalog_label ) {
1973 content_element.parentNode.setAttribute( 'aria-label', config.i18n.catalog_label );
1974 }
1975
1976 catalog_state = get_catalog_state( config.catalog_id );
1977 catalog_state.config = config;
1978 catalog_state.content_element = content_element;
1979 catalog_state.response_element = content_element.querySelector( '[data-wpbc-ui-catalog-response]' ) || content_element;
1980 catalog_state.loading_element = content_element.querySelector( '[data-wpbc-ui-catalog-loading]' );
1981 catalog_state.latest_sequence = 0;
1982 catalog_state.request_values = Object.assign( {}, config.initial_request || {} );
1983 bind_catalog_controls( config, mount_element );
1984 if ( window.wpbc_ui_catalog_actions && 'function' === typeof window.wpbc_ui_catalog_actions.initialize ) {
1985 catalog_state.actions_controller = window.wpbc_ui_catalog_actions.initialize( mount_element, config );
1986 }
1987 if (
1988 config.features
1989 && config.features.hierarchy
1990 && window.wpbc_ui_catalog_hierarchy
1991 && 'function' === typeof window.wpbc_ui_catalog_hierarchy.initialize
1992 ) {
1993 catalog_state.hierarchy_controller = window.wpbc_ui_catalog_hierarchy.initialize( mount_element, config, function ( hierarchy_state ) {
1994 var hierarchy_configuration = config.hierarchy || {};
1995 var preference_key = String( hierarchy_configuration.preference_key || '' );
1996 var preference_values = {};
1997
1998 if ( 'global' !== hierarchy_configuration.persistence || ! preference_key ) {
1999 return Promise.resolve( false );
2000 }
2001 preference_values[ preference_key ] = JSON.stringify( hierarchy_state || {} );
2002
2003 return save_catalog_preferences( config, preference_values );
2004 } );
2005 }
2006 if (
2007 config.features
2008 && config.features.selection
2009 && window.wpbc_ui_catalog_selection
2010 && 'function' === typeof window.wpbc_ui_catalog_selection.initialize
2011 ) {
2012 catalog_state.selection_controller = window.wpbc_ui_catalog_selection.initialize( mount_element, config );
2013 }
2014
2015 if ( ! set_catalog_loading_state( config, true ) ) {
2016 render_template( config, 'shell', {
2017 catalog_id: config.catalog_id,
2018 aria_label: config.i18n && config.i18n.catalog_label ? config.i18n.catalog_label : '',
2019 loading_message: config.i18n && config.i18n.loading ? config.i18n.loading : ''
2020 } );
2021 }
2022
2023 if ( config.auto_load ) {
2024 request_catalog( config, config.initial_request || {} );
2025 initial_sequence = catalog_state.latest_sequence;
2026 } else {
2027 initial_sequence = next_request_sequence( config.catalog_id );
2028 if ( config.initial_response ) {
2029 render_response( config, config.initial_response, initial_sequence );
2030 }
2031 }
2032
2033 return {
2034 catalog_id: config.catalog_id,
2035 clear_selection: function () {
2036 if ( catalog_state.selection_controller && 'function' === typeof catalog_state.selection_controller.clear ) {
2037 catalog_state.selection_controller.clear();
2038 }
2039 },
2040 get_selected_ids: function () {
2041 return catalog_state.selection_controller && 'function' === typeof catalog_state.selection_controller.get_selected_ids
2042 ? catalog_state.selection_controller.get_selected_ids()
2043 : [];
2044 },
2045 get_hierarchy_controller: function () {
2046 return catalog_state.hierarchy_controller || false;
2047 },
2048 sequence: initial_sequence,
2049 load: function ( request_values ) {
2050 return request_catalog( config, request_values || {} );
2051 },
2052 save_preferences: function ( preference_values ) {
2053 return save_catalog_preferences( config, preference_values || {} );
2054 },
2055 refresh_controls: function () {
2056 refresh_catalog_controls( config );
2057 },
2058 sync_table_min_width: function () {
2059 sync_catalog_table_min_width( config );
2060 },
2061 next_sequence: function () {
2062 return next_request_sequence( config.catalog_id );
2063 },
2064 render_response: function ( response, request_sequence ) {
2065 return render_response( config, response, request_sequence );
2066 }
2067 };
2068 }
2069
2070 window.wpbc_ui_catalog = window.wpbc_ui_catalog || {};
2071 window.wpbc_ui_catalog.create_inspector_workflow = create_inspector_workflow;
2072 window.wpbc_ui_catalog.create_inline_editing_workflow = create_inline_editing_workflow;
2073 window.wpbc_ui_catalog.create_inline_review_workflow = create_inline_review_workflow;
2074 window.wpbc_ui_catalog.create_delete_review_workflow = create_delete_review_workflow;
2075 window.wpbc_ui_catalog.is_stale_response = is_stale_response;
2076 window.wpbc_ui_catalog.load_template = load_template;
2077 window.wpbc_ui_catalog.mount = mount_catalog;
2078 window.wpbc_ui_catalog.next_request_sequence = next_request_sequence;
2079 window.wpbc_ui_catalog.render_response = render_response;
2080 window.wpbc_ui_catalog.request = request_catalog;
2081 window.wpbc_ui_catalog.sync_table_min_width = sync_catalog_table_min_width;
2082 window.wpbc_ui_catalog.synchronize_overflow_tooltips = synchronize_overflow_tooltips;
2083 window.wpbc_ui_catalog.validate_response = validate_response;
2084 }( window, document ) );
2085