PNG %k25u25%fgd5n!
/**
* WoodMart Images Optimizer
* Modern, modular image optimization JavaScript
*/
(function($) {
'use strict';
// Configuration constants
const CONFIG = {
SELECTORS: {
optimizeButton: '.xts-optimizer-button',
restoreButton: '.xts-restore-button',
bulkProgress: '#bulk-progress',
bulkProgressBar: '#bulk-progress-bar',
bulkProgressContainer: '#bulk-progress-bar'
},
CLASSES: {
optimizationError: 'optimization-error',
optimizationSuccess: 'optimization-success',
optimizationCompleted: 'optimization-completed',
processing: 'processing',
restoreSuccess: 'restore-success',
restoreError: 'restore-error'
},
TIMEOUTS: {
buttonRenable: 2000,
bulkDelay: 1000,
pageReload: 2000,
restoreTransition: 800
},
MESSAGES: {
optimizing: 'Optimizing...',
restoring: 'Restoring...',
optimized: 'Optimized!',
restored: 'Restored!',
optimize: 'Optimize',
error: 'Error',
timeout: 'Timeout',
requestFailed: 'Request failed',
connectionError: 'Connection error',
restoreFailed: 'Restore failed'
},
OPTIONS: {
preventReOptimization: true, // Set to false to allow re-optimization
debugMode: false // Set to true for console debugging
}
};
/**
* Base AJAX handler class
*/
class AjaxHandler {
constructor() {
this.activeRequests = new Set();
}
/**
* Make AJAX request with consistent error handling
*/
async request(options) {
const requestId = Math.random().toString(36).substr(2, 9);
this.activeRequests.add(requestId);
const defaultOptions = {
url: xts_optimizer.ajax_url,
type: 'POST',
data: {
nonce: xts_optimizer.nonce
}
};
const mergedOptions = { ...defaultOptions, ...options };
return new Promise((resolve, reject) => {
$.ajax({
...mergedOptions,
success: (response) => {
this.activeRequests.delete(requestId);
resolve(response);
},
error: (xhr, status, error) => {
this.activeRequests.delete(requestId);
reject({ xhr, status, error });
}
});
});
}
/**
* Cancel all active requests
*/
cancelAllRequests() {
this.activeRequests.clear();
}
}
/**
* Button state manager
*/
class ButtonStateManager {
constructor(button) {
this.button = $(button);
this.originalText = this.button.text();
this.timeoutId = null;
}
setLoading(loadingText) {
this.button.prop('disabled', true).text(loadingText);
return this;
}
setSuccess(successText, cssClass) {
this.button.text(successText).addClass(cssClass);
return this;
}
setError(errorText, cssClass, tooltipText = null) {
this.button.text(errorText).addClass(cssClass);
if (tooltipText) {
this.button.attr('title', tooltipText);
}
return this;
}
enableAfterDelay(delay = CONFIG.TIMEOUTS.buttonRenable) {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.timeoutId = setTimeout(() => {
// Don't re-enable if optimization was completed successfully or still processing
if (!this.button.hasClass(CONFIG.CLASSES.optimizationCompleted) &&
!this.button.hasClass(CONFIG.CLASSES.processing)) {
this.button.prop('disabled', false);
}
this.timeoutId = null;
}, delay);
return this;
}
destroy() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
}
}
/**
* Individual image optimizer
*/
class ImageOptimizer extends AjaxHandler {
constructor() {
super();
this.bindEvents();
}
bindEvents() {
$(document).on('click', CONFIG.SELECTORS.optimizeButton, this.handleOptimize.bind(this));
}
async handleOptimize(event) {
event.preventDefault();
const button = $(event.currentTarget);
const imageId = button.data('id');
// Prevent optimization if button is disabled or already processing
if (button.prop('disabled')) {
return;
}
// Prevent optimization if already completed (if option is enabled)
if (CONFIG.OPTIONS.preventReOptimization && button.hasClass(CONFIG.CLASSES.optimizationCompleted)) {
return;
}
// Prevent multiple simultaneous optimizations of the same image
if (button.hasClass(CONFIG.CLASSES.processing)) {
return;
}
const buttonManager = new ButtonStateManager(button);
if (!imageId) {
buttonManager.setError(CONFIG.MESSAGES.error, CONFIG.CLASSES.optimizationError, 'Invalid image ID');
return;
}
// Mark as processing and disable immediately
button.addClass(CONFIG.CLASSES.processing);
buttonManager.setLoading(CONFIG.MESSAGES.optimizing);
try {
const response = await this.request({
data: {
action: 'xts_optimizer_run',
image_id: imageId,
nonce: xts_optimizer.nonce
}
});
this.handleOptimizeResponse(response, buttonManager, imageId);
} catch (error) {
this.handleOptimizeError(error, buttonManager);
} finally {
// Remove processing class
button.removeClass(CONFIG.CLASSES.processing);
// Only re-enable if optimization didn't complete successfully
if (!CONFIG.OPTIONS.preventReOptimization || !button.hasClass(CONFIG.CLASSES.optimizationCompleted)) {
buttonManager.enableAfterDelay();
}
}
}
handleOptimizeResponse(response, buttonManager, imageId) {
if (!response.success) {
buttonManager.setError(CONFIG.MESSAGES.requestFailed, CONFIG.CLASSES.optimizationError);
return;
}
const { data } = response;
const { result } = data;
if (result.error) {
this.handleApiError(result, buttonManager);
} else {
this.handleOptimizeSuccess(result, buttonManager, imageId);
}
}
handleApiError(result, buttonManager) {
const errorText = result.message?.includes('timed out')
? CONFIG.MESSAGES.timeout
: CONFIG.MESSAGES.error;
const fullErrorText = `${errorText}: ${result.message || 'Optimization failed'}`;
const tooltipText = `Error: ${result.message || 'Unknown error'}`;
buttonManager.setError(fullErrorText, CONFIG.CLASSES.optimizationError, tooltipText);
}
handleOptimizeSuccess(result, buttonManager, imageId) {
let displayText = CONFIG.MESSAGES.optimized;
// Add compression info
const compressionPercentage = result.compression_percentage ||
result.optimization?.compression_ratio;
if (compressionPercentage) {
displayText += ` (${compressionPercentage}% smaller)`;
}
// Add replacement status
if (result.replacement_error === false) {
displayText += ' & Replaced';
} else if (result.replacement_error === true) {
displayText = 'Optimized (Replace Failed)';
}
buttonManager.setSuccess(displayText, CONFIG.CLASSES.optimizationSuccess);
// Disable the button permanently after successful optimization (if option is enabled)
if (CONFIG.OPTIONS.preventReOptimization) {
buttonManager.button.prop('disabled', true).addClass(CONFIG.CLASSES.optimizationCompleted);
}
// Add restore button if backup was created
if (result.backup_created) {
this.addRestoreButton(buttonManager.button, imageId, result.backup_filename);
}
}
addRestoreButton(button, imageId, backupFilename) {
const restoreButton = $(`
<br>
<a href="#" class="xts-restore-button"
data-id="${imageId}"
title="Restore from: ${backupFilename || 'backup'}">
Restore Backup
</a>
`);
button.parent().append(restoreButton);
}
handleOptimizeError(error, buttonManager) {
const errorMessage = error.error || 'Connection error';
const tooltipText = `Connection error: ${errorMessage}`;
buttonManager.setError(CONFIG.MESSAGES.connectionError, CONFIG.CLASSES.optimizationError, tooltipText);
}
}
/**
* Image restore handler
*/
class ImageRestorer extends AjaxHandler {
constructor() {
super();
this.bindEvents();
}
bindEvents() {
$(document).on('click', CONFIG.SELECTORS.restoreButton, this.handleRestore.bind(this));
}
async handleRestore(event) {
event.preventDefault();
const button = $(event.currentTarget);
const imageId = button.data('id');
// Prevent restore if button is disabled or already processing
if (button.prop('disabled') || button.hasClass(CONFIG.CLASSES.processing)) {
return;
}
if (!this.confirmRestore()) {
return;
}
if (!imageId) {
this.showRestoreError(button, 'Invalid image ID');
return;
}
// Mark as processing and disable immediately
button.addClass(CONFIG.CLASSES.processing);
const buttonManager = new ButtonStateManager(button);
buttonManager.setLoading(CONFIG.MESSAGES.restoring);
try {
const response = await this.request({
data: {
action: 'xts_optimizer_restore',
image_id: imageId,
nonce: xts_optimizer.nonce
}
});
this.handleRestoreResponse(response, buttonManager, imageId);
} catch (error) {
this.handleRestoreError(error, buttonManager);
} finally {
// Remove processing class
button.removeClass(CONFIG.CLASSES.processing);
buttonManager.enableAfterDelay();
}
}
confirmRestore() {
return confirm('Are you sure you want to restore the original image? This will replace the optimized version.');
}
handleRestoreResponse(response, buttonManager, imageId) {
if (response.success) {
buttonManager.setSuccess(CONFIG.MESSAGES.restored, CONFIG.CLASSES.restoreSuccess);
// Delay the UI updates slightly for better user feedback
setTimeout(() => {
this.resetOptimizeButton(imageId);
this.removeOptimizationInfo(imageId);
this.removeRestoreButton(buttonManager.button);
}, CONFIG.TIMEOUTS.restoreTransition);
} else {
const errorMessage = response.data || 'Unknown error';
buttonManager.setError(CONFIG.MESSAGES.restoreFailed, CONFIG.CLASSES.restoreError, `Error: ${errorMessage}`);
}
}
handleRestoreError(error, buttonManager) {
const errorMessage = error.error || 'Connection error';
buttonManager.setError(CONFIG.MESSAGES.connectionError, CONFIG.CLASSES.restoreError, `Connection error: ${errorMessage}`);
}
schedulePageReload() {
// Legacy method - no longer used since we reset buttons instead of reloading
setTimeout(() => {
window.location.reload();
}, CONFIG.TIMEOUTS.pageReload);
}
showRestoreError(button, message) {
const buttonManager = new ButtonStateManager(button);
buttonManager.setError(CONFIG.MESSAGES.restoreFailed, CONFIG.CLASSES.restoreError, message);
}
resetOptimizeButton(imageId) {
if (CONFIG.OPTIONS.debugMode) {
console.log('Resetting optimize button for image ID:', imageId);
}
// Find the optimize button for this image
let optimizeButton = $(CONFIG.SELECTORS.optimizeButton).filter(`[data-id="${imageId}"]`);
if (optimizeButton.length) {
if (CONFIG.OPTIONS.debugMode) {
console.log('Found existing optimize button, resetting state');
}
// Reset existing button
optimizeButton.fadeOut(200, function() {
// Reset button to initial state
$(this)
.prop('disabled', false)
.removeClass([
CONFIG.CLASSES.optimizationSuccess,
CONFIG.CLASSES.optimizationError,
CONFIG.CLASSES.optimizationCompleted,
CONFIG.CLASSES.processing
].join(' '))
.attr('title', '') // Clear any error tooltips
.text(CONFIG.MESSAGES.optimize) // Reset to original text
.fadeIn(200); // Fade back in with new state
});
} else {
if (CONFIG.OPTIONS.debugMode) {
console.log('No existing optimize button found, creating new one');
}
// No optimize button exists, create a new one
// Find the container for this image's optimizer buttons
const buttonsContainer = $(`.xts-optimizer-buttons`).has(`[data-id="${imageId}"]`);
if (buttonsContainer.length) {
if (CONFIG.OPTIONS.debugMode) {
console.log('Found buttons container, adding new optimize button');
}
// Create a new optimize button
const newOptimizeButton = $(`
<a href="#" class="xts-optimizer-button" data-id="${imageId}">
${CONFIG.MESSAGES.optimize}
</a>
`);
// Add it to the beginning of the container with animation
newOptimizeButton.hide().prependTo(buttonsContainer).fadeIn(400);
// Note: Event binding is handled by event delegation in bindEvents(), so no need to rebind
}
}
}
removeRestoreButton(restoreButton) {
// Remove the restore button and its preceding <br> tag with animation
const $restoreButton = $(restoreButton);
const $prevBr = $restoreButton.prev('br');
$restoreButton.fadeOut(300, function() {
if ($prevBr.length) {
$prevBr.remove();
}
$restoreButton.remove();
});
}
removeOptimizationInfo(imageId) {
// Find and remove the optimization info display for this image
const buttonsContainer = $(`.xts-optimizer-buttons`).has(`[data-id="${imageId}"]`);
if (buttonsContainer.length) {
// Find and remove optimization info within the same parent container
const optimizationInfo = buttonsContainer.siblings('.xts-optimization-info').add(
buttonsContainer.parent().find('.xts-optimization-info')
);
if (optimizationInfo.length) {
optimizationInfo.fadeOut(300, function() {
$(this).remove();
});
}
}
}
}
/**
* Bulk optimization handler
*/
class BulkOptimizer extends AjaxHandler {
constructor() {
super();
this.isProcessing = false;
this.processed = 0;
this.errors = 0;
this.successes = 0;
}
initialize() {
if (typeof window.woodmartBulkOptimize === 'undefined') {
return;
}
this.batchId = window.woodmartBulkOptimize.batch_id;
this.total = window.woodmartBulkOptimize.total;
this.resetCounters();
this.startProcessing();
}
resetCounters() {
this.processed = 0;
this.errors = 0;
this.successes = 0;
}
async startProcessing() {
if (this.isProcessing) {
return;
}
this.isProcessing = true;
await this.processBatch(0);
}
async processBatch(offset) {
try {
const response = await this.request({
data: {
action: 'xts_optimizer_bulk',
batch_id: this.batchId,
offset: offset,
nonce: xts_optimizer.nonce
}
});
this.handleBatchResponse(response);
} catch (error) {
this.handleBatchError();
}
}
handleBatchResponse(response) {
if (!response.success) {
this.showBatchError(response.data || 'Unknown error');
return;
}
const { data } = response;
this.processed = data.processed;
// Count results
data.results.forEach(result => {
if (result.success) {
this.successes++;
} else {
this.errors++;
}
});
this.updateProgress(data.progress_percentage);
if (data.complete) {
this.handleBatchCompletion();
} else {
this.scheduleContinuation();
}
}
updateProgress(progressPercentage) {
$(CONFIG.SELECTORS.bulkProgress).text(this.processed);
$(`${CONFIG.SELECTORS.bulkProgressBar} div`).css('width', `${progressPercentage}%`);
}
handleBatchCompletion() {
const message = this.buildCompletionMessage();
const progressContainer = $(CONFIG.SELECTORS.bulkProgressBar).parent();
progressContainer.html(`<p style="color: green;"><strong>${message}</strong></p>`);
this.schedulePageRedirect();
}
buildCompletionMessage() {
let message = `Bulk optimization complete! ${this.successes} images optimized`;
if (this.errors > 0) {
message += `, ${this.errors} errors occurred`;
}
return message;
}
scheduleContinuation() {
setTimeout(() => {
this.processBatch(this.processed);
}, CONFIG.TIMEOUTS.bulkDelay);
}
schedulePageRedirect() {
setTimeout(() => {
const url = window.location.href.replace(/[?&](bulk_optimize|image_count)=[^&]*/g, '');
window.location.href = url;
}, CONFIG.TIMEOUTS.pageReload);
}
showBatchError(errorMessage) {
const progressContainer = $(CONFIG.SELECTORS.bulkProgressBar).parent();
progressContainer.html(`<p style="color: red;"><strong>Error: ${errorMessage}</strong></p>`);
}
handleBatchError() {
const progressContainer = $(CONFIG.SELECTORS.bulkProgressBar).parent();
progressContainer.html('<p style="color: red;"><strong>Network error occurred during bulk optimization.</strong></p>');
}
}
/**
* Main application controller
*/
class WoodMartOptimizer {
constructor() {
this.optimizer = new ImageOptimizer();
this.restorer = new ImageRestorer();
this.bulkOptimizer = new BulkOptimizer();
}
initialize() {
this.bulkOptimizer.initialize();
}
destroy() {
// Clean up any resources
this.optimizer.cancelAllRequests();
this.restorer.cancelAllRequests();
this.bulkOptimizer.cancelAllRequests();
}
}
// Initialize when DOM is ready
$(document).ready(() => {
const app = new WoodMartOptimizer();
app.initialize();
// Clean up on page unload
$(window).on('beforeunload', () => {
app.destroy();
});
});
})(jQuery);