/**
* Synchronous AJAX Requests.
*
* This jQuery plugin allows feature rich web applications to send a specified number of requests, sequentially and synchronously,
* to a given endpoint, binding the progress feedback to a jQuery UI Progressbar.
*
* This plugin acts as a wrapper for $.post, with some extra callback functions when each request succeeds or fails, plus a final
* callback function when the entire routine completes.
*
* The advantage of this approach is that the UI is not locked - so updates can be posted to the web page - and the server isn't flooded
* with 100 requests at once. Each request must complete before the next one can run.
*
* Your server-side script will be sent all data as a POST array, including POST['current_index'], telling your script what number this
* request is.
*
* @package WPZincDashboardWidget
* @author WP Zinc
*/
( function ( $ ) {
/**
* Init Synchronous Request
*
* @param object options Override Default Settings
*/
$.fn.synchronous_request = function ( options ) {
// Default Settings.
let settings = $.extend(
{
// Required.
url: '', // AJAX url.
number_requests:0, // Total number of requests that will be sent.
offset: 0, // The offset to start at.
index_increment:1, // The number to increment the current index by after each request.
action: '', // The WordPress registered AJAX action name to use for each request.
nonce: '', // WordPress nonce, which your AJAX function should validate.
ids: '', // Array of IDs or keys to iterate through, sending one with each request.
wait: 5000, // Number of milliseconds to wait.
stop_on_error: 0, // 1: stop, 0: continue and retry the same request, -1: continue but skip the failed request.
// Optional.
progress_count: '#progress-number', // DOM selector that contains successful request count.
log: '#log', // DOM selector for the log.
spinner: '#progress .spinner', // DOM selector for the spinner.
cancel_button: '.cancel', // DOM selector for the cancel button.
type: 'post', // AJAX request type.
cache: false, // Whether to cache requests.
dataType: 'json', // Response data type.
/**
* Called when an AJAX request returns a successful response.
*
* @since 1.0.0
*
* @param object response Response
* @param int currentIndex Current Index
*/
onRequestSuccess: function ( response, currentIndex ) {
// Maybe reset log if it's more than 100 lines, for UI performance.
this.maybeResetLog();
if ( response.success ) {
// Output Log.
$( 'ul', $( this.log ) ).append( '
' + ( currentIndex + 1 ) + '/' + this.number_requests + ': ' + response.data + '' );
} else {
// Something went wrong.
// Define message.
let message = ( currentIndex + 1 ) + '/' + this.number_requests + ': Response Error: ' + response.data;
switch ( this.stop_on_error ) {
// Stop sending any further requests.
case 1:
break;
// Continue, reattempting the failed request.
case 0:
message = message + '. Waiting ' + ( this.stop_on_error_pause / 1000 ) + ' seconds before reattempting this request.';
break;
// Continue, skipping the failed request.
case -1:
message = message + '. Waiting ' + ( this.stop_on_error_pause / 1000 ) + ' seconds before attempting next request.';
break;
}
// Output Log.
$( 'ul', $( this.log ) ).append( '' + message + '' );
}
// Run the next request, unless the user clicked the 'Stop Generation' button.
if ( this.cancelled == true ) {
return false;
}
// Run the next request.
return true;
},
/**
* Called when an AJAX request results in a HTTP or server error.
*
* @since 1.0.0
*/
onRequestError: function ( xhr, textStatus, e, currentIndex ) {
// Maybe reset log if it's more than 100 lines, for UI performance.
this.maybeResetLog();
// Output Log.
$( '#log ul' ).append( '' + ( currentIndex + 1 ) + '/' + settings.number_requests + ': Request Error: ' + xhr.status + ' ' + xhr.statusText + '' );
// Run the next request, unless the user clicked the cancel button.
if ( this.cancelled == true ) {
return false;
}
// Try again.
return true;
},
/**
* Change any settings configuration, which will be included in the next request.
*
* Called immediately before the next request is made, if the current request
* was successful.
*
* @since 1.0.0
*
* @param object settings Settings.
* @return object Settings
*/
updateSettings: function ( settings ) {
return settings;
},
/**
* Called when all requests have completed, or the user cancelled.
*
* @since 1.0.0
*/
onFinished: function () {
if ( this.cancelled ) {
$( 'ul', $( this.log ) ).append( 'Process cancelled by user.' );
} else {
$( 'ul', $( this.log ) ).append( 'Finished.' );
// Disable the cancel button.
$( settings.cancel_button ).attr( 'disabled', 'disabled' );
}
// Remove the spinner.
$( this.spinner ).remove();
},
/**
* If the on screen log exceeds 100 entries, clear it
* for UI / browser performance.
*
* @since 1.0.0
*/
maybeResetLog: function () {
// If the spinner exists in the log, remove it.
if ( $( 'li.spinner', $( this.log ) ).length > 0 ) {
$( 'li.spinner', $( this.log ) ).remove();
}
// If the log exceeds 100 items, reset it.
if ( $( 'ul li', $( this.log ) ).length >= 100 ) {
$( 'ul', $( this.log ) ).html( '' );
}
}
},
options
);
// Initialize Progress Bar.
progressbar = $( this ).progressbar(
{
value: 0
}
);
// Bind a listener to the cancel button.
if ( settings.cancel_button ) {
$( settings.cancel_button ).on(
'click',
function ( e ) {
e.preventDefault();
settings.cancelled = true;
// Disable the cancel button.
$( settings.cancel_button ).attr( 'disabled', 'disabled' );
}
);
}
// Initialize first request.
synchronousAjaxRequest( settings, ( -1 + Number( settings.offset ) ), progressbar, settings.progress_count, true );
};
/**
* Main function to perform an AJAX request.
*
* @since 1.0.0
*/
function synchronousAjaxRequest( settings, currentIndex, progressbar, progressCounter, isFirstRequest ) {
if ( isFirstRequest ) {
currentIndex++;
} else {
currentIndex = currentIndex + Number( settings.index_increment );
}
// If currentIndex exceeds or equals settings.number_requests, we have finished
// currentIndex is a zero based count.
if ( currentIndex > ( Number( settings.offset ) + Number( settings.number_requests ) - 1 ) ) {
// Call completion closure.
settings.onFinished();
return true;
}
// Merge data.
let data = {
action: settings.action,
nonce: settings.nonce,
id: settings.ids[ currentIndex ],
current_index: currentIndex,
index_increment:settings.index_increment,
number_requests:settings.number_requests,
offset: settings.offset,
};
let mergedData = {...data, ...settings.data};
// Send AJAX request.
$.ajax(
{
url: settings.url,
type: settings.type,
async: true,
cache: settings.cache,
dataType: settings.dataType,
data: mergedData,
success: function ( response ) {
// Call onRequestSuccess closure.
let cancelled = settings.onRequestSuccess( response, currentIndex );
// Define the count, ensuring it doesn't go above the number of requests.
let nextIndex = currentIndex + Number( settings.index_increment );
if ( nextIndex > settings.number_requests ) {
nextIndex = settings.number_requests;
}
// If the response indicates success, update the progress bar and count.
if ( response.success ) {
// Update progress bar and text count.
progressbar.progressbar( 'value', Number( ( nextIndex / settings.number_requests ) * 100 ) );
$( progressCounter ).text( nextIndex );
} else {
// If Stop on Error is enabled, call onFinished closure and exit.
if ( settings.stop_on_error == 1 ) {
settings.onFinished();
return;
}
// If Stop on Error is -1, update the progress bar and count as this request won't be retried.
if ( settings.stop_on_error == -1 ) {
progressbar.progressbar( 'value', Number( ( nextIndex / settings.number_requests ) * 100 ) );
$( progressCounter ).text( nextIndex );
}
// If Stop on Error is zero, decrement the currentIndex so the same request is attempted again.
if ( settings.stop_on_error == 0 ) {
currentIndex = currentIndex - Number( settings.index_increment );
}
}
// If false was returned from the closure, the calling script has requested we stop the loop
// Call onFinished closure and exit.
if ( ! cancelled ) {
settings.onFinished();
return;
}
// If the response indicates an error, wait the required period of time before sending the
// next request.
if ( ! response.success && settings.stop_on_error !== -1 ) {
setTimeout(
function () {
// Start next request.
synchronousAjaxRequest( settings, currentIndex, progressbar, progressCounter );
return;
},
settings.wait
);
} else {
// Call updateSettings closure.
settings = settings.updateSettings( settings );
// Start next request.
synchronousAjaxRequest( settings, currentIndex, progressbar, progressCounter );
return;
}
},
error: function (xhr, textStatus, e) {
// Call closure.
let cancelled = settings.onRequestError( xhr, textStatus, e, currentIndex );
// If Stop on Error is enabled, call onFinished closure and exit.
if ( settings.stop_on_error == 1 ) {
settings.onFinished();
return;
}
// If stop on Error is zero, decrement the currentIndex so the same request is attempted again.
if ( settings.stop_on_error == 0 ) {
currentIndex--;
}
// If false was returned from the closure, the calling script has requested we stop the loop.
// Call onFinished closure and exit.
if ( ! cancelled ) {
settings.onFinished();
return;
}
// Wait the required period of time before sending the next request.
setTimeout(
function () {
// Start next request.
synchronousAjaxRequest( settings, currentIndex, progressbar, progressCounter );
return;
},
settings.wait
);
}
}
);
}
} )( jQuery );