Redesigned JetFormBuilder Media Upload Field with Progress Bar & Compression
Redesigned JetFormBuilder Media Upload Field with Progress Bar & Compression

When building forms in WordPress with JetForm Builder, the file upload field often feels like it could use a little extra polish. The default design works, but it doesn’t always match your branding, and features like progress tracking or image compression aren’t built in.

That’s why we’ve rolled out a completely redesigned media upload field that puts you in control — visually, functionally, and technically — all from your WordPress dashboard.

Here’s a deep dive into what’s new.

🎨 A Brand-New Design, Fully Customizable

The old “Choose File” button is gone. In its place, you’ll find a modern, responsive upload field that feels like part of your site’s design.

  • Change button colors, text, alignment, and borders.

  • Control padding, margins, radius, and even hover effects.

  • Adjust error styles (background, text color, font size).

Over 90% of the design can now be customized in the backend — no need to dig into CSS unless you want to.

📊 Real Network Progress Bar

Nobody likes uploading files in the dark. With this update, users now see a true network progress bar for every file they upload.

  • Uploads show percentage progress in real time.

  • Each file displays its own preview (image thumbnail or document icon).

  • Users can remove files before submission if needed.

This makes the form feel faster, smoother, and more trustworthy.

🗜 Built-In Image Compression

Large images can cause long uploads and failed submissions. To solve this, we’ve added a toggleable image compression option:

  • Compress images automatically before upload.

  • Resize oversized images based on max width/height settings.

  • Reduce file size while maintaining good quality.

👉 Note: Image compression currently works only on desktop browsers, not mobile.

You can easily switch this feature on or off from the backend.

				
					function jfbm_defaults() {
    return [
        'target_css_class'        => 'media_image',
        'upload_style'            => 'drag_drop', 
        'button_bg_color'         => '#8b5cf6',
        'button_text_color'       => '#ffffff',
        'button_alignment'        => 'left',
        'upload_text'             => 'Click to Upload',
        'upload_text_color'       => '#8b5cf6',
        'sub_text'                => 'or drag and drop',
        'sub_text_color'          => '#64748b',
        'box_padding'             => '40px 20px',
        'box_margin'              => '0 0 1rem 0',
        'box_border_color'        => '#ddd',
        'box_border_radius'       => '12px',
        'primary_color'           => '#8b5cf6',
        'progress_color'          => '#8b5cf6',
        'item_bg_color'           => '#f8fafc',
        'file_text_color'         => '#334155',
        'file_text_weight'        => '500',
        'filename_trim_length'    => 20,
        'remove_icon_color'       => '#cbd5e1',
        'remove_icon_size'        => '18px',
        'upload_status_text'      => '{count} file(s) uploading...',
        'total_status_text'       => '{count} file(s) uploaded',
        'scroll_threshold'        => 6,
        'max_files_error'         => 'You can only upload a maximum of {max} file(s)',
        'max_size_error'          => 'File "{name}" exceeds the maximum allowed size of {size}',
        'file_type_error'         => 'File type "{type}" is not allowed',
        'upload_failed_error'     => 'Upload failed. Please try again.',
        // Error styling
        'error_bg_color'          => '#fecaca',
        'error_text_color'        => '#ffffff',
        'error_font_size'         => '14px',
        'error_border_radius'     => '6px',
        'error_padding'           => '12px 16px',
        'error_auto_hide_duration'=> 4,
        // Compression
        'enable_compression'      => 0,
        'max_width'               => 4096,
        'max_height'              => 4096,
        'quality'                 => 85,
        'keep_original_dimensions'=> 0,// ✅ NEW: when 1, never resize, only re-encode using 'keep_original_dimensions'=> 0,//
        'drag_over_bg_color'      => '#f5f3ff',
        'drag_over_border_color'  => '#8b5cf6',
    ];
}
function jfbm_opts() {
    return array_merge(jfbm_defaults(), (array) get_option('jfbm_settings', []));
}

/** ----------------------------------------------------------------
 *  Admin: menu + settings
 *  ---------------------------------------------------------------- */
add_action('admin_menu', function () {
    add_menu_page(
        'JFB Media Field',
        'JFB Media Field',
        'manage_options',
        'jfbm-settings',
        'jfbm_settings_page',
        'dashicons-upload',
        80
    );
});
add_action('admin_init', function () {
    register_setting('jfbm_settings_group', 'jfbm_settings');
});

