| 1 |
/** |
| 2 |
* Reusable sortable time-choice Setup Wizard editor. |
| 3 |
* |
| 4 |
* @package Booking Calendar |
| 5 |
*/ |
| 6 |
|
| 7 |
( function ( window ) { |
| 8 |
'use strict'; |
| 9 |
|
| 10 |
var module_config = window.wpbc_setup_wizard_start_end_times || { i18n: {} }; |
| 11 |
|
| 12 |
/** |
| 13 |
* Create an isolated time-choice editor adapter. |
| 14 |
* |
| 15 |
* The server owns field IDs, list IDs, option allow-lists, and validation |
| 16 |
* semantics. This browser adapter owns only interaction, ordering, focus, |
| 17 |
* and serialization mechanics shared by Start/End and Start/Duration. |
| 18 |
* |
| 19 |
* @param {Object} config Module limits, identifiers, and translations. |
| 20 |
* @return {Object} Step adapter accepted by the shared wizard shell. |
| 21 |
*/ |
| 22 |
function create_time_choices_adapter( config ) { |
| 23 |
var shell_api = null; |
| 24 |
var editor = null; |
| 25 |
|
| 26 |
/** |
| 27 |
* Return one translated string with a safe fallback. |
| 28 |
* |
| 29 |
* @param {string} key Translation key. |
| 30 |
* @param {string} fallback Fallback text. |
| 31 |
* @return {string} Translation or fallback. |
| 32 |
*/ |
| 33 |
function get_message( key, fallback ) { |
| 34 |
return config.i18n && config.i18n[ key ] ? config.i18n[ key ] : fallback; |
| 35 |
} |
| 36 |
|
| 37 |
/** |
| 38 |
* Convert a clock or duration `HH:MM` value to minutes. |
| 39 |
* |
| 40 |
* `24:00` is accepted for a full-day duration but cannot appear in the |
| 41 |
* clock-time option list supplied by the server. |
| 42 |
* |
| 43 |
* @param {string} time_value Time or duration candidate. |
| 44 |
* @return {number|null} Minute value, or null when invalid. |
| 45 |
*/ |
| 46 |
function time_to_minutes( time_value ) { |
| 47 |
var normalized_value = String( time_value || '' ); |
| 48 |
var match = /^(?:(?:[01]\d|2[0-3]):[0-5]\d|24:00)$/.exec( normalized_value ); |
| 49 |
|
| 50 |
if ( ! match ) { |
| 51 |
return null; |
| 52 |
} |
| 53 |
|
| 54 |
return parseInt( normalized_value.substring( 0, 2 ), 10 ) * 60 + parseInt( normalized_value.substring( 3, 5 ), 10 ); |
| 55 |
} |
| 56 |
|
| 57 |
/** |
| 58 |
* Convert a bounded minute value into `HH:MM`. |
| 59 |
* |
| 60 |
* @param {number} minute_value Minute value between zero and 1440. |
| 61 |
* @return {string} Normalized value. |
| 62 |
*/ |
| 63 |
function minutes_to_time( minute_value ) { |
| 64 |
var hours = Math.floor( minute_value / 60 ); |
| 65 |
var minutes = minute_value % 60; |
| 66 |
|
| 67 |
return String( hours ).padStart( 2, '0' ) + ':' + String( minutes ).padStart( 2, '0' ); |
| 68 |
} |
| 69 |
|
| 70 |
/** |
| 71 |
* Return one registered list. |
| 72 |
* |
| 73 |
* @param {string} list_id Stable server-owned list identifier. |
| 74 |
* @return {Object|null} List state or null. |
| 75 |
*/ |
| 76 |
function get_list( list_id ) { |
| 77 |
return editor && editor.lists && editor.lists[ list_id ] ? editor.lists[ list_id ] : null; |
| 78 |
} |
| 79 |
|
| 80 |
/** |
| 81 |
* Return the visible label for one list option. |
| 82 |
* |
| 83 |
* @param {Object} list List state. |
| 84 |
* @param {string} option_value Stored option value. |
| 85 |
* @return {string} Visible server-owned label. |
| 86 |
*/ |
| 87 |
function get_choice_label( list, option_value ) { |
| 88 |
var select = list.template.content.querySelector( '[data-wpbc-time-choice]' ); |
| 89 |
var option = null; |
| 90 |
var option_index; |
| 91 |
|
| 92 |
if ( select ) { |
| 93 |
for ( option_index = 0; option_index < select.options.length; option_index++ ) { |
| 94 |
if ( select.options[ option_index ].value === option_value ) { |
| 95 |
option = select.options[ option_index ]; |
| 96 |
break; |
| 97 |
} |
| 98 |
} |
| 99 |
} |
| 100 |
|
| 101 |
return option ? option.textContent : option_value; |
| 102 |
} |
| 103 |
|
| 104 |
/** |
| 105 |
* Serialize every ordered list into the hidden wizard field. |
| 106 |
* |
| 107 |
* @return {void} |
| 108 |
*/ |
| 109 |
function sync_time_choices_draft() { |
| 110 |
var serialized_lists = {}; |
| 111 |
|
| 112 |
if ( ! editor ) { |
| 113 |
return; |
| 114 |
} |
| 115 |
|
| 116 |
editor.list_order.forEach( function ( list_id ) { |
| 117 |
serialized_lists[ list_id ] = editor.lists[ list_id ].values.slice(); |
| 118 |
} ); |
| 119 |
editor.transport.value = JSON.stringify( serialized_lists ); |
| 120 |
} |
| 121 |
|
| 122 |
/** |
| 123 |
* Return the preferred validation focus target for a list item. |
| 124 |
* |
| 125 |
* @param {string} list_id Stable list identifier. |
| 126 |
* @param {number} item_index Preferred item index. |
| 127 |
* @return {HTMLElement|null} Select, Add button, or null. |
| 128 |
*/ |
| 129 |
function get_choice_control( list_id, item_index ) { |
| 130 |
var list = get_list( list_id ); |
| 131 |
var item = list ? list.node.querySelector( '[data-time-index="' + item_index + '"]' ) : null; |
| 132 |
|
| 133 |
return item ? item.querySelector( '[data-wpbc-time-choice]' ) : ( list ? list.add_button : null ); |
| 134 |
} |
| 135 |
|
| 136 |
/** |
| 137 |
* Focus one choice after rendering. |
| 138 |
* |
| 139 |
* @param {string} list_id Stable list identifier. |
| 140 |
* @param {number} item_index Preferred item index. |
| 141 |
* @return {void} |
| 142 |
*/ |
| 143 |
function focus_choice( list_id, item_index ) { |
| 144 |
var target = get_choice_control( list_id, item_index ); |
| 145 |
|
| 146 |
if ( target ) { |
| 147 |
target.focus(); |
| 148 |
} |
| 149 |
} |
| 150 |
|
| 151 |
/** |
| 152 |
* Focus a move handle after reordering. |
| 153 |
* |
| 154 |
* @param {string} list_id Stable list identifier. |
| 155 |
* @param {number} item_index New item index. |
| 156 |
* @return {void} |
| 157 |
*/ |
| 158 |
function focus_move_control( list_id, item_index ) { |
| 159 |
var list = get_list( list_id ); |
| 160 |
var item = list ? list.node.querySelector( '[data-time-index="' + item_index + '"]' ) : null; |
| 161 |
var target = item ? item.querySelector( '[data-wpbc-move-time]' ) : null; |
| 162 |
|
| 163 |
if ( target ) { |
| 164 |
target.focus(); |
| 165 |
} |
| 166 |
} |
| 167 |
|
| 168 |
/** |
| 169 |
* Show one field error and identify its first invalid control. |
| 170 |
* |
| 171 |
* @param {string} message Validation message. |
| 172 |
* @param {string} list_id Stable list identifier. |
| 173 |
* @param {number} item_index Preferred item index. |
| 174 |
* @return {HTMLElement|null} Invalid control or null. |
| 175 |
*/ |
| 176 |
function set_choice_error( message, list_id, item_index ) { |
| 177 |
var target; |
| 178 |
|
| 179 |
shell_api.set_field_error( editor.field_id, message ); |
| 180 |
target = get_choice_control( list_id, item_index ); |
| 181 |
if ( target ) { |
| 182 |
target.setAttribute( 'aria-invalid', 'true' ); |
| 183 |
} |
| 184 |
|
| 185 |
return target; |
| 186 |
} |
| 187 |
|
| 188 |
/** |
| 189 |
* Find the second occurrence of the first duplicate. |
| 190 |
* |
| 191 |
* @param {string[]} choices Ordered choices. |
| 192 |
* @return {number} Duplicate index or -1. |
| 193 |
*/ |
| 194 |
function find_duplicate_index( choices ) { |
| 195 |
var seen_choices = {}; |
| 196 |
var duplicate_index = -1; |
| 197 |
|
| 198 |
choices.some( function ( choice, item_index ) { |
| 199 |
if ( Object.prototype.hasOwnProperty.call( seen_choices, choice ) ) { |
| 200 |
duplicate_index = item_index; |
| 201 |
return true; |
| 202 |
} |
| 203 |
seen_choices[ choice ] = true; |
| 204 |
return false; |
| 205 |
} ); |
| 206 |
|
| 207 |
return duplicate_index; |
| 208 |
} |
| 209 |
|
| 210 |
/** |
| 211 |
* Render one list from data-only state. |
| 212 |
* |
| 213 |
* @param {string} list_id Stable list identifier. |
| 214 |
* @return {void} |
| 215 |
*/ |
| 216 |
function render_list( list_id ) { |
| 217 |
var list = get_list( list_id ); |
| 218 |
|
| 219 |
if ( ! list || ! list.template ) { |
| 220 |
return; |
| 221 |
} |
| 222 |
|
| 223 |
list.node.textContent = ''; |
| 224 |
list.add_button.removeAttribute( 'data-wpbc-setup-wizard-error-control-for' ); |
| 225 |
if ( ! list.values.length ) { |
| 226 |
list.add_button.setAttribute( 'data-wpbc-setup-wizard-error-control-for', editor.field_id ); |
| 227 |
} |
| 228 |
|
| 229 |
list.values.forEach( function ( choice_value, item_index ) { |
| 230 |
var choice = list.template.content.firstElementChild.cloneNode( true ); |
| 231 |
var select = choice.querySelector( '[data-wpbc-time-choice]' ); |
| 232 |
var select_label = choice.querySelector( '[data-wpbc-time-choice-label]' ); |
| 233 |
var move_button = choice.querySelector( '[data-wpbc-move-time]' ); |
| 234 |
var remove_button = choice.querySelector( '[data-wpbc-remove-time]' ); |
| 235 |
var visible_choice = get_choice_label( list, choice_value ); |
| 236 |
var select_id = 'wpbc-' + editor.field_id.replace( /_/g, '-' ) + '-' + list_id.replace( /_/g, '-' ) + '-' + item_index; |
| 237 |
|
| 238 |
choice.dataset.timeList = list_id; |
| 239 |
choice.dataset.timeIndex = String( item_index ); |
| 240 |
select.id = select_id; |
| 241 |
select.value = choice_value; |
| 242 |
if ( 0 === item_index ) { |
| 243 |
select.setAttribute( 'data-wpbc-setup-wizard-error-control-for', editor.field_id ); |
| 244 |
} |
| 245 |
select_label.htmlFor = select_id; |
| 246 |
select_label.textContent = list.label + ': ' + visible_choice; |
| 247 |
move_button.setAttribute( 'aria-label', get_message( 'move_time_choice', 'Move %1$s in the %2$s list' ).replace( '%1$s', visible_choice ).replace( '%2$s', list.label ) ); |
| 248 |
remove_button.setAttribute( 'aria-label', get_message( 'remove_time_choice', 'Remove %1$s from the %2$s list' ).replace( '%1$s', visible_choice ).replace( '%2$s', list.label ) ); |
| 249 |
list.node.appendChild( choice ); |
| 250 |
} ); |
| 251 |
|
| 252 |
list.add_button.disabled = list.values.length >= editor.max_times; |
| 253 |
list.clear_button.disabled = ! list.values.length; |
| 254 |
sync_time_choices_draft(); |
| 255 |
} |
| 256 |
|
| 257 |
/** |
| 258 |
* Render every registered list. |
| 259 |
* |
| 260 |
* @return {void} |
| 261 |
*/ |
| 262 |
function render_editor() { |
| 263 |
editor.list_order.forEach( render_list ); |
| 264 |
} |
| 265 |
|
| 266 |
/** |
| 267 |
* Generate an inclusive ordered range from one list's toolbar. |
| 268 |
* |
| 269 |
* @param {string} list_id Stable list identifier. |
| 270 |
* @return {void} |
| 271 |
*/ |
| 272 |
function generate_choices( list_id ) { |
| 273 |
var list = get_list( list_id ); |
| 274 |
var start_minutes; |
| 275 |
var end_minutes; |
| 276 |
var interval_minutes; |
| 277 |
var generated_values = []; |
| 278 |
var minute; |
| 279 |
|
| 280 |
if ( ! list ) { |
| 281 |
return; |
| 282 |
} |
| 283 |
|
| 284 |
start_minutes = time_to_minutes( list.generator_from.value ); |
| 285 |
end_minutes = time_to_minutes( list.generator_to.value ); |
| 286 |
interval_minutes = parseInt( list.generator_interval.value, 10 ); |
| 287 |
if ( null === start_minutes || null === end_minutes || end_minutes < start_minutes || ! isFinite( interval_minutes ) || interval_minutes < editor.time_increment || 0 !== interval_minutes % editor.time_increment ) { |
| 288 |
list.generator_to.setAttribute( 'aria-invalid', 'true' ); |
| 289 |
shell_api.set_status( get_message( 'generator_invalid', 'The last generated choice must not be earlier than the first choice.' ) ); |
| 290 |
list.generator_to.focus(); |
| 291 |
return; |
| 292 |
} |
| 293 |
|
| 294 |
list.generator_to.removeAttribute( 'aria-invalid' ); |
| 295 |
for ( minute = start_minutes; minute <= end_minutes && generated_values.length < editor.max_times; minute += interval_minutes ) { |
| 296 |
if ( -1 !== list.option_values.indexOf( minutes_to_time( minute ) ) ) { |
| 297 |
generated_values.push( minutes_to_time( minute ) ); |
| 298 |
} |
| 299 |
} |
| 300 |
list.values = generated_values; |
| 301 |
render_list( list_id ); |
| 302 |
focus_choice( list_id, 0 ); |
| 303 |
shell_api.clear_field_error( editor.field_id ); |
| 304 |
} |
| 305 |
|
| 306 |
/** |
| 307 |
* Find the next unused server-owned option. |
| 308 |
* |
| 309 |
* @param {Object} list List state. |
| 310 |
* @return {string|null} Proposed value or null. |
| 311 |
*/ |
| 312 |
function find_choice_proposal( list ) { |
| 313 |
var start_index = list.values.length ? list.option_values.indexOf( list.values[ list.values.length - 1 ] ) + 1 : 0; |
| 314 |
var offset; |
| 315 |
var candidate_value; |
| 316 |
|
| 317 |
for ( offset = 0; offset < list.option_values.length; offset += 1 ) { |
| 318 |
candidate_value = list.option_values[ ( start_index + offset ) % list.option_values.length ]; |
| 319 |
if ( -1 === list.values.indexOf( candidate_value ) ) { |
| 320 |
return candidate_value; |
| 321 |
} |
| 322 |
} |
| 323 |
|
| 324 |
return null; |
| 325 |
} |
| 326 |
|
| 327 |
/** |
| 328 |
* Add one unused server-owned choice. |
| 329 |
* |
| 330 |
* @param {string} list_id Stable list identifier. |
| 331 |
* @return {void} |
| 332 |
*/ |
| 333 |
function add_choice( list_id ) { |
| 334 |
var list = get_list( list_id ); |
| 335 |
var proposal; |
| 336 |
|
| 337 |
if ( ! list || list.values.length >= editor.max_times ) { |
| 338 |
shell_api.set_status( get_message( 'time_limit', 'No additional choice can be added to this list.' ) ); |
| 339 |
return; |
| 340 |
} |
| 341 |
|
| 342 |
proposal = find_choice_proposal( list ); |
| 343 |
if ( ! proposal ) { |
| 344 |
shell_api.set_status( get_message( 'time_limit', 'No additional choice can be added to this list.' ) ); |
| 345 |
return; |
| 346 |
} |
| 347 |
|
| 348 |
list.values.push( proposal ); |
| 349 |
render_list( list_id ); |
| 350 |
focus_choice( list_id, list.values.length - 1 ); |
| 351 |
shell_api.clear_field_error( editor.field_id ); |
| 352 |
} |
| 353 |
|
| 354 |
/** |
| 355 |
* Clear one list and keep focus within its action area. |
| 356 |
* |
| 357 |
* @param {string} list_id Stable list identifier. |
| 358 |
* @return {void} |
| 359 |
*/ |
| 360 |
function clear_choices( list_id ) { |
| 361 |
var list = get_list( list_id ); |
| 362 |
|
| 363 |
if ( ! list || ! list.values.length ) { |
| 364 |
return; |
| 365 |
} |
| 366 |
|
| 367 |
list.values = []; |
| 368 |
render_list( list_id ); |
| 369 |
list.add_button.focus(); |
| 370 |
shell_api.clear_field_error( editor.field_id ); |
| 371 |
shell_api.set_status( list.clear_status || get_message( list_id + '_cleared', 'Choices cleared.' ) ); |
| 372 |
} |
| 373 |
|
| 374 |
/** |
| 375 |
* Move one choice inside its list. |
| 376 |
* |
| 377 |
* @param {string} list_id Stable list identifier. |
| 378 |
* @param {number} source_index Current index. |
| 379 |
* @param {number} target_index Target index. |
| 380 |
* @return {void} |
| 381 |
*/ |
| 382 |
function move_choice( list_id, source_index, target_index ) { |
| 383 |
var list = get_list( list_id ); |
| 384 |
var moved_value; |
| 385 |
|
| 386 |
if ( ! list || source_index === target_index || source_index < 0 || target_index < 0 || source_index >= list.values.length || target_index >= list.values.length ) { |
| 387 |
return; |
| 388 |
} |
| 389 |
|
| 390 |
moved_value = list.values.splice( source_index, 1 )[0]; |
| 391 |
list.values.splice( target_index, 0, moved_value ); |
| 392 |
render_list( list_id ); |
| 393 |
focus_move_control( list_id, target_index ); |
| 394 |
shell_api.clear_field_error( editor.field_id ); |
| 395 |
} |
| 396 |
|
| 397 |
/** |
| 398 |
* Initialize SortableJS for one list without cross-list moves. |
| 399 |
* |
| 400 |
* @param {string} list_id Stable list identifier. |
| 401 |
* @return {void} |
| 402 |
*/ |
| 403 |
function initialize_sortable_list( list_id ) { |
| 404 |
var list = get_list( list_id ); |
| 405 |
|
| 406 |
if ( ! list || 'function' !== typeof window.Sortable ) { |
| 407 |
return; |
| 408 |
} |
| 409 |
|
| 410 |
list.sortable = new window.Sortable( list.node, { |
| 411 |
animation: 120, |
| 412 |
draggable: '[data-time-index]', |
| 413 |
handle: '[data-wpbc-move-time]', |
| 414 |
ghostClass: 'is-drag-placeholder', |
| 415 |
chosenClass: 'is-dragging', |
| 416 |
dragClass: 'is-dragging', |
| 417 |
onEnd: function ( event ) { |
| 418 |
var source_index = Number( event.oldIndex ); |
| 419 |
var target_index = Number( event.newIndex ); |
| 420 |
|
| 421 |
if ( Number.isInteger( source_index ) && Number.isInteger( target_index ) ) { |
| 422 |
move_choice( list_id, source_index, target_index ); |
| 423 |
} else { |
| 424 |
render_list( list_id ); |
| 425 |
} |
| 426 |
} |
| 427 |
} ); |
| 428 |
} |
| 429 |
|
| 430 |
/** |
| 431 |
* Handle generator, add, clear, copy, and remove actions. |
| 432 |
* |
| 433 |
* @param {MouseEvent} event Delegated click event. |
| 434 |
* @return {void} |
| 435 |
*/ |
| 436 |
function handle_click( event ) { |
| 437 |
var copy_button = event.target.closest( '[data-wpbc-copy-times]' ); |
| 438 |
var generator = event.target.closest( '[data-wpbc-time-generator]' ); |
| 439 |
var list_panel = event.target.closest( '[data-wpbc-time-list-panel]' ); |
| 440 |
var choice = event.target.closest( '[data-time-index]' ); |
| 441 |
|
| 442 |
if ( copy_button ) { |
| 443 |
var source_list = get_list( copy_button.dataset.sourceList ); |
| 444 |
var target_list = get_list( copy_button.dataset.targetList ); |
| 445 |
|
| 446 |
event.preventDefault(); |
| 447 |
if ( source_list && target_list ) { |
| 448 |
target_list.values = source_list.values.filter( function ( choice_value ) { |
| 449 |
return -1 !== target_list.option_values.indexOf( choice_value ); |
| 450 |
} ); |
| 451 |
render_list( target_list.id ); |
| 452 |
focus_choice( target_list.id, 0 ); |
| 453 |
shell_api.clear_field_error( editor.field_id ); |
| 454 |
shell_api.set_status( copy_button.dataset.status || '' ); |
| 455 |
} |
| 456 |
return; |
| 457 |
} |
| 458 |
|
| 459 |
if ( generator && event.target.closest( '[data-wpbc-generate-times]' ) ) { |
| 460 |
event.preventDefault(); |
| 461 |
generate_choices( generator.dataset.wpbcTimeGenerator ); |
| 462 |
return; |
| 463 |
} |
| 464 |
|
| 465 |
if ( list_panel && event.target.closest( '[data-wpbc-add-time]' ) ) { |
| 466 |
event.preventDefault(); |
| 467 |
add_choice( list_panel.dataset.wpbcTimeListPanel ); |
| 468 |
return; |
| 469 |
} |
| 470 |
|
| 471 |
if ( list_panel && event.target.closest( '[data-wpbc-clear-times]' ) ) { |
| 472 |
event.preventDefault(); |
| 473 |
clear_choices( list_panel.dataset.wpbcTimeListPanel ); |
| 474 |
return; |
| 475 |
} |
| 476 |
|
| 477 |
if ( choice && event.target.closest( '[data-wpbc-remove-time]' ) ) { |
| 478 |
var list = get_list( choice.dataset.timeList ); |
| 479 |
var item_index = parseInt( choice.dataset.timeIndex, 10 ); |
| 480 |
|
| 481 |
event.preventDefault(); |
| 482 |
if ( list ) { |
| 483 |
list.values.splice( item_index, 1 ); |
| 484 |
render_list( list.id ); |
| 485 |
focus_choice( list.id, Math.min( item_index, list.values.length - 1 ) ); |
| 486 |
shell_api.clear_field_error( editor.field_id ); |
| 487 |
} |
| 488 |
} |
| 489 |
} |
| 490 |
|
| 491 |
/** |
| 492 |
* Synchronize one changed choice. |
| 493 |
* |
| 494 |
* @param {Event} event Delegated change event. |
| 495 |
* @return {void} |
| 496 |
*/ |
| 497 |
function handle_change( event ) { |
| 498 |
var choice; |
| 499 |
var list; |
| 500 |
var item_index; |
| 501 |
|
| 502 |
if ( ! event.target.matches( '[data-wpbc-time-choice]' ) ) { |
| 503 |
return; |
| 504 |
} |
| 505 |
|
| 506 |
choice = event.target.closest( '[data-time-index]' ); |
| 507 |
list = choice ? get_list( choice.dataset.timeList ) : null; |
| 508 |
item_index = choice ? parseInt( choice.dataset.timeIndex, 10 ) : -1; |
| 509 |
if ( ! list || item_index < 0 ) { |
| 510 |
return; |
| 511 |
} |
| 512 |
|
| 513 |
list.values[ item_index ] = event.target.value; |
| 514 |
render_list( list.id ); |
| 515 |
focus_choice( list.id, item_index ); |
| 516 |
shell_api.clear_field_error( editor.field_id ); |
| 517 |
} |
| 518 |
|
| 519 |
/** |
| 520 |
* Provide keyboard reordering equivalent to drag and drop. |
| 521 |
* |
| 522 |
* @param {KeyboardEvent} event Delegated keydown event. |
| 523 |
* @return {void} |
| 524 |
*/ |
| 525 |
function handle_keydown( event ) { |
| 526 |
var move_button = event.target.closest( '[data-wpbc-move-time]' ); |
| 527 |
var choice; |
| 528 |
var item_index; |
| 529 |
var target_index; |
| 530 |
|
| 531 |
if ( ! move_button || ! [ 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown' ].includes( event.key ) ) { |
| 532 |
return; |
| 533 |
} |
| 534 |
|
| 535 |
choice = move_button.closest( '[data-time-index]' ); |
| 536 |
item_index = parseInt( choice.dataset.timeIndex, 10 ); |
| 537 |
target_index = [ 'ArrowLeft', 'ArrowUp' ].includes( event.key ) ? item_index - 1 : item_index + 1; |
| 538 |
event.preventDefault(); |
| 539 |
move_choice( choice.dataset.timeList, item_index, target_index ); |
| 540 |
} |
| 541 |
|
| 542 |
/** |
| 543 |
* Initialize one server-defined list from DOM and checkpoint values. |
| 544 |
* |
| 545 |
* @param {HTMLElement} list_node List container. |
| 546 |
* @param {string[]} values Stored list values. |
| 547 |
* @return {Object|null} List state or null. |
| 548 |
*/ |
| 549 |
function initialize_list( list_node, values ) { |
| 550 |
var list_id = list_node.dataset.wpbcTimeChoiceList; |
| 551 |
var panel = editor.node.querySelector( '[data-wpbc-time-list-panel="' + list_id + '"]' ); |
| 552 |
var generator = panel ? panel.querySelector( '[data-wpbc-time-generator]' ) : null; |
| 553 |
var template = shell_api.root.querySelector( '[data-wpbc-time-choice-template="' + list_id + '"]' ); |
| 554 |
var template_select = template ? template.content.querySelector( '[data-wpbc-time-choice]' ) : null; |
| 555 |
|
| 556 |
if ( ! panel || ! generator || ! template || ! template_select ) { |
| 557 |
return null; |
| 558 |
} |
| 559 |
|
| 560 |
return { |
| 561 |
id: list_id, |
| 562 |
node: list_node, |
| 563 |
template: template, |
| 564 |
label: list_node.dataset.listLabel || list_id, |
| 565 |
clear_status: list_node.dataset.clearStatus || '', |
| 566 |
values: Array.isArray( values ) ? values.slice() : [], |
| 567 |
option_values: Array.prototype.map.call( template_select.options, function ( option ) { return option.value; } ), |
| 568 |
add_button: panel.querySelector( '[data-wpbc-add-time]' ), |
| 569 |
clear_button: panel.querySelector( '[data-wpbc-clear-times]' ), |
| 570 |
generator_from: generator.querySelector( '[data-wpbc-generator-from]' ), |
| 571 |
generator_to: generator.querySelector( '[data-wpbc-generator-to]' ), |
| 572 |
generator_interval: generator.querySelector( '[data-wpbc-generator-interval]' ), |
| 573 |
sortable: null |
| 574 |
}; |
| 575 |
} |
| 576 |
|
| 577 |
/** |
| 578 |
* Initialize this module against shell-provided services. |
| 579 |
* |
| 580 |
* @param {Object} registered_shell_api Shared wizard adapter services. |
| 581 |
* @return {void} |
| 582 |
*/ |
| 583 |
function initialize( registered_shell_api ) { |
| 584 |
var parsed_values; |
| 585 |
var editor_node; |
| 586 |
var transport; |
| 587 |
|
| 588 |
shell_api = registered_shell_api; |
| 589 |
editor_node = shell_api.root.querySelector( '[data-wpbc-time-choices-editor]' ); |
| 590 |
transport = shell_api.root.querySelector( '[data-wpbc-time-choices-draft]' ); |
| 591 |
if ( ! editor_node || ! transport ) { |
| 592 |
return; |
| 593 |
} |
| 594 |
|
| 595 |
try { |
| 596 |
parsed_values = JSON.parse( transport.value || '{}' ); |
| 597 |
} catch ( parse_error ) { |
| 598 |
parsed_values = {}; |
| 599 |
} |
| 600 |
|
| 601 |
editor = { |
| 602 |
node: editor_node, |
| 603 |
transport: transport, |
| 604 |
field_id: editor_node.dataset.fieldId || config.field_id || 'start_end_times', |
| 605 |
validation_rule: editor_node.dataset.validationRule || 'independent_lists', |
| 606 |
max_times: parseInt( transport.dataset.maxTimes || String( config.max_times_per_list || 288 ), 10 ), |
| 607 |
time_increment: parseInt( transport.dataset.timeIncrement || String( config.time_increment || 5 ), 10 ), |
| 608 |
list_order: [], |
| 609 |
lists: {} |
| 610 |
}; |
| 611 |
|
| 612 |
editor.node.querySelectorAll( '[data-wpbc-time-choice-list]' ).forEach( function ( list_node ) { |
| 613 |
var list_id = list_node.dataset.wpbcTimeChoiceList; |
| 614 |
var list = initialize_list( list_node, parsed_values[ list_id ] ); |
| 615 |
|
| 616 |
if ( list ) { |
| 617 |
editor.list_order.push( list_id ); |
| 618 |
editor.lists[ list_id ] = list; |
| 619 |
} |
| 620 |
} ); |
| 621 |
|
| 622 |
if ( ! editor.list_order.length ) { |
| 623 |
editor = null; |
| 624 |
return; |
| 625 |
} |
| 626 |
|
| 627 |
editor.node.addEventListener( 'click', handle_click ); |
| 628 |
editor.node.addEventListener( 'change', handle_change ); |
| 629 |
editor.node.addEventListener( 'keydown', handle_keydown ); |
| 630 |
render_editor(); |
| 631 |
editor.list_order.forEach( initialize_sortable_list ); |
| 632 |
} |
| 633 |
|
| 634 |
/** |
| 635 |
* Validate required, unique, allow-listed, and route-specific choices. |
| 636 |
* |
| 637 |
* @return {HTMLElement|null} First invalid control or null. |
| 638 |
*/ |
| 639 |
function validate() { |
| 640 |
var first_invalid = null; |
| 641 |
|
| 642 |
if ( ! editor ) { |
| 643 |
return null; |
| 644 |
} |
| 645 |
|
| 646 |
editor.node.querySelectorAll( '[aria-invalid="true"]' ).forEach( function ( control ) { |
| 647 |
control.removeAttribute( 'aria-invalid' ); |
| 648 |
} ); |
| 649 |
|
| 650 |
editor.list_order.some( function ( list_id ) { |
| 651 |
var list = editor.lists[ list_id ]; |
| 652 |
var duplicate_index; |
| 653 |
|
| 654 |
if ( ! list.values.length ) { |
| 655 |
first_invalid = set_choice_error( get_message( list_id + '_required', 'Add at least one choice.' ), list_id, -1 ); |
| 656 |
return true; |
| 657 |
} |
| 658 |
|
| 659 |
if ( list.values.some( function ( choice_value ) { return -1 === list.option_values.indexOf( choice_value ); } ) ) { |
| 660 |
first_invalid = set_choice_error( get_message( 'time_value_invalid', 'Choose a supported value for every choice.' ), list_id, 0 ); |
| 661 |
return true; |
| 662 |
} |
| 663 |
|
| 664 |
duplicate_index = find_duplicate_index( list.values ); |
| 665 |
if ( -1 !== duplicate_index ) { |
| 666 |
first_invalid = set_choice_error( get_message( 'time_duplicate', 'The same choice cannot appear twice in one list.' ), list_id, duplicate_index ); |
| 667 |
return true; |
| 668 |
} |
| 669 |
|
| 670 |
return false; |
| 671 |
} ); |
| 672 |
|
| 673 |
if ( ! first_invalid && 'end_after_start' === editor.validation_rule && editor.lists.start_times && editor.lists.end_times ) { |
| 674 |
var start_minutes = editor.lists.start_times.values.map( time_to_minutes ); |
| 675 |
var end_minutes = editor.lists.end_times.values.map( time_to_minutes ); |
| 676 |
|
| 677 |
if ( start_minutes.some( function ( value ) { return null === value; } ) || end_minutes.some( function ( value ) { return null === value; } ) || Math.max.apply( null, end_minutes ) <= Math.min.apply( null, start_minutes ) ) { |
| 678 |
first_invalid = set_choice_error( get_message( 'time_pair_invalid', 'Add an end time that is later than at least one start time.' ), 'end_times', 0 ); |
| 679 |
} |
| 680 |
} |
| 681 |
|
| 682 |
sync_time_choices_draft(); |
| 683 |
return first_invalid; |
| 684 |
} |
| 685 |
|
| 686 |
return { |
| 687 |
initialize: initialize, |
| 688 |
sync: sync_time_choices_draft, |
| 689 |
validate: validate |
| 690 |
}; |
| 691 |
} |
| 692 |
|
| 693 |
window.wpbc_setup_editor_modules = window.wpbc_setup_editor_modules || {}; |
| 694 |
window.wpbc_setup_editor_modules[ module_config.module_id || 'start_end_times' ] = { |
| 695 |
create: create_time_choices_adapter |
| 696 |
}; |
| 697 |
|
| 698 |
if ( window.wpbc_setup_wizard_api && 'function' === typeof window.wpbc_setup_wizard_api.register_step_adapter ) { |
| 699 |
window.wpbc_setup_wizard_api.register_step_adapter( create_time_choices_adapter( module_config ) ); |
| 700 |
} |
| 701 |
}( window ) ); |
| 702 |
|