← All changes
|
includes/admin/settings/js/settings-admin-scripts.js
+320
-16
4.3.1
→
4.5.2
View file →
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | jQuery(document).ready(function ($) { |
| 2 | 2 | |
| 3 | - const prefix = WZSettingsAdmin.prefix || 'wz'; | |
| 3 | + const prefix = WebberSettingsAdmin.prefix || 'wz'; | |
| 4 | 4 | |
| 5 | 5 | // File browser. |
| 6 | 6 | $('.file-browser').on('click', function (event) { |
| 7 | 7 | event.preventDefault(); |
| @@ -33,12 +33,264 @@ | ||
| 33 | 33 | }, |
| 34 | 34 | activate: function (event, ui) { |
| 35 | 35 | $(ui.oldTab.find("a")).removeClass("nav-tab-active"); |
| 36 | 36 | $(ui.newTab.find("a")).addClass("nav-tab-active"); |
| 37 | + window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| 37 | 38 | } |
| 38 | 39 | }); |
| 39 | 40 | }); |
| 40 | 41 | |
| 42 | + // Settings search: live-filters setting rows across all tabs. | |
| 43 | + (function () { | |
| 44 | + var $container = $('#post-body-content'); | |
| 45 | + var $searchInput = $('#' + prefix + '-settings-search'); | |
| 46 | + var $form = $('#' + prefix + '-settings-form'); | |
| 47 | + | |
| 48 | + if (!$searchInput.length || !$form.length || !$container.length) { | |
| 49 | + return; | |
| 50 | + } | |
| 51 | + | |
| 52 | + var $panels = $form.children('div[id]'); | |
| 53 | + var $navLinks = $container.find('> ul.nav-tab-wrapper a.nav-tab'); | |
| 54 | + var $clearButton = $container.find('.wz-settings-search-clear'); | |
| 55 | + var $status = $container.find('.wz-settings-search-status'); | |
| 56 | + var strings = (typeof WebberSettingsAdmin !== 'undefined' && WebberSettingsAdmin.strings) || {}; | |
| 57 | + var $noResults = null; | |
| 58 | + var $actionsBar = null; | |
| 59 | + var indexBuilt = false; | |
| 60 | + var debounceTimer = null; | |
| 61 | + | |
| 62 | + function getRows($panel) { | |
| 63 | + return $panel.children('table.form-table').find('> tbody > tr, > tr'); | |
| 64 | + } | |
| 65 | + | |
| 66 | + // Build a searchable text string per row once, on first search. | |
| 67 | + // Strips noise (select options, dropdown widgets, code editors) and adds input ids/names. | |
| 68 | + function buildIndex() { | |
| 69 | + $panels.each(function () { | |
| 70 | + getRows($(this)).each(function () { | |
| 71 | + var $row = $(this); | |
| 72 | + var $clone = $row.clone(); | |
| 73 | + $clone.find('option, script, style, template, .ts-dropdown, .CodeMirror').remove(); | |
| 74 | + var text = $clone.text() + ' '; | |
| 75 | + $row.find(':input[id], :input[name]').each(function () { | |
| 76 | + text += ' ' + (this.id || '') + ' ' + (this.name || ''); | |
| 77 | + }); | |
| 78 | + $row.data('wzSearchText', text.toLowerCase()); | |
| 79 | + }); | |
| 80 | + }); | |
| 81 | + indexBuilt = true; | |
| 82 | + } | |
| 83 | + | |
| 84 | + function clearHighlights() { | |
| 85 | + $form.find('mark.wz-search-mark').each(function () { | |
| 86 | + var parent = this.parentNode; | |
| 87 | + $(this).replaceWith(document.createTextNode($(this).text())); | |
| 88 | + parent.normalize(); | |
| 89 | + }); | |
| 90 | + } | |
| 91 | + | |
| 92 | + // Elements the highlighter must never descend into. | |
| 93 | + var highlightSkip = 'input, textarea, select, button, script, style, template, mark, .CodeMirror, .ts-wrapper, .wp-picker-container, .wz-repeater-wrapper'; | |
| 94 | + | |
| 95 | + // Wrap the first occurrence of query in each matching text node with <mark>. | |
| 96 | + function highlightTerm($el, query) { | |
| 97 | + $el.contents().each(function () { | |
| 98 | + if (3 === this.nodeType) { | |
| 99 | + var text = this.nodeValue; | |
| 100 | + var idx = text.toLowerCase().indexOf(query); | |
| 101 | + if (-1 !== idx) { | |
| 102 | + var matched = text.slice(idx, idx + query.length); | |
| 103 | + var afterNode = document.createTextNode(text.slice(idx + query.length)); | |
| 104 | + var mark = $('<mark class="wz-search-mark"></mark>').text(matched); | |
| 105 | + this.nodeValue = text.slice(0, idx); | |
| 106 | + $(this).after(afterNode).after(mark); | |
| 107 | + } | |
| 108 | + } else if (1 === this.nodeType && !$(this).is(highlightSkip)) { | |
| 109 | + highlightTerm($(this), query); | |
| 110 | + } | |
| 111 | + }); | |
| 112 | + } | |
| 113 | + | |
| 114 | + function updateTabBadge(panelId, count) { | |
| 115 | + var $link = $navLinks.filter('[href="#' + panelId + '"]'); | |
| 116 | + if (!$link.length) { | |
| 117 | + return; | |
| 118 | + } | |
| 119 | + var $badge = $link.find('.wz-tab-count'); | |
| 120 | + if (0 === count) { | |
| 121 | + $badge.remove(); | |
| 122 | + } else { | |
| 123 | + if (!$badge.length) { | |
| 124 | + $badge = $('<span class="wz-tab-count"><span class="wz-tab-count-number"></span><span class="screen-reader-text"></span></span>').appendTo($link); | |
| 125 | + $badge.find('.screen-reader-text').text(' ' + (strings.search_matches_label || 'matching settings')); | |
| 126 | + } | |
| 127 | + $badge.find('.wz-tab-count-number').text(count); | |
| 128 | + } | |
| 129 | + $link.toggleClass('wz-tab-no-matches', 0 === count); | |
| 130 | + } | |
| 131 | + | |
| 132 | + // Announce the result count to screen readers via the polite live region. | |
| 133 | + function announceResults(total) { | |
| 134 | + var template; | |
| 135 | + if (0 === total) { | |
| 136 | + template = strings.search_no_results || 'No settings found.'; | |
| 137 | + } else if (1 === total) { | |
| 138 | + template = strings.search_results_single || '%d setting found.'; | |
| 139 | + } else { | |
| 140 | + template = strings.search_results_plural || '%d settings found.'; | |
| 141 | + } | |
| 142 | + $status.text(template.replace('%d', total)); | |
| 143 | + } | |
| 144 | + | |
| 145 | + // Single Save Changes bar shown while searching, replacing the hidden per-tab button rows. | |
| 146 | + function toggleActionsBar(show) { | |
| 147 | + if (show && !$actionsBar) { | |
| 148 | + var $save = $form.find('input[type="submit"][name="submit"]').first().clone().removeAttr('id'); | |
| 149 | + if (!$save.length) { | |
| 150 | + return; | |
| 151 | + } | |
| 152 | + $actionsBar = $('<p class="wz-search-actions"></p>').append($save).appendTo($form); | |
| 153 | + } | |
| 154 | + if ($actionsBar) { | |
| 155 | + $actionsBar.toggle(show); | |
| 156 | + } | |
| 157 | + } | |
| 158 | + | |
| 159 | + function toggleNoResults(show) { | |
| 160 | + if (show && !$noResults) { | |
| 161 | + $noResults = $('<p class="wz-search-no-results"></p>') | |
| 162 | + .text(strings.search_no_results || 'No settings found.') | |
| 163 | + .appendTo($form); | |
| 164 | + } | |
| 165 | + if ($noResults) { | |
| 166 | + $noResults.toggle(show); | |
| 167 | + } | |
| 168 | + } | |
| 169 | + | |
| 170 | + function resetSearch() { | |
| 171 | + $container.removeClass('wz-searching'); | |
| 172 | + clearHighlights(); | |
| 173 | + $form.find('.wz-search-hidden').removeClass('wz-search-hidden'); | |
| 174 | + $form.find('.wz-search-match').removeClass('wz-search-match'); | |
| 175 | + $panels.removeClass('wz-has-matches'); | |
| 176 | + $navLinks.removeClass('wz-tab-no-matches').find('.wz-tab-count').remove(); | |
| 177 | + toggleNoResults(false); | |
| 178 | + toggleActionsBar(false); | |
| 179 | + $clearButton.prop('hidden', true); | |
| 180 | + $status.text(''); | |
| 181 | + } | |
| 182 | + | |
| 183 | + function applySearch() { | |
| 184 | + var query = $.trim($searchInput.val()).toLowerCase(); | |
| 185 | + | |
| 186 | + if (!query) { | |
| 187 | + resetSearch(); | |
| 188 | + return; | |
| 189 | + } | |
| 190 | + | |
| 191 | + if (!indexBuilt) { | |
| 192 | + buildIndex(); | |
| 193 | + } | |
| 194 | + | |
| 195 | + clearHighlights(); | |
| 196 | + $container.addClass('wz-searching'); | |
| 197 | + | |
| 198 | + var total = 0; | |
| 199 | + | |
| 200 | + $panels.each(function () { | |
| 201 | + var $panel = $(this); | |
| 202 | + var $rows = getRows($panel); | |
| 203 | + var count = 0; | |
| 204 | + | |
| 205 | + $rows.each(function () { | |
| 206 | + var $row = $(this); | |
| 207 | + var matched = -1 !== ($row.data('wzSearchText') || '').indexOf(query); | |
| 208 | + $row.toggleClass('wz-search-match', matched).toggleClass('wz-search-hidden', !matched); | |
| 209 | + if (matched) { | |
| 210 | + count++; | |
| 211 | + } | |
| 212 | + }); | |
| 213 | + | |
| 214 | + // Keep a section header visible when any row in its group matched. | |
| 215 | + $rows.filter('.wz-settings-header-row').each(function () { | |
| 216 | + var $header = $(this); | |
| 217 | + if ($header.hasClass('wz-search-match')) { | |
| 218 | + return; | |
| 219 | + } | |
| 220 | + var groupHasMatch = $header.nextUntil('.wz-settings-header-row').filter('.wz-search-match').length > 0; | |
| 221 | + if (groupHasMatch) { | |
| 222 | + $header.removeClass('wz-search-hidden'); | |
| 223 | + } | |
| 224 | + }); | |
| 225 | + | |
| 226 | + $panel.toggleClass('wz-has-matches', count > 0); | |
| 227 | + updateTabBadge($panel.attr('id'), count); | |
| 228 | + total += count; | |
| 229 | + }); | |
| 230 | + | |
| 231 | + // Highlight the matched term in the visible rows (labels and descriptions). | |
| 232 | + $form.find('tr.wz-search-match').children('th, td').each(function () { | |
| 233 | + highlightTerm($(this), query); | |
| 234 | + }); | |
| 235 | + | |
| 236 | + toggleNoResults(0 === total); | |
| 237 | + toggleActionsBar(total > 0); | |
| 238 | + $clearButton.prop('hidden', false); | |
| 239 | + announceResults(total); | |
| 240 | + } | |
| 241 | + | |
| 242 | + // 'search' also fires when the native clear (x) button is used. | |
| 243 | + $searchInput.on('input search', function () { | |
| 244 | + clearTimeout(debounceTimer); | |
| 245 | + debounceTimer = setTimeout(applySearch, 200); | |
| 246 | + }); | |
| 247 | + | |
| 248 | + $searchInput.on('keydown', function (e) { | |
| 249 | + if ('Escape' === e.key) { | |
| 250 | + $(this).val(''); | |
| 251 | + applySearch(); | |
| 252 | + } | |
| 253 | + }); | |
| 254 | + | |
| 255 | + // Clear button: reset the search and return focus to the input. | |
| 256 | + $clearButton.on('click', function () { | |
| 257 | + $searchInput.val(''); | |
| 258 | + applySearch(); | |
| 259 | + $searchInput.trigger('focus'); | |
| 260 | + }); | |
| 261 | + | |
| 262 | + // While searching, tab clicks scroll to that tab's results instead of switching tabs. | |
| 263 | + // Capture-phase listener so it runs before, and blocks, the jQuery UI Tabs handlers. | |
| 264 | + var navWrapper = $container.find('> ul.nav-tab-wrapper').get(0); | |
| 265 | + if (navWrapper) { | |
| 266 | + navWrapper.addEventListener( | |
| 267 | + 'click', | |
| 268 | + function (e) { | |
| 269 | + if (!$container.hasClass('wz-searching')) { | |
| 270 | + return; | |
| 271 | + } | |
| 272 | + var link = e.target.closest ? e.target.closest('a.nav-tab') : null; | |
| 273 | + if (!link) { | |
| 274 | + return; | |
| 275 | + } | |
| 276 | + e.preventDefault(); | |
| 277 | + e.stopPropagation(); | |
| 278 | + var $panel = $(link.getAttribute('href')); | |
| 279 | + if ($panel.hasClass('wz-has-matches')) { | |
| 280 | + // Move focus to the section heading for keyboard/screen-reader users. | |
| 281 | + var title = $panel.find('.wz-section-title').get(0); | |
| 282 | + if (title) { | |
| 283 | + title.focus({ preventScroll: true }); | |
| 284 | + } | |
| 285 | + window.scrollTo({ top: $panel.offset().top - 80, behavior: 'smooth' }); | |
| 286 | + } | |
| 287 | + }, | |
| 288 | + true | |
| 289 | + ); | |
| 290 | + } | |
| 291 | + })(); | |
| 292 | + | |
| 41 | 293 | // Initialize ColorPicker. |
| 42 | 294 | $('.color-field').each(function (i, element) { |
| 43 | 295 | $(element).wpColorPicker(); |
| 44 | 296 | }); |
| @@ -44,13 +296,13 @@ | ||
| 44 | 296 | }); |
| 45 | 297 | |
| 46 | 298 | // Reset default thumbnail - uses plugin-specific localized data. |
| 47 | 299 | $('.reset-default-thumb').on('click', function () { |
| 48 | - var settingsKey = WZSettingsAdmin.settings_key || ''; | |
| 49 | - var thumbDefault = (typeof window[WZSettingsAdmin.prefix + '_admin'] !== 'undefined') | |
| 50 | - ? window[WZSettingsAdmin.prefix + '_admin'].thumb_default | |
| 300 | + var settingsKey = WebberSettingsAdmin.settings_key || ''; | |
| 301 | + var thumbDefault = (typeof window[WebberSettingsAdmin.prefix + '_admin'] !== 'undefined') | |
| 302 | + ? window[WebberSettingsAdmin.prefix + '_admin'].thumb_default | |
| 51 | 303 | : ''; |
| 52 | - $('#' + settingsKey + '\\[thumb_default\\]').val(thumbDefault); | |
| 304 | + $('#' + settingsKey + '-thumb_default').val(thumbDefault); | |
| 53 | 305 | }); |
| 54 | 306 | |
| 55 | 307 | // Reset formmodified on submit. |
| 56 | 308 | $('#' + prefix + '-settings-form').on('submit', function () { |
| @@ -90,12 +342,16 @@ | ||
| 90 | 342 | template = template.replace(/{{ROW_ID}}/g, uniqueId); |
| 91 | 343 | itemsContainer.append(template); |
| 92 | 344 | index++; |
| 93 | 345 | var newItem = itemsContainer.find('.wz-repeater-item:last'); |
| 94 | - itemsContainer.find('.repeater-item-header:last .toggle-icon').text('\u25b2'); | |
| 95 | - itemsContainer.find('.repeater-item-content:last').css('display', 'block'); | |
| 96 | - if (window.WZInitTomSelect) { | |
| 97 | - window.WZInitTomSelect(newItem.get(0)); | |
| 346 | + itemsContainer.find('.repeater-item-header:last') | |
| 347 | + .attr('aria-expanded', 'true') | |
| 348 | + .find('.toggle-icon').text('\u25b2'); | |
| 349 | + itemsContainer.find('.repeater-item-content:last') | |
| 350 | + .attr('aria-hidden', 'false') | |
| 351 | + .css('display', 'block'); | |
| 352 | + if (window.WebberInitTomSelect) { | |
| 353 | + window.WebberInitTomSelect(newItem.get(0)); | |
| 98 | 354 | } |
| 99 | 355 | document.dispatchEvent(new CustomEvent('wz:repeater-item-added', { detail: { container: newItem.get(0) } })); |
| 100 | 356 | }); |
| 101 | 357 | |
| @@ -125,20 +381,68 @@ | ||
| 125 | 381 | } |
| 126 | 382 | }); |
| 127 | 383 | |
| 128 | 384 | // Toggle Accordion. |
| 129 | - wrapper.on('click', '.repeater-item-header', function () { | |
| 130 | - var $this = $(this); | |
| 131 | - var $toggleIcon = $this.find('.toggle-icon'); | |
| 132 | - var $content = $this.next('.repeater-item-content'); | |
| 133 | - if ($content.is(':visible')) { | |
| 134 | - $content.slideUp(); | |
| 385 | + wrapper.on('click', '.repeater-item-header', function (e) { | |
| 386 | + e.preventDefault(); | |
| 387 | + e.stopImmediatePropagation(); | |
| 388 | + var $button = $(this); | |
| 389 | + var $toggleIcon = $button.find('.toggle-icon'); | |
| 390 | + var $content = $button.next('.repeater-item-content'); | |
| 391 | + var isExpanded = 'true' === $button.attr('aria-expanded'); | |
| 392 | + if (isExpanded) { | |
| 393 | + $button.attr('aria-expanded', 'false'); | |
| 394 | + $content.attr('aria-hidden', 'true').stop(true, true).hide(); | |
| 135 | 395 | $toggleIcon.text('\u25bc'); |
| 136 | 396 | } else { |
| 137 | - $content.slideDown(); | |
| 397 | + $button.attr('aria-expanded', 'true'); | |
| 398 | + $content.attr('aria-hidden', 'false').stop(true, true).show(); | |
| 138 | 399 | $toggleIcon.text('\u25b2'); |
| 139 | 400 | } |
| 140 | 401 | }); |
| 402 | + | |
| 403 | + // Enforce unique selection across rows for the field named in data-unique-field. | |
| 404 | + var uniqueField = wrapper.data('unique-field') || ''; | |
| 405 | + function syncUniqueSelects() { | |
| 406 | + if (!uniqueField) { | |
| 407 | + return; | |
| 408 | + } | |
| 409 | + var $selects = itemsContainer.find('.wz-repeater-item select[name$="[fields][' + uniqueField + ']"]'); | |
| 410 | + var usedValues = {}; | |
| 411 | + $selects.each(function () { | |
| 412 | + var val = $(this).val(); | |
| 413 | + if (val) { | |
| 414 | + usedValues[val] = true; | |
| 415 | + } | |
| 416 | + }); | |
| 417 | + $selects.each(function () { | |
| 418 | + var $sel = $(this); | |
| 419 | + var ownVal = $sel.val(); | |
| 420 | + $sel.find('option').each(function () { | |
| 421 | + var optVal = $(this).val(); | |
| 422 | + if (!optVal) { | |
| 423 | + return; | |
| 424 | + } | |
| 425 | + $(this).prop('disabled', optVal !== ownVal && usedValues[optVal]); | |
| 426 | + }); | |
| 427 | + }); | |
| 428 | + } | |
| 429 | + | |
| 430 | + if (uniqueField) { | |
| 431 | + syncUniqueSelects(); | |
| 432 | + wrapper.on('change', '.wz-repeater-item select[name$="[fields][' + uniqueField + ']"]', function () { | |
| 433 | + syncUniqueSelects(); | |
| 434 | + }); | |
| 435 | + wrapper.on('click', '.remove-item', function () { | |
| 436 | + // Sync after DOM removal; removal handler fires before remove, so defer. | |
| 437 | + setTimeout(syncUniqueSelects, 0); | |
| 438 | + }); | |
| 439 | + document.addEventListener('wz:repeater-item-added', function (e) { | |
| 440 | + if (wrapper.get(0).contains(e.detail.container)) { | |
| 441 | + syncUniqueSelects(); | |
| 442 | + } | |
| 443 | + }); | |
| 444 | + } | |
| 141 | 445 | |
| 142 | 446 | // Live update repeater title when the specified field changes. |
| 143 | 447 | // Handles text inputs (input event), selects (change event, uses option text), |
| 144 | 448 | // and TomSelect-enhanced inputs (change event, uses displayed text or value). |