PluginProbe
Booking Calendar / 11.2
Booking Calendar v11.2
11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 10.11 10.11.2 All 203 releases
booking / includes / __js / admin / ui_tabs / ui_tabs.js

ui_tabs.js in Booking Calendar 11.2, at includes/__js/admin/ui_tabs/ui_tabs.js

267 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Booking Calendar — Generic UI Tabs Utility (JS)
3 *
4 * Purpose: Lightweight, dependency-free tabs controller for any small tab group in admin UIs.
5 * - Auto-initializes groups marked with data-wpbc-tabs.
6 * - Assigns ARIA roles and toggles aria-selected/aria-hidden/tabindex.
7 * - Supports keyboard navigation (Left/Right/Home/End).
8 * - Public API: window.wpbc_ui_tabs.{init_on, init_group, set_active}
9 * - Emits 'wpbc:tabs:change' on the group root when the active tab changes.
10 *
11 * Markup contract:
12 * - Root: [data-wpbc-tabs]
13 * - Tabs: [data-wpbc-tab-key="K"]
14 * - Panels: [data-wpbc-tab-panel="K"]
15 *
16 * @package Booking Calendar
17 * @subpackage Admin\UI
18 * @since 11.0.0
19 * @version 1.0.0
20 * @see /includes/__js/admin/ui_tabs/ui_tabs.js
21 *
22 *
23 * How it works:
24 * - Root node must have [data-wpbc-tabs] attribute (any value).
25 * - Tab buttons must carry [data-wpbc-tab-key="..."] (unique per group).
26 * - Panels must carry [data-wpbc-tab-panel="..."] with matching keys.
27 * - Adds WAI-ARIA roles and aria-selected/hidden wiring.
28 *
29 * <div data-wpbc-tabs="column-styles" data-wpbc-tab-active="1" class="wpbc_ui_tabs_root" >
30 * <!-- Top Tabs -->
31 * <div data-wpbc-tablist="" role="tablist" class=" wpbc_ui_el__horis_top_bar__wrapper" >
32 * <div class="wpbc_ui_el__horis_top_bar__content">
33 * <h2 class="wpbc_ui_el__horis_nav_label">Column:</h2>
34 *
35 * <div class="wpbc_ui_el__horis_nav_item wpbc_ui_el__horis_nav_item__1">
36 * <a
37 * data-wpbc-tab-key="1"
38 * aria-selected="true" role="tab" tabindex="0" aria-controls="wpbc_tab_panel_col_1"
39 *
40 * href="javascript:void(0);"
41 * class="wpbc_ui_el__horis_nav_item__a wpbc_ui_el__horis_nav_item__single"
42 * id="wpbc_tab_col_1"
43 * title="Column 1"
44 * ><span class="wpbc_ui_el__horis_nav_title">Title 1</span></a>
45 * </div>
46 * ...
47 * </div>
48 * </div>
49 * <!-- Tabs Content -->
50 * <div class="wpbc_tab__panel group__fields" data-wpbc-tab-panel="1" id="wpbc_tab_panel_col_1" role="tabpanel" aria-labelledby="wpbc_tab_col_1">
51 * ...
52 * </div>
53 * ...
54 * </div>
55 *
56 * Public API:
57 * - wpbc_ui_tabs.init_on(root_or_selector) // find and init groups within a container
58 * - wpbc_ui_tabs.init_group(root_el) // init a single group root
59 * - wpbc_ui_tabs.set_active(root_el, key) // programmatically change active tab
60 *
61 * Events:
62 * - Dispatches CustomEvent 'wpbc:tabs:change' on root when tab changes:
63 * detail: { active_key: '2', prev_key: '1' }
64 *
65 * Switch a local (generic) tabs group to tab 3: var group = document.querySelector('[data-wpbc-tabs="column-styles"]'); if ( group ) { wpbc_ui_tabs.set_active(group, '3'); }
66 */
67 (function ( w ) {
68 'use strict';
69
70 if ( w.wpbc_ui_tabs ) {
71 return;
72 }
73
74 /**
75 * Internal: toggle active state.
76 *
77 * @param {HTMLElement} root_el
78 * @param {string} key
79 * @param {boolean} should_emit
80 */
81 function set_active_internal( root_el, key, should_emit ) {
82 var tab_btns = root_el.querySelectorAll( '[data-wpbc-tab-key]' );
83 var panels = root_el.querySelectorAll( '[data-wpbc-tab-panel]' );
84
85 var prev_key = root_el.getAttribute( 'data-wpbc-tab-active' ) || null;
86 if ( String( prev_key ) === String( key ) ) {
87 return;
88 }
89
90 // Buttons: aria + class
91 for ( var i = 0; i < tab_btns.length; i++ ) {
92 var btn = tab_btns[i];
93 var b_key = btn.getAttribute( 'data-wpbc-tab-key' );
94 var is_on = String( b_key ) === String( key );
95
96 btn.setAttribute( 'role', 'tab' );
97 btn.setAttribute( 'aria-selected', is_on ? 'true' : 'false' );
98 btn.setAttribute( 'tabindex', is_on ? '0' : '-1' );
99
100 if ( is_on ) {
101 btn.classList.add( 'active' );
102 } else {
103 btn.classList.remove( 'active' );
104 }
105 }
106
107 // Panels: aria + visibility
108 for ( var j = 0; j < panels.length; j++ ) {
109 var pn = panels[j];
110 var pkey = pn.getAttribute( 'data-wpbc-tab-panel' );
111 var show = String( pkey ) === String( key );
112
113 pn.setAttribute( 'role', 'tabpanel' );
114 pn.setAttribute( 'aria-hidden', show ? 'false' : 'true' );
115 if ( show ) {
116 pn.removeAttribute( 'hidden' );
117 } else {
118 pn.setAttribute( 'hidden', '' );
119 }
120 }
121
122 root_el.setAttribute( 'data-wpbc-tab-active', String( key ) );
123
124 if ( should_emit ) {
125 try {
126 var ev = new w.CustomEvent( 'wpbc:tabs:change', {
127 bubbles : true,
128 detail : { active_key : String( key ), prev_key : prev_key }
129 } );
130 root_el.dispatchEvent( ev );
131 } catch ( _e ) {}
132 }
133 }
134
135 /**
136 * Internal: get ordered keys from buttons.
137 *
138 * @param {HTMLElement} root_el
139 * @returns {string[]}
140 */
141 function get_keys( root_el ) {
142 var list = [];
143 var btns = root_el.querySelectorAll( '[data-wpbc-tab-key]' );
144 for ( var i = 0; i < btns.length; i++ ) {
145 var k = btns[i].getAttribute( 'data-wpbc-tab-key' );
146 if ( k != null && k !== '' ) {
147 list.push( String( k ) );
148 }
149 }
150 return list;
151 }
152
153 /**
154 * Internal: move focus between tabs using keyboard.
155 *
156 * @param {HTMLElement} root_el
157 * @param {number} dir +1 (next) / -1 (prev)
158 */
159 function focus_relative( root_el, dir ) {
160 var keys = get_keys( root_el );
161 var current = root_el.getAttribute( 'data-wpbc-tab-active' ) || keys[0] || null;
162 var idx = Math.max( 0, keys.indexOf( String( current ) ) );
163 var next = keys[ ( idx + ( dir > 0 ? 1 : keys.length - 1 ) ) % keys.length ];
164
165 var next_btn = root_el.querySelector( '[data-wpbc-tab-key="' + next + '"]' );
166 if ( next_btn ) {
167 next_btn.focus();
168 set_active_internal( root_el, next, true );
169 }
170 }
171
172 /**
173 * Initialize a single tabs group root.
174 *
175 * @param {HTMLElement} root_el
176 */
177 function init_group( root_el ) {
178 if ( ! root_el || root_el.__wpbc_tabs_inited ) {
179 return;
180 }
181 root_el.__wpbc_tabs_inited = true;
182
183 // Roles
184 var tablist = root_el.querySelector( '[data-wpbc-tablist]' ) || root_el;
185 tablist.setAttribute( 'role', 'tablist' );
186
187 // Default active: from attribute or first button
188 var keys = get_keys( root_el );
189 var def = root_el.getAttribute( 'data-wpbc-tab-active' ) || ( keys[0] || '1' );
190 set_active_internal( root_el, def, false );
191
192 // Clicks
193 root_el.addEventListener( 'click', function ( e ) {
194 var btn = e.target.closest ? e.target.closest( '[data-wpbc-tab-key]' ) : null;
195 if ( ! btn || ! root_el.contains( btn ) ) {
196 return;
197 }
198 e.preventDefault();
199 var key = btn.getAttribute( 'data-wpbc-tab-key' );
200 if ( key != null ) {
201 set_active_internal( root_el, key, true );
202 }
203 }, true );
204
205 // Keyboard (Left/Right/Home/End)
206 root_el.addEventListener( 'keydown', function ( e ) {
207 var tgt = e.target;
208 if ( ! tgt || ! tgt.hasAttribute || ! tgt.hasAttribute( 'data-wpbc-tab-key' ) ) {
209 return;
210 }
211 switch ( e.key ) {
212 case 'ArrowLeft':
213 e.preventDefault(); focus_relative( root_el, -1 ); break;
214 case 'ArrowRight':
215 e.preventDefault(); focus_relative( root_el, +1 ); break;
216 case 'Home':
217 e.preventDefault(); set_active_internal( root_el, ( get_keys( root_el )[0] || '1' ), true ); break;
218 case 'End':
219 e.preventDefault(); var ks = get_keys( root_el ); set_active_internal( root_el, ( ks[ ks.length - 1 ] || '1' ), true ); break;
220 }
221 }, true );
222 }
223
224 /**
225 * Initialize all groups within a container (or document).
226 *
227 * @param {HTMLElement|string|null} container
228 */
229 function init_on( container ) {
230 var ctx = container ? ( typeof container === 'string' ? document.querySelector( container ) : container ) : document;
231 if ( ! ctx ) {
232 return;
233 }
234 var groups = ctx.querySelectorAll( '[data-wpbc-tabs]' );
235 for ( var i = 0; i < groups.length; i++ ) {
236 init_group( groups[i] );
237 }
238 }
239
240 /**
241 * Programmatically set active tab by key.
242 *
243 * @param {HTMLElement} root_el
244 * @param {string|number} key
245 */
246 function set_active( root_el, key ) {
247 if ( root_el && root_el.hasAttribute && root_el.hasAttribute( 'data-wpbc-tabs' ) ) {
248 set_active_internal( root_el, String( key ), true );
249 }
250 }
251
252 // Public API (snake_case)
253 w.wpbc_ui_tabs = {
254 init_on : init_on,
255 init_group : init_group,
256 set_active : set_active
257 };
258
259 // Auto-init on DOM ready
260 if ( document.readyState === 'loading' ) {
261 document.addEventListener( 'DOMContentLoaded', function () { init_on( document ); } );
262 } else {
263 init_on( document );
264 }
265
266 })( window );
267