function jfbm_settings_page() {
    $opt = jfbm_opts();
    ?>
    <div class="wrap" id="jfbm-settings-page">
      <h1 class="jfbm-page-title">JFB Media Field Styling</h1>
      
        

        <h2>Design</h2>
        <table class="form-table">
          <tr><th>Target CSS Class</th>
            <td>
              &lt;input type=&quot;text&quot; name=&quot;jfbm_settings[target_css_class]&quot; value=&quot;" class="regular-text" /&gt;
              <br><small>Only apply styling to media fields with this CSS class name (e.g., "media_image")</small>
            </td>
          </tr>
          <tr><th>Upload Style</th>
            <td>
              <div class="jfbm-radio-group">
                <label class="jfbm-radio">
                  &lt;input type=&quot;radio&quot; name=&quot;jfbm_settings[upload_style]&quot; value=&quot;drag_drop&quot; &gt;
                  <span class="jfbm-radio-mark"></span>
                  <span>Drag and Drop (Default)</span>
                </label>
                <label class="jfbm-radio">
                  &lt;input type=&quot;radio&quot; name=&quot;jfbm_settings[upload_style]&quot; value=&quot;button&quot; &gt;
                  <span class="jfbm-radio-mark"></span>
                  <span>Choose File Button Only</span>
                </label>
              </div>
            </td>
          </tr>
          <tr><th>Button Background Color</th>
            <td>
              <div class="jfbm-color-field">
                &lt;input type=&quot;color&quot; name=&quot;jfbm_settings[button_bg_color]&quot; value=&quot;"&gt;
                <code></code>
              </div>
            </td>
          </tr>
          <tr><th>Button Text Color</th>
            <td>
              <div class="jfbm-color-field">
                &lt;input type=&quot;color&quot; name=&quot;jfbm_settings[button_text_color]&quot; value=&quot;"&gt;
                <code></code>
              </div>
            </td>
          </tr>
          <tr><th>Button Alignment</th>
            <td>
              
                &lt;option value=&quot;left&quot;   &gt;Left
                &lt;option value=&quot;center&quot; &gt;Center
                &lt;option value=&quot;right&quot;  &gt;Right
              
            </td>
          </tr>
          <tr><th>Upload Text</th>
            <td>&lt;input type=&quot;text&quot; name=&quot;jfbm_settings[upload_text]&quot; value=&quot;" class="regular-text" /&gt;</td>
          </tr>
          <tr><th>Upload Text Color</th>
            <td>
              <div class="jfbm-color-field">
                &lt;input type=&quot;color&quot; name=&quot;jfbm_settings[upload_text_color]&quot; value=&quot;"&gt;
                <code></code>
              </div>
            </td>
          </tr>
<tr><th>Error Background Color</th>
  <td>
    <div class="jfbm-color-field">
      &lt;input type=&quot;color&quot; name=&quot;jfbm_settings[error_bg_color]&quot; value=&quot;"&gt;
      <code></code>
    </div>
  </td>
</tr>
<tr><th>Error Text Color</th>
  <td>
    <div class="jfbm-color-field">
      &lt;input type=&quot;color&quot; name=&quot;jfbm_settings[error_text_color]&quot; value=&quot;"&gt;
      <code></code>
    </div>
  </td>
</tr>

          <tr><th>Box Padding</th>
            <td>&lt;input type=&quot;text&quot; name=&quot;jfbm_settings[box_padding]&quot; value=&quot;" class="regular-text" /&gt;</td>
          </tr>
          <tr><th>Box Margin</th>
            <td>&lt;input type=&quot;text&quot; name=&quot;jfbm_settings[box_margin]&quot; value=&quot;" class="regular-text" /&gt;</td>
          </tr>
          <tr><th>Box Border Color</th>
            <td>
              <div class="jfbm-color-field">
                &lt;input type=&quot;color&quot; name=&quot;jfbm_settings[box_border_color]&quot; value=&quot;"&gt;
                <code></code>
              </div>
            </td>
          </tr>
          <tr><th>Box Border Radius</th>
            <td>&lt;input type=&quot;text&quot; name=&quot;jfbm_settings[box_border_radius]&quot; value=&quot;" class="regular-text" /&gt;</td>
          </tr>
          <tr><th>Progress Color</th>
            <td>
              <div class="jfbm-color-field">
                &lt;input type=&quot;color&quot; name=&quot;jfbm_settings[progress_color]&quot; value=&quot;"&gt;
                <code></code>
              </div>
            </td>
          </tr>
          <tr><th>Item Background Color</th>
            <td>
              <div class="jfbm-color-field">
                &lt;input type=&quot;color&quot; name=&quot;jfbm_settings[item_bg_color]&quot; value=&quot;"&gt;
                <code></code>
              </div>
            </td>
          </tr>
          <tr><th>File Text Color</th>
            <td>
              <div class="jfbm-color-field">
                &lt;input type=&quot;color&quot; name=&quot;jfbm_settings[file_text_color]&quot; value=&quot;"&gt;
                <code></code>
              </div>
            </td>
          </tr>
          <tr><th>File Text Weight</th>
            <td>&lt;input type=&quot;text&quot; name=&quot;jfbm_settings[file_text_weight]&quot; value=&quot;" class="regular-text" /&gt;</td>
          </tr>
          <tr><th>Filename Trim Length</th>
            <td>&lt;input type=&quot;number&quot; name=&quot;jfbm_settings[filename_trim_length]&quot; value=&quot;" class="small-text" /&gt; characters</td>
          </tr>
          <tr><th>Remove Icon Color</th>
            <td>
              <div class="jfbm-color-field">
                &lt;input type=&quot;color&quot; name=&quot;jfbm_settings[remove_icon_color]&quot; value=&quot;"&gt;
                <code></code>
              </div>
            </td>
          </tr>
          <tr><th>Remove Icon Size</th>
            <td>&lt;input type=&quot;text&quot; name=&quot;jfbm_settings[remove_icon_size]&quot; value=&quot;" class="regular-text" /&gt;</td>
          </tr>
          <tr><th>Scroll Threshold</th>
            <td>&lt;input type=&quot;number&quot; name=&quot;jfbm_settings[scroll_threshold]&quot; value=&quot;" class="small-text" /&gt; items</td>
          </tr>
          <tr><th>Drag Over Background Color</th>
            <td>
              <div class="jfbm-color-field">
                &lt;input type=&quot;color&quot; name=&quot;jfbm_settings[drag_over_bg_color]&quot; value=&quot;"&gt;
                <code></code>
              </div>
            </td>
          </tr>
          <tr><th>Drag Over Border Color</th>
            <td>
              <div class="jfbm-color-field">
                &lt;input type=&quot;color&quot; name=&quot;jfbm_settings[drag_over_border_color]&quot; value=&quot;"&gt;
                <code></code>
              </div>
            </td>
          </tr>
        </table>

        <h2>Translations</h2>
        <table class="form-table">
          <tr><th>Uploading Status Text</th>
            <td>
              &lt;input type=&quot;text&quot; name=&quot;jfbm_settings[upload_status_text]&quot; value=&quot;" class="regular-text" /&gt;<br>
              <small>Use <code>{count}</code> for number of files.</small>
            </td>
          </tr>
          <tr><th>Total Status Text</th>
            <td>
              &lt;input type=&quot;text&quot; name=&quot;jfbm_settings[total_status_text]&quot; value=&quot;" class="regular-text" /&gt;<br>
              <small>Use <code>{count}</code> for number of files.</small>
            </td>
          </tr>
        </table>

        <h2>Error Messages</h2>
        <table class="form-table">
          <tr><th>Max Files Exceeded</th>
            <td>
              &lt;input type=&quot;text&quot; name=&quot;jfbm_settings[max_files_error]&quot; value=&quot;" class="regular-text" /&gt;<br>
              <small>Use <code>{max}</code> for maximum number of files.</small>
            </td>
          </tr>
          <tr><th>Max Size Exceeded</th>
            <td>
              &lt;input type=&quot;text&quot; name=&quot;jfbm_settings[max_size_error]&quot; value=&quot;" class="regular-text" /&gt;<br>
              <small>Use <code>{name}</code> for filename and <code>{size}</code> for max size.</small>
            </td>
          </tr>
          <tr><th>Invalid File Type</th>
            <td>
              &lt;input type=&quot;text&quot; name=&quot;jfbm_settings[file_type_error]&quot; value=&quot;" class="regular-text" /&gt;<br>
              <small>Use <code>{type}</code> for file type/extension.</small>
            </td>
          </tr>
          <tr><th>Upload Failed</th>
            <td>
              &lt;input type=&quot;text&quot; name=&quot;jfbm_settings[upload_failed_error]&quot; value=&quot;" class="regular-text" /&gt;
            </td>
          </tr>
        </table>

        <h2>Image Compression</h2>
        <table class="form-table">
          <tr><th>Enable Image Compression</th>
            <td>
              <label class="jfbm-toggle">
                &lt;input type=&quot;checkbox&quot; name=&quot;jfbm_settings[enable_compression]&quot; value=&quot;1&quot; &gt;
                <span class="jfbm-toggle-slider"></span>
                <span class="jfbm-toggle-label">Automatically compress large images before upload</span>
              </label>
            </td>
          </tr>
          <tr><th>Keep Original Dimensions</th>
            <td>
              <label class="jfbm-toggle">
                &lt;input type=&quot;checkbox&quot; name=&quot;jfbm_settings[keep_original_dimensions]&quot; value=&quot;1&quot; &gt;
                <span class="jfbm-toggle-slider"></span>
                <span class="jfbm-toggle-label">Do not resize images (ignore Max W/H); only re-encode using JPEG quality</span>
              </label>
            </td>
          </tr>
          <tr><th>Max Image Width</th>
            <td>&lt;input type=&quot;number&quot; name=&quot;jfbm_settings[max_width]&quot; value=&quot;" class="regular-text" /&gt; pixels</td>
          </tr>
          <tr><th>Max Image Height</th>
            <td>&lt;input type=&quot;number&quot; name=&quot;jfbm_settings[max_height]&quot; value=&quot;" class="regular-text" /&gt; pixels</td>
          </tr>
          <tr><th>Compression Quality</th>
            <td>&lt;input type=&quot;number&quot; name=&quot;jfbm_settings[quality]&quot; value=&quot;" min="10" max="100" class="small-text" /&gt;%</td>
          </tr>
        </table>

        
      
    </div>

    
      #jfbm-settings-page{max-width:1200px;margin:20px auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;--primary-color:#8b5cf6}
      .jfbm-page-title{color:#23282d;font-size:28px;font-weight:500;margin-bottom:30px;padding-bottom:15px;border-bottom:4px solid var(--primary-color);display:inline-block}
      #jfbm-settings-page h2{background:#f9fafb;padding:15px 20px;margin:30px 0 0;border-radius:8px 8px 0 0;font-size:18px;font-weight:500;color:#111827;border:1px solid #e5e7eb;border-bottom:none}
      #jfbm-settings-page .form-table{background:white;border-radius:0 0 8px 8px;box-shadow:0 1px 3px rgba(0,0,0,0.1);margin-top:0;border:1px solid #e5e7eb;border-top:none}
      #jfbm-settings-page .form-table th{padding:20px;width:200px;font-weight:500;color:#4B5563}
      #jfbm-settings-page .form-table td{padding:15px 20px}
      #jfbm-settings-page input[type="text"],#jfbm-settings-page input[type="number"],#jfbm-settings-page select{padding:8px 12px;border:1px solid #D1D5DB;border-radius:6px;font-size:14px;color:#111827;transition:border-color 0.2s}
      #jfbm-settings-page input[type="text"]:focus,#jfbm-settings-page input[type="number"]:focus,#jfbm-settings-page select:focus{border-color:var(--primary-color);box-shadow:0 0 0 2px rgba(139,92,246,0.2);outline:none}
      #jfbm-settings-page input[type="color"]{width:50px;height:50px;border:1px solid #D1D5DB;border-radius:8px;padding:3px;cursor:pointer;-webkit-appearance:none}
      #jfbm-settings-page input[type="color"]::-webkit-color-swatch-wrapper{padding:0}
      #jfbm-settings-page input[type="color"]::-webkit-color-swatch{border:none;border-radius:4px}
      .jfbm-color-field{display:flex;align-items:center;gap:10px}
      .jfbm-color-field code{background:#f3f4f6;padding:8px;border-radius:6px;font-family:monospace;font-size:14px;color:#4B5563;display:inline-block}
      #jfbm-settings-page small code{background:#f3f4f6;padding:2px 5px;border-radius:4px;font-family:monospace}
      #jfbm-settings-page .submit{text-align:right;margin-top:30px;padding:0}
      #jfbm-settings-page .button-primary{background:var(--primary-color);border-color:#7c3aed;color:white;padding:8px 20px;height:auto;font-size:14px;font-weight:500;border-radius:6px;text-shadow:none;box-shadow:0 2px 4px rgba(0,0,0,0.05)}
      #jfbm-settings-page .button-primary:hover{background:#7c3aed;border-color:#6d28d9}
      #jfbm-settings-page .button-primary:focus{box-shadow:0 0 0 1px white,0 0 0 3px #a78bfa;outline:none}
      .jfbm-radio-group{display:flex;gap:24px}
      .jfbm-radio{display:flex;align-items:center;position:relative;cursor:pointer;padding:5px 0}
      .jfbm-radio input[type="radio"]{position:absolute;opacity:0;width:0;height:0}
      .jfbm-radio-mark{position:relative;display:inline-block;width:22px;height:22px;margin-right:10px;border:2px solid #d1d5db;border-radius:50%;transition:all 2s}
      .jfbm-radio input[type="radio"]:checked+.jfbm-radio-mark{border-color:var(--primary-color);background-color:white}
      .jfbm-radio input[type="radio"]:checked+.jfbm-radio-mark:after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:12px;height:12px;border-radius:50%;background-color:var(--primary-color)}
      .jfbm-radio input[type="radio"]:focus+.jfbm-radio-mark{box-shadow:0 0 0 2px rgba(139,92,246,0.3)}
      .jfbm-toggle{display:flex;align-items:center;gap:12px}
      .jfbm-toggle input[type="checkbox"]{display:none}
      .jfbm-toggle-slider{position:relative;width:44px;height:24px;background:#cbd5e1;border-radius:24px;transition:0.3s;cursor:pointer}
      .jfbm-toggle-slider:before{content:'';position:absolute;height:18px;width:18px;left:3px;top:3px;background:white;border-radius:50%;transition:0.3s}
      .jfbm-toggle input:checked+.jfbm-toggle-slider{background:var(--primary-color)}
      .jfbm-toggle input:checked+.jfbm-toggle-slider:before{transform:translateX(20px)}
      .jfbm-toggle-label{color:#4B5563;font-size:14px}
      @media(max-width:767px){
        #jfbm-settings-page .form-table,#jfbm-settings-page .form-table tbody,#jfbm-settings-page .form-table tr,#jfbm-settings-page .form-table th,#jfbm-settings-page .form-table td{display:block;width:100%}
        #jfbm-settings-page .form-table th{padding-bottom:5px}
        #jfbm-settings-page .form-table td{padding-top:0}
        #jfbm-settings-page input[type="text"],#jfbm-settings-page select{width:100%}
        .jfbm-radio-group{flex-direction:column;gap:12px}
      }
    
     true,
        'url'  =&gt; $temp_url,
        'name' =&gt; $filename,
        'type' =&gt; $check['type'],
        'size' =&gt; (int) $_FILES['file']['size'],
    ]);
}

/** ----------------------------------------------------------------
 *  AJAX handler (probe-only; no disk write, no custom folder)
 *  ---------------------------------------------------------------- */
add_action('wp_ajax_jfbm_probe', 'jfbm_probe_handler');
add_action('wp_ajax_nopriv_jfbm_probe', 'jfbm_probe_handler');
function jfbm_probe_handler() {
    $nonce = isset($_REQUEST['nonce']) ? (string) $_REQUEST['nonce'] : '';
    if (!wp_verify_nonce($nonce, 'jfbm_upload')) {
        wp_send_json_error('Invalid nonce', 400);
    }
    if (empty($_FILES['file']['tmp_name']) || !is_uploaded_file($_FILES['file']['tmp_name'])) {
        wp_send_json_error('No file', 400);
    }

    // Do NOT move the file anywhere. Let PHP temp handle it; WP/JFB will save on real submit.
    $size = (int) $_FILES['file']['size'];
    $type = isset($_FILES['file']['type']) ? (string) $_FILES['file']['type'] : '';
    $name = sanitize_file_name($_FILES['file']['name']);

    wp_send_json_success([
        'probe' =&gt; true,
        'name'  =&gt; $name,
        'type'  =&gt; $type,
        'size'  =&gt; $size,
    ]);
}

/** ----------------------------------------------------------------
 *  Frontend output (CSS + JS + data)
 *  ---------------------------------------------------------------- */
add_action('wp_head', function () {
    $s = jfbm_opts();
    ?&gt;
    
      var jfbm_ajax = { url: '', nonce: '' };
    
    
    /* Hide JFB default UI we replace */
    .jet-form-builder-file-upload__content{all:unset!important;display:none!important}
    .jet-form-builder-file-upload__message{display:none!important}

    /* Hide JFB default file field errors; show only our styled ones */
    .jet-form-builder-file-upload__errors:not(.jfbm-error-message),
    .jet-form-builder__field-error:not(.jfbm-error-message){display:none!important}

    /* Button style wrapper */
    .jfb-button-style .jfb-upload-area{font-family:Inter,sans-serif;text-align:;border:none;background:transparent;padding:0;margin:0 0 20px 0;cursor:pointer}
    .jfb-button-style .jfb-upload-icon,.jfb-button-style .jfb-upload-subtext,.jfb-button-style .jfb-upload-max{display:none}
    .jfb-button-style .jfb-upload-text{display:inline-block;background-color:;color:;padding:12px 30px;border-radius:6px;font-weight:500;font-size:16px;text-align:center;cursor:pointer;transition:opacity .2s}
    .jfb-button-style .jfb-upload-text:hover{opacity:.9}

     /* Upload area */
    .jfb-upload-area{font-family:Inter,sans-serif;text-align:center;border:2px dashed ;background:#fff;padding:;margin:;border-radius:;cursor:pointer;transition:all .3s ease}
    .jfb-upload-area.dragover{background:;border-color:;transform:scale(1.02)}
    .jfb-upload-icon{font-size:40px;color:;margin-bottom:10px}
    .jfb-upload-text{font-size:16px;color:;font-weight:600}
    .jfb-upload-subtext{font-size:14px;color:}
    .jfb-upload-max{font-size:12px;color:#94a3b8;margin-top:6px}
    .jfb-upload-hidden{display:none!important}
    .jfb-upload-status,.jfb-upload-total{margin:20px 0 8px;font-size:14px;color:#334155;font-weight:500;transition:opacity .2s}
    .jfb-upload-total.hidden{opacity:0;display:none}
    .jfb-upload-list{display:flex;flex-direction:column;gap:24px;font-family:Inter,sans-serif}
    .jfb-upload-list.scrollable{overflow-y:auto;max-height:calc(*60px)}
    .jfb-upload-item{background:;border-radius:;padding:16px 16px 16px 60px;position:relative;box-shadow:0 1px 3px rgba(0,0,0,.05)}
    .jfb-upload-item .preview{position:absolute;top:16px;left:16px;width:30px;height:30px;object-fit:cover;border-radius:4px;background:#e5e7eb;display:flex;align-items:center;justify-content:center;font-size:16px}
    .jfb-upload-item .name{font-weight:;color:;margin-bottom:4px;font-size:14px}
    .jfb-upload-item .info{font-size:12px;color:#64748b;margin-bottom:6px}
    .jfb-upload-item .progress-container{background:#e2e8f0;height:6px;border-radius:4px;overflow:hidden}
    .jfb-upload-item .progress-bar{background:;height:100%;width:0%;transition:width .2s}
    .jfb-upload-item .progress-text{position:absolute;right:12px;top:-22px;font-size:12px;color:#475569}
    .jfb-upload-item .remove{position:absolute;top:10px;right:14px;color:;font-size:;cursor:pointer}
    .jfb-upload-item .remove:hover{color:#ef4444}

    /* Unified custom error style */
    .jfbm-error-message{
      background-color:!important;
      color:!important;
      font-size:!important;
      border-radius:!important;
      padding:!important;
      margin:10px 0!important;border:none!important;
      box-shadow:0 2px 4px rgba(0,0,0,0.1)!important;
      font-weight:500!important;opacity:1!important;transition:opacity .3s ease!important;display:block!important;font-family:Inter,sans-serif!important
    }
    .jfbm-error-message.hiding{opacity:0!important}
    

    
    document.addEventListener('DOMContentLoaded',function(){
        const targetClass = '';
        const uploadStyle = '';
        const TPL = {
          uploadStatus : ,
          totalStatus  : ,
          maxFiles     : ,
          maxSize      : ,
          fileType     : ,
          failed       : ,
        };
        const CFG = {
          trimLen     : ,
          threshold   : ,
          errDuration :  * 1000,
          compress    :  === 1,
          keepDims    :  === 1,
          maxW        : ,
          maxH        : ,
          quality     : Math.max( / 100, 0.6)
        };

        // Utilities
        const $ = (sel, ctx=document) =&gt; ctx.querySelector(sel);
        const $$ = (sel, ctx=document) =&gt; Array.from(ctx.querySelectorAll(sel));
        const fmtSize = s =&gt; s&gt;=1073741824? (s/1073741824).toFixed(2)+' GB' : s&gt;=1048576? (s/1048576).toFixed(2)+' MB' : s&gt;=1024? (s/1024).toFixed(2)+' KB' : s+' B';
        const decodeHTML = str =&gt; { const t=document.createElement('textarea'); t.innerHTML=str; return t.value; };

        // TRUE network upload (probe only; no disk write) – minimal helper
        function uploadWithProgress(file, onProgress, onSuccess, onError){
            try{
                const xhr = new XMLHttpRequest();
                const fd = new FormData();
                fd.append('action','jfbm_probe'); // probe endpoint does NOT persist file
                fd.append('nonce', jfbm_ajax.nonce);
                fd.append('file', file, file.name);
                xhr.upload.onprogress = e=&gt;{
                    if(e.lengthComputable &amp;&amp; onProgress){
                        onProgress(Math.round((e.loaded / e.total) * 100)); // 0..100
                    }
                };
                xhr.onreadystatechange = ()=&gt;{
                    if(xhr.readyState===4){
                        if(xhr.status&gt;=200 &amp;&amp; xhr.status{
            const wrap = input.closest('.jet-form-builder__field-wrap');
            const field = input.closest('.jet-form-builder__field');
            const media = input.closest('.jet-form-builder-file-upload');
            const hasTarget = (wrap &amp;&amp; wrap.classList.contains(targetClass)) ||
                              (field &amp;&amp; field.classList.contains(targetClass)) ||
                              (media &amp;&amp; media.classList.contains(targetClass));
            if (!hasTarget) return;
            initCustomField(input);
        });

        function initCustomField(f){
            const msgs = f.dataset.messages ? JSON.parse(f.dataset.messages) : {};
            const opt = {
              uploadStatusTpl : TPL.uploadStatus,
              totalStatusTpl  : TPL.totalStatus,
              filenameTrimLen : CFG.trimLen,
              scrollThreshold : CFG.threshold,
              maxFilesError   : msgs.file_max_files || TPL.maxFiles,
              maxSizeError    : msgs.file_max_size || TPL.maxSize,
              fileTypeError   : msgs.file_type || TPL.fileType,
              uploadFailedError: TPL.failed,
              errorDuration   : CFG.errDuration
            };

            f.classList.add('jfb-upload-hidden');

            const maxFiles = f.hasAttribute('data-max_files') ? parseInt(f.getAttribute('data-max_files'),10) : 1;
            const maxSize  = f.hasAttribute('data-max_size')  ? parseInt(f.getAttribute('data-max_size'),10)  : 0;
            const accepts  = f.getAttribute('accept') || '';

            // Track files for native JFB submission
            const state = { files: [] };
            function syncInput(){
                const dt = new DataTransfer();
                state.files.forEach(entry=&gt;{
                    try { dt.items.add(entry.file); } catch(e){}
                });
                f.files = dt.files;
            }

            // Build UI
            const area = el('div','jfb-upload-area');
            const icon = el('div','jfb-upload-icon','⇪');
            const text = el('div','jfb-upload-text',);
            const sub  = el('div','jfb-upload-subtext',);
            const max  = el('div','jfb-upload-max', buildRestrictionsText());
            const status = el('div','jfb-upload-status'); status.style.display='none';
            const total  = el('div','jfb-upload-total hidden');
            const list   = el('div','jfb-upload-list');
            const wrap   = document.createElement('div');

            wrap.append(area,status,total,list);
            area.append(icon,text,sub,max);
            f.parentNode.insertBefore(wrap,f);
            area.appendChild(f);
            if (uploadStyle === 'button') wrap.classList.add('jfb-button-style');

            // ✅ PRELOAD: find &amp; render existing files (URLs) on edit, without re-uploading
            (function preloadExisting(){
                const fieldWrap = f.closest('.jet-form-builder__field-wrap') || f.parentNode; // original context
                if(!fieldWrap) return;

                function isURL(u){ return typeof u === 'string' &amp;&amp; /^https?:///i.test(u.trim()); }
                function isImg(u){ return /.(png|jpe?g|gif|webp|bmp|svg)(?.*)?$/i.test(u); }
                function fileNameFromURL(u){
                    try{ return decodeURIComponent((u.split('/').pop() || '').split('?')[0] || 'file'); }
                    catch(_){ return 'file'; }
                }

                // Collect candidates (hidden inputs, anchors, images) excluding our new UI
                const urlMap = new Map(); // url =&gt; [elements that reference it]
                function addMap(url, el){
                    const arr = urlMap.get(url) || [];
                    arr.push(el);
                    urlMap.set(url, arr);
                }

                // Hidden inputs
                $$('input[type="hidden"]', fieldWrap).forEach(h=&gt;{
                    if (wrap.contains(h)) return; // ignore our UI
                    const v = (h.value || '').trim();
                    if (isURL(v)) addMap(v, h);
                });
                // Anchors
                $$('a[href]', fieldWrap).forEach(a=&gt;{
                    if (wrap.contains(a)) return;
                    const u = a.getAttribute('href');
                    if (isURL(u)) addMap(u, a);
                });
                // Images
                $$('img[src]', fieldWrap).forEach(img=&gt;{
                    if (wrap.contains(img)) return;
                    const u = img.getAttribute('src');
                    if (isURL(u)) addMap(u, img);
                });

                if (!urlMap.size) return;

                // Render each existing file as an already "uploaded" item
                urlMap.forEach((elements, url)=&gt;{
                    const item = el('div','jfb-upload-item');
                    const preview = el('div','preview');
                    if (isImg(url)){
                        const img = document.createElement('img');
                        img.src = url;
                        img.loading = 'lazy';
                        img.style.cssText='width:30px;height:30px;object-fit:cover;border-radius:4px';
                        preview.appendChild(img);
                    } else {
                        preview.innerHTML = '📄';
                    }

                    const name = el('div','name');
                    const fname = fileNameFromURL(url);
                    name.textContent = (fname.length&gt;opt.filenameTrimLen ? (fname.slice(0,opt.filenameTrimLen)+'...') : fname);

                    const info = el('div','info','');
                    const remove = el('div','remove','×');

                    remove.onclick = ()=&gt;{
                        // Remove UI item
                        item.remove();
                        // Try to clear any backing hidden inputs so it won’t be re-submitted
                        elements.forEach(el=&gt;{
                            if (el &amp;&amp; el.tagName){
                                const tag = el.tagName.toLowerCase();
                                if (tag === 'input' &amp;&amp; el.type === 'hidden'){
                                    el.setAttribute('data-jfbm-cleared','1');
                                    try { el.value = ''; } catch(_) {}
                                } else if (tag === 'a' || tag === 'img'){
                                    // purely visual in many cases; hide to avoid duplicates
                                    el.style.display = 'none';
                                }
                            }
                        });
                        updateTotal(); updateScroll();
                        // If maxFiles prevented new files earlier, removing may free a slot:
                        const err = (f.closest('.jet-form-builder__field-wrap')||document).querySelector('.jfbm-error-message');
                        if(err &amp;&amp; /maximum/i.test(err.textContent)){ err.style.display='none'; err.classList.remove('jfbm-error-message'); }
                    };

                    item.append(preview,name,info,remove);
                    item.classList.add('uploaded','existing');
                    list.appendChild(item);
                });

                updateScroll(); updateTotal();
            })();
            // ✅ PRELOAD END

            // Drag &amp; drop (+ earlier fixes)
            let dragCounter=0;
            area.addEventListener('click',()=&gt;{ f.value=''; f.click(); });
            area.addEventListener('dragenter',e=&gt;{e.preventDefault(); e.stopPropagation(); if(++dragCounter&gt;0) area.classList.add('dragover');});
            area.addEventListener('dragleave',e=&gt;{e.preventDefault(); e.stopPropagation(); if(--dragCounter{e.preventDefault(); e.stopPropagation();});
            area.addEventListener('drop',e=&gt;{
                e.preventDefault(); e.stopPropagation(); dragCounter=0; area.classList.remove('dragover');
                const valid = validateFiles(e.dataTransfer.files);
                if(!valid.length) return;
                // Keep files in input by merging existing + new via DataTransfer
                const dt = new DataTransfer();
                state.files.forEach(entry=&gt;{ try{ dt.items.add(entry.file);}catch(_){}}); // existing
                valid.forEach(file=&gt;dt.items.add(file)); // new
                f.files = dt.files;
                renderFiles(valid);
                f.value='';
            });
            f.addEventListener('change', ()=&gt;{
                const valid = validateFiles(f.files);
                if(!valid.length){ f.value=''; return; }
                renderFiles(valid);
                f.value='';
            });

            function el(tag, cls, html=''){
                const n=document.createElement(tag);
                if(cls) n.className=cls;
                if(html!=='' ) n.innerHTML=html;
                return n;
            }
            function buildRestrictionsText(){
                const sizeTxt = maxSize ? `(Max. File size: ${fmtSize(maxSize)})` : '';
                const typeTxt = accepts ? `Allowed types: ${accepts.replace(/,/g, ', ')}` : '';
                return sizeTxt &amp;&amp; typeTxt ? `${sizeTxt} • ${typeTxt}` : (sizeTxt || typeTxt);
            }
            function showError(message){
                const msg = decodeHTML(message);
                let err = (f.closest('.jet-form-builder__field-wrap') || f.parentNode).querySelector('.jet-form-builder-file-upload__errors') ||
                          (f.closest('.jet-form-builder__field-wrap') || f.parentNode).querySelector('.jet-form-builder__field-error');
                if(!err){
                    err = document.createElement('div');
                    err.className = 'jet-form-builder-file-upload__errors';
                    (f.closest('.jet-form-builder__field-wrap') || f.parentNode).appendChild(err);
                }
                err.textContent = msg;
                err.classList.add('jfbm-error-message'); err.classList.remove('is-hidden'); err.style.display='block';
                setTimeout(()=&gt;{ err.classList.add('hiding');
                    setTimeout(()=&gt;{
                        err.classList.add('is-hidden'); err.style.display='none';
                        err.classList.remove('jfbm-error-message','hiding'); err.textContent='';
                        // keep current items; no reload
                    },300);
                }, opt.errorDuration);
            }

            // cap + validate without wiping
            function validateFiles(files){
                (f.closest('.jet-form-builder__field-wrap')||document).querySelectorAll('.jet-form-builder-file-upload__errors, .jet-form-builder__field-error').forEach(e=&gt;{
                    if(!e.classList.contains('jfbm-error-message')) e.style.display='none';
                });

                const current = list.querySelectorAll('.jfb-upload-item').length;
                const incoming = Array.from(files || []);
                let allowed = incoming;

                if (maxFiles) {
                    const remaining = Math.max(0, maxFiles - current);
                    if (incoming.length &gt; remaining) {
                        if (remaining s.trim().toLowerCase());
                    for (const file of allowed){
                        if (maxSize &amp;&amp; file.size &gt; maxSize) { showError(opt.maxSizeError.replace('{name}', file.name).replace('{size}', fmtSize(maxSize))); return []; }
                        const ext='.'+(file.name.split('.').pop()||'').toLowerCase(), type=(file.type||'').toLowerCase();
                        let ok=false;
                        for (const m of mimeList){
                            if (m.startsWith('.') &amp;&amp; ext===m){ ok=true; break; }
                            if (m.includes('*') &amp;&amp; type.startsWith(m.replace('*',''))){ ok=true; break; }
                            if (type===m){ ok=true; break; }
                        }
                        if (!ok){ showError(opt.fileTypeError.replace('{type}', type||ext)); return []; }
                    }
                } else {
                    for (const file of allowed){
                        if (maxSize &amp;&amp; file.size &gt; maxSize){ showError(opt.maxSizeError.replace('{name}', file.name).replace('{size}', fmtSize(maxSize))); return []; }
                    }
                }
                return allowed;
            }

            function updateTotal(){
                const up=list.querySelectorAll('.jfb-upload-item.uploaded').length;
                if(up&gt;0){ total.textContent = opt.totalStatusTpl.replace('{count}', up); total.classList.remove('hidden'); }
                else total.classList.add('hidden');
            }
            function updateScroll(){ list.classList.toggle('scrollable', list.children.length &gt; opt.scrollThreshold); }
            function resetForm(){ status.style.display='none'; total.classList.add('hidden'); list.innerHTML=''; updateScroll(); }

            // Image compression (optional) with REAL-TIME progress + probe network upload at the end (no persistence)
            function compressImage(file, cb, onProgress){
                if (!CFG.compress || !file.type.startsWith('image/')){
                    if (onProgress) onProgress(90); // cap pre-network at 90%
                    return cb(file);
                }

                const reader = new FileReader();
                reader.onprogress = e=&gt;{
                    if (e.lengthComputable &amp;&amp; onProgress){
                        const p = Math.max(1, Math.min(40, Math.round((e.loaded/e.total)*40)));
                        onProgress(p); // 0–40
                    }
                };
                reader.onerror = ()=&gt;{ if(onProgress) onProgress(90); cb(file); };
                reader.onload = ()=&gt;{
                    const img = new Image();
                    img.onload = ()=&gt;{
                        let w=img.width, h=img.height;

                        const needsResize = (!CFG.keepDims) &amp;&amp; ((w &gt; CFG.maxW) || (h &gt; CFG.maxH));
                        if(needsResize){
                            if (w &gt; h){
                                if (w &gt; CFG.maxW){ h = Math.round(h * CFG.maxW / w); w = CFG.maxW; }
                            } else {
                                if (h &gt; CFG.maxH){ w = Math.round(w * CFG.maxH / h); h = CFG.maxH; }
                            }
                        }
                        const canvas = document.createElement('canvas');
                        const ctx = canvas.getContext('2d');
                        canvas.width = w; canvas.height = h;

                        const stripes = 50; let i = 0;
                        function drawStripe(){
                            const sy = Math.floor(i * img.height / stripes);
                            const sh = Math.floor((i+1) * img.height / stripes) - sy;
                            const dy = Math.floor(i * h / stripes);
                            const dh = Math.floor((i+1) * h / stripes) - dy;
                            ctx.drawImage(img, 0, sy, img.width, sh, 0, dy, w, dh);
                            i++;
                            if (onProgress){
                                const p = 40 + Math.round((i/stripes)*50); // 40–90
                                onProgress(Math.min(90, p));
                            }
                            if (i {
                                    if (onProgress) onProgress(90);
                                    if (blob &amp;&amp; blob.size { if(onProgress) onProgress(90); cb(file); };
                    img.src = reader.result;
                };
                reader.readAsDataURL(file);
            }

            function renderFiles(files){
                const totalFiles = files.length; let done=0;
                if(totalFiles&gt;0){ status.textContent = opt.uploadStatusTpl.replace('{count}', totalFiles); status.style.display='block'; }

                [...files].forEach(file=&gt;{
                    const item = el('div','jfb-upload-item');

                    const preview = el('div','preview');
                    if (file.type.startsWith('image/')){
                        const img=document.createElement('img');
                        img.src=URL.createObjectURL(file);
                        img.style.cssText='width:30px;height:30px;object-fit:cover;border-radius:4px';
                        preview.appendChild(img);
                    } else { preview.innerHTML='📄'; }

                    // ✅ XSS-safe filename rendering
                    const name = el('div','name');
                    name.textContent = (file.name.length&gt;opt.filenameTrimLen ? (file.name.slice(0,opt.filenameTrimLen)+'...') : file.name);

                    const info = el('div','info', `<span>${(file.size/1024/1024).toFixed(2)} MB</span>`);
                    const barWrap = el('div','progress-container');
                    const fill = el('div','progress-bar');
                    const percent = el('div','progress-text','0%');
                    const remove = el('div','remove','×');

                    barWrap.appendChild(fill);
                    const fileId = Math.random().toString(36).slice(2);
                    remove.onclick = ()=&gt;{
                        item.remove(); updateTotal(); updateScroll();
                        // Remove from state and sync native input for JFB
                        const idx = state.files.findIndex(e=&gt;e.id===fileId);
                        if(idx&gt;-1){ state.files.splice(idx,1); syncInput(); }
                        const remaining = list.querySelectorAll('.jfb-upload-item').length;
                        if(remaining {
                        if(finalFile !== file &amp;&amp; finalFile.size &lt; file.size){
                            const original = (file.size/1024/1024).toFixed(2);
                            const reduced  = (finalFile.size/1024/1024).toFixed(2);
                            const savePct  = ((file.size-finalFile.size)/file.size*100).toFixed(0);
                            info.innerHTML = `<span style="text-decoration:line-through;color:#94a3b8">${original} MB</span> → <span style="color:#10b981;font-weight:600">${reduced} MB</span> <small style="color:#10b981;font-weight:500">(${savePct}% compressed)</small>`;
                        }

                        uploadWithProgress(finalFile, netP=&gt;{
                            const mapped = Math.min(100, 90 + Math.round(netP * 0.10)); // map 0..100 =&gt; 90..100
                            fill.style.width = mapped+'%';
                            percent.textContent = mapped+'%';
                        }, ()=&gt;{
                            item.classList.add('uploaded');
                            state.files.push({ id:fileId, file: finalFile });
                            syncInput();

                            done++; updateTotal();
                            if(done===totalFiles) status.style.display='none';
                            else status.textContent = opt.uploadStatusTpl.replace('{count}', totalFiles-done);
                        }, ()=&gt;{
                            showError(opt.uploadFailedError);
                        });

                    }, p=&gt;{
                        const clamped = Math.max(0, Math.min(100, Math.round(p)));
                        const mapped = Math.min(90, clamped); // cap pre-network progress at 90%
                        fill.style.width = mapped+'%';
                        percent.textContent = mapped+'%';
                    });
                });
            }
        }
    });
    
    &lt;?php
});


				
			

🎯 Target CSS Class for Precision Control

Sometimes, you don’t want every upload field on your site to look the same. That’s where Target CSS Class comes in.

Here’s how it works:

  1. In the backend, set a unique Target CSS Class for your upload field.

  2. Open your JetForm Builder form → Media Field → Block Settings → Advanced.

  3. Paste the same classname into the CSS Classname box and save.

Now, only that form will use the new upload design. Perfect if you have multiple forms but only want custom styling on specific ones.

📱 Fully Responsive Design

Your users may be on a desktop, tablet, or mobile device — and this upload field adjusts seamlessly to all screen sizes. The layout stacks neatly on smaller screens while keeping progress bars and error messages clear.

⚙️ Future Plans: Image Conversion

We’re not stopping here. A convert image feature is on the roadmap — giving you the ability to automatically re-encode files (for example, converting PNGs to lightweight JPGs or WebP) to save even more space and speed up uploads.

🚀 Why This Matters

With this upgrade, file uploads in JetForm Builder feel professional, reliable, and user-friendly.

  • For site owners: less support from frustrated users.

  • For users: faster, smoother, and clearer uploads.

  • For developers/designers: full control without custom coding.

This is a big step toward making forms not just functional, but delightful.

In short: A modern design, real progress bars, toggleable image compression, targeted styling, responsive layouts, and future-proofing with image conversion.

Your forms — and your users — will thank you.

Faqs