Cropper js usage

Here’s how to implement Cropper.js with a tile-triggered modal upload in WordPress, using Bootstrap for the modal (which works well with Cropper.js controls):

1. Enqueue Required Scripts & Styles

function enqueue_cropper_assets() {
    // Bootstrap CSS (for modal)
    wp_enqueue_style('bootstrap-css', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css');
    
    // Cropper.js CSS
    wp_enqueue_style('cropper-css', 'https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.css');
    
    // Bootstrap JS (for modal)
    wp_enqueue_script('bootstrap-js', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js', ['jquery'], null, true);
    
    // Cropper.js
    wp_enqueue_script('cropper-js', 'https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.js', [], null, true);
    
    // Custom JS for handling Cropper in modal
    wp_enqueue_script('custom-cropper', get_stylesheet_directory_uri() . '/js/custom-cropper.js', ['jquery', 'cropper-js'], null, true);
}
add_action('wp_enqueue_scripts', 'enqueue_cropper_assets');

2. Create the Tile + Modal HTML (Shortcode)

add_shortcode('image_upload_tile', 'image_upload_tile_shortcode');
function image_upload_tile_shortcode() {
    if (!is_user_logged_in()) return '<p>Please log in to upload images.</p>';
    
    ob_start();
    ?>
    <!-- Tile Trigger -->
    <div class="upload-tile" data-bs-toggle="modal" data-bs-target="#cropperModal">
        <span>+ Upload Image</span>
    </div>

    <!-- Bootstrap Modal -->
    <div class="modal fade" id="cropperModal" tabindex="-1" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Crop & Upload</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <!-- Cropper Container -->
                    <div class="img-container">
                        <img id="cropper-image" src="" alt="">
                    </div>
                    <!-- Hidden File Input -->
                    <input type="file" id="image-input" accept="image/*" style="display: none;">
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                    <button type="button" class="btn btn-primary" id="upload-cropped">Upload</button>
                </div>
            </div>
        </div>
    </div>
    <?php
    return ob_get_clean();
}

3. JavaScript (custom-cropper.js)

jQuery(document).ready(function($) {
    let cropper;
    const modal = new bootstrap.Modal(document.getElementById('cropperModal'));
    
    // When tile is clicked, trigger hidden file input
    $('.upload-tile').click(function() {
        $('#image-input').click();
    });
    
    // When file is selected, initialize Cropper
    $('#image-input').change(function(e) {
        if (e.target.files.length) {
            const file = e.target.files[0];
            const reader = new FileReader();
            
            reader.onload = function(event) {
                $('#cropper-image').attr('src', event.target.result);
                modal.show();
                
                // Initialize Cropper after modal is shown
                $('#cropperModal').on('shown.bs.modal', function() {
                    const image = document.getElementById('cropper-image');
                    cropper = new Cropper(image, {
                        aspectRatio: 1, // Square crop (adjust as needed)
                        viewMode: 1,
                        autoCropArea: 0.8,
                    });
                });
            };
            reader.readAsDataURL(file);
        }
    });
    
    // Handle upload
    $('#upload-cropped').click(function() {
        if (cropper) {
            // Get cropped canvas
            const canvas = cropper.getCroppedCanvas({
                width: 800,  // Set desired output width
                height: 800, // Set desired output height
            });
            
            // Convert to Blob for AJAX upload
            canvas.toBlob(function(blob) {
                const formData = new FormData();
                formData.append('action', 'handle_cropped_upload');
                formData.append('cropped_image', blob, 'upload.jpg');
                formData.append('security', my_ajax_object.nonce); // Localized nonce
                
                $.ajax({
                    url: my_ajax_object.ajaxurl,
                    type: 'POST',
                    data: formData,
                    processData: false,
                    contentType: false,
                    success: function(response) {
                        modal.hide();
                        alert('Upload successful!');
                    },
                });
            }, 'image/jpeg', 0.9); // 0.9 = quality (0-1)
        }
    });
    
    // Destroy Cropper when modal closes
    $('#cropperModal').on('hidden.bs.modal', function() {
        if (cropper) {
            cropper.destroy();
            $('#image-input').val(''); // Reset file input
        }
    });
});

4. PHP AJAX Handler (in functions.php)

// Localize AJAX URL and nonce
add_action('wp_enqueue_scripts', 'localize_cropper_script');
function localize_cropper_script() {
    wp_localize_script('custom-cropper', 'my_ajax_object', [
        'ajaxurl' => admin_url('admin-ajax.php'),
        'nonce'   => wp_create_nonce('cropper_upload_nonce'),
    ]);
}

// Handle AJAX upload
add_action('wp_ajax_handle_cropped_upload', 'handle_cropped_upload');
function handle_cropped_upload() {
    check_ajax_referer('cropper_upload_nonce', 'security');
    
    if (!is_user_logged_in()) wp_send_json_error('Not logged in');
    
    $user_id = get_current_user_id();
    $upload = wp_upload_bits($_FILES['cropped_image']['name'], null, file_get_contents($_FILES['cropped_image']['tmp_name']));
    
    if ($upload['error']) wp_send_json_error('Upload failed');
    
    $attachment_id = wp_insert_attachment([
        'post_mime_type' => $_FILES['cropped_image']['type'],
        'post_title'    => sanitize_file_name($_FILES['cropped_image']['name']),
        'post_content'  => '',
        'post_status'   => 'inherit',
    ], $upload['file']);
    
    require_once(ABSPATH . 'wp-admin/includes/image.php');
    wp_update_attachment_metadata($attachment_id, wp_generate_attachment_metadata($attachment_id, $upload['file']));
    
    // Update user meta
    update_user_meta($user_id, 'custom_cropped_image_id', $attachment_id);
    wp_send_json_success();
}

Why Bootstrap Modal Works Well

  1. Built-in Responsiveness: Works on all devices.
  2. Focus Trap: Keeps user interaction within the cropper.
  3. Easy Trigger: Simple data-bs-toggle attribute for the tile.
  4. Clean Backdrop: Dims the rest of the page, focusing attention on cropping.

Alternative Approach

If you don’t want Bootstrap, use WordPress’s native wp_enqueue_media() + a <dialog> element, but Bootstrap provides better cross-browser support for modals.