/**
* Blink specific HTML element to set attention to this element.
*
* @param {string} element_to_blink - class or id of element: '.wpbc_widget_available_unavailable'
* @param {int} how_many_times - 4
* @param {int} how_long_to_blink - 350
*/
function wpbc_blink_element( element_to_blink, how_many_times = 4, how_long_to_blink = 350 ){
for ( let i = 0; i < how_many_times; i++ ){
jQuery( element_to_blink ).fadeOut( how_long_to_blink ).fadeIn( how_long_to_blink );
}
jQuery( element_to_blink ).animate( {opacity: 1}, 500 );
}
/**
* Support Functions - Spin Icon in Buttons ------------------------------------------------------------------ */
/**
* Remove spin icon from button and Enable this button.
*
* @param button_clicked_element_id - HTML ID attribute of this button
* @return string - CSS classes that was previously in button icon
*/
function wpbc_button__remove_spin(button_clicked_element_id) {
var previos_classes = '';
if (
(undefined != button_clicked_element_id)
&& ('' != button_clicked_element_id)
) {
var jElement = jQuery( '#' + button_clicked_element_id );
if ( jElement.length ) {
previos_classes = wpbc_button_disable_loading_icon( jElement.get( 0 ) );
}
}
return previos_classes;
}
/**
* Show Loading (rotating arrow) icon for button that has been clicked
*
* @param this_button - this object of specific button
* @return string - CSS classes that was previously in button icon
*/
function wpbc_button_enable_loading_icon(this_button) {
var jButton = jQuery( this_button );
var jIcon = jButton.find( 'i' );
var previos_classes = jIcon.attr( 'class' );
jIcon.removeClass().addClass( 'menu_icon icon-1x wpbc_icn_rotate_right wpbc_spin' ); // Set Rotate icon.
// jIcon.addClass( 'wpbc_animation_pause' ); // Pause animation.
// jIcon.addClass( 'wpbc_ui_red' ); // Set icon color red.
jIcon.attr( 'wpbc_previous_class', previos_classes )
jButton.addClass( 'disabled' ); // Disable button
// We need to set here attr instead of prop, because for A elements, attribute 'disabled' do not added with jButton.prop( "disabled", true );.
jButton.attr( 'wpbc_previous_onclick', jButton.attr( 'onclick' ) ); // Save this value.
jButton.attr( 'onclick', '' ); // Disable actions "on click".
return previos_classes;
}
/**
* Hide Loading (rotating arrow) icon for button that was clicked and show previous icon and enable button
*
* @param this_button - this object of specific button
* @return string - CSS classes that was previously in button icon
*/
function wpbc_button_disable_loading_icon(this_button) {
var jButton = jQuery( this_button );
var jIcon = jButton.find( 'i' );
var previos_classes = jIcon.attr( 'wpbc_previous_class' );
if (
(undefined != previos_classes)
&& ('' != previos_classes)
) {
jIcon.removeClass().addClass( previos_classes );
}
jButton.removeClass( 'disabled' ); // Remove Disable button.
var previous_onclick = jButton.attr( 'wpbc_previous_onclick' )
if (
(undefined != previous_onclick)
&& ('' != previous_onclick)
) {
jButton.attr( 'onclick', previous_onclick );
}
return previos_classes;
}
/**
* On selection of radio button, adjust attributes of radio container
*
* @param _this
*/
function wpbc_ui_el__radio_container_selection(_this) {
if ( jQuery( _this ).is( ':checked' ) ) {
jQuery( _this ).parents( '.wpbc_ui_radio_section' ).find( '.wpbc_ui_radio_container' ).removeAttr( 'data-selected' );
jQuery( _this ).parents( '.wpbc_ui_radio_container:not(.disabled)' ).attr( 'data-selected', true );
}
if ( jQuery( _this ).is( ':disabled' ) ) {
jQuery( _this ).parents( '.wpbc_ui_radio_container' ).addClass( 'disabled' );
}
}
/**
* On click on Radio Container, we will select the radio button and then adjust attributes of radio container
*
* @param _this
*/
function wpbc_ui_el__radio_container_click(_this) {
if ( jQuery( _this ).hasClass( 'disabled' ) ) {
return false;
}
var j_radio = jQuery( _this ).find( 'input[type=radio]:not(.wpbc-form-radio-internal)' );
if ( j_radio.length ) {
j_radio.prop( 'checked', true ).trigger( 'change' );
}
}
"use strict";
// =====================================================================================================================
// == Full Screen - support functions ==
// =====================================================================================================================
/**
* Return every cookie path that can apply to the current WordPress admin URL.
*
* WordPress may run from a subdirectory. Updating the root, site, and admin
* paths prevents an older, more-specific cookie from overriding the new mode.
*
* @return {string[]} Unique absolute cookie paths.
*/
function wpbc_admin_ui__full_screen__get_cookie_paths() {
var cookie_paths = [ '/' ];
var admin_marker = '/wp-admin/';
var current_path = window.location && window.location.pathname ? window.location.pathname : '';
var admin_index = current_path.indexOf( admin_marker );
if ( admin_index >= 0 ) {
cookie_paths.push( current_path.substring( 0, admin_index + 1 ) );
cookie_paths.push( current_path.substring( 0, admin_index + admin_marker.length ) );
}
return cookie_paths.filter( function ( path, index ) {
return path && cookie_paths.indexOf( path ) === index;
} );
}
/**
* Save Full Screen preference in a short-lived browser cookie.
*
* This makes the next admin page load deterministic even if the asynchronous
* user-meta request is interrupted. The timestamp lets PHP distinguish this
* pending value from a stale legacy cookie.
*
* @param {string} value Fullscreen mode, either `On` or `Off`.
*
* @return {void}
*/
function wpbc_admin_ui__full_screen__set_cookie( value ) {
var max_age = 5 * 60;
var issued_at = Math.floor( Date.now() / 1000 );
var cookie_value = encodeURIComponent( value + '|' + issued_at );
wpbc_admin_ui__full_screen__get_cookie_paths().forEach( function ( cookie_path ) {
document.cookie = 'wpbc_admin_full_screen=' + cookie_value + '; path=' + cookie_path + '; max-age=' + max_age + '; SameSite=Lax';
} );
}
/**
* Apply Full Screen mode from a user click.
*
* @param HTMLElement el Clicked control.
* @param bool is_save_user_state Whether to save user preference.
*/
function wpbc_admin_ui__full_screen__do_on( el, is_save_user_state ) {
jQuery( 'body' ).addClass( 'wpbc_admin_full_screen' );
wpbc_check_full_screen_mode();
if ( is_save_user_state ) {
wpbc_admin_ui__full_screen__set_cookie( 'On' );
if ( 'function' === typeof wpbc_save_custom_user_data_from_element ) {
wpbc_save_custom_user_data_from_element( el );
}
}
}
/**
* Exit Full Screen mode from a user click.
*
* @param HTMLElement el Clicked control.
* @param bool is_save_user_state Whether to save user preference.
*/
function wpbc_admin_ui__full_screen__do_off( el, is_save_user_state ) {
jQuery( 'body' ).removeClass( 'wpbc_admin_full_screen' );
wpbc_check_full_screen_mode();
if ( is_save_user_state ) {
wpbc_admin_ui__full_screen__set_cookie( 'Off' );
if ( 'function' === typeof wpbc_save_custom_user_data_from_element ) {
wpbc_save_custom_user_data_from_element( el );
}
}
}
/**
* Check Full screen mode, by removing top tab
*/
function wpbc_check_full_screen_mode(){
if ( jQuery( 'body' ).hasClass( 'wpbc_admin_full_screen' ) ) {
jQuery( 'html' ).removeClass( 'wp-toolbar' );
} else {
jQuery( 'html' ).addClass( 'wp-toolbar' );
}
wpbc_check_buttons_max_min_in_full_screen_mode();
}
function wpbc_check_buttons_max_min_in_full_screen_mode() {
if ( jQuery( 'body' ).hasClass( 'wpbc_admin_full_screen' ) ) {
jQuery( '.wpbc_ui__top_nav__btn_full_screen' ).addClass( 'wpbc_ui__hide' );
jQuery( '.wpbc_ui__top_nav__btn_normal_screen' ).removeClass( 'wpbc_ui__hide' );
} else {
jQuery( '.wpbc_ui__top_nav__btn_full_screen' ).removeClass( 'wpbc_ui__hide' );
jQuery( '.wpbc_ui__top_nav__btn_normal_screen' ).addClass( 'wpbc_ui__hide' );
}
}
jQuery( document ).ready( function () {
wpbc_check_full_screen_mode();
} );
/**
* Checkbox Selection functions for Listing.
*/
/**
* Selections of several checkboxes like in gMail with shift :)
* Need to have this structure:
* .wpbc_selectable_table
* .wpbc_selectable_head
* .check-column
* :checkbox
* .wpbc_selectable_body
* .wpbc_row
* .check-column
* :checkbox
* .wpbc_selectable_foot
* .check-column
* :checkbox
*/
function wpbc_define_gmail_checkbox_selection( $ ){
var checks, first, last, checked, sliced, lastClicked = false;
// Check all checkboxes.
$( '.wpbc_selectable_body' ).find( '.check-column' ).find( ':checkbox' ).on(
'click',
function (e) {
if ( 'undefined' == e.shiftKey ) {
return true;
}
if ( e.shiftKey ) {
if ( ! lastClicked ) {
return true;
}
checks = $( lastClicked ).closest( '.wpbc_selectable_body' ).find( ':checkbox' ).filter( ':visible:enabled' );
first = checks.index( lastClicked );
last = checks.index( this );
checked = $( this ).prop( 'checked' );
if ( 0 < first && 0 < last && first != last ) {
sliced = (last > first) ? checks.slice( first, last ) : checks.slice( last, first );
sliced.prop(
'checked',
function () {
if ( $( this ).closest( '.wpbc_row' ).is( ':visible' ) ) {
return checked;
}
return false;
}
).trigger( 'change' );
}
}
lastClicked = this;
// toggle "check all" checkboxes.
var unchecked = $( this ).closest( '.wpbc_selectable_body' ).find( ':checkbox' ).filter( ':visible:enabled' ).not( ':checked' );
$( this ).closest( '.wpbc_selectable_table' ).children( '.wpbc_selectable_head, .wpbc_selectable_foot' ).find( ':checkbox' ).prop(
'checked',
function () {
return (0 === unchecked.length);
}
).trigger( 'change' );
return true;
}
);
// Head || Foot clicking to select / deselect ALL.
$( '.wpbc_selectable_head, .wpbc_selectable_foot' ).find( '.check-column :checkbox' ).on(
'click',
function (event) {
var $this = $( this ),
$table = $this.closest( '.wpbc_selectable_table' ),
controlChecked = $this.prop( 'checked' ),
toggle = event.shiftKey || $this.data( 'wp-toggle' );
$table.children( '.wpbc_selectable_body' ).filter( ':visible' )
.find( '.check-column' ).find( ':checkbox' )
.prop(
'checked',
function () {
if ( $( this ).is( ':hidden,:disabled' ) ) {
return false;
}
if ( toggle ) {
return ! $( this ).prop( 'checked' );
} else if ( controlChecked ) {
return true;
}
return false;
}
).trigger( 'change' );
$table.children( '.wpbc_selectable_head, .wpbc_selectable_foot' ).filter( ':visible' )
.find( '.check-column' ).find( ':checkbox' )
.prop(
'checked',
function () {
if ( toggle ) {
return false;
} else if ( controlChecked ) {
return true;
}
return false;
}
);
}
);
// Visually show selected border.
$( '.wpbc_selectable_body' ).find( '.check-column :checkbox' ).on(
'change',
function (event) {
if ( jQuery( this ).is( ':checked' ) ) {
jQuery( this ).closest( '.wpbc_list_row' ).addClass( 'row_selected_color' );
} else {
jQuery( this ).closest( '.wpbc_list_row' ).removeClass( 'row_selected_color' );
}
// Disable text selection while pressing 'shift'.
document.getSelection().removeAllRanges();
// Show or hide buttons on Actions toolbar at Booking Listing page, if we have some selected bookings.
wpbc_show_hide_action_buttons_for_selected_bookings();
}
);
wpbc_show_hide_action_buttons_for_selected_bookings();
}
/**
* Get ID array of selected elements
*/
function wpbc_get_selected_row_id() {
var $table = jQuery( '.wpbc__wrap__booking_listing .wpbc_selectable_table' );
var checkboxes = $table.children( '.wpbc_selectable_body' ).filter( ':visible' ).find( '.check-column' ).find( ':checkbox' );
var selected_id = [];
jQuery.each(
checkboxes,
function (key, checkbox) {
if ( jQuery( checkbox ).is( ':checked' ) ) {
var element_id = wpbc_get_row_id_from_element( checkbox );
selected_id.push( element_id );
}
}
);
return selected_id;
}
/**
* Get ID of row, based on clciked element
*
* @param this_inbound_element - ususlly this
* @returns {number}
*/
function wpbc_get_row_id_from_element(this_inbound_element) {
var element_id = jQuery( this_inbound_element ).closest( '.wpbc_listing_usual_row' ).attr( 'id' );
element_id = parseInt( element_id.replace( 'row_id_', '' ) );
return element_id;
}
/**
* == Booking Listing == Show or hide buttons on Actions toolbar at page, if we have some selected bookings.
*/
function wpbc_show_hide_action_buttons_for_selected_bookings(){
var selected_rows_arr = wpbc_get_selected_row_id();
if ( selected_rows_arr.length > 0 ) {
jQuery( '.hide_button_if_no_selection' ).show();
} else {
jQuery( '.hide_button_if_no_selection' ).hide();
}
}
"use strict";
// =====================================================================================================================
// == Left Bar - expand / colapse functions ==
// =====================================================================================================================
/**
* Save user's preferred left sidebar mode.
*
* @param string mode
*/
function wpbc_admin_ui__sidebar_left__save_mode( mode ) {
var allowed_modes = [ 'min', 'compact', 'max' ];
if ( allowed_modes.indexOf( mode ) === -1 ) {
return;
}
var $saver = jQuery( '#wpbc_left_sidebar_view_mode_saver' );
if ( ! $saver.length ) {
return;
}
if ( 'function' !== typeof wpbc_save_custom_user_data_from_element ) {
return;
}
$saver.data( 'wpbc-u-save-value', mode );
$saver.attr( 'data-wpbc-u-save-value', mode );
wpbc_save_custom_user_data_from_element( $saver.get( 0 ) );
}
/**
* Reveal the active item inside the scrollable left navigation.
*
* The active item itself is aligned with a small leading offset, regardless of
* its position inside a root section. Scrolling is applied only to SimpleBar's
* internal scroll element so the WordPress administration document does not
* move.
*
* @param {Object|null|undefined} simplebar_instance Optional initialized SimpleBar instance.
* @return {void}
*/
function wpbc_admin_ui__sidebar_left__scroll_to_active_item( simplebar_instance ) {
var left_navigation_element = document.querySelector( '.wpbc_ui_el__vert_left_bar__content' );
if (
! simplebar_instance
&& 'undefined' !== typeof SimpleBar
&& SimpleBar.instances
&& left_navigation_element
) {
simplebar_instance = SimpleBar.instances.get( left_navigation_element );
}
if (
! simplebar_instance
|| 'function' !== typeof simplebar_instance.getScrollElement
|| 'function' !== typeof simplebar_instance.getContentElement
) {
return;
}
window.requestAnimationFrame( function () {
var scroll_element = simplebar_instance.getScrollElement();
var content_element = simplebar_instance.getContentElement();
if (
! scroll_element
|| ! content_element
|| ! content_element.closest( '.wpbc_ui_el__vert_left_bar__content' )
) {
return;
}
simplebar_instance.recalculate();
var active_item = content_element.querySelector( '.wpbc_ui_el__vert_nav_item.active' );
if ( ! active_item || null === active_item.offsetParent || 0 >= scroll_element.clientHeight ) {
return;
}
var scroll_rect = scroll_element.getBoundingClientRect();
var active_rect = active_item.getBoundingClientRect();
var current_top = scroll_element.scrollTop;
var leading_offset = 85;
var active_top = current_top + active_rect.top - scroll_rect.top;
var target_top = active_top - leading_offset;
var maximum_top = Math.max( 0, scroll_element.scrollHeight - scroll_element.clientHeight );
scroll_element.scrollTop = Math.max( 0, Math.min( Math.round( target_top ), maximum_top ) );
} );
}
/**
* Expand Vertical Left Bar.
*
* @param bool is_save_user_state Save this mode as user's preference.
*/
function wpbc_admin_ui__sidebar_left__do_max( is_save_user_state ) {
jQuery( '.wpbc_settings_page_wrapper' ).removeClass( 'min max compact none' );
jQuery( '.wpbc_settings_page_wrapper' ).addClass( 'max' );
jQuery( '.wpbc_ui__top_nav__btn_open_left_vertical_nav' ).addClass( 'wpbc_ui__hide' );
jQuery( '.wpbc_ui__top_nav__btn_hide_left_vertical_nav' ).removeClass( 'wpbc_ui__hide' );
jQuery( '.wp-admin' ).removeClass( 'wpbc_page_wrapper_left_min wpbc_page_wrapper_left_max wpbc_page_wrapper_left_compact wpbc_page_wrapper_left_none' );
jQuery( '.wp-admin' ).addClass( 'wpbc_page_wrapper_left_max' );
wpbc_admin_ui__sidebar_left__scroll_to_active_item();
if ( is_save_user_state ) {
wpbc_admin_ui__sidebar_left__save_mode( 'max' );
}
}
/**
* Hide Vertical Left Bar.
*
* @param bool is_save_user_state Save this mode as user's preference.
*/
function wpbc_admin_ui__sidebar_left__do_min( is_save_user_state ) {
jQuery( '.wpbc_settings_page_wrapper' ).removeClass( 'min max compact none' );
jQuery( '.wpbc_settings_page_wrapper' ).addClass( 'min' );
jQuery( '.wpbc_ui__top_nav__btn_open_left_vertical_nav' ).removeClass( 'wpbc_ui__hide' );
jQuery( '.wpbc_ui__top_nav__btn_hide_left_vertical_nav' ).addClass( 'wpbc_ui__hide' );
jQuery( '.wp-admin' ).removeClass( 'wpbc_page_wrapper_left_min wpbc_page_wrapper_left_max wpbc_page_wrapper_left_compact wpbc_page_wrapper_left_none' );
jQuery( '.wp-admin' ).addClass( 'wpbc_page_wrapper_left_min' );
if ( is_save_user_state ) {
wpbc_admin_ui__sidebar_left__save_mode( 'min' );
}
}
/**
* Colapse Vertical Left Bar.
*
* @param bool is_save_user_state Save this mode as user's preference.
*/
function wpbc_admin_ui__sidebar_left__do_compact( is_save_user_state ) {
jQuery( '.wpbc_settings_page_wrapper' ).removeClass( 'min max compact none' );
jQuery( '.wpbc_settings_page_wrapper' ).addClass( 'compact' );
jQuery( '.wpbc_ui__top_nav__btn_open_left_vertical_nav' ).removeClass( 'wpbc_ui__hide' );
jQuery( '.wpbc_ui__top_nav__btn_hide_left_vertical_nav' ).addClass( 'wpbc_ui__hide' );
jQuery( '.wp-admin' ).removeClass( 'wpbc_page_wrapper_left_min wpbc_page_wrapper_left_max wpbc_page_wrapper_left_compact wpbc_page_wrapper_left_none' );
jQuery( '.wp-admin' ).addClass( 'wpbc_page_wrapper_left_compact' );
wpbc_admin_ui__sidebar_left__scroll_to_active_item();
if ( is_save_user_state ) {
wpbc_admin_ui__sidebar_left__save_mode( 'compact' );
}
}
/**
* Completely Hide Vertical Left Bar.
*/
function wpbc_admin_ui__sidebar_left__do_hide() {
jQuery( '.wpbc_settings_page_wrapper' ).removeClass( 'min max compact none' );
jQuery( '.wpbc_settings_page_wrapper' ).addClass( 'none' );
jQuery( '.wpbc_ui__top_nav__btn_open_left_vertical_nav' ).removeClass( 'wpbc_ui__hide' );
jQuery( '.wpbc_ui__top_nav__btn_hide_left_vertical_nav' ).addClass( 'wpbc_ui__hide' );
// Hide top "Menu" button with divider.
jQuery( '.wpbc_ui__top_nav__btn_show_left_vertical_nav,.wpbc_ui__top_nav__btn_show_left_vertical_nav_divider' ).addClass( 'wpbc_ui__hide' );
jQuery( '.wp-admin' ).removeClass( 'wpbc_page_wrapper_left_min wpbc_page_wrapper_left_max wpbc_page_wrapper_left_compact wpbc_page_wrapper_left_none' );
jQuery( '.wp-admin' ).addClass( 'wpbc_page_wrapper_left_none' );
}
/**
* Action on click "Go Back" - show root menu
* or some other section in left sidebar.
*
* @param string menu_to_show - menu slug.
*/
function wpbc_admin_ui__sidebar_left__show_section( menu_to_show ) {
jQuery( '.wpbc_ui_el__vert_left_bar__section' ).addClass( 'wpbc_ui__hide' )
jQuery( '.wpbc_ui_el__vert_left_bar__section_' + menu_to_show ).removeClass( 'wpbc_ui__hide' );
}
// =====================================================================================================================
// == Right Side Bar - expand / colapse functions ==
// =====================================================================================================================
/**
* Synchronize the document body marker for the expanded right sidebar.
*
* The marker is domain-neutral so individual administration pages can adjust
* their presentation without duplicating right-sidebar state handling.
*
* @param {boolean} is_open Whether the right sidebar is fully expanded.
* @return {void}
*/
function wpbc_admin_ui__sidebar_right__set_body_open_state( is_open ) {
jQuery( 'body' ).toggleClass( 'wpbc_ui_el__vert_right_bar__wrapper_opened', !! is_open );
}
/**
* Expand Vertical Right Bar.
*/
function wpbc_admin_ui__sidebar_right__do_max() {
jQuery( '.wpbc_settings_page_wrapper' ).removeClass( 'min_right max_right compact_right none_right' );
jQuery( '.wpbc_settings_page_wrapper' ).addClass( 'max_right' );
jQuery( '.wpbc_ui__top_nav__btn_open_right_vertical_nav' ).addClass( 'wpbc_ui__hide' );
jQuery( '.wpbc_ui__top_nav__btn_hide_right_vertical_nav' ).removeClass( 'wpbc_ui__hide' );
wpbc_admin_ui__sidebar_right__set_body_open_state( true );
}
/**
* Hide Vertical Right Bar.
*/
function wpbc_admin_ui__sidebar_right__do_min() {
jQuery( '.wpbc_settings_page_wrapper' ).removeClass( 'min_right max_right compact_right none_right' );
jQuery( '.wpbc_settings_page_wrapper' ).addClass( 'min_right' );
jQuery( '.wpbc_ui__top_nav__btn_open_right_vertical_nav' ).removeClass( 'wpbc_ui__hide' );
jQuery( '.wpbc_ui__top_nav__btn_hide_right_vertical_nav' ).addClass( 'wpbc_ui__hide' );
wpbc_admin_ui__sidebar_right__set_body_open_state( false );
}
/**
* Colapse Vertical Right Bar.
*/
function wpbc_admin_ui__sidebar_right__do_compact() {
jQuery( '.wpbc_settings_page_wrapper' ).removeClass( 'min_right max_right compact_right none_right' );
jQuery( '.wpbc_settings_page_wrapper' ).addClass( 'compact_right' );
jQuery( '.wpbc_ui__top_nav__btn_open_right_vertical_nav' ).removeClass( 'wpbc_ui__hide' );
jQuery( '.wpbc_ui__top_nav__btn_hide_right_vertical_nav' ).addClass( 'wpbc_ui__hide' );
wpbc_admin_ui__sidebar_right__set_body_open_state( false );
}
/**
* Completely Hide Vertical Right Bar.
*/
function wpbc_admin_ui__sidebar_right__do_hide() {
jQuery( '.wpbc_settings_page_wrapper' ).removeClass( 'min_right max_right compact_right none_right' );
jQuery( '.wpbc_settings_page_wrapper' ).addClass( 'none_right' );
jQuery( '.wpbc_ui__top_nav__btn_open_right_vertical_nav' ).removeClass( 'wpbc_ui__hide' );
jQuery( '.wpbc_ui__top_nav__btn_hide_right_vertical_nav' ).addClass( 'wpbc_ui__hide' );
// Hide top "Menu" button with divider.
jQuery( '.wpbc_ui__top_nav__btn_show_right_vertical_nav,.wpbc_ui__top_nav__btn_show_right_vertical_nav_divider' ).addClass( 'wpbc_ui__hide' );
wpbc_admin_ui__sidebar_right__set_body_open_state( false );
}
/**
* Restore the body marker when a page renders with the right sidebar open.
*/
jQuery( document ).ready( function () {
wpbc_admin_ui__sidebar_right__set_body_open_state( 0 < jQuery( '.wpbc_settings_page_wrapper.max_right' ).length );
} );
/**
* Collapse an expanded right sidebar after an opted-in page-content click.
*
* Pages enable this behavior through the page-structure
* right_vertical_sidebar__content_click_collapse_mode option. Interactive
* controls that open or retain sidebar content can opt out by placing the
* data-wpbc-right-sidebar-keep-open attribute on themselves or an ancestor.
*
* @param {MouseEvent} event Content click event captured before catalog rows.
* @return {void}
*/
function wpbc_admin_ui__sidebar_right__collapse_from_content_click( event ) {
var event_target = event.target && 1 === event.target.nodeType ? event.target : null;
var content_element = event_target && 'function' === typeof event_target.closest
? event_target.closest( '.wpbc_settings_page_wrapper[data-wpbc-right-sidebar-content-click-collapse-mode] > .wpbc_settings_page_content' )
: null;
var $content;
var $wrapper;
var collapse_mode;
var before_collapse_event;
if ( ! content_element ) {
return;
}
$content = jQuery( content_element );
$wrapper = $content.closest( '.wpbc_settings_page_wrapper' );
collapse_mode = String( $wrapper.attr( 'data-wpbc-right-sidebar-content-click-collapse-mode' ) || '' );
if ( ! $wrapper.hasClass( 'max_right' ) || [ 'min', 'compact', 'none' ].indexOf( collapse_mode ) === -1 ) {
return;
}
if ( jQuery( event.target ).closest( '[data-wpbc-right-sidebar-keep-open]' ).length ) {
return;
}
/*
* This click belongs to the open-sidebar dismissal layer. Consume it before
* domain row handlers run so the same pointer action cannot close one
* inspector and immediately open another one underneath it.
*/
event.preventDefault();
event.stopImmediatePropagation();
before_collapse_event = jQuery.Event( 'wpbc:right-sidebar-before-content-collapse' );
$wrapper.trigger( before_collapse_event, [ event ] );
if ( before_collapse_event.isDefaultPrevented() ) {
return;
}
if ( 'compact' === collapse_mode ) {
wpbc_admin_ui__sidebar_right__do_compact();
} else if ( 'none' === collapse_mode ) {
wpbc_admin_ui__sidebar_right__do_hide();
} else {
wpbc_admin_ui__sidebar_right__do_min();
}
jQuery( document ).trigger( 'wpbc_setup_wizard_layout_changed' );
}
document.addEventListener( 'click', wpbc_admin_ui__sidebar_right__collapse_from_content_click, true );
/**
* Action on click "Go Back" - show root menu
* or some other section in right sidebar.
*
* @param string menu_to_show - menu slug.
*/
function wpbc_admin_ui__sidebar_right__show_section( menu_to_show ) {
jQuery( '.wpbc_ui_el__vert_right_bar__section' ).addClass( 'wpbc_ui__hide' )
jQuery( '.wpbc_ui_el__vert_right_bar__section_' + menu_to_show ).removeClass( 'wpbc_ui__hide' );
}
// =====================================================================================================================
// == End Right Side Bar section ==
// =====================================================================================================================
/**
* Get anchor(s) array from URL.
* Doc: https://developer.mozilla.org/en-US/docs/Web/API/Location
*
* @returns {*[]}
*/
function wpbc_url_get_anchors_arr() {
var hashes = window.location.hash.replace( '%23', '#' );
var hashes_arr = hashes.split( '#' );
var result = [];
var hashes_arr_length = hashes_arr.length;
for ( var i = 0; i < hashes_arr_length; i++ ) {
if ( hashes_arr[i].length > 0 ) {
result.push( hashes_arr[i] );
}
}
return result;
}
/**
* Auto Expand Settings section based on URL anchor, after page loaded.
*/
jQuery( document ).ready( function () { wpbc_admin_ui__redirect_legacy_general_availability_url(); } );
jQuery( document ).ready( function () { wpbc_admin_ui__do_expand_section(); setTimeout( 'wpbc_admin_ui__do_expand_section', 10 ); } );
jQuery( document ).ready( function () { wpbc_admin_ui__do_expand_section(); setTimeout( 'wpbc_admin_ui__do_expand_section', 150 ); } );
/**
* Redirect old Settings > Availability anchors to the dedicated General Availability page.
*/
function wpbc_admin_ui__redirect_legacy_general_availability_url() {
if (
( window.location.href.indexOf( 'page=wpbc-settings' ) > -1 )
&& (
( window.location.hash.indexOf( 'wpbc_general_settings_availability_metabox' ) > -1 )
|| ( window.location.hash.indexOf( 'wpbc_general_settings_availability_tab' ) > -1 )
)
) {
window.location.replace( window.location.href.split( '?' )[0] + '?page=wpbc-availability&tab=general_availability' );
}
}
/**
* Expand section in General Settings page and select Menu item.
*/
function wpbc_admin_ui__do_expand_section() {
// window.location.hash = #section_id / doc: https://developer.mozilla.org/en-US/docs/Web/API/Location .
var anchors_arr = wpbc_url_get_anchors_arr();
var anchors_arr_length = anchors_arr.length;
if ( anchors_arr_length > 0 ) {
var one_anchor_prop_value = anchors_arr[0].split( 'do_expand__' );
if ( one_anchor_prop_value.length > 1 ) {
// 'wpbc_general_settings_calendar_metabox'
var section_to_show = one_anchor_prop_value[1];
var section_id_to_show = '#' + section_to_show;
// -- Remove selected background in all left menu items ---------------------------------------------------
jQuery( '.wpbc_ui_el__vert_nav_item ' ).removeClass( 'active' );
// Set left menu selected.
jQuery( '.do_expand__' + section_to_show + '_link' ).addClass( 'active' );
var selected_title = jQuery( '.do_expand__' + section_to_show + '_link a .wpbc_ui_el__vert_nav_title ' ).text();
// Expand section, if it colapsed.
if ( ! jQuery( '.do_expand__' + section_to_show + '_link' ).parents( '.wpbc_ui_el__level__folder' ).hasClass( 'expanded' ) ) {
jQuery( '.wpbc_ui_el__level__folder' ).removeClass( 'expanded' );
jQuery( '.do_expand__' + section_to_show + '_link' ).parents( '.wpbc_ui_el__level__folder' ).addClass( 'expanded' );
}
// -- Expand section ---------------------------------------------------------------------------------------
var container_to_hide_class = '.postbox';
// Hide sections '.postbox' in admin page and show specific one.
jQuery( '.wpbc_admin_page ' + container_to_hide_class ).hide();
jQuery( '.wpbc_container_always_hide__on_left_nav_click' ).hide();
jQuery( section_id_to_show ).show();
// Show all other sections, if provided in URL: ..?page=wpbc-settings#do_expand__wpbc_general_settings_capacity_metabox#wpbc_general_settings_capacity_upgrade_metabox .
for ( let i = 1; i < anchors_arr_length; i++ ) {
jQuery( '#' + anchors_arr[i] ).show();
}
if ( false ) {
var targetOffset = wpbc_scroll_to( section_id_to_show );
}
// -- Set Value to Input about selected Nav element --------------------------------------------------------------- // FixIn: 9.8.6.1.
var section_id_tab = section_id_to_show.substring( 0, section_id_to_show.length - 8 ) + '_tab';
if ( container_to_hide_class == section_id_to_show ) {
section_id_tab = '#wpbc_general_settings_all_tab'
}
if ( '#wpbc_general_settings_capacity_metabox,#wpbc_general_settings_capacity_upgrade_metabox' == section_id_to_show ) {
section_id_tab = '#wpbc_general_settings_capacity_tab'
}
jQuery( '#form_visible_section' ).val( section_id_tab );
}
// Like blinking some elements.
wpbc_admin_ui__do__anchor__another_actions();
}
}
function wpbc_admin_ui__is_in_mobile_screen_size() {
return wpbc_admin_ui__is_in_this_screen_size( 605 );
}
function wpbc_admin_ui__is_in_this_screen_size(size) {
return (window.screen.width <= size);
}
/**
* Open settings page | Expand section | Select Menu item.
*/
function wpbc_admin_ui__do__open_url__expand_section(url, section_id) {
// window.location.href = url + '&do_expand=' + section_id + '#do_expand__' + section_id; //.
window.location.href = url + '#do_expand__' + section_id;
if ( wpbc_admin_ui__is_in_mobile_screen_size() ) {
wpbc_admin_ui__sidebar_left__do_min();
}
wpbc_admin_ui__do_expand_section();
}
/**
* Check for Other actions: Like blinking some elements in settings page. E.g. Days selection or change-over days.
*/
function wpbc_admin_ui__do__anchor__another_actions() {
var anchors_arr = wpbc_url_get_anchors_arr();
var anchors_arr_length = anchors_arr.length;
// Other actions: Like blinking some elements.
for ( var i = 0; i < anchors_arr_length; i++ ) {
var this_anchor = anchors_arr[i];
var this_anchor_prop_value = this_anchor.split( 'do_other_actions__' );
if ( this_anchor_prop_value.length > 1 ) {
var section_action = this_anchor_prop_value[1];
switch ( section_action ) {
case 'blink_day_selections':
// wpbc_ui_settings__panel__click( '#wpbc_general_settings_calendar_tab a', '#wpbc_general_settings_calendar_metabox', 'Days Selection' );.
wpbc_blink_element( '.wpbc_tr_set_gen_booking_type_of_day_selections', 4, 350 );
wpbc_scroll_to( '.wpbc_tr_set_gen_booking_type_of_day_selections' );
break;
case 'blink_change_over_days':
// wpbc_ui_settings__panel__click( '#wpbc_general_settings_calendar_tab a', '#wpbc_general_settings_calendar_metabox', 'Changeover Days' );.
wpbc_blink_element( '.wpbc_tr_set_gen_booking_range_selection_time_is_active', 4, 350 );
wpbc_scroll_to( '.wpbc_tr_set_gen_booking_range_selection_time_is_active' );
break;
case 'blink_captcha':
wpbc_blink_element( '.wpbc_tr_set_gen_booking_is_use_captcha', 4, 350 );
wpbc_scroll_to( '.wpbc_tr_set_gen_booking_is_use_captcha' );
break;
default:
}
}
}
}
/**
* Copy txt to clipbrd from Text fields.
*
* @param html_element_id - e.g. 'data_field'
* @returns {boolean}
*/
function wpbc_copy_text_to_clipbrd_from_element( html_element_id ) {
// Get the text field.
var copyText = document.getElementById( html_element_id );
// Select the text field.
copyText.select();
copyText.setSelectionRange( 0, 99999 ); // For mobile devices.
// Copy the text inside the text field.
var is_copied = wpbc_copy_text_to_clipbrd( copyText.value );
if ( ! is_copied ) {
console.error( 'Oops, unable to copy', copyText.value );
}
return is_copied;
}
/**
* Copy txt to clipbrd.
*
* @param text
* @returns {boolean}
*/
function wpbc_copy_text_to_clipbrd(text) {
if ( ! navigator.clipboard ) {
return wpbc_fallback_copy_text_to_clipbrd( text );
}
navigator.clipboard.writeText( text ).then(
function () {
// console.log( 'Async: Copying to clipboard was successful!' );.
return true;
},
function (err) {
// console.error( 'Async: Could not copy text: ', err );.
return false;
}
);
}
/**
* Copy txt to clipbrd - depricated method.
*
* @param text
* @returns {boolean}
*/
function wpbc_fallback_copy_text_to_clipbrd( text ) {
// -----------------------------------------------------------------------------------------------------------------
// var textArea = document.createElement( "textarea" );
// textArea.value = text;
//
// // Avoid scrolling to bottom.
// textArea.style.top = "0";
// textArea.style.left = "0";
// textArea.style.position = "fixed";
// textArea.style.zIndex = "999999999";
// document.body.appendChild( textArea );
// textArea.focus();
// textArea.select();
// -----------------------------------------------------------------------------------------------------------------
// Now get it as HTML (original here https://stackoverflow.com/questions/34191780/javascript-copy-string-to-clipboard-as-text-html ).
// [1] - Create container for the HTML.
var container = document.createElement( 'div' );
container.innerHTML = text;
// [2] - Hide element.
container.style.position = 'fixed';
container.style.pointerEvents = 'none';
container.style.opacity = 0;
// Detect all style sheets of the page.
var activeSheets = Array.prototype.slice.call( document.styleSheets ).filter(
function (sheet) {
return ! sheet.disabled;
}
);
// [3] - Mount the container to the DOM to make `contentWindow` available.
document.body.appendChild( container );
// [4] - Copy to clipboard.
window.getSelection().removeAllRanges();
var range = document.createRange();
range.selectNode( container );
window.getSelection().addRange( range );
// -----------------------------------------------------------------------------------------------------------------
var result = false;
try {
result = document.execCommand( 'copy' );
// console.log( 'Fallback: Copying text command was ' + msg ); //.
} catch ( err ) {
// console.error( 'Fallback: Oops, unable to copy', err ); //.
}
// document.body.removeChild( textArea ); //.
// [5.4] - Enable CSS.
var activeSheets_length = activeSheets.length;
for ( var i = 0; i < activeSheets_length; i++ ) {
activeSheets[i].disabled = false;
}
// [6] - Remove the container
document.body.removeChild( container );
return result;
}
/**
* WPBC Collapsible Groups
*
* Universal, dependency-free controller for expanding/collapsing grouped sections in right-side panels (Inspector/Library/Form Settings, or any other WPBC page).
*
* === How to use it (quick) ? ===
*
* -- 1. Markup (independent mode: multiple open allowed) --
*
*
*
*
…
*
*
*
*
…
*
*
*
* -- 2. Exclusive/accordion mode (one open at a time) --
*
…
*
* -- 3. Auto-init --
* The script auto-initializes on DOMContentLoaded. No extra code needed.
*
* -- 4. Programmatic control (optional)
* const root = document.querySelector('#wpbc_bfb__inspector');
* const api = root.__wpbc_collapsible_instance; // set by auto-init
*
* api.open_by_heading('Validation'); // open by heading text
* api.open_by_index(0); // open the first group
*
* -- 5.Listen to events (e.g., to persist “open group” state) --
* root.addEventListener('wpbc:collapsible:open', (e) => { console.log( e.detail.group ); });
* root.addEventListener('wpbc:collapsible:close', (e) => { console.log( e.detail.group ); });
*
*
*
* Markup expectations (minimal):
*
*
*
*
...
*
* ... more ...
*
*
* Notes:
* - Add `is-open` to any section you want initially expanded.
* - Add `wpbc_collapsible--exclusive` to the container for "open one at a time" behavior.
* - Works with your existing BFB markup (classes used there are the defaults).
*
* Accessibility:
* - Sets aria-expanded on .group__header
* - Sets aria-hidden + [hidden] on .group__fields
* - ArrowUp/ArrowDown move focus between headers; Enter/Space toggles
*
* Events (bubbles from the ):
* - 'wpbc:collapsible:open' (detail: { group, root, instance })
* - 'wpbc:collapsible:close' (detail: { group, root, instance })
*
* Public API (instance methods):
* - init(), destroy(), refresh()
* - expand(group, [exclusive]), collapse(group), toggle(group)
* - open_by_index(index), open_by_heading(text)
* - is_exclusive(), is_open(group)
*
* @version 2025-08-26
* @since 2025-08-26
*/
// ---------------------------------------------------------------------------------------------------------------------
// == File /collapsible_groups.js == Time point: 2025-08-26 14:13
// ---------------------------------------------------------------------------------------------------------------------
(function (w, d) {
'use strict';
class WPBC_Collapsible_Groups {
/**
* Create a collapsible controller for a container.
*
* @param {HTMLElement|string} root_el
* The container element (or CSS selector) that wraps collapsible groups.
* The container usually has the class `.wpbc_collapsible`.
* @param {Object} [opts={}]
* @param {string} [opts.group_selector='.wpbc_ui__collapsible_group']
* Selector for each collapsible group inside the container.
* @param {string} [opts.header_selector='.group__header']
* Selector for the clickable header inside a group.
* @param {string} [opts.fields_selector='.group__fields']
* Selector for the content/panel element inside a group.
* @param {string} [opts.open_class='is-open']
* Class name that indicates the group is open.
* @param {boolean} [opts.exclusive=false]
* If true, only one group can be open at a time in this container.
*
* @constructor
* @since 2025-08-26
*/
constructor(root_el, opts = {}) {
this.root = (typeof root_el === 'string') ? d.querySelector( root_el ) : root_el;
this.opts = Object.assign( {
group_selector : '.wpbc_ui__collapsible_group',
header_selector: '.group__header',
fields_selector: '.group__fields,.group__content',
open_class : 'is-open',
exclusive : false
}, opts );
// Bound handlers (for add/removeEventListener symmetry).
/** @private */
this._on_click = this._on_click.bind( this );
/** @private */
this._on_keydown = this._on_keydown.bind( this );
/** @type {HTMLElement[]} @private */
this._groups = [];
/** @type {MutationObserver|null} @private */
this._observer = null;
}
/**
* Initialize the controller: cache groups, attach listeners, set ARIA,
* and start observing DOM changes inside the container.
*
* @returns {WPBC_Collapsible_Groups} The instance (chainable).
* @listens click
* @listens keydown
* @since 2025-08-26
*/
init() {
if ( !this.root ) {
return this;
}
this._groups = Array.prototype.slice.call(
this.root.querySelectorAll( this.opts.group_selector )
);
this.root.addEventListener( 'click', this._on_click, false );
this.root.addEventListener( 'keydown', this._on_keydown, false );
// Observe dynamic inserts/removals (Inspector re-renders).
this._observer = new MutationObserver( () => {
this.refresh();
} );
this._observer.observe( this.root, { childList: true, subtree: true } );
this._sync_all_aria();
return this;
}
/**
* Tear down the controller: detach listeners, stop the observer,
* and drop internal references.
*
* @returns {void}
* @since 2025-08-26
*/
destroy() {
if ( !this.root ) {
return;
}
this.root.removeEventListener( 'click', this._on_click, false );
this.root.removeEventListener( 'keydown', this._on_keydown, false );
if ( this._observer ) {
this._observer.disconnect();
this._observer = null;
}
this._groups = [];
}
/**
* Re-scan the DOM for current groups and re-apply ARIA to all of them.
* Useful after dynamic (re)renders.
*
* @returns {void}
* @since 2025-08-26
*/
refresh() {
if ( !this.root ) {
return;
}
this._groups = Array.prototype.slice.call(
this.root.querySelectorAll( this.opts.group_selector )
);
this._sync_all_aria();
}
/**
* Check whether the container is in exclusive (accordion) mode.
*
* Order of precedence:
* 1) Explicit option `opts.exclusive`
* 2) Container has class `.wpbc_collapsible--exclusive`
* 3) Container matches `[data-wpbc-accordion="exclusive"]`
*
* @returns {boolean} True if exclusive mode is active.
* @since 2025-08-26
*/
is_exclusive() {
return !!(
this.opts.exclusive ||
this.root.classList.contains( 'wpbc_collapsible--exclusive' ) ||
this.root.matches( '[data-wpbc-accordion="exclusive"]' )
);
}
/**
* Determine whether a specific group is open.
*
* @param {HTMLElement} group The group element to test.
* @returns {boolean} True if the group is currently open.
* @since 2025-08-26
*/
is_open(group) {
return group.classList.contains( this.opts.open_class );
}
/**
* Open a group. Honors exclusive mode by collapsing all sibling groups
* (queried from the live DOM at call-time).
*
* @param {HTMLElement} group The group element to open.
* @param {boolean} [exclusive]
* If provided, overrides container mode for this action only.
* @returns {void}
* @fires CustomEvent#wpbc:collapsible:open
* @since 2025-08-26
*/
expand(group, exclusive) {
if ( !group ) {
return;
}
const do_exclusive = (typeof exclusive === 'boolean') ? exclusive : this.is_exclusive();
if ( do_exclusive ) {
// Always use the live DOM, not the cached list.
Array.prototype.forEach.call(
this.root.querySelectorAll( this.opts.group_selector ),
(g) => {
if ( g !== group ) {
this._set_open( g, false );
}
}
);
}
this._set_open( group, true );
}
/**
* Close a group.
*
* @param {HTMLElement} group The group element to close.
* @returns {void}
* @fires CustomEvent#wpbc:collapsible:close
* @since 2025-08-26
*/
collapse(group) {
if ( !group ) {
return;
}
this._set_open( group, false );
}
/**
* Toggle a group's open/closed state.
*
* @param {HTMLElement} group The group element to toggle.
* @returns {void}
* @since 2025-08-26
*/
toggle(group) {
if ( !group ) {
return;
}
this[this.is_open( group ) ? 'collapse' : 'expand']( group );
}
/**
* Open a group by its index within the container (0-based).
*
* @param {number} index Zero-based index of the group.
* @returns {void}
* @since 2025-08-26
*/
open_by_index(index) {
const group = this._groups[index];
if ( group ) {
this.expand( group );
}
}
/**
* Open a group by matching text contained within the
inside the header.
* The comparison is case-insensitive and substring-based.
*
* @param {string} text Text to match against the heading contents.
* @returns {void}
* @since 2025-08-26
*/
open_by_heading(text) {
if ( !text ) {
return;
}
const t = String( text ).toLowerCase();
const match = this._groups.find( (g) => {
const h = g.querySelector( this.opts.header_selector + ' h3' );
return h && h.textContent.toLowerCase().indexOf( t ) !== -1;
} );
if ( match ) {
this.expand( match );
}
}
// -------------------------------------------------------------------------------------------------------------
// Internal
// -------------------------------------------------------------------------------------------------------------
/**
* Delegated click handler for headers.
*
* @private
* @param {MouseEvent} ev The click event.
* @returns {void}
* @since 2025-08-26
*/
_on_click(ev) {
const btn = ev.target.closest( this.opts.header_selector );
if ( !btn || !this.root.contains( btn ) ) {
return;
}
ev.preventDefault();
ev.stopPropagation();
const group = btn.closest( this.opts.group_selector );
if ( group ) {
this.toggle( group );
}
}
/**
* Keyboard handler for header interactions and roving focus:
* - Enter/Space toggles the active group.
* - ArrowUp/ArrowDown moves focus between group headers.
*
* @private
* @param {KeyboardEvent} ev The keyboard event.
* @returns {void}
* @since 2025-08-26
*/
_on_keydown(ev) {
const btn = ev.target.closest( this.opts.header_selector );
if ( !btn ) {
return;
}
const key = ev.key;
// Toggle on Enter / Space.
if ( key === 'Enter' || key === ' ' ) {
ev.preventDefault();
const group = btn.closest( this.opts.group_selector );
if ( group ) {
this.toggle( group );
}
return;
}
// Move focus with ArrowUp/ArrowDown between headers in this container.
if ( key === 'ArrowUp' || key === 'ArrowDown' ) {
ev.preventDefault();
const headers = Array.prototype.map.call(
this.root.querySelectorAll( this.opts.group_selector ),
(g) => g.querySelector( this.opts.header_selector )
).filter( Boolean );
const idx = headers.indexOf( btn );
if ( idx !== -1 ) {
const next_idx = (key === 'ArrowDown')
? Math.min( headers.length - 1, idx + 1 )
: Math.max( 0, idx - 1 );
headers[next_idx].focus();
}
}
}
/**
* Apply ARIA synchronization to all known groups based on their open state.
*
* @private
* @returns {void}
* @since 2025-08-26
*/
_sync_all_aria() {
this._groups.forEach( (g) => this._sync_group_aria( g ) );
}
/**
* Sync ARIA attributes and visibility on a single group.
*
* @private
* @param {HTMLElement} group The group element to sync.
* @returns {void}
* @since 2025-08-26
*/
_sync_group_aria(group) {
const is_open = this.is_open( group );
const header = group.querySelector( this.opts.header_selector );
// Only direct children that match.
const panels = Array.prototype.filter.call( group.children, (el) => el.matches( this.opts.fields_selector ) );
// Header ARIA.
if ( header ) {
header.setAttribute( 'role', 'button' );
header.setAttribute( 'aria-expanded', is_open ? 'true' : 'false' );
if ( panels.length ) {
// Ensure each panel has an id; then wire aria-controls with space-separated ids.
const ids = panels.map( (p) => {
if ( !p.id ) p.id = this._generate_id( 'wpbc_collapsible_panel' );
return p.id;
} );
header.setAttribute( 'aria-controls', ids.join( ' ' ) );
}
}
// (3) Panels ARIA + visibility.
panels.forEach( (p) => {
p.hidden = !is_open; // actual visibility.
p.setAttribute( 'aria-hidden', is_open ? 'false' : 'true' ); // ARIA.
} );
}
/**
* Internal state change: set a group's open/closed state, sync ARIA,
* manage focus on collapse, and emit a custom event.
*
* @private
* @param {HTMLElement} group The group element to mutate.
* @param {boolean} open Whether the group should be open.
* @returns {void}
* @fires CustomEvent#wpbc:collapsible:open
* @fires CustomEvent#wpbc:collapsible:close
* @since 2025-08-26
*/
_set_open(group, open) {
if ( !open && group.contains( document.activeElement ) ) {
const header = group.querySelector( this.opts.header_selector );
header && header.focus();
}
group.classList.toggle( this.opts.open_class, open );
this._sync_group_aria( group );
const ev_name = open ? 'wpbc:collapsible:open' : 'wpbc:collapsible:close';
group.dispatchEvent( new CustomEvent( ev_name, {
bubbles: true,
detail : { group, root: this.root, instance: this }
} ) );
}
/**
* Generate a unique DOM id with the specified prefix.
*
* @private
* @param {string} prefix The id prefix to use.
* @returns {string} A unique element id not present in the document.
* @since 2025-08-26
*/
_generate_id(prefix) {
let i = 1;
let id;
do {
id = prefix + '_' + (i++);
}
while ( d.getElementById( id ) );
return id;
}
}
/**
* Auto-initialize collapsible controllers on the page.
* Finds top-level `.wpbc_collapsible` containers (ignoring nested ones),
* and instantiates {@link WPBC_Collapsible_Groups} on each.
*
* @function WPBC_Collapsible_AutoInit
* @returns {void}
* @since 2025-08-26
* @example
* // Runs automatically on DOMContentLoaded; can also be called manually:
* WPBC_Collapsible_AutoInit();
*/
function wpbc_collapsible__auto_init() {
var ROOT = '.wpbc_collapsible';
var nodes = Array.prototype.slice.call( d.querySelectorAll( ROOT ) )
.filter( function (n) {
return !n.parentElement || !n.parentElement.closest( ROOT );
} );
nodes.forEach( function (node) {
if ( node.__wpbc_collapsible_instance ) {
return;
}
var exclusive = node.classList.contains( 'wpbc_collapsible--exclusive' ) || node.matches( '[data-wpbc-accordion="exclusive"]' );
node.__wpbc_collapsible_instance = new WPBC_Collapsible_Groups( node, { exclusive } ).init();
} );
}
// Export to global for manual control if needed.
w.WPBC_Collapsible_Groups = WPBC_Collapsible_Groups;
w.WPBC_Collapsible_AutoInit = wpbc_collapsible__auto_init;
// DOM-ready auto init.
if ( d.readyState === 'loading' ) {
d.addEventListener( 'DOMContentLoaded', wpbc_collapsible__auto_init, { once: true } );
} else {
wpbc_collapsible__auto_init();
}
})( window, document );
/* globals window, document */
/**
* WPBC Slider Length Groups
*
* Universal, dependency-free controller that keeps a "length" control in sync:
* - number input (data-wpbc_slider_len_value)
* - unit select (data-wpbc_slider_len_unit)
* - range slider (data-wpbc_slider_len_range)
* - writer input (data-wpbc_slider_len_writer) [optional but recommended]
*
* The "writer" stores the combined value like: "100%", "420px", "12.5rem".
* When number/unit/slider change -> writer updates and emits 'input' (bubbles).
* When writer is changed externally (apply-from-JSON, etc) -> UI updates.
*
* Markup expectations (minimal):
*
*
*
*
*
*
*
* Performance notes:
* - MutationObserver is DISABLED by default (prevents performance issues).
* - If your UI re-renders and inserts new groups dynamically, call:
* WPBC_Slider_Len_AutoInit(); OR instance.refresh();
* Or enable observer via: new WPBC_Slider_Len_Groups(root, { enable_observer:true }).init();
*
* Public API (instance methods):
* - init(), destroy(), refresh()
*
* @version 2026-01-25
* @since 2026-01-25
* @file ../includes/__js/admin/slider_groups/wpbc_len_groups.js
*/
(function (w, d) {
'use strict';
// -------------------------------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------------------------------
function clamp_num(v, min, max) {
if (typeof min === 'number' && !isNaN(min)) v = Math.max(min, v);
if (typeof max === 'number' && !isNaN(max)) v = Math.min(max, v);
return v;
}
function parse_float(v) {
var n = parseFloat(v);
return isNaN(n) ? null : n;
}
function safe_json_parse(str) {
try {
return JSON.parse(str);
} catch (e) {
return null;
}
}
function parse_len_combined(raw, default_unit) {
var s = (raw == null) ? '' : String(raw).trim();
if (!s) return { num: '', unit: default_unit || '%' };
var m = s.match(/^\s*([\-]?\d+(?:\.\d+)?)\s*([a-z%]*)\s*$/i);
if (!m) {
// If it's not parseable, treat as number and keep default unit.
return { num: s, unit: default_unit || '%' };
}
var num = m[1] ? String(m[1]) : '';
var unit = m[2] ? String(m[2]) : '';
if (!unit) unit = default_unit || '%';
return { num: num, unit: unit };
}
function build_combined(num, unit) {
if (num == null || String(num).trim() === '') return '';
return String(num) + String(unit || '');
}
function emit_input(el) {
if (!el) return;
el.dispatchEvent(new Event('input', { bubbles: true }));
}
// -------------------------------------------------------------------------------------------------
// Controller
// -------------------------------------------------------------------------------------------------
class WPBC_Slider_Len_Groups {
/**
* @param {HTMLElement|string} root_el Container (or selector). If omitted, uses document.
* @param {Object} [opts={}]
*/
constructor(root_el, opts) {
this.root = root_el
? ((typeof root_el === 'string') ? d.querySelector(root_el) : root_el)
: d;
this.opts = Object.assign({
// Strict selectors (NO backward compatibility).
group_selector : '.wpbc_slider_len_group',
value_selector : '[data-wpbc_slider_len_value]',
unit_selector : '[data-wpbc_slider_len_unit]',
range_selector : '[data-wpbc_slider_len_range]',
writer_selector : '[data-wpbc_slider_len_writer]',
default_unit : '%',
fallback_bounds : {
'px' : { min: 0, max: 512, step: 1 },
'%' : { min: 0, max: 100, step: 1 },
'rem': { min: 0, max: 10, step: 0.1 },
'em' : { min: 0, max: 10, step: 0.1 }
},
// Disabled by default for performance.
enable_observer : false,
observer_debounce_ms: 150
}, opts || {});
this._on_input = this._on_input.bind(this);
this._on_change = this._on_change.bind(this);
this._bounds_cache = new WeakMap(); // group -> bounds_map_object
this._observer = null;
this._refresh_tmr = null;
}
init() {
if (!this.root) return this;
this.root.addEventListener('input', this._on_input, true);
this.root.addEventListener('change', this._on_change, true);
if (this.opts.enable_observer && w.MutationObserver) {
this._observer = new MutationObserver(() => { this._debounced_refresh(); });
this._observer.observe(this.root === d ? d.documentElement : this.root, { childList: true, subtree: true });
}
this.refresh();
return this;
}
destroy() {
if (!this.root) return;
this.root.removeEventListener('input', this._on_input, true);
this.root.removeEventListener('change', this._on_change, true);
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
if (this._refresh_tmr) {
clearTimeout(this._refresh_tmr);
this._refresh_tmr = null;
}
}
refresh() {
if (!this.root) return;
var scope = (this.root === d ? d : this.root);
var groups = Array.prototype.slice.call(scope.querySelectorAll(this.opts.group_selector));
for (var i = 0; i < groups.length; i++) {
this._sync_group_from_writer(groups[i]);
this._apply_bounds_for_current_unit(groups[i]);
}
}
// -------------------------------------------------------------------------------------------------
// Internal
// -------------------------------------------------------------------------------------------------
_debounced_refresh() {
if (this._refresh_tmr) clearTimeout(this._refresh_tmr);
this._refresh_tmr = setTimeout(() => {
this._refresh_tmr = null;
this.refresh();
}, Number(this.opts.observer_debounce_ms) || 0);
}
_find_group(el) {
return (el && el.closest) ? el.closest(this.opts.group_selector) : null;
}
_get_parts(group) {
if (!group) return null;
return {
group : group,
num : group.querySelector(this.opts.value_selector),
unit : group.querySelector(this.opts.unit_selector),
range : group.querySelector(this.opts.range_selector),
writer: group.querySelector(this.opts.writer_selector)
};
}
_get_default_unit(group) {
var du = (group && group.getAttribute)
? group.getAttribute('data-wpbc_slider_len_default_unit')
: '';
return du ? String(du) : this.opts.default_unit;
}
_get_bounds_map(group) {
if (!group) return null;
if (this._bounds_cache.has(group)) {
return this._bounds_cache.get(group);
}
var raw = group.getAttribute('data-wpbc_slider_len_bounds_map');
var map = raw ? safe_json_parse(raw) : null;
if (!map || typeof map !== 'object') map = null;
this._bounds_cache.set(group, map);
return map;
}
_get_bounds_for_unit(group, unit) {
var map = this._get_bounds_map(group);
if (map && unit && map[unit]) {
return map[unit];
}
return this.opts.fallback_bounds[unit] || this.opts.fallback_bounds['px'];
}
_apply_bounds(parts, bounds) {
if (!parts || !bounds) return;
var min = (bounds.min != null) ? Number(bounds.min) : null;
var max = (bounds.max != null) ? Number(bounds.max) : null;
var step = (bounds.step != null) ? Number(bounds.step) : null;
if (parts.range) {
if (!isNaN(min)) parts.range.min = String(min);
if (!isNaN(max)) parts.range.max = String(max);
if (!isNaN(step)) parts.range.step = String(step);
}
if (parts.num) {
if (!isNaN(min)) parts.num.min = String(min);
if (!isNaN(max)) parts.num.max = String(max);
if (!isNaN(step)) parts.num.step = String(step);
}
}
_apply_bounds_for_current_unit(group) {
var parts = this._get_parts(group);
if (!parts || !parts.unit) return;
var unit = parts.unit.value || this._get_default_unit(group);
var b = this._get_bounds_for_unit(group, unit);
this._apply_bounds(parts, b);
// Clamp current value to new bounds.
var v = parse_float(parts.num && parts.num.value ? parts.num.value : (parts.range ? parts.range.value : ''));
if (v == null) return;
var min = (b && b.min != null) ? Number(b.min) : null;
var max = (b && b.max != null) ? Number(b.max) : null;
v = clamp_num(v, isNaN(min) ? null : min, isNaN(max) ? null : max);
if (parts.num) parts.num.value = String(v);
if (parts.range) parts.range.value = String(v);
this._write_combined(parts, String(v), unit, /*emit*/ false);
}
_write_combined(parts, num, unit, emit) {
if (!parts) return;
var combined = build_combined(num, unit);
if (parts.writer) {
// Avoid recursion: mark as internal write.
parts.writer.__wpbc_slider_len_internal = true;
parts.writer.value = combined;
if (emit) emit_input(parts.writer);
parts.writer.__wpbc_slider_len_internal = false;
} else if (parts.num) {
// If writer is missing, at least notify via number input.
if (emit) emit_input(parts.num);
}
}
_sync_group_from_writer(group) {
var parts = this._get_parts(group);
if (!parts || !parts.writer) return;
var raw = String(parts.writer.value || '').trim();
if (!raw) return;
var du = this._get_default_unit(group);
var p = parse_len_combined(raw, du);
if (parts.unit) parts.unit.value = p.unit;
if (parts.num) parts.num.value = p.num;
if (parts.range) parts.range.value = p.num;
}
_on_input(ev) {
var t = ev.target;
if (!t) return;
var group = this._find_group(t);
if (!group) return;
var parts = this._get_parts(group);
if (!parts) return;
// Writer changed externally -> update UI.
if (parts.writer && t === parts.writer) {
if (t.__wpbc_slider_len_internal) return;
this._sync_group_from_writer(group);
this._apply_bounds_for_current_unit(group);
return;
}
// Slider moved -> update number + writer.
if (t.matches && t.matches(this.opts.range_selector)) {
if (parts.num) parts.num.value = t.value;
var unit = (parts.unit && parts.unit.value) ? parts.unit.value : this._get_default_unit(group);
this._write_combined(parts, t.value, unit, /*emit*/ true);
return;
}
// Number typed -> update slider + writer (clamp if slider has bounds).
if (t.matches && t.matches(this.opts.value_selector)) {
var v = parse_float(t.value);
if (v != null && parts.range) {
var rmin = Number(parts.range.min);
var rmax = Number(parts.range.max);
v = clamp_num(v, isNaN(rmin) ? null : rmin, isNaN(rmax) ? null : rmax);
parts.range.value = String(v);
if (String(v) !== t.value) t.value = String(v);
}
var unit2 = (parts.unit && parts.unit.value) ? parts.unit.value : this._get_default_unit(group);
this._write_combined(parts, t.value, unit2, /*emit*/ true);
}
}
_on_change(ev) {
var t = ev.target;
if (!t) return;
var group = this._find_group(t);
if (!group) return;
var parts = this._get_parts(group);
if (!parts) return;
// Unit changed -> update bounds + writer.
if (t.matches && t.matches(this.opts.unit_selector)) {
this._apply_bounds_for_current_unit(group);
var num = parts.num ? parts.num.value : (parts.range ? parts.range.value : '');
var unit = t.value || this._get_default_unit(group);
this._write_combined(parts, num, unit, /*emit*/ true);
}
}
}
// -------------------------------------------------------------------------------------------------
// Auto-init
// -------------------------------------------------------------------------------------------------
function wpbc_slider_len_groups__auto_init() {
var ROOT = '.wpbc_slider_len_groups';
var nodes = Array.prototype.slice.call(d.querySelectorAll(ROOT))
.filter(function (n) { return !n.parentElement || !n.parentElement.closest(ROOT); });
// If no explicit containers, install a single document-root instance.
if (!nodes.length) {
if (!d.__wpbc_slider_len_groups_global_instance) {
d.__wpbc_slider_len_groups_global_instance = new WPBC_Slider_Len_Groups(d).init();
}
return;
}
nodes.forEach(function (node) {
if (node.__wpbc_slider_len_groups_instance) return;
node.__wpbc_slider_len_groups_instance = new WPBC_Slider_Len_Groups(node).init();
});
}
// Export globals (manual control if needed).
w.WPBC_Slider_Len_Groups = WPBC_Slider_Len_Groups;
w.WPBC_Slider_Len_AutoInit = wpbc_slider_len_groups__auto_init;
// DOM-ready auto init.
if (d.readyState === 'loading') {
d.addEventListener('DOMContentLoaded', wpbc_slider_len_groups__auto_init, { once: true });
} else {
wpbc_slider_len_groups__auto_init();
}
})(window, document);
/* globals window, document */
/**
* WPBC Slider Range Groups
*
* Universal, dependency-free controller that keeps a "range + number" pair in sync:
* - number input (data-wpbc_slider_range_value)
* - range slider (data-wpbc_slider_range_range)
* - writer input (data-wpbc_slider_range_writer) [optional]
*
* If writer exists: number/slider update writer and emit 'input' on writer (bubbles).
* If writer is missing: emits 'input' on the number input.
* If writer changes externally: updates number/slider.
*
* Markup expectations (minimal):
*
*
*
*
*
*
*
* Performance notes:
* - MutationObserver is DISABLED by default.
* - If your UI re-renders and inserts new groups dynamically, call:
* WPBC_Slider_Range_AutoInit(); OR instance.refresh();
* Or enable observer via: new WPBC_Slider_Range_Groups(root, { enable_observer:true }).init();
*
* Public API (instance methods):
* - init(), destroy(), refresh()
*
* @version 2026-01-25
* @since 2026-01-25
* @file ../includes/__js/admin/slider_groups/wpbc_range_groups.js
*/
(function (w, d) {
'use strict';
// -------------------------------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------------------------------
function clamp_num(v, min, max) {
if (typeof min === 'number' && !isNaN(min)) v = Math.max(min, v);
if (typeof max === 'number' && !isNaN(max)) v = Math.min(max, v);
return v;
}
function parse_float(v) {
var n = parseFloat(v);
return isNaN(n) ? null : n;
}
function emit_input(el) {
if (!el) return;
el.dispatchEvent(new Event('input', { bubbles: true }));
}
// -------------------------------------------------------------------------------------------------
// Controller
// -------------------------------------------------------------------------------------------------
class WPBC_Slider_Range_Groups {
/**
* @param {HTMLElement|string} root_el Container (or selector). If omitted, uses document.
* @param {Object} [opts={}]
*/
constructor(root_el, opts) {
this.root = root_el
? ((typeof root_el === 'string') ? d.querySelector(root_el) : root_el)
: d;
this.opts = Object.assign({
// Strict selectors (NO backward compatibility).
group_selector : '.wpbc_slider_range_group',
value_selector : '[data-wpbc_slider_range_value]',
range_selector : '[data-wpbc_slider_range_range]',
writer_selector : '[data-wpbc_slider_range_writer]',
// Disabled by default for performance.
enable_observer : false,
observer_debounce_ms: 150
}, opts || {});
this._on_input = this._on_input.bind(this);
this._on_change = this._on_change.bind(this);
this._observer = null;
this._refresh_tmr = null;
}
init() {
if (!this.root) return this;
this.root.addEventListener('input', this._on_input, true);
this.root.addEventListener('change', this._on_change, true);
if (this.opts.enable_observer && w.MutationObserver) {
this._observer = new MutationObserver(() => { this._debounced_refresh(); });
this._observer.observe(this.root === d ? d.documentElement : this.root, { childList: true, subtree: true });
}
this.refresh();
return this;
}
destroy() {
if (!this.root) return;
this.root.removeEventListener('input', this._on_input, true);
this.root.removeEventListener('change', this._on_change, true);
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
if (this._refresh_tmr) {
clearTimeout(this._refresh_tmr);
this._refresh_tmr = null;
}
}
refresh() {
if (!this.root) return;
var scope = (this.root === d ? d : this.root);
var groups = Array.prototype.slice.call(scope.querySelectorAll(this.opts.group_selector));
for (var i = 0; i < groups.length; i++) {
this._sync_from_writer(groups[i]);
this._clamp_to_range(groups[i]);
}
}
// -------------------------------------------------------------------------------------------------
// Internal
// -------------------------------------------------------------------------------------------------
_debounced_refresh() {
if (this._refresh_tmr) clearTimeout(this._refresh_tmr);
this._refresh_tmr = setTimeout(() => {
this._refresh_tmr = null;
this.refresh();
}, Number(this.opts.observer_debounce_ms) || 0);
}
_find_group(el) {
return (el && el.closest) ? el.closest(this.opts.group_selector) : null;
}
_get_parts(group) {
if (!group) return null;
return {
group : group,
num : group.querySelector(this.opts.value_selector),
range : group.querySelector(this.opts.range_selector),
writer: group.querySelector(this.opts.writer_selector)
};
}
_write(parts, value, emit) {
if (!parts) return;
if (parts.writer) {
parts.writer.__wpbc_slider_range_internal = true;
parts.writer.value = String(value);
if (emit) emit_input(parts.writer);
parts.writer.__wpbc_slider_range_internal = false;
} else if (parts.num) {
// If writer is missing, at least notify via number input.
if (emit) emit_input(parts.num);
}
}
_sync_from_writer(group) {
var parts = this._get_parts(group);
if (!parts || !parts.writer) return;
var raw = String(parts.writer.value || '').trim();
if (!raw) return;
if (parts.num) parts.num.value = raw;
if (parts.range) parts.range.value = raw;
}
_clamp_to_range(group) {
var parts = this._get_parts(group);
if (!parts || !parts.range || !parts.num) return;
var v = parse_float(parts.num.value);
if (v == null) return;
var min = Number(parts.range.min);
var max = Number(parts.range.max);
var vv = clamp_num(v, isNaN(min) ? null : min, isNaN(max) ? null : max);
if (String(vv) !== parts.num.value) parts.num.value = String(vv);
parts.range.value = String(vv);
}
_on_input(ev) {
var t = ev.target;
if (!t) return;
var group = this._find_group(t);
if (!group) return;
var parts = this._get_parts(group);
if (!parts) return;
// Writer changed externally -> update UI.
if (parts.writer && t === parts.writer) {
if (t.__wpbc_slider_range_internal) return;
this._sync_from_writer(group);
this._clamp_to_range(group);
return;
}
// Range moved -> update number + writer.
if (t.matches && t.matches(this.opts.range_selector)) {
if (parts.num) parts.num.value = t.value;
this._write(parts, t.value, /*emit*/ true);
return;
}
// Number typed -> update range + writer (clamp by slider bounds).
if (t.matches && t.matches(this.opts.value_selector)) {
if (parts.range) {
var v = parse_float(t.value);
if (v != null) {
var min = Number(parts.range.min);
var max = Number(parts.range.max);
v = clamp_num(v, isNaN(min) ? null : min, isNaN(max) ? null : max);
parts.range.value = String(v);
if (String(v) !== t.value) t.value = String(v);
}
}
this._write(parts, t.value, /*emit*/ true);
}
}
_on_change(ev) {
// No special "change" handling needed currently; kept for symmetry/future.
}
}
// -------------------------------------------------------------------------------------------------
// Auto-init
// -------------------------------------------------------------------------------------------------
function wpbc_slider_range_groups__auto_init() {
var ROOT = '.wpbc_slider_range_groups';
var nodes = Array.prototype.slice.call(d.querySelectorAll(ROOT))
.filter(function (n) { return !n.parentElement || !n.parentElement.closest(ROOT); });
if (!nodes.length) {
if (!d.__wpbc_slider_range_groups_global_instance) {
d.__wpbc_slider_range_groups_global_instance = new WPBC_Slider_Range_Groups(d).init();
}
return;
}
nodes.forEach(function (node) {
if (node.__wpbc_slider_range_groups_instance) return;
node.__wpbc_slider_range_groups_instance = new WPBC_Slider_Range_Groups(node).init();
});
}
// Export globals.
w.WPBC_Slider_Range_Groups = WPBC_Slider_Range_Groups;
w.WPBC_Slider_Range_AutoInit = wpbc_slider_range_groups__auto_init;
if (d.readyState === 'loading') {
d.addEventListener('DOMContentLoaded', wpbc_slider_range_groups__auto_init, { once: true });
} else {
wpbc_slider_range_groups__auto_init();
}
})(window, document);
/**
* Booking Calendar — Generic UI Tabs Utility (JS)
*
* Purpose: Lightweight, dependency-free tabs controller for any small tab group in admin UIs.
* - Auto-initializes groups marked with data-wpbc-tabs.
* - Assigns ARIA roles and toggles aria-selected/aria-hidden/tabindex.
* - Supports keyboard navigation (Left/Right/Home/End).
* - Public API: window.wpbc_ui_tabs.{init_on, init_group, set_active}
* - Emits 'wpbc:tabs:change' on the group root when the active tab changes.
*
* Markup contract:
* - Root: [data-wpbc-tabs]
* - Tabs: [data-wpbc-tab-key="K"]
* - Panels: [data-wpbc-tab-panel="K"]
*
* @package Booking Calendar
* @subpackage Admin\UI
* @since 11.0.0
* @version 1.0.0
* @see /includes/__js/admin/ui_tabs/ui_tabs.js
*
*
* How it works:
* - Root node must have [data-wpbc-tabs] attribute (any value).
* - Tab buttons must carry [data-wpbc-tab-key="..."] (unique per group).
* - Panels must carry [data-wpbc-tab-panel="..."] with matching keys.
* - Adds WAI-ARIA roles and aria-selected/hidden wiring.
*
*