| 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 |
|