( function ( $, rwmb, i18n ) { 'use strict'; /** * Extract the validation key from an input's name attribute. Usually it's the field ID, but sometimes (like for `file`), it's the field's input name. * * field[] => field // Fields with multiple values: file, checkbox list, etc. * field[1] => field // Cloneable fields * field[1][] => field // Cloneable fields with multiple values: file, checkbox list, etc. * * group[field][] => field // Group with fields with multiple values: file, checkbox list, etc. * group[field][1] => field // Group with cloneable fields * group[field][1][] => field // Group with cloneable fields with multiple values: file, checkbox list, etc. * * group[1][field][] => field // Cloneable group with fields with multiple values: file, checkbox list, etc. * group[1][field][1] => field // Cloneable group with cloneable fields * group[1][field][1][] => field // Cloneable group with cloneable fields with multiple values: file, checkbox list, etc. * * group[subgroup][field][] => field // Subgroup with fields with multiple values: file, checkbox list, etc. * group[subgroup][field][1] => field // Subgroup with cloneable fields * group[subgroup][field][1][] => field // Subgroup with cloneable fields with multiple values: file, checkbox list, etc. * * group[subgroup][1][field][] => field // Cloneable subgroup with fields with multiple values: file, checkbox list, etc. * group[subgroup][1][field][1] => field // Cloneable subgroup with cloneable fields * group[subgroup][1][field][1][] => field // Cloneable subgroup with cloneable fields with multiple values: file, checkbox list, etc. * * group[1][subgroup][field][] => field // Cloneable group with subgroup with fields with multiple values: file, checkbox list, etc. * group[1][subgroup][field][1] => field // Cloneable group with subgroup with cloneable fields * group[1][subgroup][field][1][] => field // Cloneable group with subgroup with cloneable fields with multiple values: file, checkbox list, etc. * * group[1][subgroup][1][field][] => field // Cloneable group with cloneable subgroup with fields with multiple values: file, checkbox list, etc. * group[1][subgroup][1][field][1] => field // Cloneable group with cloneable subgroup with cloneable fields * group[1][subgroup][1][field][1][] => field // Cloneable group with cloneable subgroup with cloneable fields with multiple values: file, checkbox list, etc. */ const getValidationKey = name => { // Detect name parts in format of anything[] or anything[1]. let parts = name.match( /^(.+?)(?:\[\d+\]|(?:\[\]))?$/ ); if ( parts[ 1 ] && isNaN( parts[ 1 ] ) ) { // Remove [] let words = name.match( /(\w+)|(\[\w+\])/g ); let resultArray = [ words.join( "" ) ]; // Remove characters "[" and "]". words.forEach( matchedValue => { if ( matchedValue.startsWith( "[" ) ) { resultArray.push( matchedValue.substring( 1, matchedValue.length - 1 ) ); } else { resultArray.push( matchedValue ); } } ); parts[ 0 ] = resultArray[ 0 ]; parts[ 1 ] = isNaN( resultArray[ resultArray.length - 1 ] ) ? resultArray[ resultArray.length - 1 ] : resultArray[ resultArray.length - 2 ]; } return parts.pop(); }; /** * Fix validation not working for cloneable files or fields in groups. */ $.validator.staticRules = function ( element ) { let rules = {}, validator = $.data( element.form, "validator" ); // No rules. if ( validator.settings.rules === null || Object.keys( validator.settings.rules ).length === 0 ) { return rules; } // Do not validate hidden fields. if ( element.type === 'hidden' ) { return rules; } let key = getValidationKey( element.name ); /** * Cloneable files or files in groups. * Input name is transformed into format `_file_{unique_id}` * There is also a hidden input with name `_index_{field_id}` with value `_file_{unique_id}` * * In this case, `key` is always `_file_{unique_id}` * * Note that for cloneable files, validation rule is set for `_index_{field_id}`. For files in groups, validation rule is still `{field_id}`. */ if ( element.type === 'file' && ( $( element ).closest( '.rwmb-clone' ).length > 0 || $( element ).closest( '.rwmb-group-wrapper' ).length > 0 ) ) { const $input = $( element ).closest( '.rwmb-input' ); const $indexInput = $input.find( '*[value="' + key + '"]' ); key = getValidationKey( $indexInput.attr( 'name' ) ); // Remove prefix `_index_` from input name when in groups. if ( !validator.settings.rules[ key ] && key.includes( '_index_' ) ) { key = key.slice( 7 ); } if ( validator.settings.rules[ key ] ) { // Set message for element. validator.settings.messages[ element.name ] = validator.settings.messages[ key ]; // Set rule for element. return $.validator.normalizeRule( validator.settings.rules[ key ] ) || {}; } return rules; } // For normal fields and fields in groups: set rules by their field IDs (validation keys). // Set message for element. validator.settings.messages[ element.name ] = validator.settings.messages[ key ]; // Set rule for element. return $.validator.normalizeRule( validator.settings.rules[ key ] ) || {}; }; /** * Make jQuery Validation works with multiple inputs with same names. * Need for file, image fields where users can upload multiple files with same input names. * * @link https://stackoverflow.com/q/931687/371240 */ $.validator.prototype.checkForm = function () { this.prepareForm(); for ( var i = 0, elements = ( this.currentElements = this.elements() ); elements[ i ]; i++ ) { if ( this.findByName( elements[ i ].name ).length !== undefined && this.findByName( elements[ i ].name ).length > 1 ) { for ( var cnt = 0; cnt < this.findByName( elements[ i ].name ).length; cnt++ ) { this.check( this.findByName( elements[ i ].name )[ cnt ] ); } } else { this.check( elements[ i ] ); } } return this.valid(); }; class Validation { constructor( formSelector ) { this.$form = $( formSelector ); this.validationElements = this.$form.find( '.rwmb-validation' ); this.showAsterisks(); this.getSettings(); } init() { this.$form // Update underlying textarea before submit. // Don't use submitHandler() because form can be submitted via Ajax on the front end. .on( 'submit', function () { if ( typeof tinyMCE !== 'undefined' ) { tinyMCE.triggerSave(); } } ) .validate( this.settings ); } showAsterisks() { this.validationElements.each( function () { const data = $( this ).data( 'validation' ); $.each( data.rules, function ( k, v ) { if ( !v[ 'required' ] ) { return; } let $el = $( '[name="' + k + '"]' ); if ( !$el.length ) { $el = $( '[name*="[' + k + ']"]' ); // Subfields in groups. } if ( $el.length ) { $el.closest( '.rwmb-input' ).siblings( '.rwmb-label' ).find( 'label' ).append( '*' ); } } ); } ); } getSettings() { this.settings = { ignore: ':not(.rwmb-media,.rwmb-image_select,.rwmb-wysiwyg,.rwmb-color,.rwmb-map,.rwmb-osm,.rwmb-switch,[class|="rwmb"])', errorPlacement: function ( error, element ) { error.appendTo( element.closest( '.rwmb-input' ) ); }, errorClass: 'rwmb-error', errorElement: 'p', invalidHandler: this.invalidHandler.bind( this ) }; // Gather all validation rules. var that = this; this.validationElements.each( function () { $.extend( true, that.settings, $( this ).data( 'validation' ) ); } ); } invalidHandler() { this.showMessage(); // Group field will automatically expand and show an error warning when collapsing for ( var i = 0; i < this.$form.data( 'validator' ).errorList.length; i++ ) { $( '#' + this.$form.data( 'validator' ).errorList[ i ].element.id ).closest( '.rwmb-group-collapsed' ).removeClass( 'rwmb-group-collapsed' ); } // Custom event for showing error fields inside tabs/hidden divs. Use setTimeout() to run after error class is added to inputs. var that = this; setTimeout( function () { that.$form.trigger( 'after_validate' ); }, 200 ); } showMessage() { // Re-enable the submit ( publish/update ) button and hide the ajax indicator $( '#publish' ).removeClass( 'button-primary-disabled' ); $( '#ajax-loading' ).attr( 'style', '' ); $( '#rwmb-validation-message' ).remove(); this.$form.before( '

' + i18n.message + '

' ); } }; class GutenbergValidation extends Validation { init() { var that = this, editor = wp.data.dispatch( 'core/editor' ), savePost = editor.savePost; // Reference original method. if ( that.settings ) { that.$form.validate( that.settings ); } // Change the editor method. editor.savePost = function ( options = {} ) { // Bypass the validation when previewing in Gutenberg. if ( typeof options === 'object' && options.isPreview ) { return savePost( options ); } // Must call savePost() here instead of in submitHandler() because the form has inline onsubmit callback. if ( that.$form.valid() ) { return savePost( options ); } }; } showMessage() { wp.data.dispatch( 'core/notices' ).createErrorNotice( i18n.message, { id: 'meta-box-validation', isDismissible: true } ); } }; // Run on document ready. function init() { if ( rwmb.isGutenberg ) { var advanced = new GutenbergValidation( '.metabox-location-advanced' ), normal = new GutenbergValidation( '.metabox-location-normal' ), side = new GutenbergValidation( '.metabox-location-side' ); side.init(); normal.init(); advanced.init(); return; } // Edit post, edit term, edit user, front-end form. var $forms = $( '#post, #edittag, #your-profile, .rwmb-form' ); $forms.each( function () { var form = new Validation( this ); form.init(); } ); }; rwmb.$document .on( 'mb_ready', init ); } )( jQuery, rwmb, rwmbValidation );