| 1 |
/** |
| 2 |
* Contact Forms — Tab component for WordPress admin. |
| 3 |
* |
| 4 |
* Implements the W3C ARIA Tabs pattern with keyboard navigation. |
| 5 |
* Supports nesting (each instance scopes to its own direct tabs/panels). |
| 6 |
* |
| 7 |
* HTML structure: |
| 8 |
* <div class="accua-tabs" data-default-tab="first-tab-id"> |
| 9 |
* <div class="accua-tabs__tablist" role="tablist"> |
| 10 |
* <button role="tab" data-tab="tab-id">Label</button> |
| 11 |
* ... |
| 12 |
* </div> |
| 13 |
* <div role="tabpanel" data-tab="tab-id"> |
| 14 |
* ...content... |
| 15 |
* </div> |
| 16 |
* ... |
| 17 |
* </div> |
| 18 |
* |
| 19 |
* Usage: |
| 20 |
* $(selector).cfTabs(); |
| 21 |
* $(selector).cfTabs({ onSelect: function(tabId, $panel) { ... } }); |
| 22 |
*/ |
| 23 |
(function($) { |
| 24 |
'use strict'; |
| 25 |
|
| 26 |
$.fn.cfTabs = function(options) { |
| 27 |
var defaults = { |
| 28 |
onSelect: null |
| 29 |
}; |
| 30 |
var settings = $.extend({}, defaults, options); |
| 31 |
|
| 32 |
return this.each(function() { |
| 33 |
var $container = $(this); |
| 34 |
|
| 35 |
// Prevent double-init |
| 36 |
if ($container.hasClass('is-initialized')) { |
| 37 |
return; |
| 38 |
} |
| 39 |
|
| 40 |
// Scope: only direct tablist and panels (not nested ones) |
| 41 |
// Tablist may be a direct child or inside a header wrapper |
| 42 |
var $tablist = $container.children('[role="tablist"]'); |
| 43 |
if (!$tablist.length) { |
| 44 |
$tablist = $container.children('.accua-tabs__header').children('[role="tablist"]'); |
| 45 |
} |
| 46 |
var $tabs = $tablist.find('> [role="tab"]'); |
| 47 |
var $panels = $container.children('[role="tabpanel"]'); |
| 48 |
|
| 49 |
if (!$tabs.length || !$panels.length) { |
| 50 |
return; |
| 51 |
} |
| 52 |
|
| 53 |
// Wire up ARIA ids |
| 54 |
$tabs.each(function() { |
| 55 |
var $tab = $(this); |
| 56 |
var tabId = $tab.attr('data-tab'); |
| 57 |
var tabElId = $container.attr('id') + '-tab-' + tabId; |
| 58 |
var panelElId = $container.attr('id') + '-panel-' + tabId; |
| 59 |
$tab.attr('id', tabElId); |
| 60 |
$tab.attr('aria-controls', panelElId); |
| 61 |
$panels.filter('[data-tab="' + tabId + '"]') |
| 62 |
.attr('id', panelElId) |
| 63 |
.attr('aria-labelledby', tabElId); |
| 64 |
}); |
| 65 |
|
| 66 |
// Determine default tab |
| 67 |
var defaultTab = $container.attr('data-default-tab') || $tabs.first().attr('data-tab'); |
| 68 |
|
| 69 |
function selectTab(tabId, focus) { |
| 70 |
// Deselect all |
| 71 |
$tabs.attr('aria-selected', 'false').attr('tabindex', '-1'); |
| 72 |
$panels.attr('hidden', ''); |
| 73 |
|
| 74 |
// Select target |
| 75 |
var $activeTab = $tabs.filter('[data-tab="' + tabId + '"]'); |
| 76 |
var $activePanel = $panels.filter('[data-tab="' + tabId + '"]'); |
| 77 |
$activeTab.attr('aria-selected', 'true').attr('tabindex', '0'); |
| 78 |
$activePanel.removeAttr('hidden'); |
| 79 |
|
| 80 |
if (focus) { |
| 81 |
$activeTab.trigger('focus'); |
| 82 |
} |
| 83 |
|
| 84 |
if (typeof settings.onSelect === 'function') { |
| 85 |
settings.onSelect(tabId, $activePanel); |
| 86 |
} |
| 87 |
} |
| 88 |
|
| 89 |
// Click handler |
| 90 |
$tabs.on('click', function(e) { |
| 91 |
e.preventDefault(); |
| 92 |
selectTab($(this).attr('data-tab'), false); |
| 93 |
}); |
| 94 |
|
| 95 |
// Keyboard navigation (roving tabindex) |
| 96 |
$tabs.on('keydown', function(e) { |
| 97 |
var index = $tabs.index(this); |
| 98 |
var newIndex = null; |
| 99 |
|
| 100 |
switch (e.key) { |
| 101 |
case 'ArrowLeft': |
| 102 |
case 'ArrowUp': |
| 103 |
newIndex = index > 0 ? index - 1 : $tabs.length - 1; |
| 104 |
break; |
| 105 |
case 'ArrowRight': |
| 106 |
case 'ArrowDown': |
| 107 |
newIndex = index < $tabs.length - 1 ? index + 1 : 0; |
| 108 |
break; |
| 109 |
case 'Home': |
| 110 |
newIndex = 0; |
| 111 |
break; |
| 112 |
case 'End': |
| 113 |
newIndex = $tabs.length - 1; |
| 114 |
break; |
| 115 |
} |
| 116 |
|
| 117 |
if (newIndex !== null) { |
| 118 |
e.preventDefault(); |
| 119 |
selectTab($tabs.eq(newIndex).attr('data-tab'), true); |
| 120 |
} |
| 121 |
}); |
| 122 |
|
| 123 |
// Initialize: select default tab, mark container |
| 124 |
selectTab(defaultTab, false); |
| 125 |
$container.addClass('is-initialized'); |
| 126 |
}); |
| 127 |
}; |
| 128 |
|
| 129 |
// Form editor initialization |
| 130 |
$(document).ready(function() { |
| 131 |
var $tabs = $('#accua_tabs'); |
| 132 |
if (!$tabs.length) { |
| 133 |
return; |
| 134 |
} |
| 135 |
|
| 136 |
var $preview = $('#accua_form_preview_pane'); |
| 137 |
var $saveBtn = $('#accua_form_save_settings_top'); |
| 138 |
|
| 139 |
// Tabs that show the preview pane alongside |
| 140 |
var previewTabs = { fields: true, customise: true }; |
| 141 |
|
| 142 |
$tabs.find('.hidden').removeClass('hidden'); |
| 143 |
$tabs.cfTabs({ |
| 144 |
onSelect: function(tabId) { |
| 145 |
// Show/hide preview (no DOM movement — avoids iframe reload) |
| 146 |
if (previewTabs[tabId]) { |
| 147 |
$preview.removeAttr('hidden'); |
| 148 |
} else { |
| 149 |
$preview.attr('hidden', ''); |
| 150 |
} |
| 151 |
// Hide save button on tokens tab (read-only reference) |
| 152 |
if (tabId === 'tokens') { |
| 153 |
$saveBtn.hide(); |
| 154 |
} else { |
| 155 |
$saveBtn.show(); |
| 156 |
} |
| 157 |
} |
| 158 |
}); |
| 159 |
}); |
| 160 |
|
| 161 |
})(jQuery); |
| 162 |
|