PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / 6.9.1
Secure Custom Fields v6.9.1
6.9.1 6.9.0 6.8.9 6.8.7 6.8.8 6.8.6 6.8.4 6.8.5 trunk 6.4.0-beta1 6.4.0-beta2 6.4.1 6.4.1-beta3 6.4.1-beta4 6.4.1-beta5 6.4.1-beta6 6.4.1-beta7 6.4.2 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.6.0 6.7.0 6.7.1 6.8.0 6.8.1 6.8.2 6.8.3
secure-custom-fields / assets / src / js / _acf-validation.js
secure-custom-fields / assets / src / js Last commit date
bindings 6 months ago commands 1 week ago pro 1 week ago _acf-compatibility.js 1 year ago _acf-condition-types.js 7 months ago _acf-condition.js 1 year ago _acf-conditions.js 1 year ago _acf-field-accordion.js 6 months ago _acf-field-button-group.js 7 months ago _acf-field-checkbox.js 1 month ago _acf-field-color-picker.js 7 months ago _acf-field-date-picker.js 1 month ago _acf-field-date-time-picker.js 1 month ago _acf-field-file.js 1 month ago _acf-field-google-map.js 6 months ago _acf-field-icon-picker.js 1 month ago _acf-field-image.js 1 month ago _acf-field-link.js 1 year ago _acf-field-oembed.js 1 month ago _acf-field-page-link.js 1 year ago _acf-field-post-object.js 1 year ago _acf-field-radio.js 1 month ago _acf-field-range.js 1 year ago _acf-field-relationship.js 1 month ago _acf-field-select.js 1 month ago _acf-field-tab.js 3 weeks ago _acf-field-taxonomy.js 7 months ago _acf-field-time-picker.js 1 month ago _acf-field-true-false.js 1 month ago _acf-field-url.js 1 year ago _acf-field-user.js 1 year ago _acf-field-wysiwyg.js 1 month ago _acf-field.js 1 year ago _acf-fields.js 1 year ago _acf-helpers.js 1 year ago _acf-hooks.js 1 year ago _acf-internal-post-type.js 7 months ago _acf-media.js 1 week ago _acf-modal.js 1 year ago _acf-model.js 1 year ago _acf-notice.js 7 months ago _acf-panel.js 1 year ago _acf-popup.js 7 months ago _acf-postbox.js 1 year ago _acf-screen.js 10 months ago _acf-select2.js 1 year ago _acf-tinymce.js 6 months ago _acf-tooltip.js 10 months ago _acf-unload.js 1 year ago _acf-validation.js 1 month ago _acf.js 7 months ago _browse-fields-modal.js 7 months ago _field-group-compatibility.js 1 year ago _field-group-conditions.js 1 year ago _field-group-field.js 3 months ago _field-group-fields.js 10 months ago _field-group-locations.js 1 year ago _field-group-settings.js 1 year ago _field-group.js 10 months ago acf-escaped-html-notice.js 1 year ago acf-field-group.js 1 year ago acf-input.js 1 year ago acf-internal-post-type.js 1 year ago acf.js 1 year ago
_acf-validation.js
1315 lines
1 ( function ( $, undefined ) {
2 /**
3 * Validator
4 *
5 * The model for validating forms
6 *
7 * @date 4/9/18
8 * @since ACF 5.7.5
9 *
10 * @param void
11 * @return void
12 */
13 var Validator = acf.Model.extend( {
14 /** @var string The model identifier. */
15 id: 'Validator',
16
17 /** @var object The model data. */
18 data: {
19 /** @var array The form errors. */
20 errors: [],
21
22 /** @var object The form notice. */
23 notice: null,
24
25 /** @var string The form status. loading, invalid, valid */
26 status: '',
27 },
28
29 /** @var object The model events. */
30 events: {
31 'changed:status': 'onChangeStatus',
32 },
33
34 /**
35 * addErrors
36 *
37 * Adds errors to the form.
38 *
39 * @date 4/9/18
40 * @since ACF 5.7.5
41 *
42 * @param array errors An array of errors.
43 * @return void
44 */
45 addErrors: function ( errors ) {
46 errors.map( this.addError, this );
47 },
48
49 /**
50 * addError
51 *
52 * Adds and error to the form.
53 *
54 * @date 4/9/18
55 * @since ACF 5.7.5
56 *
57 * @param object error An error object containing input and message.
58 * @return void
59 */
60 addError: function ( error ) {
61 this.data.errors.push( error );
62 },
63
64 /**
65 * hasErrors
66 *
67 * Returns true if the form has errors.
68 *
69 * @date 4/9/18
70 * @since ACF 5.7.5
71 *
72 * @param void
73 * @return bool
74 */
75 hasErrors: function () {
76 return this.data.errors.length;
77 },
78
79 /**
80 * clearErrors
81 *
82 * Removes any errors.
83 *
84 * @date 4/9/18
85 * @since ACF 5.7.5
86 *
87 * @param void
88 * @return void
89 */
90 clearErrors: function () {
91 return ( this.data.errors = [] );
92 },
93
94 /**
95 * getErrors
96 *
97 * Returns the forms errors.
98 *
99 * @date 4/9/18
100 * @since ACF 5.7.5
101 *
102 * @param void
103 * @return array
104 */
105 getErrors: function () {
106 return this.data.errors;
107 },
108
109 /**
110 * getFieldErrors
111 *
112 * Returns the forms field errors.
113 *
114 * @date 4/9/18
115 * @since ACF 5.7.5
116 *
117 * @param void
118 * @return array
119 */
120 getFieldErrors: function () {
121 // vars
122 var errors = [];
123 var inputs = [];
124
125 // loop
126 this.getErrors().map( function ( error ) {
127 // bail early if global
128 if ( ! error.input ) return;
129
130 // update if exists
131 var i = inputs.indexOf( error.input );
132 if ( i > -1 ) {
133 errors[ i ] = error;
134
135 // update
136 } else {
137 errors.push( error );
138 inputs.push( error.input );
139 }
140 } );
141
142 // return
143 return errors;
144 },
145
146 /**
147 * getGlobalErrors
148 *
149 * Returns the forms global errors (errors without a specific input).
150 *
151 * @date 4/9/18
152 * @since ACF 5.7.5
153 *
154 * @param void
155 * @return array
156 */
157 getGlobalErrors: function () {
158 // return array of errors that contain no input
159 return this.getErrors().filter( function ( error ) {
160 return ! error.input;
161 } );
162 },
163
164 /**
165 * showErrors
166 *
167 * Displays all errors for this form.
168 *
169 * @since ACF 5.7.5
170 *
171 * @param {string} [location=before] - The location to add the error, before or after the input. Default before. Since ACF 6.3.
172 * @return void
173 */
174 showErrors: function ( location = 'before' ) {
175 // bail early if no errors
176 if ( ! this.hasErrors() ) {
177 return;
178 }
179
180 // vars
181 var fieldErrors = this.getFieldErrors();
182 var globalErrors = this.getGlobalErrors();
183
184 // vars
185 var errorCount = 0;
186 var $scrollTo = false;
187
188 // loop
189 fieldErrors.map( function ( error ) {
190 // get input
191 var $input = this.$( '[name="' + error.input + '"]' ).first();
192
193 // if $_POST value was an array, this $input may not exist
194 if ( ! $input.length ) {
195 $input = this.$( '[name^="' + error.input + '"]' ).first();
196 }
197
198 // bail early if input doesn't exist
199 if ( ! $input.length ) {
200 return;
201 }
202
203 // increase
204 errorCount++;
205
206 // get field
207 var field = acf.getClosestField( $input );
208
209 // make sure the postbox containing this field is not hidden by screen options
210 ensureFieldPostBoxIsVisible( field.$el );
211
212 // show error
213 field.showError( error.message, location );
214
215 // set $scrollTo
216 if ( ! $scrollTo ) {
217 $scrollTo = field.$el;
218 }
219 }, this );
220
221 // errorMessage
222 var errorMessage = acf.__( 'Validation failed' );
223 globalErrors.map( function ( error ) {
224 errorMessage += '. ' + error.message;
225 } );
226 if ( errorCount == 1 ) {
227 errorMessage += '. ' + acf.__( '1 field requires attention' );
228 } else if ( errorCount > 1 ) {
229 errorMessage +=
230 '. ' +
231 acf
232 .__( '%d fields require attention' )
233 .replace( '%d', errorCount );
234 }
235
236 // notice
237 if ( this.has( 'notice' ) ) {
238 this.get( 'notice' ).update( {
239 type: 'error',
240 text: errorMessage,
241 } );
242 } else {
243 var notice = acf.newNotice( {
244 type: 'error',
245 text: errorMessage,
246 target: this.$el,
247 } );
248 this.set( 'notice', notice );
249 }
250
251 // If in a modal, don't try to scroll.
252 if ( this.$el.parents( '.acf-popup-box' ).length ) {
253 return;
254 }
255
256 // if no $scrollTo, set to message
257 if ( ! $scrollTo ) {
258 $scrollTo = this.get( 'notice' ).$el;
259 }
260
261 // timeout
262 setTimeout( function () {
263 $( 'html, body' ).animate(
264 {
265 scrollTop:
266 $scrollTo.offset().top - $( window ).height() / 2,
267 },
268 500
269 );
270 }, 10 );
271 },
272
273 /**
274 * onChangeStatus
275 *
276 * Update the form class when changing the 'status' data
277 *
278 * @date 4/9/18
279 * @since ACF 5.7.5
280 *
281 * @param object e The event object.
282 * @param jQuery $el The form element.
283 * @param string value The new status.
284 * @param string prevValue The old status.
285 * @return void
286 */
287 onChangeStatus: function ( e, $el, value, prevValue ) {
288 this.$el.removeClass( 'is-' + prevValue ).addClass( 'is-' + value );
289 },
290
291 /**
292 * validate
293 *
294 * Validates the form via AJAX.
295 *
296 * @date 4/9/18
297 * @since ACF 5.7.5
298 *
299 * @param object args A list of settings to customize the validation process.
300 * @return bool True if the form is valid.
301 */
302 validate: function ( args ) {
303 // default args
304 args = acf.parseArgs( args, {
305 // trigger event
306 event: false,
307
308 // reset the form after submit
309 reset: false,
310
311 // loading callback
312 loading: function () {},
313
314 // complete callback
315 complete: function () {},
316
317 // failure callback
318 failure: function () {},
319
320 // success callback
321 success: function ( $form ) {
322 $form.submit();
323 },
324 } );
325
326 // return true if is valid - allows form submit
327 if ( this.get( 'status' ) == 'valid' ) {
328 return true;
329 }
330
331 // return false if is currently validating - prevents form submit
332 if ( this.get( 'status' ) == 'validating' ) {
333 return false;
334 }
335
336 // return true if no ACF fields exist (no need to validate)
337 if ( ! this.$( '.acf-field' ).length ) {
338 return true;
339 }
340
341 // if event is provided, create a new success callback.
342 if ( args.event ) {
343 var event = $.Event( null, args.event );
344 args.success = function () {
345 acf.enableSubmit( $( event.target ) ).trigger( event );
346 };
347 }
348
349 // action for 3rd party
350 acf.doAction( 'validation_begin', this.$el );
351
352 // lock form
353 acf.lockForm( this.$el );
354
355 // loading callback
356 args.loading( this.$el, this );
357
358 // update status
359 this.set( 'status', 'validating' );
360
361 // success callback
362 var onSuccess = function ( json ) {
363 // validate
364 if ( ! acf.isAjaxSuccess( json ) ) {
365 return;
366 }
367
368 // filter
369 var data = acf.applyFilters(
370 'validation_complete',
371 json.data,
372 this.$el,
373 this
374 );
375
376 // add errors
377 if ( ! data.valid ) {
378 this.addErrors( data.errors );
379 }
380 };
381
382 // complete
383 var onComplete = function () {
384 // unlock form
385 acf.unlockForm( this.$el );
386
387 // failure
388 if ( this.hasErrors() ) {
389 // update status
390 this.set( 'status', 'invalid' );
391
392 // action
393 acf.doAction( 'validation_failure', this.$el, this );
394
395 // display errors
396 this.showErrors();
397
398 // failure callback
399 args.failure( this.$el, this );
400
401 // success
402 } else {
403 // update status
404 this.set( 'status', 'valid' );
405
406 // remove previous error message
407 if ( this.has( 'notice' ) ) {
408 this.get( 'notice' ).update( {
409 type: 'success',
410 text: acf.__( 'Validation successful' ),
411 timeout: 1000,
412 } );
413 }
414
415 // action
416 acf.doAction( 'validation_success', this.$el, this );
417 acf.doAction( 'submit', this.$el );
418
419 // success callback (submit form)
420 args.success( this.$el, this );
421
422 // lock form
423 acf.lockForm( this.$el );
424
425 // reset
426 if ( args.reset ) {
427 this.reset();
428 }
429 }
430
431 // complete callback
432 args.complete( this.$el, this );
433
434 // clear errors
435 this.clearErrors();
436 };
437
438 // serialize form data
439 var data = acf.serialize( this.$el );
440 data.action = 'acf/validate_save_post';
441
442 // ajax
443 $.ajax( {
444 url: acf.get( 'ajaxurl' ),
445 data: acf.prepareForAjax( data, true ),
446 type: 'post',
447 dataType: 'json',
448 context: this,
449 success: onSuccess,
450 complete: onComplete,
451 } );
452
453 // return false to fail validation and allow AJAX
454 return false;
455 },
456
457 /**
458 * setup
459 *
460 * Called during the constructor function to setup this instance
461 *
462 * @date 4/9/18
463 * @since ACF 5.7.5
464 *
465 * @param jQuery $form The form element.
466 * @return void
467 */
468 setup: function ( $form ) {
469 // set $el
470 this.$el = $form;
471 },
472
473 /**
474 * reset
475 *
476 * Rests the validation to be used again.
477 *
478 * @date 6/9/18
479 * @since ACF 5.7.5
480 *
481 * @param void
482 * @return void
483 */
484 reset: function () {
485 // reset data
486 this.set( 'errors', [] );
487 this.set( 'notice', null );
488 this.set( 'status', '' );
489
490 // unlock form
491 acf.unlockForm( this.$el );
492 },
493 } );
494
495 /**
496 * getValidator
497 *
498 * Returns the instance for a given form element.
499 *
500 * @date 4/9/18
501 * @since ACF 5.7.5
502 *
503 * @param jQuery $el The form element.
504 * @return object
505 */
506 var getValidator = function ( $el ) {
507 // instantiate
508 var validator = $el.data( 'acf' );
509 if ( ! validator ) {
510 validator = new Validator( $el );
511 }
512
513 // return
514 return validator;
515 };
516
517 /**
518 * A helper function to generate a Validator for a block form, so .addErrors can be run via block logic.
519 *
520 * @since ACF 6.3
521 *
522 * @param $el The jQuery block form wrapper element.
523 * @return bool
524 */
525 acf.getBlockFormValidator = function ( $el ) {
526 return getValidator( $el );
527 };
528
529 /**
530 * A helper function for the Validator.validate() function.
531 * Returns true if form is valid, or fetches a validation request and returns false.
532 *
533 * @since ACF 5.6.9
534 *
535 * @param object args A list of settings to customize the validation process.
536 * @return bool
537 */
538 acf.validateForm = function ( args ) {
539 return getValidator( args.form ).validate( args );
540 };
541
542 /**
543 * acf.enableSubmit
544 *
545 * Enables a submit button and returns the element.
546 *
547 * @date 30/8/18
548 * @since ACF 5.7.4
549 *
550 * @param jQuery $submit The submit button.
551 * @return jQuery
552 */
553 acf.enableSubmit = function ( $submit ) {
554 return $submit.removeClass( 'disabled' ).removeAttr( 'disabled' );
555 };
556
557 /**
558 * acf.disableSubmit
559 *
560 * Disables a submit button and returns the element.
561 *
562 * @date 30/8/18
563 * @since ACF 5.7.4
564 *
565 * @param jQuery $submit The submit button.
566 * @return jQuery
567 */
568 acf.disableSubmit = function ( $submit ) {
569 return $submit.addClass( 'disabled' ).attr( 'disabled', true );
570 };
571
572 /**
573 * acf.showSpinner
574 *
575 * Shows the spinner element.
576 *
577 * @date 4/9/18
578 * @since ACF 5.7.5
579 *
580 * @param jQuery $spinner The spinner element.
581 * @return jQuery
582 */
583 acf.showSpinner = function ( $spinner ) {
584 $spinner.addClass( 'is-active' ); // add class (WP > 4.2)
585 $spinner.css( 'display', 'inline-block' ); // css (WP < 4.2)
586 return $spinner;
587 };
588
589 /**
590 * acf.hideSpinner
591 *
592 * Hides the spinner element.
593 *
594 * @date 4/9/18
595 * @since ACF 5.7.5
596 *
597 * @param jQuery $spinner The spinner element.
598 * @return jQuery
599 */
600 acf.hideSpinner = function ( $spinner ) {
601 $spinner.removeClass( 'is-active' ); // add class (WP > 4.2)
602 $spinner.css( 'display', 'none' ); // css (WP < 4.2)
603 return $spinner;
604 };
605
606 /**
607 * acf.lockForm
608 *
609 * Locks a form by disabling its primary inputs and showing a spinner.
610 *
611 * @date 4/9/18
612 * @since ACF 5.7.5
613 *
614 * @param jQuery $form The form element.
615 * @return jQuery
616 */
617 acf.lockForm = function ( $form ) {
618 // vars
619 var $wrap = findSubmitWrap( $form );
620 var $submit = $wrap
621 .find( '.button, [type="submit"]' )
622 .not( '.acf-nav, .acf-repeater-add-row' );
623 var $spinner = $wrap.find( '.spinner, .acf-spinner' );
624
625 // hide all spinners (hides the preview spinner)
626 acf.hideSpinner( $spinner );
627
628 // lock
629 acf.disableSubmit( $submit );
630 acf.showSpinner( $spinner.last() );
631 return $form;
632 };
633
634 /**
635 * acf.unlockForm
636 *
637 * Unlocks a form by enabling its primary inputs and hiding all spinners.
638 *
639 * @date 4/9/18
640 * @since ACF 5.7.5
641 *
642 * @param jQuery $form The form element.
643 * @return jQuery
644 */
645 acf.unlockForm = function ( $form ) {
646 // vars
647 var $wrap = findSubmitWrap( $form );
648 var $submit = $wrap
649 .find( '.button, [type="submit"]' )
650 .not( '.acf-nav, .acf-repeater-add-row' );
651 var $spinner = $wrap.find( '.spinner, .acf-spinner' );
652
653 // unlock
654 acf.enableSubmit( $submit );
655 acf.hideSpinner( $spinner );
656 return $form;
657 };
658
659 /**
660 * findSubmitWrap
661 *
662 * An internal function to find the 'primary' form submit wrapping element.
663 *
664 * @date 4/9/18
665 * @since ACF 5.7.5
666 *
667 * @param jQuery $form The form element.
668 * @return jQuery
669 */
670 var findSubmitWrap = function ( $form ) {
671 // default post submit div
672 var $wrap = $form.find( '#submitdiv' );
673 if ( $wrap.length ) {
674 return $wrap;
675 }
676
677 // 3rd party publish box
678 var $wrap = $form.find( '#submitpost' );
679 if ( $wrap.length ) {
680 return $wrap;
681 }
682
683 // term, user
684 var $wrap = $form.find( 'p.submit' ).last();
685 if ( $wrap.length ) {
686 return $wrap;
687 }
688
689 // front end form
690 var $wrap = $form.find( '.acf-form-submit' );
691 if ( $wrap.length ) {
692 return $wrap;
693 }
694
695 // ACF 6.2 options page modal
696 var $wrap = $( '#acf-create-options-page-form .acf-actions' );
697 if ( $wrap.length ) {
698 return $wrap;
699 }
700
701 // ACF 6.0+ headerbar submit
702 var $wrap = $( '.acf-headerbar-actions' );
703 if ( $wrap.length ) {
704 return $wrap;
705 }
706
707 // default
708 return $form;
709 };
710
711 /**
712 * A debounced function to trigger a form submission.
713 *
714 * @date 15/07/2020
715 * @since ACF 5.9.0
716 *
717 * @param type Var Description.
718 * @return type Description.
719 */
720 var submitFormDebounced = acf.debounce( function ( $form ) {
721 $form.submit();
722 } );
723
724 /**
725 * Ensure field is visible for validation errors
726 *
727 * @date 20/10/2021
728 * @since ACF 5.11.0
729 */
730 var ensureFieldPostBoxIsVisible = function ( $el ) {
731 // Find the postbox element containing this field.
732 var $postbox = $el.parents( '.acf-postbox' );
733 if ( $postbox.length ) {
734 var acf_postbox = acf.getPostbox( $postbox );
735 if ( acf_postbox && acf_postbox.isHiddenByScreenOptions() ) {
736 // Rather than using .show() here, we don't want the field to appear next reload.
737 // So just temporarily show the field group so validation can complete.
738 acf_postbox.$el.removeClass( 'hide-if-js' );
739 acf_postbox.$el.css( 'display', '' );
740 }
741 }
742 };
743
744 /**
745 * Ensure metaboxes which contain browser validation failures are visible.
746 *
747 * @date 20/10/2021
748 * @since ACF 5.11.0
749 */
750 var ensureInvalidFieldVisibility = function () {
751 // Load each ACF input field and check it's browser validation state.
752 var $inputs = $( '.acf-field input' );
753 $inputs.each( function () {
754 if ( ! this.checkValidity() ) {
755 // Field is invalid, so we need to make sure it's metabox is visible.
756 ensureFieldPostBoxIsVisible( $( this ) );
757 }
758 } );
759 };
760
761 /**
762 * acf.validation
763 *
764 * Global validation logic
765 *
766 * @date 4/4/18
767 * @since ACF 5.6.9
768 *
769 * @param void
770 * @return void
771 */
772
773 acf.validation = new acf.Model( {
774 /** @var string The model identifier. */
775 id: 'validation',
776
777 /** @var bool The active state. Set to false before 'prepare' to prevent validation. */
778 active: true,
779
780 /** @var string The model initialize time. */
781 wait: 'prepare',
782
783 /** @var object The model actions. */
784 actions: {
785 ready: 'addInputEvents',
786 append: 'addInputEvents',
787 },
788
789 /** @var object The model events. */
790 events: {
791 'click input[type="submit"]': 'onClickSubmit',
792 'click button[type="submit"]': 'onClickSubmit',
793 'click #save-post': 'onClickSave',
794 'submit form#post': 'onSubmitPost',
795 'submit form': 'onSubmit',
796 },
797
798 /**
799 * initialize
800 *
801 * Called when initializing the model.
802 *
803 * @date 4/9/18
804 * @since ACF 5.7.5
805 *
806 * @param void
807 * @return void
808 */
809 initialize: function () {
810 // check 'validation' setting
811 if ( ! acf.get( 'validation' ) ) {
812 this.active = false;
813 this.actions = {};
814 this.events = {};
815 }
816 },
817
818 /**
819 * enable
820 *
821 * Enables validation.
822 *
823 * @date 4/9/18
824 * @since ACF 5.7.5
825 *
826 * @param void
827 * @return void
828 */
829 enable: function () {
830 this.active = true;
831 },
832
833 /**
834 * disable
835 *
836 * Disables validation.
837 *
838 * @date 4/9/18
839 * @since ACF 5.7.5
840 *
841 * @param void
842 * @return void
843 */
844 disable: function () {
845 this.active = false;
846 },
847
848 /**
849 * reset
850 *
851 * Rests the form validation to be used again
852 *
853 * @date 6/9/18
854 * @since ACF 5.7.5
855 *
856 * @param jQuery $form The form element.
857 * @return void
858 */
859 reset: function ( $form ) {
860 getValidator( $form ).reset();
861 },
862
863 /**
864 * addInputEvents
865 *
866 * Adds 'invalid' event listeners to HTML inputs.
867 *
868 * @date 4/9/18
869 * @since ACF 5.7.5
870 *
871 * @param jQuery $el The element being added / readied.
872 * @return void
873 */
874 addInputEvents: function ( $el ) {
875 // Bug exists in Safari where custom "invalid" handling prevents draft from saving.
876 if ( acf.get( 'browser' ) === 'safari' ) return;
877
878 // vars
879 var $inputs = $( '.acf-field [name]', $el );
880
881 // check
882 if ( $inputs.length ) {
883 this.on( $inputs, 'invalid', 'onInvalid' );
884 }
885 },
886
887 /**
888 * onInvalid
889 *
890 * Callback for the 'invalid' event.
891 *
892 * @date 4/9/18
893 * @since ACF 5.7.5
894 *
895 * @param object e The event object.
896 * @param jQuery $el The input element.
897 * @return void
898 */
899 onInvalid: function ( e, $el ) {
900 // prevent default
901 // - prevents browser error message
902 // - also fixes chrome bug where 'hidden-by-tab' field throws focus error
903 e.preventDefault();
904
905 // vars
906 var $form = $el.closest( 'form' );
907
908 // check form exists
909 if ( $form.length ) {
910 // add error to validator
911 getValidator( $form ).addError( {
912 input: $el.attr( 'name' ),
913 message: acf.strEscape( e.target.validationMessage ),
914 } );
915
916 // trigger submit on $form
917 // - allows for "save", "preview" and "publish" to work
918 submitFormDebounced( $form );
919 }
920 },
921
922 /**
923 * onClickSubmit
924 *
925 * Callback when clicking submit.
926 *
927 * @date 4/9/18
928 * @since ACF 5.7.5
929 *
930 * @param object e The event object.
931 * @param jQuery $el The input element.
932 * @return void
933 */
934 onClickSubmit: function ( e, $el ) {
935 // Some browsers (safari) force their browser validation before our AJAX validation,
936 // so we need to make sure fields are visible earlier than showErrors()
937 ensureInvalidFieldVisibility();
938
939 // store the "click event" for later use in this.onSubmit()
940 this.set( 'originalEvent', e );
941 },
942
943 /**
944 * onClickSave
945 *
946 * Set ignore to true when saving a draft.
947 *
948 * @date 4/9/18
949 * @since ACF 5.7.5
950 *
951 * @param object e The event object.
952 * @param jQuery $el The input element.
953 * @return void
954 */
955 onClickSave: function ( e, $el ) {
956 this.set( 'ignore', true );
957 },
958
959 /**
960 * onSubmitPost
961 *
962 * Callback when the 'post' form is submit.
963 *
964 * @date 5/3/19
965 * @since ACF 5.7.13
966 *
967 * @param object e The event object.
968 * @param jQuery $el The input element.
969 * @return void
970 */
971 onSubmitPost: function ( e, $el ) {
972 // Check if is preview.
973 if ( $( 'input#wp-preview' ).val() === 'dopreview' ) {
974 // Ignore validation.
975 this.set( 'ignore', true );
976
977 // Unlock form to fix conflict with core "submit.edit-post" event causing all submit buttons to be disabled.
978 acf.unlockForm( $el );
979 }
980 },
981
982 /**
983 * onSubmit
984 *
985 * Callback when the form is submit.
986 *
987 * @date 4/9/18
988 * @since ACF 5.7.5
989 *
990 * @param object e The event object.
991 * @param jQuery $el The input element.
992 * @return void
993 */
994 onSubmit: function ( e, $el ) {
995 // Allow form to submit if...
996 if (
997 // Validation has been disabled.
998 ! this.active ||
999 // Or this event is to be ignored.
1000 this.get( 'ignore' ) ||
1001 // Or this event has already been prevented.
1002 e.isDefaultPrevented()
1003 ) {
1004 // Return early and call reset function.
1005 return this.allowSubmit();
1006 }
1007
1008 // Validate form.
1009 var valid = acf.validateForm( {
1010 form: $el,
1011 event: this.get( 'originalEvent' ),
1012 } );
1013
1014 // If not valid, stop event to prevent form submit.
1015 if ( ! valid ) {
1016 e.preventDefault();
1017 }
1018 },
1019
1020 /**
1021 * allowSubmit
1022 *
1023 * Resets data during onSubmit when the form is allowed to submit.
1024 *
1025 * @date 5/3/19
1026 * @since ACF 5.7.13
1027 *
1028 * @param void
1029 * @return void
1030 */
1031 allowSubmit: function () {
1032 // Reset "ignore" state.
1033 this.set( 'ignore', false );
1034
1035 // Reset "originalEvent" object.
1036 this.set( 'originalEvent', false );
1037
1038 // Return true
1039 return true;
1040 },
1041 } );
1042
1043 var gutenbergValidation = new acf.Model( {
1044 wait: 'prepare',
1045 initialize: function () {
1046 // Bail early if not Gutenberg.
1047 if ( ! acf.isGutenberg() ) {
1048 return;
1049 }
1050
1051 // Customize the editor.
1052 this.customizeEditor();
1053 },
1054 customizeEditor: function () {
1055 // Extract vars.
1056 var editor = wp.data.dispatch( 'core/editor' );
1057 var editorSelect = wp.data.select( 'core/editor' );
1058 var notices = wp.data.dispatch( 'core/notices' );
1059
1060 // Backup original method.
1061 var savePost = editor.savePost;
1062
1063 // Listen for changes to post status and perform actions:
1064 // a) Enable validation for "publish" action.
1065 // b) Remember last non "publish" status used for restoring after validation fail.
1066 var useValidation = false;
1067 var lastPostStatus = '';
1068 wp.data.subscribe( function () {
1069 var postStatus =
1070 editorSelect.getEditedPostAttribute( 'status' );
1071 useValidation =
1072 postStatus === 'publish' || postStatus === 'future';
1073 lastPostStatus =
1074 postStatus !== 'publish' ? postStatus : lastPostStatus;
1075 } );
1076
1077 // Create validation version.
1078 editor.savePost = function ( options ) {
1079 options = options || {};
1080
1081 // Backup vars.
1082 var _this = this;
1083 var _args = arguments;
1084 // Perform validation within a Promise.
1085 return new Promise( function ( resolve, reject ) {
1086 if ( typeof acf.gutenbergEditPost === 'function' ) {
1087 acf.gutenbergEditPost();
1088 }
1089
1090 // Bail early if is autosave or preview.
1091 if ( options.isAutosave || options.isPreview ) {
1092 return resolve( 'Validation ignored (autosave).' );
1093 }
1094
1095 // Bail early if validation is not needed.
1096 if ( ! useValidation ) {
1097 return resolve( 'Validation ignored (draft).' );
1098 }
1099
1100 // Check if we've currently got an ACF block selected which is failing validation, but might not be presented yet.
1101 if ( 'undefined' !== typeof acf.blockInstances ) {
1102 const selectedBlockId = wp.data
1103 .select( 'core/block-editor' )
1104 .getSelectedBlockClientId();
1105
1106 if (
1107 selectedBlockId &&
1108 selectedBlockId in acf.blockInstances
1109 ) {
1110 const acfBlockState =
1111 acf.blockInstances[ selectedBlockId ];
1112
1113 if ( acfBlockState.validation_errors ) {
1114 // Deselect the block to show the error and lock the save.
1115 acf.debug(
1116 'Rejecting save because the block editor has a invalid ACF block selected.'
1117 );
1118 notices.createErrorNotice(
1119 acf.__(
1120 'An ACF Block on this page requires attention before you can save.'
1121 ),
1122 {
1123 id: 'acf-validation',
1124 isDismissible: true,
1125 }
1126 );
1127
1128 wp.data
1129 .dispatch( 'core/editor' )
1130 .lockPostSaving(
1131 'acf/block/' + selectedBlockId
1132 );
1133 wp.data
1134 .dispatch( 'core/block-editor' )
1135 .selectBlock( false );
1136
1137 return reject(
1138 'ACF Validation failed for selected block.'
1139 );
1140 }
1141 }
1142 }
1143
1144 // Recursive function to check all blocks (including nested innerBlocks) for ACF validation errors
1145 function checkBlocksForErrors( blocks ) {
1146 const errors = [];
1147 return new Promise( function ( resolve ) {
1148 // Iterate through each block
1149 blocks.forEach( ( block ) => {
1150 // If this block has nested blocks, recursively check them
1151 if ( block.innerBlocks.length > 0 ) {
1152 checkBlocksForErrors(
1153 block.innerBlocks
1154 ).then( ( hasError ) => {
1155 if ( hasError ) {
1156 return resolve( true );
1157 }
1158 } );
1159 }
1160
1161 // Check if this block has an ACF error attribute
1162 if ( block.attributes.hasAcfError ) {
1163 // Check if the publish panel is open and close it if so
1164 const publishPanel =
1165 document.getElementsByClassName(
1166 'editor-post-publish-panel'
1167 )[ 0 ];
1168 if ( publishPanel ) {
1169 // See https://github.com/WordPress/secure-custom-fields/pull/272
1170 // `togglePublishSidebar` was only introduced in WP 6.6, it should be optional
1171 wp.data
1172 .dispatch( 'core/editor' )
1173 .togglePublishSidebar?.();
1174 }
1175
1176 // Add block to errors array
1177 errors.push( block );
1178
1179 // Dispatch a custom event to notify about the block with validation error
1180 document.dispatchEvent(
1181 new CustomEvent(
1182 'acf/block/has-error',
1183 {
1184 detail: {
1185 acfBlocksWithValidationErrors:
1186 block.clientId,
1187 },
1188 }
1189 )
1190 );
1191
1192 // Log debug message
1193 acf.debug(
1194 'Rejecting save because the block editor has a invalid ACF block selected.'
1195 );
1196
1197 // Resolve with true (error found)
1198 return resolve( true );
1199 }
1200 } );
1201
1202 // If errors were found, select the first one
1203 if ( errors.length > 0 ) {
1204 const blockClientId = errors[ 0 ].clientId;
1205 wp.data
1206 .dispatch( 'core/block-editor' )
1207 .selectBlock( blockClientId );
1208 }
1209
1210 // No errors found, resolve with false
1211 return resolve( false );
1212 } );
1213 }
1214
1215 // Call the function with all blocks from the editor
1216 checkBlocksForErrors(
1217 wp.data.select( 'core/block-editor' ).getBlocks()
1218 ).then( ( hasError ) => {
1219 // If errors were found
1220 if ( hasError ) {
1221 // Display an error notice
1222 notices.createErrorNotice(
1223 acf.__(
1224 'An ACF Block on this page requires attention before you can save.'
1225 ),
1226 {
1227 id: 'acf-blocks-validation',
1228 isDismissible: true,
1229 }
1230 );
1231
1232 // Reject the save operation
1233 return reject( 'ACF Block Validation failed' );
1234 }
1235 } );
1236
1237 // Validate the editor form.
1238 var valid = acf.validateForm( {
1239 form: $( '#wpbody-content > .block-editor' ),
1240 reset: true,
1241 complete: function ( $form, validator ) {
1242 // Always unlock the form after AJAX.
1243 editor.unlockPostSaving( 'acf' );
1244 },
1245 failure: function ( $form, validator ) {
1246 // Get validation error and append to Gutenberg notices.
1247 var notice = validator.get( 'notice' );
1248 var action = validator.get( 'action' );
1249 if (
1250 action &&
1251 'object' === typeof action &&
1252 action.label &&
1253 action.url
1254 ) {
1255 notices.createErrorNotice(
1256 notice.get( 'text', {
1257 id: 'acf-validation',
1258 isDismissible: true,
1259 actions: [
1260 {
1261 label: action.label,
1262 url: action.url,
1263 },
1264 ],
1265 } )
1266 );
1267 } else {
1268 notices.createErrorNotice(
1269 notice.get( 'text' ),
1270 {
1271 id: 'acf-validation',
1272 isDismissible: true,
1273 }
1274 );
1275 }
1276 notice.remove();
1277 // Restore last non "publish" status.
1278 if ( lastPostStatus ) {
1279 editor.editPost( {
1280 status: lastPostStatus,
1281 } );
1282 }
1283
1284 // Reject promise and prevent savePost().
1285 reject( 'Validation failed.' );
1286 },
1287 success: function () {
1288 notices.removeNotice( 'acf-validation' );
1289
1290 // Resolve promise and allow savePost().
1291 resolve( 'Validation success.' );
1292 },
1293 } );
1294
1295 // Resolve promise and allow savePost() if no validation is needed.
1296 if ( valid ) {
1297 resolve( 'Validation bypassed.' );
1298
1299 // Otherwise, lock the form and wait for AJAX response.
1300 } else {
1301 editor.lockPostSaving( 'acf' );
1302 }
1303 } ).then(
1304 function () {
1305 return savePost.apply( _this, _args );
1306 },
1307 ( err ) => {
1308 // Nothing to do here, user is alerted of validation issues.
1309 }
1310 );
1311 };
1312 },
1313 } );
1314 } )( jQuery );
1315