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