PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
← All changes | js/admin/style.js +558 -264 6.5.2 → trunk View file →
@@ -2,13 +2,16 @@
2 2 * This script file handles style settings in the Lite plugin.
3 3 * Pro-specific features are in the style-settings.js file in Pro.
4 4 */
5 5 ( function() {
6 - /* globals wp, frmDom, frmAdminBuild */
7 6 'use strict';
8 7
9 - const { __ } = wp.i18n;
10 - const state = {
8 + if ( ! document.getElementById( 'frm_active_style_form' ) ) {
9 + return;
10 + }
11 +
12 + const { __, sprintf } = wp.i18n;
13 + const state = {
11 14 showingSampleForm: document.getElementById( 'frm_active_style_form' ).classList.contains( 'frm_hidden' ), // boolean
12 15 unsavedChanges: false, // boolean
13 16 autoId: 0, // Number
14 17 // Track the value of the selected style ID on page (on the list page).
@@ -16,11 +19,11 @@
16 19 // This way when you switch back to the initial value it doesn't count as a change.
17 20 initialSelectedStyleValue: false // String|false
18 21 };
19 22 const { div, span, a, labelledTextInput, tag, svg, success } = frmDom;
20 - const { onClickPreventDefault } = frmDom.util;
21 - const { maybeCreateModal, footerButton } = frmDom.modal;
22 - const { doJsonPost } = frmDom.ajax;
23 + const { onClickPreventDefault } = frmDom.util;
24 + const { maybeCreateModal, footerButton } = frmDom.modal;
25 + const { doJsonPost } = frmDom.ajax;
23 26
24 27 const isListPage = document.getElementsByClassName( 'frm-style-card' ).length > 0;
25 28 if ( isListPage ) {
26 29 initListPage();
@@ -33,9 +36,9 @@
33 36 /**
34 37 * These are shared events for both the edit/list views like the sample form toggle.
35 38 * This includes preview events, but also the update button click event handling for both views.
36 39 *
37 - * @returns {void}
40 + * @return {void}
38 41 */
39 42 function initCommonEventListeners() {
40 43 document.addEventListener( 'click', handleCommonClickEvents );
41 44 window.addEventListener( 'beforeunload', maybeConfirmExit );
@@ -44,9 +47,9 @@
44 47
45 48 /**
46 49 * Initialize common functions required for the preview in both the edit and list views.
47 50 *
48 - * @returns {void}
51 + * @return {void}
49 52 */
50 53 function initPreview() {
51 54 initFloatingLabels();
52 55 fillMissingSignatureValidationFunction();
@@ -55,15 +58,120 @@
55 58 // Remove .wp-core-ui from the body so the preview can avoid it.
56 59 // Then add it back where we want to use admin styles (the sidebar, otherwise inputs appear short).
57 60 document.body.classList.remove( 'wp-core-ui' );
58 61 document.getElementById( 'frm_style_sidebar' ).classList.add( 'wp-core-ui' );
62 +
63 + jQuery( document ).on( 'input change', 'input[data-frmrange]', initSliderPreview );
64 +
65 + initOptionLayoutPreview();
59 66 }
60 67
61 68 /**
69 + * Sync the radio/checkbox option layout (alignment) style settings to the preview form in real time.
70 + *
71 + * The layout is applied to the front end through a container class, so changing the style setting
72 + * does not re-render the preview form. This swaps the container class on preview fields that use the
73 + * style setting. The styler preview marks those fields with .frm-default-option-align, so fields with
74 + * their own alignment override are left untouched.
75 + *
76 + * @return {void}
77 + */
78 + function initOptionLayoutPreview() {
79 + bindOptionLayoutSelect( document.getElementById( 'frm_radio_align' ), '.frm_radio' );
80 + bindOptionLayoutSelect( document.getElementById( 'frm_check_align' ), '.frm_checkbox' );
81 + }
82 +
83 + /**
84 + * Listen for changes on an option layout style setting and update the preview to match.
85 + *
86 + * @param {HTMLSelectElement|null} select The style setting dropdown.
87 + * @param {string} optionSelector The single option selector ('.frm_radio' or '.frm_checkbox').
88 + * @return {void}
89 + */
90 + function bindOptionLayoutSelect( select, optionSelector ) {
91 + if ( ! select ) {
92 + return;
93 + }
94 +
95 + select.addEventListener( 'change', () => {
96 + updatePreviewOptionLayout( optionSelector, select.value );
97 + } );
98 + }
99 +
100 + /**
101 + * Replace the alignment container class on preview fields that are using the style setting.
102 + *
103 + * @param {string} optionSelector The single option selector ('.frm_radio' or '.frm_checkbox').
104 + * @param {string} newAlign The newly selected style alignment value.
105 + * @return {void}
106 + */
107 + function updatePreviewOptionLayout( optionSelector, newAlign ) {
108 + const newClass = optionLayoutAlignToClass( newAlign );
109 +
110 + if ( ! newClass ) {
111 + return;
112 + }
113 +
114 + const activeForm = document.getElementById( 'frm_active_style_form' );
115 + if ( ! activeForm ) {
116 + return;
117 + }
118 +
119 + const alignClasses = [ 'vertical_radio', 'horizontal_radio', 'frm_two_col', 'frm_three_col', 'frm_four_col' ];
120 + const containers = new Set();
121 +
122 + activeForm.querySelectorAll( optionSelector ).forEach( option => {
123 + const container = option.closest( '.frm_form_field' );
124 +
125 + // Only fields using the style setting (no override) are marked in the styler preview.
126 + if ( container?.classList.contains( 'frm-default-option-align' ) ) {
127 + containers.add( container );
128 + }
129 + } );
130 +
131 + containers.forEach( container => {
132 + container.classList.remove( ...alignClasses );
133 + container.classList.add( newClass );
134 + } );
135 + }
136 +
137 + /**
138 + * Map an option layout style value to its front-end container class.
139 + *
140 + * @param {string} align The style alignment value.
141 + * @return {string} The matching container class.
142 + */
143 + function optionLayoutAlignToClass( align ) {
144 + if ( 'inline' === align ) {
145 + return 'horizontal_radio';
146 + }
147 +
148 + if ( 'block' === align ) {
149 + return 'vertical_radio';
150 + }
151 +
152 + return align;
153 + }
154 +
155 + /**
156 + * Initialize the slider functionality in the style preview.
157 + *
158 + * @param {HTMLElement} event
159 + * @return {void}
160 + */
161 + function initSliderPreview( event ) {
162 + const wrapper = event.target.closest( '.frm_range_container' );
163 + if ( null === wrapper ) {
164 + return;
165 + }
166 + wrapper.querySelector( '.frm_range_value' ).innerHTML = parseInt( this.value, 10 );
167 + }
168 +
169 + /**
62 170 * Add the wp-core-ui class to the #wp-auth-check-wrap element.
63 171 * As this style isn't included on the body for the styler, the close button on the auth modal wasn't getting styled properly.
64 172 *
65 - * @returns {void}
173 + * @return {void}
66 174 */
67 175 function fixWpAuthModal() {
68 176 const authWrap = document.getElementById( 'wp-auth-check-wrap' );
69 177 if ( authWrap ) {
@@ -71,17 +179,18 @@
71 179 }
72 180 }
73 181
74 182 /**
75 - * @returns {void}
183 + * @return {void}
76 184 */
77 185 function initListPage() {
78 186 document.addEventListener( 'click', handleClickEventsForListPage );
79 - setTimeout( addHamburgerMenusToCards, 0 ); // Add a timeout so Pro has a chance to add a filter first.
187 + // The dropdown options are added on the first open, so there is no longer a filter from Pro to wait for here.
188 + addHamburgerMenusToCards();
80 189 initDatepickerSample();
81 190
82 - const enableToggle = document.getElementById( 'frm_enable_styling' );
83 - const styleIdInput = getStyleIdInput();
191 + const enableToggle = document.getElementById( 'frm_enable_styling' );
192 + const styleIdInput = getStyleIdInput();
84 193 state.initialSelectedStyleValue = styleIdInput.value;
85 194
86 195 enableToggle.addEventListener( 'change', handleEnableStylingToggleChange );
87 196
@@ -102,16 +211,16 @@
102 211
103 212 /**
104 213 * Handle pagination click events.
105 214 *
106 - * @returns {void}
215 + * @return {void}
107 216 */
108 217 function initStyleCardPagination() {
109 218 document.querySelectorAll( '.frm-style-card-pagination' ).forEach(
110 219 pagination => {
111 - const wrapper = pagination.closest( '.frm-style-card-wrapper' );
220 + const wrapper = pagination.closest( '.frm-style-card-wrapper' );
112 221 const showAllAnchor = pagination.querySelector( '.frm-show-all-styles' );
113 - let showingAll = false;
222 + let showingAll = false;
114 223
115 224 onClickPreventDefault(
116 225 showAllAnchor,
117 226 () => {
@@ -127,10 +236,11 @@
127 236
128 237 wrapper.querySelectorAll( '.frm-style-card:nth-child(3) ~ .frm-style-card' ).forEach(
129 238 card => card.classList.add( 'frm_hidden' )
130 239 );
131 - const hiddenCount = wrapper.querySelectorAll( '.frm-style-card.frm_hidden' ).length;
132 - showAllAnchor.textContent = __( 'Show all (%d)', 'formidable' ).replace( '%d', hiddenCount );
240 + const hiddenCount = wrapper.querySelectorAll( '.frm-style-card.frm_hidden' ).length;
241 + /* translators: %d: The number of hidden items to show. */
242 + showAllAnchor.textContent = sprintf( __( 'Show all (%d)', 'formidable' ), hiddenCount );
133 243 }
134 244 );
135 245 }
136 246 );
@@ -136,10 +246,10 @@
136 246 );
137 247 }
138 248
139 249 /**
140 - * @param {String} labelPosition
141 - * @returns {void}
250 + * @param {string} labelPosition
251 + * @return {void} Changes the label position in the preview.
142 252 */
143 253 function changeLabelPositionsInPreview( labelPosition ) {
144 254 const input = tag( 'input' );
145 255 input.value = labelPosition;
@@ -146,9 +256,9 @@
146 256 setPosClass.bind( input )();
147 257 }
148 258
149 259 /**
150 - * @returns {HTMLElement}
260 + * @return {HTMLElement} The active style card.
151 261 */
152 262 function getActiveCard() {
153 263 return document.querySelector( '.frm-active-style-card' );
154 264 }
@@ -158,9 +268,9 @@
158 268 * The style ID value associated with the selected style card gets cleared.
159 269 * This is because disabling styles is linked to the custom_style option as well.
160 270 *
161 271 * @param {Event} event
162 - * @returns {void}
272 + * @return {void}
163 273 */
164 274 function handleEnableStylingToggleChange( event ) {
165 275 const stylesEnabled = event.target.checked;
166 276
@@ -192,21 +302,21 @@
192 302 * All settings on the list page are mapped to the value of the styleIdInput.
193 303 * We track the value on load with state.initialSelectedStyleValue.
194 304 * Only consider unsaved changes on the page when this variable is no longer set to the original value.
195 305 *
196 - * @returns {void}
306 + * @return {void}
197 307 */
198 308 function trackListPageChange() {
199 - const styleIdInput = getStyleIdInput();
309 + const styleIdInput = getStyleIdInput();
200 310 state.unsavedChanges = styleIdInput.value !== state.initialSelectedStyleValue;
201 311 }
202 312
203 313 /**
204 314 * @param {boolean} on
205 - * @returns {void}
315 + * @return {void}
206 316 */
207 317 function toggleFormidableStylingInPreviewForms( on ) {
208 - const preview = document.getElementById( 'frm_style_preview' );
318 + const preview = document.getElementById( 'frm_style_preview' );
209 319 const activeCard = getActiveCard();
210 320
211 321 let selector = '.frm_forms';
212 322 if ( ! on ) {
@@ -221,9 +331,9 @@
221 331 );
222 332 }
223 333
224 334 /**
225 - * @returns {HTMLElement}
335 + * @return {HTMLElement} The style ID input element.
226 336 */
227 337 function getStyleIdInput() {
228 338 return document.getElementById( 'frm_style_list_form' ).querySelector( '[name="style_id"]' );
229 339 }
@@ -229,12 +339,12 @@
229 339 }
230 340
231 341 /**
232 342 * @param {Event} event
233 - * @returns {void}
343 + * @return {void}
234 344 */
235 345 function handleCommonClickEvents( event ) {
236 - const target = event.target;
346 + const { target } = event;
237 347
238 348 if ( 'frm_toggle_sample_form' === target.id || target.closest( '#frm_toggle_sample_form' ) ) {
239 349 toggleSampleForm();
240 350 return;
@@ -239,9 +349,10 @@
239 349 toggleSampleForm();
240 350 return;
241 351 }
242 352
243 - if ( 'frm_submit_side_top' === target.id || target.closest( '#frm_submit_side_top' ) ) {
353 + if ( 'frm_submit_side_top' === target.id || target.closest( '#frm_submit_side_top' ) || 'frm-style-advanced-settings-button' === target.id || target.closest( 'a#frm_style_back_to_quick_settings' ) ) {
354 + switchAdvancedSettingsFormAction( target );
244 355 handleUpdateClick();
245 356 return;
246 357 }
247 358
@@ -246,14 +357,33 @@
246 357 }
247 358
248 359 if ( target.classList.contains( 'frm-edit-style' ) || null !== target.closest( '.frm-edit-style' ) || 'frm_edit_style' === target.id ) {
249 360 modifyStylerUrl( target );
361 + }
362 + }
363 +
364 + /**
365 + * This function is used to update the form action when switching from the advanced settings and quick-settings.
366 + *
367 + * @param {Object} target The submit button event target
368 + * @return {void}
369 + */
370 + function switchAdvancedSettingsFormAction( target ) {
371 + const form = document.querySelector( '#frm_styling_form' );
372 + if ( ! form ) {
250 373 return;
251 374 }
375 + if ( target.closest( 'a#frm_style_back_to_quick_settings' ) ) {
376 + form.action = form.action.replace( '&section=advanced-settings', '' );
377 + return;
378 + }
379 + if ( 'frm-style-advanced-settings-button' === target.id ) {
380 + form.action += '&section=advanced-settings';
381 + }
252 382 }
253 383
254 384 /**
255 - * @returns {void}
385 + * @return {void}
256 386 */
257 387 function disablePreviewSubmitButtons() {
258 388 const preview = document.getElementById( 'frm_style_preview' );
259 389 preview.querySelectorAll( 'form' ).forEach(
@@ -262,9 +392,9 @@
262 392 /**
263 393 * Prevent form submit event.
264 394 *
265 395 * @param {Event} event
266 - * @returns {false}
396 + * @return {false} Prevents the default action and stops the event from bubbling.
267 397 */
268 398 event => {
269 399 event.preventDefault();
270 400 event.stopPropagation();
@@ -275,16 +405,15 @@
275 405 }
276 406
277 407 /**
278 408 * @param {Event} event
279 - * @returns {void}
409 + * @return {void}
280 410 */
281 411 function handleClickEventsForListPage( event ) {
282 - const target = event.target;
412 + const { target } = event;
283 413
284 414 if ( target.classList.contains( 'frm-style-card' ) || target.closest( '.frm-style-card' ) ) {
285 415 handleStyleCardClick( event );
286 - return;
287 416 }
288 417 }
289 418
290 419 /**
@@ -291,12 +420,12 @@
291 420 * When a style card is clicked, the preview is updated.
292 421 * If the Update button is clicked after selecting a style card, the active card will be saved as the target form's style.
293 422 *
294 423 * @param {Event} event
295 - * @returns {void}
424 + * @return {void}
296 425 */
297 426 function handleStyleCardClick( event ) {
298 - const target = event.target;
427 + const { target } = event;
299 428
300 429 if ( target.closest( '.dropdown' ) ) {
301 430 // Ignore the hamburger menu inside of the card.
302 431 return;
@@ -301,9 +430,9 @@
301 430 // Ignore the hamburger menu inside of the card.
302 431 return;
303 432 }
304 433
305 - const card = target.classList.contains( 'frm-style-card' ) ? target : target.closest( '.frm-style-card' );
434 + const card = target.classList.contains( 'frm-style-card' ) ? target : target.closest( '.frm-style-card' );
306 435 const cardIsLocked = card.classList.contains( 'frm-locked-style' );
307 436
308 437 if ( cardIsLocked ) {
309 438 maybeCreateStyleTemplateModal( card );
@@ -309,11 +438,11 @@
309 438 maybeCreateStyleTemplateModal( card );
310 439 return; // Exit early as we're not actually selecting a locked template for preview.
311 440 }
312 441
313 - const previewArea = document.getElementById( 'frm_style_preview' );
314 - const activeCard = document.querySelector( '.frm-active-style-card' );
315 - const sampleForm = document.getElementById( 'frm_sample_form' ).querySelector( '.frm_forms' );
442 + const previewArea = document.getElementById( 'frm_style_preview' );
443 + const activeCard = document.querySelector( '.frm-active-style-card' );
444 + const sampleForm = document.getElementById( 'frm_sample_form' ).querySelector( '.frm_forms' );
316 445 const styleIdInput = getStyleIdInput();
317 446
318 447 disableLabelTransitions();
319 448
@@ -329,18 +458,16 @@
329 458
330 459 sampleForm.classList.remove( activeCard.dataset.classname );
331 460 sampleForm.classList.add( card.dataset.classname );
332 461
333 - if ( ! cardIsLocked ) {
334 - // Don't update the form when a locked card is clicked.
335 - styleIdInput.value = card.dataset.styleId;
336 - trackListPageChange();
337 - }
462 + // cardIsLocked is always false here due to early return above.
463 + styleIdInput.value = card.dataset.styleId;
464 + trackListPageChange();
338 465
339 466 setTimeout( enableLabelTransitions, 1 );
340 467
341 468 // We want to toggle the edit button so you can only leave the page to edit the style if it's active (to avoid unsaved changes).
342 - const editButton = document.getElementById( 'frm_edit_style' );
469 + const editButton = document.getElementById( 'frm_edit_style' );
343 470 const showEditButton = null !== card.querySelector( '.frm-style-card-info' ); // Only the "Applied style" has card info.
344 471 editButton.classList.toggle( 'frm_hidden', ! showEditButton );
345 472
346 473 changeLabelPositionsInPreview( card.dataset.labelPosition );
@@ -345,21 +472,21 @@
345 472
346 473 changeLabelPositionsInPreview( card.dataset.labelPosition );
347 474
348 475 // Trigger an action here so Pro can handle template preview updates on card click.
349 - const hookName = 'frm_style_card_click';
350 - const hookArgs = { card, styleIdInput };
476 + const hookName = 'frm_style_card_click';
477 + const hookArgs = { card, styleIdInput };
351 478 wp.hooks.doAction( hookName, hookArgs );
352 479 }
353 480
354 481 /**
355 482 * @param {HTMLElement} card
356 - * @returns {HTMLElement}
483 + * @return {HTMLElement} The modal element.
357 484 */
358 485 function maybeCreateStyleTemplateModal( card ) {
359 - const titleElement = card.querySelector( '.frm-style-card-title' );
486 + const titleElement = card.querySelector( '.frm-style-card-title' );
360 487 const templateTitle = titleElement.textContent;
361 - const modal = maybeCreateModal(
488 + const modal = maybeCreateModal(
362 489 'frm_style_template_modal',
363 490 {
364 491 content: getStyleTemplateModalContent( card ),
365 492 footer: getStyleTemplateModalFooter( card )
@@ -370,62 +497,61 @@
370 497 }
371 498
372 499 /**
373 500 * @param {HTMLElement} card
374 - * @returns {HTMLElement}
501 + * @return {HTMLElement} The modal content element.
375 502 */
376 503 function getStyleTemplateModalContent( card ) {
377 504 const children = [];
378 505
379 506 children.push(
380 - div({
507 + div( {
381 508 className: 'frm_warning_style',
382 509 children: [
383 510 span(
384 511 /* translators: %s: The required license type (ie. Plus, Business, or Elite) */
385 - __( 'Access to this style requires the %s plan.', 'formidable' )
386 - .replace( '%s', card.dataset.requires )
512 + sprintf( __( 'Access to this style requires the %s plan.', 'formidable' ), card.dataset.requires )
387 513 ),
388 - a({
514 + a( {
389 515 text: getUpgradeNowText(),
390 516 href: card.dataset.upgradeUrl,
391 517 target: '_blank'
392 - })
518 + } )
393 519 ]
394 - })
520 + } )
395 521 );
396 522
397 - return div({ children });
523 + return div( { children } );
398 524 }
399 525
400 526 /**
401 527 * @param {HTMLElement} card
402 - * @returns {HTMLElement}
528 + * @return {HTMLElement} The modal footer element.
403 529 */
404 530 function getStyleTemplateModalFooter( card ) {
405 - const viewDemoSiteButton = footerButton({
531 + const viewDemoSiteButton = footerButton( {
406 532 text: __( 'Learn More', 'formidable' ),
407 533 buttonType: 'secondary'
408 - });
534 + } );
409 535 viewDemoSiteButton.href = card.dataset.upgradeUrl;
410 536 viewDemoSiteButton.target = '_blank';
411 537
412 - let primaryActionButton = footerButton({
538 + const primaryActionButton = footerButton( {
413 539 text: getUpgradeNowText(),
414 540 buttonType: 'primary'
415 - });
541 + } );
416 542
417 543 primaryActionButton.classList.remove( 'dismiss' );
418 544 primaryActionButton.setAttribute( 'href', card.dataset.upgradeUrl );
419 545 primaryActionButton.target = '_blank';
420 546
421 - return div({
547 + return div( {
422 548 children: [ viewDemoSiteButton, primaryActionButton ]
423 - });
549 + } );
424 550 }
425 551
426 552 /**
427 - * @returns {String}
553 + * @return {string} The upgrade now text.
428 554 */
429 555 function getUpgradeNowText() {
430 556 return __( 'Upgrade Now', 'formidable' );
431 557 }
@@ -433,9 +559,9 @@
433 559 /**
434 560 * Track an unsaved change on the edit page.
435 561 * This is included in the frmStylerFunctions global so unsaved changes can be tracked in Pro as well.
436 562 *
437 - * @returns {void}
563 + * @return {void}
438 564 */
439 565 function trackUnsavedChange() {
440 566 state.unsavedChanges = true;
441 567 }
@@ -443,9 +569,9 @@
443 569 /**
444 570 * Possibly prevent leaving the page if there are unsaved changes.
445 571 *
446 572 * @param {Event} event
447 - * @returns {void}
573 + * @return {void}
448 574 */
449 575 function maybeConfirmExit( event ) {
450 576 if ( ! state.unsavedChanges ) {
451 577 return;
@@ -457,9 +583,9 @@
457 583
458 584 /**
459 585 * Floating labels have a transition style. Turn it off temporarily when switching between cards to avoid a transition between two different style classes.
460 586 *
461 - * @returns {void}
587 + * @return {void}
462 588 */
463 589 function disableLabelTransitions() {
464 590 setLabelTransitionStyle( 'none' );
465 591 }
@@ -464,9 +590,9 @@
464 590 setLabelTransitionStyle( 'none' );
465 591 }
466 592
467 593 /**
468 - * @returns {void}
594 + * @return {void}
469 595 */
470 596 function enableLabelTransitions() {
471 597 setLabelTransitionStyle( '' );
472 598 }
@@ -471,10 +597,10 @@
471 597 setLabelTransitionStyle( '' );
472 598 }
473 599
474 600 /**
475 - * @param {String} value
476 - * @returns {void}
601 + * @param {string} value
602 + * @return {void}
477 603 */
478 604 function setLabelTransitionStyle( value ) {
479 605 document.getElementById( 'frm_style_preview' ).querySelectorAll( '.frm_inside_container' ).forEach(
480 606 container => container.querySelector( 'label' ).style.transition = value
@@ -481,9 +607,9 @@
481 607 );
482 608 }
483 609
484 610 /**
485 - * @returns {void}
611 + * @return {void}
486 612 */
487 613 function toggleSampleForm() {
488 614 state.showingSampleForm = ! state.showingSampleForm;
489 615 document.getElementById( 'frm_active_style_form' ).classList.toggle( 'frm_hidden', state.showingSampleForm );
@@ -490,9 +616,9 @@
490 616 document.getElementById( 'frm_toggle_sample_form' ).querySelector( 'span' ).textContent = state.showingSampleForm ? __( 'View my form', 'formidable' ) : __( 'View sample form', 'formidable' );
491 617 }
492 618
493 619 /**
494 - * @returns {void}
620 + * @return {void}
495 621 */
496 622 function handleUpdateClick() {
497 623 state.unsavedChanges = false; // Prevent the saved changes pop up from triggering when submitting the form.
498 624
@@ -513,9 +639,9 @@
513 639 * Maybe modify an anchor's URL on click.
514 640 * If the sample form toggle is active, we want to pass that as a query parameter so we know to default to the sample form on load.
515 641 *
516 642 * @param {HTMLElement} clickTarget
517 - * @returns {void}
643 + * @return {void}
518 644 */
519 645 function modifyStylerUrl( clickTarget ) {
520 646 if ( ! state.showingSampleForm ) {
521 647 // Don't change the URL if it is not a sample form.
@@ -522,15 +648,15 @@
522 648 return;
523 649 }
524 650
525 651 const anchor = clickTarget.hasAttribute( 'href' ) ? clickTarget : clickTarget.querySelector( 'a[href]' );
526 - anchor.setAttribute( 'href', anchor.getAttribute( 'href' ) + '&sample=1' );
652 + anchor.setAttribute( 'href', `${ anchor.getAttribute( 'href' ) }&sample=1` );
527 653 }
528 654
529 655 /**
530 656 * Add menu dropdowns to style cards dynamically on load.
531 657 *
532 - * @returns {void}
658 + * @return {void}
533 659 */
534 660 function addHamburgerMenusToCards() {
535 661 const cards = Array.from( document.getElementsByClassName( 'frm-style-card' ) );
536 662 cards.forEach( card => maybeAddMenuToCard( card ) );
@@ -536,10 +662,10 @@
536 662 cards.forEach( card => maybeAddMenuToCard( card ) );
537 663 }
538 664
539 665 /**
540 - * @param {HTMLlement} card
541 - * @returns {void}
666 + * @param {HTMLElement} card
667 + * @return {void}
542 668 */
543 669 function maybeAddMenuToCard( card ) {
544 670 if ( ! shouldAddMenuToCard( card ) ) {
545 671 return;
@@ -544,16 +670,16 @@
544 670 if ( ! shouldAddMenuToCard( card ) ) {
545 671 return;
546 672 }
547 673
548 - card.appendChild( getHamburgerMenu( card.dataset ) );
674 + card.append( getHamburgerMenu( card.dataset ) );
549 675 }
550 676
551 677 /**
552 678 * Avoid adding a menu to an upsell card or a template card.
553 679 *
554 - * @param {HTMLlement} card
555 - * @returns {boolean}
680 + * @param {HTMLElement} card
681 + * @return {boolean} Whether to add a menu to the card.
556 682 */
557 683 function shouldAddMenuToCard( card ) {
558 684 return 'frm_template_style_cards_wrapper' !== card.parentNode.id || ! card.classList.contains( 'frm-locked-style' );
559 685 }
@@ -558,9 +684,9 @@
558 684 return 'frm_template_style_cards_wrapper' !== card.parentNode.id || ! card.classList.contains( 'frm-locked-style' );
559 685 }
560 686
561 687 /**
562 - * @returns {void}
688 + * @return {void}
563 689 */
564 690 function addHamburgerMenuForEditPage() {
565 691 const styleName = document.getElementById( 'frm_style_name' );
566 692 if ( ! styleName ) {
@@ -568,9 +694,9 @@
568 694 }
569 695
570 696 const styleId = document.getElementById( 'frm_styling_form' ).querySelector( 'input[name="ID"]' ).value;
571 697
572 - const hamburgerMenu = getHamburgerMenu({ styleId });
698 + const hamburgerMenu = getHamburgerMenu( { styleId } );
573 699 hamburgerMenu.classList.add( 'alignright' );
574 700 styleName.parentNode.insertBefore( hamburgerMenu, styleName );
575 701 }
576 702
@@ -577,34 +703,98 @@
577 703 /**
578 704 * Get a dropdown and the "hamburger" stacked dot menu trigger for a single style card.
579 705 *
580 706 * @param {DOMStringMap} data {
581 - * @type {String} editUrl
582 - * @type {String} styleId
583 - * @type {String} labelPosition
584 - * @type {String} classname
707 + * @type {string} editUrl
708 + * @type {string} styleId
709 + * @type {string} labelPosition
710 + * @type {string} classname
585 711 * }
586 - * @returns {HTMLElement}
712 + * @return {HTMLElement} The hamburger menu element.
587 713 */
588 714 function getHamburgerMenu( data ) {
589 - const hamburgerMenu = a({
715 + const hamburgerMenu = a( {
590 716 className: 'frm-dropdown-toggle dropdown-toggle',
591 - child: svg({ href: '#frm_thick_more_vert_icon' })
592 - });
593 - hamburgerMenu.setAttribute( 'data-toggle', 'dropdown' );
594 - hamburgerMenu.setAttribute( 'data-container', 'body' );
717 + child: svg( { href: '#frm_thick_more_vert_icon' } )
718 + } );
719 + hamburgerMenu.setAttribute( 'data-bs-toggle', 'dropdown' );
720 + hamburgerMenu.setAttribute( 'data-bs-container', 'body' );
595 721 hamburgerMenu.setAttribute( 'role', 'button' );
596 722 hamburgerMenu.setAttribute( 'tabindex', 0 );
597 723
598 - const isTemplate = 'undefined' !== typeof data.templateKey;
724 + const isTemplate = data.templateKey !== undefined;
725 +
726 + const dropdownMenu = div( {
727 + // Use dropdown-menu-right to avoid an overlapping issue with the card to the right (where the # of forms would appear above the menu).
728 + className: 'frm-dropdown-menu frm-style-options-menu frm-p-1'
729 + } );
730 +
731 + const isRtl = document.body.classList.contains( 'rtl' );
732 + dropdownMenu.classList.add( `dropdown-menu-${ isRtl ? 'left' : 'right' }` );
733 +
734 + dropdownMenu.setAttribute( 'role', 'menu' );
735 +
736 + fillDropdownMenuOnFirstOpen( hamburgerMenu, dropdownMenu, data, isTemplate );
737 +
738 + return div( {
739 + className: 'dropdown frm_wrap', // The .frm_wrap class prevents a blue outline on the active dropdown trigger.
740 + children: [ hamburgerMenu, dropdownMenu ]
741 + } );
742 + }
743 +
744 + /**
745 + * Add the options to a style card dropdown the first time that dropdown is opened.
746 + *
747 + * The options are built through the frm_style_card_dropdown_options filter, and Pro registers its
748 + * callback while its own script file runs. Adding the options while the page loads raced that file,
749 + * so a request for it that was slow to come back left every card holding the Lite only options.
750 + * Waiting for the first open takes the script order out of it, since Pro has always registered the
751 + * filter by the time a card can be clicked.
752 + *
753 + * @since 6.35
754 + *
755 + * @param {HTMLElement} hamburgerMenu The dropdown trigger.
756 + * @param {HTMLElement} dropdownMenu The dropdown to add the options to.
757 + * @param {DOMStringMap} data The dataset of the style card, or an object with the style ID on the edit page.
758 + * @param {boolean} isTemplate Whether the card is for a style template.
759 + * @return {void}
760 + */
761 + function fillDropdownMenuOnFirstOpen( hamburgerMenu, dropdownMenu, data, isTemplate ) {
762 + const fillDropdownMenu = () => {
763 + if ( dropdownMenu.children.length ) {
764 + return;
765 + }
766 +
767 + const options = getDropdownMenuOptions( data, isTemplate );
768 + dropdownMenu.append( ...options.map( wrapDropdownItem ) );
769 + };
770 +
771 + // Bootstrap fires this on the trigger before it measures and positions the dropdown.
772 + hamburgerMenu.addEventListener( 'show.bs.dropdown', fillDropdownMenu );
773 +
774 + // Fall back to the click in case the dropdown is ever opened without Bootstrap. Listen in the
775 + // capture phase so the options are in place before Bootstrap's own delegated handler runs.
776 + hamburgerMenu.addEventListener( 'click', fillDropdownMenu, true );
777 + }
778 +
779 + /**
780 + * Get the options for a single style card dropdown.
781 + *
782 + * @since 6.35
783 + *
784 + * @param {DOMStringMap} data The dataset of the style card, or an object with the style ID on the edit page.
785 + * @param {boolean} isTemplate Whether the card is for a style template.
786 + * @return {Array} The dropdown options, each an object with an anchor and a type.
787 + */
788 + function getDropdownMenuOptions( data, isTemplate ) {
599 789 let dropdownMenuOptions = [];
600 790
601 791 if ( isListPage ) {
602 - const applyOption = a({
792 + const applyOption = a( {
603 793 text: isTemplate ? __( 'Install and apply', 'formidable' ) : __( 'Apply', 'formidable' )
604 - });
794 + } );
605 795 addIconToOption( applyOption, 'frm_save_icon' );
606 - dropdownMenuOptions.push({ anchor: applyOption, type: 'apply' });
796 + dropdownMenuOptions.push( { anchor: applyOption, type: 'apply' } );
607 797 onClickPreventDefault( applyOption, handleApplyOptionClick );
608 798 }
609 799
610 800 if ( ! isTemplate ) {
@@ -609,19 +799,19 @@
609 799
610 800 if ( ! isTemplate ) {
611 801 if ( 'string' === typeof data.editUrl ) {
612 802 // The Edit option is not included on the Edit page.
613 - const editOption = a({
803 + const editOption = a( {
614 804 text: __( 'Edit', 'formidable' ),
615 805 href: data.editUrl
616 - });
806 + } );
617 807 addIconToOption( editOption, 'frm_pencil_icon' );
618 - dropdownMenuOptions.push({ anchor: editOption, type: 'edit' });
808 + dropdownMenuOptions.push( { anchor: editOption, type: 'edit' } );
619 809 }
620 810
621 - const resetOption = a({
811 + const resetOption = a( {
622 812 text: __( 'Reset to Defaults', 'formidable' )
623 - });
813 + } );
624 814 addIconToOption( resetOption, 'frm_repeater_icon' );
625 815 onClickPreventDefault( resetOption, () => confirmResetStyle( data.styleId ) );
626 816
627 817 dropdownMenuOptions.push(
@@ -629,10 +819,10 @@
629 819 { anchor: resetOption, type: 'reset' }
630 820 );
631 821 }
632 822
633 - const hookName = 'frm_style_card_dropdown_options';
634 - const hookArgs = { data, addIconToOption, isTemplate };
823 + const hookName = 'frm_style_card_dropdown_options';
824 + const hookArgs = { data, addIconToOption, isTemplate };
635 825 dropdownMenuOptions = wp.hooks.applyFilters( hookName, dropdownMenuOptions, hookArgs );
636 826
637 827 if ( isListPage && ! isTemplate ) {
638 828 maybeAddDuplicateUpsell( dropdownMenuOptions );
@@ -637,33 +827,19 @@
637 827 if ( isListPage && ! isTemplate ) {
638 828 maybeAddDuplicateUpsell( dropdownMenuOptions );
639 829 }
640 830
641 - const dropdownMenu = div({
642 - // Use dropdown-menu-right to avoid an overlapping issue with the card to the right (where the # of forms would appear above the menu).
643 - className: 'frm-dropdown-menu frm-style-options-menu frm-p-1',
644 - children: dropdownMenuOptions.map( wrapDropdownItem )
645 - });
646 -
647 - const isRtl = document.body.classList.contains( 'rtl' );
648 - dropdownMenu.classList.add( 'dropdown-menu-' + ( isRtl ? 'left' : 'right' ) );
649 -
650 - dropdownMenu.setAttribute( 'role', 'menu' );
651 -
652 - return div({
653 - className: 'dropdown frm_wrap', // The .frm_wrap class prevents a blue outline on the active dropdown trigger.
654 - children: [ hamburgerMenu, dropdownMenu ]
655 - });
831 + return dropdownMenuOptions;
656 832 }
657 833
658 834 /**
659 835 * @param {Array} dropdownMenuOptions
660 - * @returns {void}
836 + * @return {void}
661 837 */
662 838 function maybeAddDuplicateUpsell( dropdownMenuOptions ) {
663 839 let duplicateOptionExists = false;
664 840 for ( let i = 0; i < dropdownMenuOptions.length; ++i ) {
665 - if ( dropdownMenuOptions[i].type === 'duplicate' ) {
841 + if ( dropdownMenuOptions[ i ].type === 'duplicate' ) {
666 842 duplicateOptionExists = true;
667 843 break;
668 844 }
669 845 }
@@ -671,12 +847,12 @@
671 847 if ( duplicateOptionExists ) {
672 848 return;
673 849 }
674 850
675 - const duplicateUpsell = a({
851 + const duplicateUpsell = a( {
676 852 text: __( 'Duplicate', 'formidable' ),
677 853 className: 'frm_noallow'
678 - });
854 + } );
679 855 addIconToOption( duplicateUpsell, 'frm_clone_icon' );
680 856 onClickPreventDefault( duplicateUpsell, () => document.getElementById( 'frm_new_style_trigger' ).click() );
681 857 const upsellOption = { anchor: duplicateUpsell, type: 'duplicate' };
682 858 dropdownMenuOptions.splice( 3, 0, upsellOption );
@@ -683,13 +859,13 @@
683 859 }
684 860
685 861 /**
686 862 * @param {Event} event
687 - * @returns {void}
863 + * @return {void}
688 864 */
689 865 function handleApplyOptionClick( event ) {
690 866 const option = event.target;
691 - const card = option.closest( '.frm-style-card' );
867 + const card = option.closest( '.frm-style-card' );
692 868 if ( ! card ) {
693 869 return;
694 870 }
695 871
@@ -697,14 +873,14 @@
697 873 handleUpdateClick();
698 874 }
699 875
700 876 /**
701 - * @param {String} styleId
702 - * @returns {HTMLElement}
877 + * @param {string} styleId
878 + * @return {HTMLElement} The rename option element.
703 879 */
704 880 function getRenameOption( styleId ) {
705 - const renameOption = a( __( 'Rename', 'formidable-pro' ) );
706 - addIconToOption( renameOption, 'frm_signature_icon' );
881 + const renameOption = a( __( 'Rename', 'formidable' ) );
882 + addIconToOption( renameOption, 'frm_signature2_icon' );
707 883
708 884 let titleTarget;
709 885
710 886 // Depending on the page we're pulling the text from an existing element on the page.
@@ -720,9 +896,9 @@
720 896 const styleName = titleTarget.textContent;
721 897 stylerModal(
722 898 'frm_rename_style_modal',
723 899 {
724 - title: __( 'Rename style', 'formidable-pro' ),
900 + title: __( 'Rename style', 'formidable' ),
725 901 content: getStyleInputNameModalContent( 'rename', styleName ),
726 902 footer: getRenameStyleModalFooter( styleId )
727 903 }
728 904 );
@@ -732,11 +908,11 @@
732 908 return renameOption;
733 909 }
734 910
735 911 /**
736 - * @param {String} id
912 + * @param {string} id
737 913 * @param {Object} args
738 - * @returns {HTMLElement}
914 + * @return {HTMLElement} The modal element.
739 915 */
740 916 function stylerModal( id, args ) {
741 917 const modal = maybeCreateModal( id, args );
742 918 // Include both wp-core-ui and frm-white-body on the modal.
@@ -749,11 +925,11 @@
749 925 /**
750 926 * Get modal content with just a "Style Name" input.
751 927 * This is used for New style, Duplicate style, and for Rename style.
752 928 *
753 - * @param {String} context
754 - * @param {String|undefined} value
755 - * @returns {HTMLElement}
929 + * @param {string} context
930 + * @param {string|undefined} value
931 + * @return {HTMLElement} The modal content element.
756 932 */
757 933 function getStyleInputNameModalContent( context, value ) {
758 934 // Create a form so we can listen to Enter key presses that trigger a form submit event.
759 935 const form = tag(
@@ -758,9 +934,9 @@
758 934 // Create a form so we can listen to Enter key presses that trigger a form submit event.
759 935 const form = tag(
760 936 'form',
761 937 {
762 - child: labelledTextInput( 'frm_' + context + '_style_name_input', __( 'Style name', 'formidable-pro' ), 'style_name' )
938 + child: labelledTextInput( `frm_${ context }_style_name_input`, __( 'Style name', 'formidable' ), 'style_name' )
763 939 }
764 940 );
765 941 form.addEventListener(
766 942 'submit',
@@ -765,9 +941,9 @@
765 941 form.addEventListener(
766 942 'submit',
767 943 /**
768 944 * @param {Event} event
769 - * @returns {false}
945 + * @return {false} Prevents the default action and stops the event from bubbling.
770 946 */
771 947 event => {
772 948 // Prevent the form in the modal from submitting and trigger the click button in the modal footer instead.
773 949 event.preventDefault();
@@ -777,9 +953,9 @@
777 953
778 954 return false;
779 955 }
780 956 );
781 - const content = div({ child: form });
957 + const content = div( { child: form } );
782 958 content.style.padding = '20px';
783 959 content.querySelector( 'label' ).style.lineHeight = 1.5;
784 960
785 961 const styleNameInput = content.querySelector( 'input' );
@@ -804,32 +980,32 @@
804 980 return content;
805 981 }
806 982
807 983 /**
808 - * @param {String} styleId
809 - * @returns {HTMLELement}
984 + * @param {string} styleId
985 + * @return {HTMLElement} The modal footer element.
810 986 */
811 - function getRenameStyleModalFooter( styleId ) {
812 - const cancelButton = footerButton({ text: __( 'Cancel', 'formidable-pro' ), buttonType: 'cancel' });
987 + function getRenameStyleModalFooter( styleId ) {
988 + const cancelButton = footerButton( { text: __( 'Cancel', 'formidable' ), buttonType: 'cancel' } );
813 989 cancelButton.classList.add( 'dismiss' );
814 990
815 - const renameButton = footerButton({ text: __( 'Rename style', 'formidable-pro' ), buttonType: 'primary' });
991 + const renameButton = footerButton( { text: __( 'Rename style', 'formidable' ), buttonType: 'primary' } );
816 992 onClickPreventDefault( renameButton, () => renameStyle( styleId ) );
817 993
818 - return div({
994 + return div( {
819 995 children: [ cancelButton, renameButton ]
820 - });
996 + } );
821 997 }
822 998
823 999 /**
824 1000 * Call frm_rename_style action when the rename style button is clicked in rename modal.
825 1001 *
826 - * @param {String} styleId
827 - * @returns {void}
1002 + * @param {string} styleId
1003 + * @return {void}
828 1004 */
829 1005 function renameStyle( styleId ) {
830 1006 const styleNameInput = document.getElementById( 'frm_rename_style_name_input' );
831 - const newStyleName = styleNameInput.value;
1007 + const newStyleName = styleNameInput.value;
832 1008
833 1009 if ( '' === newStyleName ) {
834 1010 // Avoid setting an empty name.
835 1011 // The button gets disabled on an input event when the name is empty.
@@ -835,9 +1011,27 @@
835 1011 // The button gets disabled on an input event when the name is empty.
836 1012 return;
837 1013 }
838 1014
839 - const formData = new FormData();
1015 + if ( ! styleId || '0' === String( styleId ) ) {
1016 + // A new or duplicated style has no ID yet, so there is nothing to rename on the server.
1017 + // Update the post_title input (which overrides $_GET['style_name'] on save) and the
1018 + // visible style name instead of calling the rename_style endpoint.
1019 + const postTitleInput = document.querySelector( 'input[name="frm_style_setting[post_title]"]' );
1020 + if ( postTitleInput ) {
1021 + postTitleInput.value = newStyleName;
1022 + }
1023 +
1024 + const styleNameElement = document.getElementById( 'frm_style_name' );
1025 + if ( styleNameElement ) {
1026 + styleNameElement.textContent = newStyleName;
1027 + }
1028 +
1029 + success( __( 'Style has been renamed successfully', 'formidable' ) );
1030 + return;
1031 + }
1032 +
1033 + const formData = new FormData();
840 1034 formData.append( 'style_id', styleId );
841 1035 formData.append( 'style_name', newStyleName );
842 1036 doJsonPost( 'rename_style', formData ).then(
843 1037 /**
@@ -845,9 +1039,9 @@
845 1039 *
846 1040 * If on the list page, update the style card after renaming a style.
847 1041 * On the edit page, update the style name element instead.
848 1042 *
849 - * @returns {void}
1043 + * @return {void}
850 1044 */
851 1045 () => {
852 1046 success( __( 'Style has been renamed successfully', 'formidable' ) );
853 1047
@@ -862,33 +1056,24 @@
862 1056 );
863 1057 }
864 1058
865 1059 /**
866 - * @param {String} styleId
867 - * @param {String} newStyleName
868 - * @returns {void}
1060 + * @param {string} styleId
1061 + * @param {string} newStyleName
1062 + * @return {void}
869 1063 */
870 1064 function updateStyleNameInCard( styleId, newStyleName ) {
871 - const card = getCardByStyleId( styleId );
1065 + const card = getCardByStyleId( styleId );
872 1066 const titleElement = card.querySelector( '.frm-style-card-title' );
873 1067 titleElement.textContent = newStyleName;
874 1068 }
875 1069
876 1070 /**
877 - * @param {String} templateKey
878 - * @returns {HTMLElement}
1071 + * @param {string} styleId
1072 + * @return {HTMLElement} The card element.
879 1073 */
880 - function getTemplateCard( templateKey ) {
881 - const templateCard = document.getElementById( 'frm_template_style_cards_wrapper' ).querySelector( '.frm-style-card[data-template-key="' + templateKey + '"]' );
882 - return templateCard;
883 - }
884 -
885 - /**
886 - * @param {String} styleId
887 - * @returns {HTMLElement}
888 - */
889 1074 function getCardByStyleId( styleId ) {
890 - const defaultCard = document.querySelector( '#frm_default_style_cards_wrapper > div[data-style-id="' + styleId + '"]' );
1075 + const defaultCard = document.querySelector( `#frm_default_style_cards_wrapper > div[data-style-id="${ styleId }"]` );
891 1076 if ( defaultCard ) {
892 1077 return defaultCard;
893 1078 }
894 1079 return Array.from( document.getElementById( 'frm_custom_style_cards_wrapper' ).children ).find( card => card.dataset.styleId === styleId );
@@ -895,19 +1080,19 @@
895 1080 }
896 1081
897 1082 /**
898 1083 * @param {HTMLElement} option
899 - * @param {String} iconId
900 - * @returns {void}
1084 + * @param {string} iconId
1085 + * @return {void}
901 1086 */
902 1087 function addIconToOption( option, iconId ) {
903 - const icon = frmDom.svg({ href: '#' + iconId });
1088 + const icon = frmDom.svg( { href: `#${ iconId }` } );
904 1089 option.insertBefore( icon, option.firstChild );
905 1090 }
906 1091
907 1092 /**
908 - * @param {String} styleId
909 - * @returns {void}
1093 + * @param {string} styleId
1094 + * @return {void}
910 1095 */
911 1096 function confirmResetStyle( styleId ) {
912 1097 stylerModal(
913 1098 'frm_reset_style_modal',
@@ -919,9 +1104,9 @@
919 1104 );
920 1105 }
921 1106
922 1107 /**
923 - * @returns {HTMLElement}
1108 + * @return {HTMLElement} The modal content element.
924 1109 */
925 1110 function getResetStyleModalContent() {
926 1111 const content = div( __( 'Reset this style back to the default?', 'formidable' ) );
927 1112 content.style.padding = '20px';
@@ -928,23 +1113,23 @@
928 1113 return content;
929 1114 }
930 1115
931 1116 /**
932 - * @param {String} styleId
933 - * @returns {HTMLElement}
1117 + * @param {string} styleId
1118 + * @return {HTMLElement} The modal footer element.
934 1119 */
935 1120 function getResetStyleModalFooter( styleId ) {
936 - const cancelButton = footerButton({
1121 + const cancelButton = footerButton( {
937 1122 text: __( 'Cancel', 'formidable' ),
938 1123 buttonType: 'cancel'
939 - });
1124 + } );
940 1125 cancelButton.classList.add( 'dismiss' );
941 - const resetButton = footerButton({
1126 + const resetButton = footerButton( {
942 1127 text: __( 'Reset style', 'formidable' ),
943 1128 buttonType: 'primary'
944 - });
1129 + } );
945 1130 onClickPreventDefault( resetButton, () => resetStyle( styleId ) );
946 - return div({ children: [ cancelButton, resetButton ] });
1131 + return div( { children: [ cancelButton, resetButton ] } );
947 1132 }
948 1133
949 1134 /**
950 1135 * Handle reset dropdown action.
@@ -949,10 +1134,10 @@
949 1134 /**
950 1135 * Handle reset dropdown action.
951 1136 * This function handles the front end routing for the reset action as reset works differently for edit and list views.
952 1137 *
953 - * @param {String} styleId
954 - * @returns {void}
1138 + * @param {string} styleId
1139 + * @return {void}
955 1140 */
956 1141 function resetStyle( styleId ) {
957 1142 if ( isListPage ) {
958 1143 resetStyleOnListPage( styleId );
@@ -963,10 +1148,10 @@
963 1148
964 1149 /**
965 1150 * Make a POST request to reset the style then reload the CSS and reset the card styles.
966 1151 *
967 - * @param {String} styleId
968 - * @returns {void}
1152 + * @param {string} styleId
1153 + * @return {void}
969 1154 */
970 1155 function resetStyleOnListPage( styleId ) {
971 1156 const formData = new FormData();
972 1157 formData.append( 'style_id', styleId );
@@ -989,12 +1174,12 @@
989 1174
990 1175 /**
991 1176 * Reset the style in-page (without actually updating it).
992 1177 *
993 - * @returns {void}
1178 + * @return {void}
994 1179 */
995 1180 function resetStyleOnEditPage() {
996 - jQuery.ajax({
1181 + jQuery.ajax( {
997 1182 type: 'POST',
998 1183 url: ajaxurl,
999 1184 data: {
1000 1185 action: 'frm_settings_reset',
@@ -1000,9 +1185,9 @@
1000 1185 action: 'frm_settings_reset',
1001 1186 nonce: frmGlobal.nonce
1002 1187 },
1003 1188 success: syncEditPageAfterResetAction
1004 - });
1189 + } );
1005 1190 }
1006 1191
1007 1192 /**
1008 1193 * Handle reset success on edit page.
@@ -1010,9 +1195,9 @@
1010 1195 *
1011 1196 * @todo Stop triggering change events with jQuery. And remove the other jQuery as well.
1012 1197 *
1013 1198 * @param {Object} response
1014 - * @returns {void}
1199 + * @return {void}
1015 1200 */
1016 1201 function syncEditPageAfterResetAction( response ) {
1017 1202 let defaultValues = response.replace( /^\s+|\s+$/g, '' );
1018 1203 if ( defaultValues.indexOf( '{' ) === 0 ) {
@@ -1019,9 +1204,9 @@
1019 1204 defaultValues = JSON.parse( defaultValues );
1020 1205 }
1021 1206
1022 1207 for ( const key in defaultValues ) {
1023 - let targetInput = document.querySelector( 'input[name$="[' + key + ']"], select[name$="[' + key + ']"]' );
1208 + let targetInput = document.querySelector( `input[name$="[${ key }]"], select[name$="[${ key }]"]` );
1024 1209 if ( ! targetInput ) {
1025 1210 continue;
1026 1211 }
1027 1212
@@ -1026,9 +1211,9 @@
1026 1211 }
1027 1212
1028 1213 if ( 'radio' === targetInput.getAttribute( 'type' ) ) {
1029 1214 // Reset the repeater icon dropdown.
1030 - targetInput = document.querySelector( 'input[name$="[' + key + ']"][value="' + defaultValues[ key ] + '"]' );
1215 + targetInput = document.querySelector( `input[name$="[${ key }]"][value="${ defaultValues[ key ] }"]` );
1031 1216 if ( targetInput ) {
1032 1217 targetInput.checked = true;
1033 1218 jQuery( targetInput ).trigger( 'change' );
1034 1219 }
@@ -1042,8 +1227,9 @@
1042 1227 jQuery( targetInput ).trigger( 'change' );
1043 1228 }
1044 1229 }
1045 1230
1231 + resetCustomCSSEditor();
1046 1232 jQuery( '#frm_submit_style, #frm_auto_width' ).prop( 'checked', false );
1047 1233 jQuery( document.getElementById( 'frm_fieldset' ) ).trigger( 'change' );
1048 1234 showStyleResetSuccessMessage();
1049 1235 }
@@ -1048,11 +1234,26 @@
1048 1234 showStyleResetSuccessMessage();
1049 1235 }
1050 1236
1051 1237 /**
1238 + * Reset the custom CSS editor.
1239 + *
1240 + * @return {void}
1241 + */
1242 + function resetCustomCSSEditor() {
1243 + const checkbox = document.getElementById( 'frm_enable_single_style_custom_css' );
1244 + const editorWrapper = document.getElementById( 'frm_single_style_custom_css_editor' );
1245 + if ( ! checkbox || ! editorWrapper ) {
1246 + return;
1247 + }
1248 + checkbox.checked = false;
1249 + editorWrapper.classList.add( 'frm_hidden' );
1250 + }
1251 +
1252 + /**
1052 1253 * Reload Formidable CSS after a style is reset so the preview updates immediately without needing to reload the page.
1053 1254 *
1054 - * @returns {void}
1255 + * @return {void}
1055 1256 */
1056 1257 function reloadCSSAfterStyleReset() {
1057 1258 const style = document.getElementById( 'frm-custom-theme-css' );
1058 1259 if ( ! style ) {
@@ -1059,27 +1260,27 @@
1059 1260 return;
1060 1261 }
1061 1262
1062 1263 const newStyle = document.createElement( 'link' );
1063 - newStyle.rel = 'stylesheet';
1064 - newStyle.type = 'text/css';
1065 - newStyle.href = style.href + '&key=' + getAutoId(); // Make the URL unique so the old stylesheet doesn't get picked up by cache.
1264 + newStyle.rel = 'stylesheet';
1265 + newStyle.type = 'text/css';
1266 + newStyle.href = `${ style.href }&key=${ getAutoId() }`; // Make the URL unique so the old stylesheet doesn't get picked up by cache.
1066 1267
1067 1268 // Listen for the new style to load before removing the old style to avoid having no styles while the new style is loading.
1068 1269 newStyle.addEventListener(
1069 1270 'load',
1070 1271 () => {
1071 - style.parentNode.removeChild( style );
1272 + style.remove();
1072 1273 newStyle.id = 'frm-custom-theme-css'; // Assign the old ID to the new style so it can be removed in the next reset action.
1073 1274 }
1074 1275 );
1075 1276
1076 - const head = document.getElementsByTagName( 'HEAD' )[0];
1077 - head.appendChild( newStyle );
1277 + const head = document.getElementsByTagName( 'HEAD' )[ 0 ];
1278 + head.append( newStyle );
1078 1279 }
1079 1280
1080 1281 /**
1081 - * @returns {Number}
1282 + * @return {number} The auto ID.
1082 1283 */
1083 1284 function getAutoId() {
1084 1285 return ++state.autoId;
1085 1286 }
@@ -1084,53 +1285,128 @@
1084 1285 return ++state.autoId;
1085 1286 }
1086 1287
1087 1288 /**
1088 - * @param {Object} data {
1089 - * @type {Element} anchor
1090 - * @type {String} type
1091 - * }
1092 - * @returns {Element}
1289 + * @param {Object} data
1290 + * @param {HTMLElement} data.anchor
1291 + * @param {string} data.type
1292 + * @return {HTMLElement} The dropdown item element.
1093 1293 */
1094 - function wrapDropdownItem({ anchor, type }) {
1095 - return div({
1096 - className: 'dropdown-item frm-' + type + '-style',
1294 + function wrapDropdownItem( { anchor, type } ) {
1295 + return div( {
1296 + className: `dropdown-item frm-${ type }-style`,
1097 1297 child: anchor
1098 - });
1298 + } );
1099 1299 }
1100 1300
1101 1301 /**
1102 1302 * This gets triggered through a hook called in frmAdminBuild.styleInit() from formidable_admin.js.
1103 1303 *
1104 - * @returns {void}
1304 + * @return {void}
1105 1305 */
1106 1306 function initEditPage() {
1107 - const { debounce } = frmDom.util;
1307 + const { debounce } = frmDom.util;
1108 1308 const debouncedPreviewUpdate = debounce( () => changeStyling(), 100 );
1309 + const debouncedColorChange = debounce( ( event, value ) => {
1310 + /**
1311 + * Fires on style colorpicker change.
1312 + *
1313 + * @param {Event} data.event The color change event.
1314 + * @param {string} data.value New color value.
1315 + */
1316 + wp.hooks.doAction( 'frm_style_options_color_change', { event, value } );
1317 + }, 200 );
1109 1318
1319 + const debouncedTextSquishCheck = debounce( textSquishCheck, 300 );
1110 1320 initPosClass(); // It's important that this gets called before we add event listeners because it triggers change events.
1111 1321
1112 - document.getElementById( 'frm_field_height' ).addEventListener( 'change', textSquishCheck );
1113 - document.getElementById( 'frm_field_font_size' ).addEventListener( 'change', textSquishCheck );
1114 - document.getElementById( 'frm_field_pad' ).addEventListener( 'change', textSquishCheck );
1322 + [ 'frm_field_height', 'frm_field_font_size', 'frm_field_pad' ].forEach( selector => {
1323 + document.getElementById( selector ).addEventListener( 'change', debouncedTextSquishCheck );
1324 + } );
1115 1325
1116 - jQuery( 'input.hex' ).wpColorPicker({
1117 - change: function( event ) {
1326 + function detectColorFormat( value ) {
1327 + if ( value.startsWith( 'rgba' ) ) {
1328 + return 'rgba';
1329 + }
1330 + if ( value.startsWith( 'rgb' ) ) {
1331 + return 'rgb';
1332 + }
1333 + if ( value.startsWith( 'hsla' ) ) {
1334 + return 'hsla';
1335 + }
1336 + if ( value.startsWith( 'hsl' ) ) {
1337 + return 'hsl';
1338 + }
1339 + return 'hex';
1340 + }
1341 +
1342 + const nativeInputValueDescriptor = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, 'value' );
1343 +
1344 + jQuery( 'input.hex' ).each( function() {
1345 + this.dataset.colorFormat = detectColorFormat( this.value );
1346 +
1347 + // Prevent iris from overwriting non-hex formats with hex during color picking.
1348 + const input = this;
1349 + Object.defineProperty( input, 'value', {
1350 + get() {
1351 + return nativeInputValueDescriptor.get.call( this );
1352 + },
1353 + set( val ) {
1354 + const format = input.dataset.colorFormat;
1355 + if ( format && 'hex' !== format && /^#[0-9a-f]{3,8}$/i.test( val ) ) {
1356 + return;
1357 + }
1358 + nativeInputValueDescriptor.set.call( this, val );
1359 + },
1360 + configurable: true
1361 + } );
1362 + } ).on( 'keyup', function() {
1363 + const newFormat = detectColorFormat( this.value );
1364 + if ( this.dataset.colorFormat !== newFormat ) {
1365 + this.dataset.colorFormat = newFormat;
1366 + const container = this.closest( '.wp-picker-container' );
1367 + if ( container ) {
1368 + container.querySelector( '.wp-color-result-text' ).textContent = this.value;
1369 + }
1370 + debouncedColorChange( { target: this }, this.value );
1371 + }
1372 + } ).wpColorPicker( {
1373 + change( event, ui ) {
1374 + const input = event.target;
1375 + const format = input.dataset.colorFormat || 'hex';
1376 + let color;
1377 +
1118 1378 trackUnsavedChange();
1119 1379
1120 - if ( null !== event.target.getAttribute( 'data-alpha-color-type' ) ) {
1380 + if ( ui.color._alpha < 1 ) {
1381 + input.dataset.colorFormat = 'rgba';
1382 + color = ui.color.toCSS( 'rgba' );
1383 + } else if ( 'hex' === format ) {
1384 + color = ui.color.toString();
1385 + } else {
1386 + color = ui.color.toCSS( format );
1387 + }
1388 +
1389 + debouncedColorChange( event, color );
1390 +
1391 + input.value = color;
1392 +
1393 + if ( null !== input.getAttribute( 'data-alpha-color-type' ) ) {
1121 1394 debouncedPreviewUpdate();
1122 1395 return;
1123 1396 }
1124 1397
1125 - const hexcolor = jQuery( this ).wpColorPicker( 'color' );
1126 - jQuery( event.target ).val( hexcolor ).trigger( 'change' );
1398 + debouncedPreviewUpdate();
1127 1399 }
1128 - });
1400 + } );
1129 1401 jQuery( '.wp-color-result-text' ).text( function( _, oldText ) {
1402 + const container = jQuery( this ).closest( '.wp-picker-container' );
1403 + if ( container !== undefined && container[ 0 ].parentElement.classList.contains( 'frm-colorpicker' ) ) {
1404 + return container[ 0 ].querySelector( '.wp-color-picker' ).value;
1405 + }
1130 1406 return oldText === 'Select Color' ? 'Select' : oldText;
1131 - });
1132 - jQuery( '#frm_styling_form .styling_settings' ).on( 'change', debouncedPreviewUpdate );
1407 + } );
1408 + jQuery( '#frm_styling_form .styling_settings, #frm_styling_form .frm-field-shape, #frm_styling_form input[name="frm_style_setting[post_content][base_font_size]"]' ).on( 'change', debouncedPreviewUpdate );
1133 1409
1134 1410 // This is really only necessary for Pro. But if Pro is not up to date to initialize the datepicker in the sample form, it should still work because it's initialized here.
1135 1411 initDatepickerSample();
1136 1412
@@ -1143,17 +1419,17 @@
1143 1419 /**
1144 1420 * Sends an AJAX POST request for new CSS to use for the preview.
1145 1421 * This is called whenever a style setting is changed, generally using debouncedPreviewUpdate to avoid simultaneous requests.
1146 1422 *
1147 - * @returns {void}
1423 + * @return {void}
1148 1424 */
1149 1425 function changeStyling() {
1150 1426 const styleInputs = Array.from( document.getElementById( 'frm_style_sidebar' ).querySelectorAll( 'input, select, textarea' ) ).filter(
1151 1427 input => 'style_name' === input.name || 0 === input.name.indexOf( 'frm_style_setting[post_content]' )
1152 1428 );
1153 - const locStr = JSON.stringify( jQuery( styleInputs ).serializeArray() );
1429 + const locStr = JSON.stringify( jQuery( styleInputs ).serializeArray() );
1154 1430
1155 - jQuery.ajax({
1431 + jQuery.ajax( {
1156 1432 type: 'POST',
1157 1433 url: ajaxurl,
1158 1434 data: {
1159 1435 action: 'frm_change_styling',
@@ -1159,24 +1435,24 @@
1159 1435 action: 'frm_change_styling',
1160 1436 nonce: frmGlobal.nonce,
1161 1437 frm_style_setting: locStr
1162 1438 },
1163 - success: ( css ) => {
1439 + success: css => {
1164 1440 handleChangeStylingSuccess( css );
1165 1441 setSelectPlaceholderColor();
1166 1442 }
1167 - });
1443 + } );
1168 1444 }
1169 1445
1170 1446 /**
1171 1447 * Update the CSS used for the preview on the edit page when a styling input has been updated.
1172 1448 *
1173 - * @param {String} css The response from the frm_change_styling request.
1174 - * @returns {void}
1449 + * @param {string} css The response from the frm_change_styling request.
1450 + * @return {void}
1175 1451 */
1176 1452 function handleChangeStylingSuccess( css ) {
1177 1453 // Validate the string response. A valid output will include rules with .with_frm_style
1178 - if ( -1 === css.indexOf( '.with_frm_style' ) ) {
1454 + if ( ! css.includes( '.with_frm_style' ) ) {
1179 1455 // Handle error (possibly a permission error, or an outdated nonce).
1180 1456 alert( css );
1181 1457 return;
1182 1458 }
@@ -1186,26 +1462,31 @@
1186 1462 /**
1187 1463 * Possibly pop up with a warning that "text will not display correctly if the field height is too small relative to the field padding and text size".
1188 1464 * This can be triggered when modifying font size, height, and padding.
1189 1465 *
1190 - * @returns {void}
1466 + * @return {void}
1191 1467 */
1192 1468 function textSquishCheck() {
1193 - const size = document.getElementById( 'frm_field_font_size' ).value.replace( /\D/g, '' );
1194 - const height = document.getElementById( 'frm_field_height' ).value.replace( /\D/g, '' );
1469 + if ( null !== frmDom.util.getCookie( 'frm-style-text-squish-check' ) ) {
1470 + return;
1471 + }
1472 + const height = document.getElementById( 'frm_field_height' ).value.replace( /\D/g, '' );
1195 1473 const paddingEntered = document.getElementById( 'frm_field_pad' ).value.split( ' ' );
1196 - const paddingCount = paddingEntered.length;
1474 + const paddingCount = paddingEntered.length;
1197 1475
1476 + frmDom.util.setCookie( 'frm-style-text-squish-check', 1, 30 );
1477 +
1198 1478 // If too many or too few padding entries, leave now
1199 1479 if ( paddingCount === 0 || paddingCount > 4 || height === '' ) {
1200 1480 return;
1201 1481 }
1202 1482
1483 + const size = document.getElementById( 'frm_field_font_size' ).value.replace( /\D/g, '' );
1203 1484 // Get the top and bottom padding from entered values
1204 - const paddingTop = paddingEntered[0].replace( /\D/g, '' );
1205 - const paddingBottom = paddingTop;
1485 + const paddingTop = paddingEntered[ 0 ].replace( /\D/g, '' );
1486 + let paddingBottom = paddingTop;
1206 1487 if ( paddingCount >= 3 ) {
1207 - paddingBottom = paddingEntered[2].replace( /\D/g, '' );
1488 + paddingBottom = paddingEntered[ 2 ].replace( /\D/g, '' );
1208 1489 }
1209 1490
1210 1491 // Check if there is enough space for text
1211 1492 const textSpace = height - size - paddingTop - paddingBottom - 3;
@@ -1217,16 +1498,16 @@
1217 1498 /**
1218 1499 * When the Collapse icons are updated, sync the dropdown.
1219 1500 * Otherwise the previously selected value will still appear as the selected value.
1220 1501 *
1221 - * @returns {void}
1502 + * @return {void}
1222 1503 */
1223 1504 jQuery( document ).on( 'change', '.frm-dropdown-menu input[type="radio"]', function() {
1224 1505 trackUnsavedChange();
1225 1506
1226 - const radio = this;
1507 + const radio = this;
1227 1508 const btnGrp = radio.closest( '.btn-group' );
1228 - const btnId = btnGrp.getAttribute( 'id' );
1509 + const btnId = btnGrp.getAttribute( 'id' );
1229 1510
1230 1511 const select = document.getElementById( btnId.replace( '_select', '' ) );
1231 1512 if ( select ) {
1232 1513 select.value = radio.value;
@@ -1231,9 +1512,9 @@
1231 1512 if ( select ) {
1232 1513 select.value = radio.value;
1233 1514 }
1234 1515
1235 - jQuery( btnGrp ).children( 'button' ).html( radio.nextElementSibling.innerHTML + ' <b class="caret"></b>' );
1516 + jQuery( btnGrp ).children( 'button' ).html( `${ radio.nextElementSibling.innerHTML } <b class="caret"></b>` );
1236 1517
1237 1518 const activeItem = btnGrp.querySelector( '.dropdown-item.active' );
1238 1519 if ( activeItem ) {
1239 1520 activeItem.classList.remove( 'active' );
@@ -1239,15 +1520,25 @@
1239 1520 activeItem.classList.remove( 'active' );
1240 1521 }
1241 1522
1242 1523 radio.closest( '.dropdown-item' ).classList.add( 'active' );
1243 - });
1524 + } );
1525 +
1526 + if ( frm_admin_js.requireAccordionTitleClickListener ) {
1527 + document.querySelectorAll( '.styling_settings h3.accordion-section-title' ).forEach( el => {
1528 + el.addEventListener( 'click', event => {
1529 + if ( ! event.target.closest( 'button' ) ) {
1530 + el.querySelector( 'button' ).click();
1531 + }
1532 + } );
1533 + } );
1534 + }
1244 1535 }
1245 1536
1246 1537 /**
1247 1538 * @param {HTMLElement} input
1248 1539 * @param {HTMLElement} container
1249 - * @returns {void}
1540 + * @return {void}
1250 1541 */
1251 1542 function checkFloatingLabelsForStyles( input, container ) {
1252 1543 if ( ! container ) {
1253 1544 container = input.closest( '.frm_inside_container' );
@@ -1267,18 +1558,16 @@
1267 1558 if ( firstOpt.hasAttribute( 'data-label' ) ) {
1268 1559 firstOpt.textContent = firstOpt.getAttribute( 'data-label' );
1269 1560 firstOpt.removeAttribute( 'data-label' );
1270 1561 }
1271 - } else {
1272 - if ( firstOpt.textContent ) {
1273 - firstOpt.setAttribute( 'data-label', firstOpt.textContent );
1274 - firstOpt.textContent = '';
1275 - }
1562 + } else if ( firstOpt.textContent ) {
1563 + firstOpt.setAttribute( 'data-label', firstOpt.textContent );
1564 + firstOpt.textContent = '';
1276 1565 }
1277 1566 }
1278 1567
1279 1568 /**
1280 - * @returns {void}
1569 + * @return {void}
1281 1570 */
1282 1571 function initPosClass() {
1283 1572 const positionSetting = document.getElementById( 'frm_position' );
1284 1573
@@ -1292,13 +1581,13 @@
1292 1581
1293 1582 /**
1294 1583 * Update label container classes when the label "Position" setting is changed.
1295 1584 *
1296 - * @returns {void}
1585 + * @return {void}
1297 1586 */
1298 1587 function setPosClass() {
1299 1588 /*jshint validthis:true */
1300 - let value = this.value;
1589 + let { value } = this;
1301 1590 if ( value === 'none' ) {
1302 1591 value = 'top';
1303 1592 } else if ( value === 'no_label' ) {
1304 1593 value = 'none';
@@ -1304,23 +1593,23 @@
1304 1593 value = 'none';
1305 1594 }
1306 1595
1307 1596 document.getElementById( 'frm_style_preview' ).querySelectorAll( '.frm_form_field.frm-default-label-position, #frm_sample_form .frm_form_field' ).forEach( container => {
1308 - const input = container.querySelector( ':scope > input, :scope > select, :scope > textarea' ); // Fields that support floating label should have a directly child input/textarea/select.
1597 + const input = container.querySelector( ':scope > input, :scope > select, :scope > textarea' ); // Fields that support floating label should have a directly child input/textarea/select.
1309 1598 const shouldForceTopStyling = 'inside' === value && ( ! input || 'hidden' === input.type ); // We do not want file upload to use floating labels, or inline datepickers, which both use hidden inputs.
1310 - const currentValue = shouldForceTopStyling ? 'top' : value;
1599 + const currentValue = shouldForceTopStyling ? 'top' : value;
1311 1600
1312 1601 container.classList.remove( 'frm_top_container', 'frm_left_container', 'frm_right_container', 'frm_none_container', 'frm_inside_container' );
1313 - container.classList.add( 'frm_' + currentValue + '_container' );
1602 + container.classList.add( `frm_${ currentValue }_container` );
1314 1603
1315 1604 if ( 'inside' === currentValue ) {
1316 1605 checkFloatingLabelsForStyles( input, container );
1317 1606 }
1318 - });
1607 + } );
1319 1608 }
1320 1609
1321 1610 /**
1322 - * @returns {void}
1611 + * @return {void}
1323 1612 */
1324 1613 function initFloatingLabels() {
1325 1614 [ 'focus', 'blur', 'change' ].forEach(
1326 1615 eventName => frmDom.util.documentOn(
@@ -1336,12 +1625,12 @@
1336 1625 * The signature add on expects that validateFormSubmit is callable.
1337 1626 * Without this, drawing in a signature field triggers a "Uncaught ReferenceError: frmFrontForm is not defined" error.
1338 1627 * We don't want the validation to actually triggr, so just fill in an empty function.
1339 1628 *
1340 - * @returns {void}
1629 + * @return {void}
1341 1630 */
1342 1631 function fillMissingSignatureValidationFunction() {
1343 - if ( 'undefined' === typeof window.__FRMSIG || 'undefined' !== typeof window.frmFrontForm ) {
1632 + if ( window.__FRMSIG === undefined || window.frmFrontForm !== undefined ) {
1344 1633 return;
1345 1634 }
1346 1635
1347 1636 window.frmFrontForm = { validateFormSubmit: () => {} };
@@ -1349,14 +1638,19 @@
1349 1638
1350 1639 /**
1351 1640 * Enable the datepicker in the sample form preview.
1352 1641 *
1353 - * @returns {void}
1642 + * @return {void}
1354 1643 */
1355 1644 function initDatepickerSample() {
1645 + // If flatpickr is defined, then is a recent version of Pro which handles the datepicker preview as it's a PRO feature.
1646 + if ( 'undefined' !== typeof flatpickr ) {
1647 + return;
1648 + }
1649 +
1356 1650 const $sample = jQuery( '#datepicker_sample' );
1357 1651 if ( $sample.length && 'function' === typeof $sample.datepicker ) {
1358 - $sample.datepicker({ changeMonth: true, changeYear: true });
1652 + $sample.datepicker( { changeMonth: true, changeYear: true } );
1359 1653 }
1360 1654 }
1361 1655
1362 1656 /**
@@ -1374,10 +1668,10 @@
1374 1668 return;
1375 1669 }
1376 1670
1377 1671 // Function to change the color of a select element
1378 - const changeSelectColor = ( select ) => {
1379 - if ( select.options[select.selectedIndex] && select.options[select.selectedIndex].classList.contains( 'frm-select-placeholder' ) ) {
1672 + const changeSelectColor = select => {
1673 + if ( select.options[ select.selectedIndex ]?.classList.contains( 'frm-select-placeholder' ) ) {
1380 1674 select.style.setProperty( 'color', textColorDisabled, 'important' );
1381 1675 } else {
1382 1676 select.style.color = '';
1383 1677 }
@@ -1383,15 +1677,15 @@
1383 1677 }
1384 1678 };
1385 1679
1386 1680 // Use a loop to iterate through each select element
1387 - selects.forEach( ( select ) => {
1681 + selects.forEach( select => {
1388 1682 // Apply the color change to each select element
1389 1683 changeSelectColor( select );
1390 1684
1391 1685 // Add an event listener for future changes
1392 1686 select.addEventListener( 'change', () => changeSelectColor( select ) );
1393 - });
1687 + } );
1394 1688 }
1395 1689
1396 1690 // Hook into the styleInit function in formidable_admin.js
1397 1691 wp.hooks.addAction( 'frm_style_editor_init', 'formidable', initEditPage );