| 1 |
<fieldset class="bbp-form upload-attachments-form"> |
| 2 |
<div class="frmx-attachments-ui"> |
| 3 |
<div class="frmx-attachments-list" id="frmx-attachments-list"></div> |
| 4 |
|
| 5 |
<button type="button" class="frmx-add-file-btn" id="frmx-add-file-btn"> |
| 6 |
<span class="frmx-add-icon"> |
| 7 |
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg> |
| 8 |
</span> |
| 9 |
<?php esc_html_e( 'Add more files', 'forumax' ); ?> |
| 10 |
</button> |
| 11 |
|
| 12 |
<div class="frmx-attachment-limit-note"> |
| 13 |
<?php |
| 14 |
$size = $file_size < 1024 ? $file_size . ' KB' : floor( $file_size / 1024 ) . ' MB'; |
| 15 |
printf( esc_html__( 'Maximum file size allowed is %s.', 'forumax' ), esc_html( $size ) ); |
| 16 |
?> |
| 17 |
</div> |
| 18 |
|
| 19 |
<div id="frmx-hidden-inputs" style="display: none;"> |
| 20 |
<input type="file" name="bbpc_attachment[]" class="frmx-file-input"> |
| 21 |
</div> |
| 22 |
</div> |
| 23 |
|
| 24 |
<!-- Keep the legacy container hidden for fallback or original JS hooks --> |
| 25 |
<div class="bbpc_attachments" style="display:none;"></div> |
| 26 |
</fieldset> |
| 27 |
|
| 28 |
<script> |
| 29 |
document.addEventListener('DOMContentLoaded', function() { |
| 30 |
const addBtn = document.getElementById('frmx-add-file-btn'); |
| 31 |
const inputsWrap = document.getElementById('frmx-hidden-inputs'); |
| 32 |
const listContainer = document.getElementById('frmx-attachments-list'); |
| 33 |
if ( ! addBtn ) return; |
| 34 |
|
| 35 |
/* Max-files from bbp config (default 5) */ |
| 36 |
let maxFiles = 5; |
| 37 |
if ( typeof bbpcAttachmentsInit !== 'undefined' ) { |
| 38 |
maxFiles = parseInt( bbpcAttachmentsInit.max_files, 10 ); |
| 39 |
} |
| 40 |
|
| 41 |
/* ---- Helpers ---- */ |
| 42 |
|
| 43 |
/** |
| 44 |
* Format a byte count into a human-readable string. |
| 45 |
* |
| 46 |
* @param {number} bytes |
| 47 |
* @return {string} |
| 48 |
*/ |
| 49 |
function humanSize( bytes ) { |
| 50 |
if ( bytes < 1024 ) return bytes + ' B'; |
| 51 |
if ( bytes < 1048576 ) return ( bytes / 1024 ).toFixed( 1 ) + ' KB'; |
| 52 |
return ( bytes / 1048576 ).toFixed( 1 ) + ' MB'; |
| 53 |
} |
| 54 |
|
| 55 |
/** |
| 56 |
* Return a colour for the icon based on a rough file-type bucket. |
| 57 |
* |
| 58 |
* @param {string} name Filename. |
| 59 |
* @return {string} CSS colour value. |
| 60 |
*/ |
| 61 |
function iconColor( name ) { |
| 62 |
const ext = ( name.split( '.' ).pop() || '' ).toLowerCase(); |
| 63 |
const map = { |
| 64 |
pdf: '#e74c3c', |
| 65 |
doc: '#2b6cb0', docx: '#2b6cb0', |
| 66 |
xls: '#276749', xlsx: '#276749', |
| 67 |
csv: '#276749', |
| 68 |
ppt: '#c05621', pptx: '#c05621', |
| 69 |
zip: '#744210', rar: '#744210', '7z': '#744210', |
| 70 |
jpg: '#6b46c1', jpeg: '#6b46c1', png: '#6b46c1', gif: '#6b46c1', webp: '#6b46c1', svg: '#6b46c1', |
| 71 |
mp4: '#2c7a7b', mov: '#2c7a7b', avi: '#2c7a7b', |
| 72 |
mp3: '#553c9a', wav: '#553c9a', |
| 73 |
}; |
| 74 |
return map[ ext ] || 'var(--bbpc_brand_color)'; |
| 75 |
} |
| 76 |
|
| 77 |
/** Generic file SVG icon markup. */ |
| 78 |
const fileSvg = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>'; |
| 79 |
|
| 80 |
/** Close (×) SVG for remove button. */ |
| 81 |
const closeSvg = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>'; |
| 82 |
|
| 83 |
/* ---- Open file picker ---- */ |
| 84 |
addBtn.addEventListener( 'click', function( e ) { |
| 85 |
e.preventDefault(); |
| 86 |
|
| 87 |
let empty = Array.from( inputsWrap.querySelectorAll( '.frmx-file-input' ) ).find( i => ! i.value ); |
| 88 |
if ( ! empty ) { |
| 89 |
const total = inputsWrap.querySelectorAll( '.frmx-file-input' ).length; |
| 90 |
if ( total >= maxFiles ) return; |
| 91 |
empty = document.createElement( 'input' ); |
| 92 |
empty.type = 'file'; |
| 93 |
empty.name = 'bbpc_attachment[]'; |
| 94 |
empty.className = 'frmx-file-input'; |
| 95 |
inputsWrap.appendChild( empty ); |
| 96 |
} |
| 97 |
empty.click(); |
| 98 |
} ); |
| 99 |
|
| 100 |
/* ---- File selected ---- */ |
| 101 |
inputsWrap.addEventListener( 'change', function( e ) { |
| 102 |
if ( e.target && e.target.classList.contains( 'frmx-file-input' ) && e.target.value ) { |
| 103 |
renderList(); |
| 104 |
|
| 105 |
const total = inputsWrap.querySelectorAll( '.frmx-file-input' ).length; |
| 106 |
if ( total < maxFiles ) { |
| 107 |
const next = document.createElement( 'input' ); |
| 108 |
next.type = 'file'; |
| 109 |
next.name = 'bbpc_attachment[]'; |
| 110 |
next.className = 'frmx-file-input'; |
| 111 |
inputsWrap.appendChild( next ); |
| 112 |
} |
| 113 |
} |
| 114 |
} ); |
| 115 |
|
| 116 |
/* ---- Remove file ---- */ |
| 117 |
listContainer.addEventListener( 'click', function( e ) { |
| 118 |
const btn = e.target.closest( '.frmx-remove-file' ); |
| 119 |
if ( ! btn ) return; |
| 120 |
|
| 121 |
e.preventDefault(); |
| 122 |
const idx = parseInt( btn.dataset.index, 10 ); |
| 123 |
const inp = inputsWrap.querySelectorAll( '.frmx-file-input' )[ idx ]; |
| 124 |
if ( inp ) { |
| 125 |
inp.remove(); |
| 126 |
renderList(); |
| 127 |
|
| 128 |
const hasEmpty = Array.from( inputsWrap.querySelectorAll( '.frmx-file-input' ) ).some( i => ! i.value ); |
| 129 |
const total = inputsWrap.querySelectorAll( '.frmx-file-input' ).length; |
| 130 |
if ( ! hasEmpty && total < maxFiles ) { |
| 131 |
const blank = document.createElement( 'input' ); |
| 132 |
blank.type = 'file'; |
| 133 |
blank.name = 'bbpc_attachment[]'; |
| 134 |
blank.className = 'frmx-file-input'; |
| 135 |
inputsWrap.appendChild( blank ); |
| 136 |
} |
| 137 |
} |
| 138 |
} ); |
| 139 |
|
| 140 |
/* ---- Drag-over visual feedback on the add button ---- */ |
| 141 |
addBtn.addEventListener( 'dragover', function( e ) { e.preventDefault(); addBtn.classList.add( 'frmx-drag-over' ); } ); |
| 142 |
addBtn.addEventListener( 'dragleave', function() { addBtn.classList.remove( 'frmx-drag-over' ); } ); |
| 143 |
addBtn.addEventListener( 'drop', function( e ) { |
| 144 |
e.preventDefault(); |
| 145 |
addBtn.classList.remove( 'frmx-drag-over' ); |
| 146 |
|
| 147 |
const files = e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files : []; |
| 148 |
if ( ! files.length ) return; |
| 149 |
|
| 150 |
Array.from( files ).forEach( function( file ) { |
| 151 |
const total = inputsWrap.querySelectorAll( '.frmx-file-input' ).length; |
| 152 |
if ( total >= maxFiles ) return; |
| 153 |
|
| 154 |
/* Build a DataTransfer to assign the file to a new hidden input */ |
| 155 |
const dt = new DataTransfer(); |
| 156 |
dt.items.add( file ); |
| 157 |
const inp = document.createElement( 'input' ); |
| 158 |
inp.type = 'file'; |
| 159 |
inp.name = 'bbpc_attachment[]'; |
| 160 |
inp.className = 'frmx-file-input'; |
| 161 |
inp.files = dt.files; |
| 162 |
inputsWrap.appendChild( inp ); |
| 163 |
} ); |
| 164 |
|
| 165 |
renderList(); |
| 166 |
} ); |
| 167 |
|
| 168 |
/* ---- Render the uploaded file list ---- */ |
| 169 |
function renderList() { |
| 170 |
listContainer.innerHTML = ''; |
| 171 |
const inputs = inputsWrap.querySelectorAll( '.frmx-file-input' ); |
| 172 |
let fileCount = 0; |
| 173 |
|
| 174 |
inputs.forEach( function( input, index ) { |
| 175 |
if ( ! input.files || ! input.files.length ) return; |
| 176 |
|
| 177 |
const file = input.files[ 0 ]; |
| 178 |
const color = iconColor( file.name ); |
| 179 |
fileCount++; |
| 180 |
|
| 181 |
const item = document.createElement( 'div' ); |
| 182 |
item.className = 'frmx-attachment-item'; |
| 183 |
|
| 184 |
item.innerHTML = ` |
| 185 |
<div class="frmx-file-icon-wrapper" style="background:color-mix(in srgb,${color} 12%,transparent);"> |
| 186 |
<span style="color:${color};display:flex;align-items:center;">${fileSvg}</span> |
| 187 |
</div> |
| 188 |
<div class="frmx-file-info"> |
| 189 |
<span class="frmx-file-name">${file.name}</span> |
| 190 |
<span class="frmx-file-size">${humanSize( file.size )}</span> |
| 191 |
</div> |
| 192 |
<button type="button" class="frmx-remove-file" data-index="${index}" title="Remove file">${closeSvg}</button> |
| 193 |
`; |
| 194 |
|
| 195 |
listContainer.appendChild( item ); |
| 196 |
} ); |
| 197 |
|
| 198 |
addBtn.style.display = ( fileCount >= maxFiles ) ? 'none' : 'flex'; |
| 199 |
} |
| 200 |
} ); |
| 201 |
</script> |
| 202 |
|