PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 4.0.0
WP 2FA – Two-factor authentication for WordPress v4.0.0
4.0.0 1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / includes / assets / js / settings-design-logic.js
wp-2fa / includes / assets / js Last commit date
customize-styles.js 5 days ago exclude-self-prompt.js 5 days ago intlTelInput-vanilla.js 5 days ago policies-hash-state.js 5 days ago save-policies-new.js 5 days ago save-settings-new.js 5 days ago settings-design-logic.js 5 days ago settings-hash-state.js 5 days ago test-email-new.js 5 days ago wizard-new.js 5 days ago
settings-design-logic.js
459 lines
1 /**
2 * WP 2FA – New Settings Design Logic
3 *
4 * Handles interactive behaviour for the redesigned policy settings page.
5 * Vanilla JS only — no jQuery, no transpiler.
6 *
7 * @package wp-2fa
8 * @since 3.2.0
9 */
10
11 ( function () {
12 'use strict';
13
14 /* ──────────────────────────────────────────────
15 * Constants
16 * ────────────────────────────────────────────── */
17
18 var ZERO_SETUP_EMAIL_DATA_ID = 'enable_0_setup_email';
19 var EMAIL_BACKUP_CHECKBOX_ID = 'enable-email-backup';
20 var SPECIFY_EMAIL_CHECKBOX_ID = 'specify-email_hotp';
21 var SORTABLE_ITEM_SELECTOR = '.wp2fa-sortable-item';
22 var DISABLED_CLASS = 'disabled';
23
24 /* ──────────────────────────────────────────────
25 * Helper: find the sortable item by data-id
26 * ────────────────────────────────────────────── */
27
28 /**
29 * Returns the <li> sortable item element matching the given data-id value.
30 *
31 * @param {string} dataId The value of the data-id attribute to find.
32 * @returns {HTMLElement|null}
33 */
34 function getSortableItemByDataId( dataId ) {
35 return document.querySelector( SORTABLE_ITEM_SELECTOR + '[data-id="' + dataId + '"]' );
36 }
37
38 /**
39 * Returns an array of all sortable item elements.
40 *
41 * @returns {HTMLElement[]}
42 */
43 function getAllSortableItems() {
44 return Array.prototype.slice.call(
45 document.querySelectorAll( SORTABLE_ITEM_SELECTOR )
46 );
47 }
48
49 /* ──────────────────────────────────────────────
50 * Helper: find a checkbox by its ID attribute
51 * ────────────────────────────────────────────── */
52
53 /**
54 * Finds a checkbox input whose ID ends with the given suffix.
55 * Settings_Builder may add a prefix to IDs, so we match the tail.
56 *
57 * @param {string} idSuffix The ID (or ending portion) to look for.
58 * @returns {HTMLInputElement|null}
59 */
60 function findCheckboxById( idSuffix ) {
61 // Try exact match first.
62 var el = document.getElementById( idSuffix );
63 if ( el ) {
64 return el;
65 }
66
67 // Fallback: find by attribute selector ending with the suffix.
68 var candidates = document.querySelectorAll( 'input[type="checkbox"][id$="' + idSuffix + '"]' );
69 if ( candidates.length > 0 ) {
70 return candidates[ 0 ];
71 }
72
73 return null;
74 }
75
76 /* ──────────────────────────────────────────────
77 * Disable / Enable helpers
78 * ────────────────────────────────────────────── */
79
80 /**
81 * Disables a sortable item: adds the disabled class,
82 * unchecks its checkbox, and sets it to disabled.
83 *
84 * @param {HTMLElement} item The <li> sortable item element.
85 */
86 function disableSortableItem( item ) {
87 item.classList.add( DISABLED_CLASS );
88 item.style.opacity = '0.5';
89 item.style.pointerEvents = 'none';
90
91 var checkbox = item.querySelector( 'input[type="checkbox"]' );
92 if ( checkbox ) {
93 checkbox.disabled = true;
94 checkbox.checked = false;
95 }
96 }
97
98 /**
99 * Enables a sortable item: removes the disabled class
100 * and re-enables its checkbox.
101 *
102 * @param {HTMLElement} item The <li> sortable item element.
103 */
104 function enableSortableItem( item ) {
105 item.classList.remove( DISABLED_CLASS );
106 item.style.opacity = '';
107 item.style.pointerEvents = '';
108
109 var checkbox = item.querySelector( 'input[type="checkbox"]' );
110 if ( checkbox ) {
111 checkbox.disabled = false;
112 }
113 }
114
115 /**
116 * Disables a standalone checkbox and its parent wrapper.
117 *
118 * @param {HTMLInputElement} checkbox The checkbox element.
119 */
120 function disableStandaloneCheckbox( checkbox ) {
121 checkbox.disabled = true;
122 checkbox.checked = false;
123
124 // Walk up to the .form-group wrapper and gray it out.
125 var wrapper = checkbox.closest( '.form-group' );
126 if ( wrapper ) {
127 wrapper.classList.add( DISABLED_CLASS );
128 wrapper.style.opacity = '0.5';
129 wrapper.style.pointerEvents = 'none';
130 }
131 }
132
133 /**
134 * Enables a standalone checkbox and its parent wrapper.
135 *
136 * @param {HTMLInputElement} checkbox The checkbox element.
137 */
138 function enableStandaloneCheckbox( checkbox ) {
139 checkbox.disabled = false;
140
141 var wrapper = checkbox.closest( '.form-group' );
142 if ( wrapper ) {
143 wrapper.classList.remove( DISABLED_CLASS );
144 wrapper.style.opacity = '';
145 wrapper.style.pointerEvents = '';
146 }
147 }
148
149 /* ──────────────────────────────────────────────
150 * Generic toggle-target handler
151 *
152 * Any checkbox (typically a toggle-checkbox) with
153 * a `data-toggle-target` attribute will show or
154 * hide the element matching that CSS selector.
155 * ────────────────────────────────────────────── */
156
157 /**
158 * Applies initial visibility and binds change events
159 * for all checkboxes carrying data-toggle-target.
160 */
161 function initToggleTargets() {
162 var toggles = document.querySelectorAll( 'input[type="checkbox"][data-toggle-target]' );
163
164 toggles.forEach( function ( checkbox ) {
165 var selector = checkbox.getAttribute( 'data-toggle-target' );
166 if ( ! selector ) {
167 return;
168 }
169
170 var target = document.querySelector( selector );
171 if ( ! target ) {
172 return;
173 }
174
175 function applyState() {
176 var show = checkbox.checked;
177 target.style.display = show ? '' : 'none';
178
179 // Disable / enable form controls so hidden fields are not collected.
180 var fields = target.querySelectorAll( 'input, select, textarea' );
181 fields.forEach( function ( field ) {
182 field.disabled = ! show;
183 } );
184 }
185
186 // Set initial state.
187 applyState();
188
189 // Bind change listener.
190 checkbox.addEventListener( 'change', applyState );
191 } );
192 }
193
194 /* ──────────────────────────────────────────────
195 * Generic radio toggle-target handler
196 *
197 * Any radio button with a `data-toggle-target`
198 * attribute will show the element matching that
199 * CSS selector when checked, and hide it when
200 * another radio in the same name group is selected.
201 * ────────────────────────────────────────────── */
202
203 /**
204 * Applies initial visibility and binds change events
205 * for all radio buttons carrying data-toggle-target.
206 */
207 function initRadioToggleTargets() {
208 var radios = document.querySelectorAll( 'input[type="radio"][data-toggle-target]' );
209
210 // Collect unique targets keyed by name group so we can manage show/hide.
211 var processed = {};
212
213 radios.forEach( function ( radio ) {
214 var selector = radio.getAttribute( 'data-toggle-target' );
215 var groupName = radio.getAttribute( 'name' );
216 if ( ! selector || ! groupName ) {
217 return;
218 }
219
220 var target = document.querySelector( selector );
221 if ( ! target ) {
222 return;
223 }
224
225 // Avoid duplicate binding per name group + selector.
226 var key = groupName + '|' + selector;
227 if ( processed[ key ] ) {
228 return;
229 }
230 processed[ key ] = true;
231
232 // Gather all radios in the same name group.
233 var groupRadios = document.querySelectorAll( 'input[type="radio"][name="' + groupName + '"]' );
234
235 function applyState() {
236 var show = radio.checked;
237 target.style.display = show ? '' : 'none';
238 }
239
240 // Set initial state.
241 applyState();
242
243 // Bind change listener on ALL radios in the group.
244 groupRadios.forEach( function ( groupRadio ) {
245 groupRadio.addEventListener( 'change', applyState );
246 } );
247 } );
248 }
249
250 /* ──────────────────────────────────────────────
251 * Email Backup → Specify Backup Email visibility
252 *
253 * The "specify-email-choice-wrap" element should
254 * only be visible when the "enable-email-backup"
255 * checkbox is checked.
256 * ────────────────────────────────────────────── */
257
258 var SPECIFY_BACKUP_EMAIL_WRAP_PREFIX = 'specify-email-choice-wrap';
259 var WIZARD_EMAIL_BACKUP_CHECKBOX_ID = 'wizard-email-backup';
260 var WIZARD_SPECIFY_WRAP_ID = 'wizard-specify-backup-email-wrap';
261
262 /**
263 * Shows or hides the specify-backup-email wrapper based on
264 * the enable-email-backup checkbox state within a given scope.
265 *
266 * @param {HTMLElement} scope The container to scope queries within.
267 */
268 function applyEmailBackupSpecifyVisibility( scope ) {
269 // Settings page context.
270 var emailBackupCheckbox = scope.querySelector( 'input[type="checkbox"][id$="' + EMAIL_BACKUP_CHECKBOX_ID + '"]' );
271 if ( emailBackupCheckbox ) {
272 var wrap = scope.querySelector( '[id^="' + SPECIFY_BACKUP_EMAIL_WRAP_PREFIX + '"]' );
273 if ( wrap ) {
274 var show = emailBackupCheckbox.checked && ! emailBackupCheckbox.disabled;
275 wrap.style.display = show ? '' : 'none';
276 }
277 }
278
279 // Wizard context.
280 var wizardCheckbox = scope.querySelector( '#' + WIZARD_EMAIL_BACKUP_CHECKBOX_ID );
281 if ( wizardCheckbox ) {
282 var wizardWrap = scope.querySelector( '#' + WIZARD_SPECIFY_WRAP_ID );
283 if ( wizardWrap ) {
284 var wizardShow = wizardCheckbox.checked && ! wizardCheckbox.disabled;
285 wizardWrap.style.display = wizardShow ? '' : 'none';
286 }
287 }
288 }
289
290 /**
291 * Binds change listeners on all enable-email-backup checkboxes.
292 */
293 function bindEmailBackupSpecifyListener() {
294 var checkboxes = document.querySelectorAll( 'input[type="checkbox"][id$="' + EMAIL_BACKUP_CHECKBOX_ID + '"]' );
295
296 checkboxes.forEach( function ( checkbox ) {
297 checkbox.addEventListener( 'change', function () {
298 var scope = checkbox.closest( '.tabs-wrap' ) || checkbox.closest( '.tab-panel' ) || document;
299 applyEmailBackupSpecifyVisibility( scope );
300 } );
301 } );
302
303 // Wizard context: bind on wizard-email-backup checkbox.
304 var wizardCheckbox = document.getElementById( WIZARD_EMAIL_BACKUP_CHECKBOX_ID );
305 if ( wizardCheckbox ) {
306 wizardCheckbox.addEventListener( 'change', function () {
307 applyEmailBackupSpecifyVisibility( document );
308 } );
309 }
310 }
311
312 /**
313 * Applies initial visibility for specify-backup-email wrappers.
314 */
315 function initEmailBackupSpecifyVisibility() {
316 var scopes = document.querySelectorAll( '.tabs-wrap' );
317 if ( scopes.length === 0 ) {
318 applyEmailBackupSpecifyVisibility( document );
319 return;
320 }
321 scopes.forEach( function ( scope ) {
322 applyEmailBackupSpecifyVisibility( scope );
323 } );
324 }
325
326 /* ──────────────────────────────────────────────
327 * Core logic: Zero Setup Email exclusivity
328 *
329 * When "Zero setup One-time code via email"
330 * (data-id="0_setup_email") is checked:
331 * - All OTHER sortable methods become disabled
332 * - "Allow users to use email based 2FA as
333 * secondary backup method" becomes disabled
334 * - "Allow user to specify the email address
335 * of choice" becomes disabled
336 *
337 * Scoped per parent .tabs-wrap so each role tab
338 * is handled independently.
339 * ────────────────────────────────────────────── */
340
341 /**
342 * Applies or removes the exclusive mode within a given scope element.
343 *
344 * @param {HTMLElement} scope The container to scope queries within.
345 */
346 function applyZeroSetupEmailExclusivityInScope( scope ) {
347 var zeroSetupItem = scope.querySelector( SORTABLE_ITEM_SELECTOR + '[data-id="' + ZERO_SETUP_EMAIL_DATA_ID + '"]' );
348 if ( ! zeroSetupItem ) {
349 return;
350 }
351
352 var zeroSetupCheckbox = zeroSetupItem.querySelector( 'input[type="checkbox"]' );
353 if ( ! zeroSetupCheckbox ) {
354 return;
355 }
356
357 var isChecked = zeroSetupCheckbox.checked;
358
359 // 1. Disable / enable all OTHER sortable methods within the same scope.
360 var allItems = Array.prototype.slice.call( scope.querySelectorAll( SORTABLE_ITEM_SELECTOR ) );
361 allItems.forEach( function ( item ) {
362 var itemDataId = item.getAttribute( 'data-id' );
363 if ( itemDataId === ZERO_SETUP_EMAIL_DATA_ID ) {
364 return;
365 }
366
367 if ( isChecked ) {
368 disableSortableItem( item );
369 } else {
370 enableSortableItem( item );
371 }
372 } );
373
374 // 2. Disable / enable "Allow users to use email based 2FA as secondary backup method".
375 var emailBackupCheckbox = scope.querySelector( 'input[type="checkbox"][id$="' + EMAIL_BACKUP_CHECKBOX_ID + '"]' );
376 if ( emailBackupCheckbox ) {
377 if ( isChecked ) {
378 disableStandaloneCheckbox( emailBackupCheckbox );
379 } else {
380 enableStandaloneCheckbox( emailBackupCheckbox );
381 }
382 }
383
384 // 3. Disable / enable "Allow user to specify the email address of choice".
385 var specifyEmailCheckbox = scope.querySelector( 'input[type="checkbox"][id$="' + SPECIFY_EMAIL_CHECKBOX_ID + '"]' );
386 if ( specifyEmailCheckbox ) {
387 if ( isChecked ) {
388 disableStandaloneCheckbox( specifyEmailCheckbox );
389 } else {
390 enableStandaloneCheckbox( specifyEmailCheckbox );
391 }
392 }
393
394 // 4. Re-evaluate specify-backup-email wrap visibility.
395 applyEmailBackupSpecifyVisibility( scope );
396 }
397
398 /**
399 * Legacy wrapper – applies exclusivity across all tabs.
400 */
401 function applyZeroSetupEmailExclusivity() {
402 var scopes = document.querySelectorAll( '.tabs-wrap' );
403 if ( scopes.length === 0 ) {
404 // Fallback to document if there are no tab wrappers.
405 applyZeroSetupEmailExclusivityInScope( document );
406 return;
407 }
408 scopes.forEach( function ( scope ) {
409 applyZeroSetupEmailExclusivityInScope( scope );
410 } );
411 }
412
413 /* ──────────────────────────────────────────────
414 * Event binding
415 * ────────────────────────────────────────────── */
416
417 /**
418 * Binds change listeners on all Zero Setup Email checkboxes (one per tab).
419 */
420 function bindZeroSetupEmailListener() {
421 var zeroSetupItems = document.querySelectorAll( SORTABLE_ITEM_SELECTOR + '[data-id="' + ZERO_SETUP_EMAIL_DATA_ID + '"]' );
422
423 zeroSetupItems.forEach( function ( item ) {
424 var checkbox = item.querySelector( 'input[type="checkbox"]' );
425 if ( ! checkbox ) {
426 return;
427 }
428
429 checkbox.addEventListener( 'change', function () {
430 var scope = checkbox.closest( '.tabs-wrap' ) || document;
431 applyZeroSetupEmailExclusivityInScope( scope );
432 } );
433 } );
434 }
435
436 /* ──────────────────────────────────────────────
437 * Initialisation
438 * ────────────────────────────────────────────── */
439
440 document.addEventListener( 'DOMContentLoaded', function () {
441 // Generic toggle-target handler (checkboxes).
442 initToggleTargets();
443
444 // Generic toggle-target handler (radio buttons).
445 initRadioToggleTargets();
446
447 // Bind the Zero Setup Email exclusivity logic.
448 bindZeroSetupEmailListener();
449
450 // Apply the initial state in case the checkbox is already checked on page load.
451 applyZeroSetupEmailExclusivity();
452
453 // Bind and apply the email-backup → specify-backup-email visibility.
454 bindEmailBackupSpecifyListener();
455 initEmailBackupSpecifyVisibility();
456 } );
457
458 } )();
459