Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions classes/controllers/FrmFieldsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,34 @@ public static function load_field() {

// admin_footer never fires here, so the deferred tooltip text rides along with the html.
// Field ids are numeric, so this key can never collide with one.
$field_html['tooltips'] = FrmAppHelper::get_deferred_tooltips();
$field_html['selectOptions'] = FrmBuilderSelectHelper::get_templates();
$field_html['tooltips'] = self::get_missing_builder_definitions( FrmAppHelper::get_deferred_tooltips(), 'known_tooltips' );
$field_html['selectOptions'] = self::get_missing_builder_definitions( FrmBuilderSelectHelper::get_templates(), 'known_select_options' );

echo json_encode( $field_html );

wp_die();
}

/**
* Omit definitions the browser has already received, including from the initial page.
*
* @since x.x
*
* @param array $definitions Definitions keyed by their content hashes.
* @param string $param POST parameter containing comma-separated known hashes.
*
* @return array
*/
private static function get_missing_builder_definitions( $definitions, $param ) {
$known_keys = FrmAppHelper::get_post_param( $param, '', 'sanitize_text_field' );

if ( ! is_string( $known_keys ) || '' === $known_keys ) {
return $definitions;
}

return array_diff_key( $definitions, array_fill_keys( explode( ',', $known_keys ), true ) );
}

/**
* Get a form's fields, as the form builder sees them, indexed by field id.
*
Expand Down
2 changes: 1 addition & 1 deletion js/formidable_admin.js

Large diffs are not rendered by default.

60 changes: 55 additions & 5 deletions js/src/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2774,6 +2774,9 @@ window.frmAdminBuildJS = function() {
action: 'frm_load_field',
field_ids: fieldIds,
form_id: thisFormId,
// Only acknowledge received definitions so concurrent batches remain independent.
known_tooltips: Object.keys( frm_admin_js.tooltips || {} ).join( ',' ),
known_select_options: Object.keys( frm_admin_js.selectOptions || {} ).join( ',' ),
nonce: frmGlobal.nonce
},
success: async response => {
Expand Down Expand Up @@ -2922,6 +2925,7 @@ window.frmAdminBuildJS = function() {
placeholderSpinnerObserver?.disconnect();
renumberPageBreaks();
maybeHideQuantityProductFieldOption();
scheduleBulkOptionsOverlay();
}

function addFieldClick() {
Expand Down Expand Up @@ -4347,17 +4351,59 @@ window.frmAdminBuildJS = function() {
}
}

function initBulkOptionsOverlay() {
/*jshint validthis:true */
const $info = initModal( '#frm-bulk-modal', '700px' );
if ( $info === false ) {
let bulkOptionsOverlay;
let bulkOptionsOverlayScheduled = false;

/**
* Create the dialog once, either during idle time or on its first click.
*
* @since x.x
* @return {jQuery|boolean} The dialog, or false when its markup is absent.
*/
function initializeBulkOptionsOverlay() {
if ( bulkOptionsOverlay === undefined ) {
bulkOptionsOverlay = initModal( '#frm-bulk-modal', '700px' );
}
return bulkOptionsOverlay;
}

/**
* Wait for page assets and field rendering before scheduling optional dialog work.
*
* @since x.x
* @return {void}
*/
function scheduleBulkOptionsOverlay() {
if ( bulkOptionsOverlayScheduled ) {
return;
}
bulkOptionsOverlayScheduled = true;

const schedule = () => {
if ( window.requestIdleCallback ) {
window.requestIdleCallback( initializeBulkOptionsOverlay );
} else {
setTimeout( initializeBulkOptionsOverlay, 0 );
}
};
if ( document.readyState === 'complete' ) {
schedule();
} else {
window.addEventListener( 'load', schedule, { once: true } );
}
}

function initBulkOptionsOverlay() {
/*jshint validthis:true */

jQuery( '.frm-insert-preset' ).on( 'click', insertBulkPreset );

jQuery( builderForm ).on( 'click', 'a.frm-bulk-edit-link', function( event ) {
event.preventDefault();
const $info = initializeBulkOptionsOverlay();
if ( $info === false ) {
return;
}
let content = '';
const fieldId = jQuery( this ).closest( '[data-fid]' ).data( 'fid' );
const separate = usingSeparateValues( fieldId );
Expand Down Expand Up @@ -4408,7 +4454,7 @@ window.frmAdminBuildJS = function() {
}

this.classList.add( 'frm_loading_button' );
frmAdminBuild.updateOpts( fieldId, document.getElementById( 'frm_bulk_options' ).value, $info );
frmAdminBuild.updateOpts( fieldId, document.getElementById( 'frm_bulk_options' ).value, initializeBulkOptionsOverlay() );
fieldUpdated();
} );
}
Expand Down Expand Up @@ -12010,6 +12056,10 @@ window.frmAdminBuildJS = function() {
if ( frm_admin_js.pricingFieldsModal && 'object' === typeof frm_admin_js.pricingFieldsModal ) {
infoModal( frm_admin_js.pricingFieldsModal, '550px' );
}

if ( ! activeFieldLoadRequests ) {
scheduleBulkOptionsOverlay();
}
},

settingsInit() {
Expand Down
47 changes: 47 additions & 0 deletions tests/phpunit/fields/test_FrmFieldsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,53 @@
#[\PHPUnit\Framework\Attributes\CoversClass( FrmFieldsController::class )]
class test_FrmFieldsController extends FrmUnitTest {

public function test_builder_batches_omit_only_received_definitions() {
$definitions = array(
'known' => array( 'value' => 'Saved option' ),
'new' => array( 'value' => 'New option' ),
);
$_POST['known_select_options'] = 'known,unknown,known';
$_POST['known_tooltips'] = 'new';

try {
$this->assertSame(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldsController::assertSame()


The method you are trying to call is not defined, which can result in a fatal error.

array( 'new' => $definitions['new'] ),
$this->run_private_method( array( 'FrmFieldsController', 'get_missing_builder_definitions' ), array( $definitions, 'known_select_options' ) )
);
$this->assertSame(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldsController::assertSame()


The method you are trying to call is not defined, which can result in a fatal error.

array( 'known' => $definitions['known'] ),
$this->run_private_method( array( 'FrmFieldsController', 'get_missing_builder_definitions' ), array( $definitions, 'known_tooltips' ) )
);
} finally {
unset( $_POST['known_select_options'], $_POST['known_tooltips'] );
}
}

public function test_builder_batches_keep_definitions_for_older_or_malformed_requests() {
$definitions = array( 'known' => 'Tooltip text' );
unset( $_POST['known_tooltips'] );

try {
foreach ( array( null, '', array( 'known' ) ) as $known_keys ) {
if ( null !== $known_keys ) {
$_POST['known_tooltips'] = $known_keys;
}
$this->assertSame(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldsController::assertSame()


The method you are trying to call is not defined, which can result in a fatal error.

$definitions,
$this->run_private_method( array( 'FrmFieldsController', 'get_missing_builder_definitions' ), array( $definitions, 'known_tooltips' ) )
);
}

$_POST['known_tooltips'] = 'known';
$this->assertSame(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldsController::assertSame()


The method you are trying to call is not defined, which can result in a fatal error.

array(),
$this->run_private_method( array( 'FrmFieldsController', 'get_missing_builder_definitions' ), array( $definitions, 'known_tooltips' ) )
);
} finally {
unset( $_POST['known_tooltips'] );
}
}

public function test_prepare_placeholder() {
$name = 'Number';
$field = array(
Expand Down
Loading