1const FIELDS_KEY='nrj_offline_fields' 2const FILES_KEY='nrj_offline_files' 3const USE_IDB_FILES=true 4const NRJ_ENABLE_DRAFTS=false 5const NRJ_DRAFT_SLOT='draft' 6const NRJ_USER_SLOTS=[1,2,3,4,5] 7 8function nrj_logc(...a){try{console.log('[NRJ-OFFLINE]',...a)}catch(e){}} 9 10function nrj_find_form(){ 11 let byBtn=document.getElementById('nrj-save-report-offline')?.closest('form') 12 || document.getElementById('nrj-manage-offline')?.closest('form') 13 if(byBtn)return byBtn 14 let byFF=document.querySelector('form input[name="form_id"]') 15 if(byFF)return byFF.closest('form') 16 return document.querySelector('form') 17} 18 19function nrj_get_form_id(form){ 20 let el=form?.querySelector('input[name="form_id"]') 21 return el?String(el.value||''):'' 22} 23 24function nrj_ls_key(formId,slot,base){ 25 return base+':'+formId+':'+slot 26} 27 28function nrj_get_index(formId){ 29 let k=FIELDS_KEY+':index:'+formId 30 let s=localStorage.getItem(k) 31 let out={} 32 out[NRJ_DRAFT_SLOT]=null 33 NRJ_USER_SLOTS.forEach(slot=>{out[slot]=null}) 34 if(!s)return out 35 try{ 36 let o=JSON.parse(s)||{} 37 out[NRJ_DRAFT_SLOT]=o[NRJ_DRAFT_SLOT]||null 38 NRJ_USER_SLOTS.forEach(slot=>{out[slot]=o[String(slot)]||o[slot]||null}) 39 }catch(e){} 40 return out 41} 42 43function nrj_set_index(formId,idx){ 44 let k=FIELDS_KEY+':index:'+formId 45 let o={} 46 o[NRJ_DRAFT_SLOT]=idx[NRJ_DRAFT_SLOT]||null 47 NRJ_USER_SLOTS.forEach(slot=>{o[slot]=idx[slot]||null}) 48 localStorage.setItem(k,JSON.stringify(o)) 49} 50 51 52function nrj_slot_pick_save(idx){ 53 for(let slot of NRJ_USER_SLOTS){ 54 if(!idx[slot])return slot 55 } 56 let first=NRJ_USER_SLOTS[0] 57 let last=NRJ_USER_SLOTS[NRJ_USER_SLOTS.length-1] 58 let a=window.prompt('All '+NRJ_USER_SLOTS.length+' slots are full. Overwrite slot '+first+'â'+last+'?',String(first)) 59 let n=parseInt(a,10) 60 if(!NRJ_USER_SLOTS.includes(n))return null 61 return n 62} 63 64 65function nrj_slot_pick_restore(idx){ 66 let filled=[] 67 for(let slot of NRJ_USER_SLOTS){ 68 if(idx[slot])filled.push(slot) 69 } 70 if(filled.length===0)return null 71 if(filled.length===1)return filled[0] 72 let first=NRJ_USER_SLOTS[0] 73 let last=NRJ_USER_SLOTS[NRJ_USER_SLOTS.length-1] 74 let a=window.prompt('Restore slot '+first+'â'+last+'?',String(filled[0])) 75 let n=parseInt(a,10) 76 if(!NRJ_USER_SLOTS.includes(n))return null 77 return n 78} 79 80 81async function nrj_manage_delete_slot(formId,slot){ 82 await nrj_clear_saved(formId,slot) 83} 84 85async function nrj_restore_from_slot(formId,slot,form,btn,forcedMode=null){ 86 let fieldsStr=localStorage.getItem(nrj_ls_key(formId,slot,FIELDS_KEY)) 87 let filesObj=await nrj_load_files(formId,slot) 88 89 if(!fieldsStr&&(!filesObj||!Object.keys(filesObj).length)){ 90 nrj_toast('No saved report in slot '+slot,false,btn) 91 return false 92 } 93 94 let fields={} 95 96 try{ 97 fields=fieldsStr?JSON.parse(fieldsStr):{} 98 }catch(e){ 99 fields={} 100 } 101 102 for(let name in fields){ 103 let m=name.match(/^item_meta\[(\d+)\]\[(\d+)\]\[(\d+)\]/) 104 105 if(m){ 106 let parent=m[1] 107 let row=parseInt(m[2],10) 108 let sec=form.querySelector('#frm_section_'+parent+'-'+row) 109 110 if(!sec){ 111 let btnAdd=form.querySelector('.frm_add_form_row[data-parent="'+parent+'"]') 112 113 if(btnAdd){ 114 while(!form.querySelector('#frm_section_'+parent+'-'+row)){ 115 btnAdd.click() 116 await nrj_wait(250) 117 } 118 } 119 } 120 } 121 } 122 123 nrj_apply_subset(form,fields,el=>el.type==='radio'||el.type==='checkbox'||el.tagName==='SELECT') 124 125 await nrj_wait(350) 126 127 nrj_apply_subset(form,fields,el=>!(el.type==='radio'||el.type==='checkbox'||el.tagName==='SELECT')) 128 129 await nrj_wait(350) 130 131 nrj_apply_subset(form,fields,el=>true) 132 133 let mode=forcedMode 134 ? forcedMode 135 : (slot===NRJ_DRAFT_SLOT?nrj_prompt_restore_file_mode():'original') 136 137 let restored=await nrj_restore_files_slot(formId,slot,form,mode) 138 139 if(restored===false){ 140 nrj_toast('Restore stopped because a file could not be loaded',false,btn) 141 return false 142 } 143 144 if(slot===NRJ_DRAFT_SLOT){ 145 if(!forcedMode){ 146 nrj_toast('Restored Draft Slot using '+mode+' files',true,btn) 147 } 148 149 return true 150 } 151 152 let del=window.confirm('Delete stored data for slot '+slot+' after restoring?') 153 154 if(del){ 155 await nrj_clear_saved(formId,slot) 156 nrj_toast('Restored slot '+slot+' and deleted saved copy',true,btn) 157 }else{ 158 nrj_toast('Restored slot '+slot+' (kept saved copy)',true,btn) 159 } 160 161 return true 162} 163async function nrj_restore_files_slot(formId,slot,form,mode='original'){ 164 let files=await nrj_load_files(formId,slot) 165 166 if(!files||!Object.keys(files).length){ 167 return 168 } 169 170 let keys=Object.keys(files) 171 let batchSize=5 172 173 let restoreField=async key=>{ 174 await nrj_ensure_row_for_key(form,key) 175 176 let list=files[key]||[] 177 178 if(mode==='compressed'){ 179 let compressed=[] 180 181 for(let it of list){ 182 let f=nrj_file_from_buf(it) 183 184 if(!f)continue 185 186 let cf=await nrj_compress_image_file(f) 187 let buf=await nrj_buf_from_file(cf) 188 189 if(buf){ 190 compressed.push({ 191 name:cf.name, 192 type:cf.type||'', 193 lastModified:cf.lastModified||Date.now(), 194 buf 195 }) 196 } 197 } 198 199 list=compressed 200 } 201
202 let dz=nrj_find_dropzone_for_key(form,key) 203 204 if(dz){ 205 try{ 206 dz.removeAllFiles(true) 207 }catch(e){} 208 209 for(let it of list){ 210 let f=nrj_file_from_buf(it) 211 212 if(!f)continue 213 214 let loaded=await nrj_add_dropzone_file_with_retry( 215 dz, 216 f, 217 5 218 ) 219 220 if(!loaded){ 221 nrj_upload_error_log('restore_upload_failed',{ 222 form_id:formId, 223 field_key:key, 224 file_name:f.name, 225 file_size:f.size||'', 226 message:'Failed after 5 restore attempts' 227 }) 228 229 nrj_toast( 230 'Could not load '+f.name+' after 5 attempts', 231 false, 232 null 233 ) 234 235 return false 236 } 237 } 238 239 return true 240 } 241 242 let hidden=form.querySelector( 243 'input[type="hidden"][data-frmfile][name="'+CSS.escape(key)+'"]' 244 ) 245 246 let field=hidden 247 ? hidden.closest('.frm_form_field') 248 : null 249 250 let input=field 251 ? field.querySelector('input[type="file"]') 252 : null 253 254 if(input){ 255 nrj_apply_files(input,list) 256 } 257 258 return true 259 } 260 261 for(let i=0;i<keys.length;i+=batchSize){ 262 let batch=keys.slice(i,i+batchSize) 263 let results=await Promise.all( 264 batch.map(key=>restoreField(key)) 265 ) 266 267 if(results.some(result=>result===false)){ 268 return false 269 } 270 } 271 272 return true 273} 274 275async function nrj_add_dropzone_file_with_retry(dz,file,maxRetries=5){ 276 let attempt=0 277 278 while(attempt<maxRetries){ 279 attempt++ 280 281 let result=await new Promise(resolve=>{ 282 let finished=false 283 284 let cleanup=()=>{ 285 dz.off('success',successHandler) 286 dz.off('error',errorHandler) 287 dz.off('canceled',canceledHandler) 288 } 289 290 let finish=(ok,reason='')=>{ 291 if(finished)return 292 293 finished=true 294 cleanup() 295 resolve({ok,reason}) 296 } 297 298 let successHandler=uploadedFile=>{ 299 if(uploadedFile!==file)return 300 finish(true) 301 } 302 303 let errorHandler=(uploadedFile,message,xhr)=>{ 304 if(uploadedFile!==file)return 305 306 let errorMessage='' 307 308 if(typeof message==='string'){ 309 errorMessage=message 310 }else{ 311 try{ 312 errorMessage=JSON.stringify(message||{}) 313 }catch(e){ 314 errorMessage=String(message||'Upload failed') 315 } 316 } 317 318 if(xhr&&xhr.status){ 319 errorMessage+=' | HTTP '+xhr.status 320 } 321 322 finish(false,errorMessage) 323 } 324 325 let canceledHandler=uploadedFile=>{ 326 if(uploadedFile!==file)return 327 finish(false,'Upload canceled') 328 } 329 330 dz.on('success',successHandler) 331 dz.on('error',errorHandler) 332 dz.on('canceled',canceledHandler) 333 334 try{ 335 dz.addFile(file) 336 }catch(e){ 337 finish( 338 false, 339 e&&e.message?e.message:String(e) 340 ) 341 } 342 }) 343 344 if(result.ok){ 345 nrj_logc( 346 'restore upload accepted', 347 file.name, 348 'attempt', 349 attempt 350 ) 351 352 return true 353 } 354 355 nrj_logc( 356 'restore upload failed', 357 file.name, 358 'attempt', 359 attempt, 360 result.reason 361 ) 362 363 try{ 364 dz.removeFile(file) 365 }catch(e){} 366 367 if(attempt<maxRetries){ 368 await nrj_wait(1000) 369 } 370 } 371 372 return false 373} 374 375function nrj_idb_clear(store){ 376 return nrj_db().then(db=>new Promise((res,rej)=>{ 377 try{ 378 const tx=db.transaction([store],'readwrite'); 379 const st=tx.objectStore(store); 380 const q=st.clear(); 381 q.onsuccess=()=>res(true); 382 q.onerror=e=>rej(e); 383 tx.oncomplete=()=>db.close(); 384 }catch(e){ 385 try{db.close()}catch(_){} 386 res(true); 387 } 388 })); 389} 390 391async function nrj_clear_all_saved(){ 392 try{
393 // Remove all LocalStorage entries related to NRJ offline 394 const prefixes=[FIELDS_KEY+':', FILES_KEY+':']; 395 const idxPrefix=FIELDS_KEY+':index:'; 396 const toDelete=[]; 397 for(let i=0;i<localStorage.length;i++){ 398 const k=localStorage.key(i)||''; 399 if(k.startsWith(prefixes[0]) || k.startsWith(prefixes[1]) || k.startsWith(idxPrefix)){ 400 toDelete.push(k); 401 } 402 } 403 toDelete.forEach(k=>{ try{localStorage.removeItem(k)}catch(_){}}); 404 405 if(USE_IDB_FILES){ try{ await nrj_idb_clear('files') }catch(_){ /* ignore */ } } 406 407 return true; 408 }catch(e){ 409 nrj_logc('clear_all_error', e && e.message ? e.message : e); 410 return false; 411 } 412} 413 414 415function nrj_fmt_bytes(b){ 416 if(b==null || isNaN(b)) return 'â' 417 const u=['B','KB','MB','GB','TB'] 418 let i=0, v=Number(b) 419 while(v>=1024 && i<u.length-1){ v/=1024; i++ } 420 return Math.round(v*10)/10+' '+u[i] 421} 422 423async function nrj_get_storage_info(){ 424 let lsUsedBytes=0 425 try{ 426 for(let i=0;i<localStorage.length;i++){ 427 const k=localStorage.key(i)||'' 428 const v=localStorage.getItem(k)||'' 429 lsUsedBytes += (k.length + v.length) * 2 430 } 431 }catch(e){} 432 let lsQuotaBytes=5*1024*1024 433 let quota=null, usage=null, idbUsage=null 434 try{ 435 if(navigator.storage && navigator.storage.estimate){ 436 const est=await navigator.storage.estimate() 437 quota=est.quota||null 438 usage=est.usage||null 439 if(est.usageDetails && typeof est.usageDetails.indexedDB==='number'){ 440 idbUsage=est.usageDetails.indexedDB 441 } 442 } 443 }catch(e){} 444 return {lsUsedBytes,lsQuotaBytes,quota,usage,idbUsage} 445} 446 447async function nrj_update_storage_info(){ 448 const el=document.getElementById('nrj-storage-info') 449 if(!el)return 450 const {lsUsedBytes,lsQuotaBytes,quota,usage}=await nrj_get_storage_info() 451 const lsRemain=Math.max(0,(lsQuotaBytes||0)-(lsUsedBytes||0)) 452 const idbRemain=(quota!=null && usage!=null)?Math.max(0,quota-usage):null 453 const lsText='<b>LocalStorage remaining:</b><br>' 454 +nrj_fmt_bytes(lsRemain)+' ('+nrj_fmt_bytes(lsUsedBytes)+' used of '+nrj_fmt_bytes(lsQuotaBytes)+')' 455 const idbText='<b>IndexedDB remaining:</b><br>' 456 +(idbRemain!=null?(nrj_fmt_bytes(idbRemain)+' ('+nrj_fmt_bytes(usage)+' used of '+nrj_fmt_bytes(quota)+')'):'â') 457 el.innerHTML=lsText+'<br>'+idbText 458} 459 460 461 462function nrj_ev(el){ 463 el.dispatchEvent(new Event('input',{bubbles:true})) 464 el.dispatchEvent(new Event('change',{bubbles:true})) 465} 466 467function nrj_click(el){try{el.click()}catch(e){}} 468 469function nrj_set(el,val){ 470 if(el.type==='checkbox'){ 471 let on=Array.isArray(val)?val.includes(el.value):(val===el.value||val===true||val==='1'||String(val).toLowerCase()==='yes') 472 el.checked=!!on 473 if(on)nrj_click(el) 474 nrj_ev(el) 475 return 476 } 477 if(el.type==='radio'){ 478 let on=(String(val).toLowerCase()===String(el.value).toLowerCase()) 479 el.checked=!!on 480 if(on){nrj_click(el);nrj_ev(el)} 481 return 482 } 483 if(el.tagName==='SELECT'&&el.multiple){ 484 [].forEach.call(el.options,o=>o.selected=(Array.isArray(val)&&val.includes(o.value))) 485 nrj_ev(el);return 486 } 487 if(el.tagName==='SELECT'){ 488 el.value=Array.isArray(val)?(val[0]??''):(val??'') 489 nrj_ev(el);return 490 } 491 el.value=(Array.isArray(val)?(val[0]??''):(val??'')) 492 nrj_ev(el) 493} 494 495function nrj_open_manage_modal(formId){ 496 let m=document.getElementById('nrj-offline-modal') 497 if(!m){ 498 m=document.createElement('div') 499 m.id='nrj-offline-modal' 500 m.style.position='fixed' 501 m.style.top='0';m.style.left='0';m.style.right='0';m.style.bottom='0' 502 m.style.background='rgba(0,0,0,0.4)' 503 m.style.zIndex='99999' 504 m.innerHTML='<div id="nrj-offline-modal-box" style="background:#fff;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:96vw;max-width:1200px;min-width:320px;border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.3);overflow:hidden"></div>' 505 document.body.appendChild(m) 506 m.addEventListener('click',e=>{if(e.target===m)m.remove()}) 507 }
508 let form=nrj_find_form() 509 let formTitle=null 510 if(form){ 511 let lg=form.querySelector('legend.frm_screen_reader') 512 if(lg){ formTitle=(lg.textContent||'').trim()||null } 513 } 514 nrj_render_manage_modal(formId,formTitle) 515} 516 517 518function nrj_render_manage_modal(formId,formTitle){ 519 let box=document.getElementById('nrj-offline-modal-box') 520 if(!box)return 521 522 let idx=nrj_get_index(formId) 523 let rowsHTML='' 524 let actionStyle='font-weight:700;text-transform:uppercase;cursor:pointer;white-space:nowrap;' 525 526 if(NRJ_ENABLE_DRAFTS){ 527 let draft=idx[NRJ_DRAFT_SLOT] 528 529 if(draft){ 530 let draftText='Saved '+new Date(draft.savedAt).toLocaleString() 531 +' · fields '+(draft.fields||0) 532 +' · files '+(draft.files||0) 533 534 rowsHTML+='<div style="padding-bottom:12px;border-bottom:1px solid #ddd">' 535 +'<div style="display:flex;justify-content:space-between;align-items:center;gap:12px">' 536 +'<div><b>Draft Slot:</b></div>' 537 +'<div style="display:flex;align-items:center;gap:6px;justify-content:flex-end">' 538 +'<span data-act="restore-draft" data-slot="'+NRJ_DRAFT_SLOT+'" role="button" tabindex="0" style="'+actionStyle+'">Restore</span>' 539 +'<span aria-hidden="true">|</span>' 540 +'<span data-act="save-draft-slot" data-slot="'+NRJ_DRAFT_SLOT+'" role="button" tabindex="0" style="'+actionStyle+'">Save to Slot</span>' 541 +'</div>' 542 +'</div>' 543 +'<div style="margin-top:5px;font-size:9px;">'+draftText+'</div>' 544 +'</div>' 545 }else{ 546 rowsHTML+='<div style="padding-bottom:12px;border-bottom:1px solid #ddd">' 547 +'<div style="display:flex;justify-content:space-between;align-items:center;gap:12px">' 548 +'<div><b>Draft Slot:</b></div>' 549 +'<div style="font-weight:700;text-transform:uppercase">Empty</div>' 550 +'</div>' 551 +'</div>' 552 } 553 } 554 555 for(let slot of NRJ_USER_SLOTS){ 556 let info=idx[slot] 557 558 if(info){ 559 let savedText='Saved '+new Date(info.savedAt).toLocaleString() 560 +' · fields '+(info.fields||0) 561 +' · files '+(info.files||0) 562 563 rowsHTML+='<div style="padding-bottom:12px;border-bottom:1px solid #ddd">' 564 +'<div style="display:flex;justify-content:space-between;align-items:center;gap:12px">' 565 +'<div><b>Slot '+slot+':</b></div>' 566 +'<div style="display:flex;align-items:center;gap:6px;justify-content:flex-end">' 567 +'<span data-act="restore" data-slot="'+slot+'" role="button" tabindex="0" style="'+actionStyle+'">Restore</span>' 568 +'<span aria-hidden="true">|</span>' 569 +'<span data-act="delete" data-slot="'+slot+'" role="button" tabindex="0" style="'+actionStyle+'">Delete</span>' 570 +'<span aria-hidden="true">|</span>' 571 +'<span data-act="details" data-slot="'+slot+'" role="button" tabindex="0" style="'+actionStyle+'">Details</span>' 572 +'</div>' 573 +'</div>' 574 +'<div style="margin-top:5px;font-size:9px;">'+savedText+'</div>' 575 +'</div>' 576 }else{ 577 rowsHTML+='<div style="padding-bottom:12px;border-bottom:1px solid #ddd">' 578 +'<div style="display:flex;justify-content:space-between;align-items:center;gap:12px">' 579 +'<div><b>Slot '+slot+':</b></div>' 580 +'<div style="font-weight:700;text-transform:uppercase">Empty</div>' 581 +'</div>' 582 +'</div>' 583 } 584 } 585 586 let headerTitle=formTitle 587 ? nrj_htmlesc(formTitle) 588 : 'Form '+nrj_htmlesc(formId) 589 590 box.innerHTML='<div style="padding:14px 16px;font-weight:700;border-bottom:1px solid #eee">' 591 +'Offline Entries for '+headerTitle 592 +'</div>' 593 +'<div id="nrj-offline-slot-list" style="padding:16px;display:grid;gap:12px;max-height:60vh;overflow-y:auto;overscroll-behavior:contain">' 594 +rowsHTML 595 +'</div>' 596 +'<div style="padding:12px 16px;border-top:1px solid #eee;display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap">' 597 +'<div style="display:flex;align-items:center;gap:8px;flex:1 1 auto;min-width:240px">' 598 +'<div id="nrj-storage-info" style="font-size:10px;color:#555">Calculating storageâ¦</div>' 599 +'<button type="button" id="nrj-clear-all-storage" class="viewbtn">Clear All Storage</button>' 600 +'</div>' 601 +'<button type="button" id="nrj-offline-close" class="viewbtn">Close</button>' 602 +'</div>' 603 604 box.querySelector('#nrj-offline-close').addEventListener('click',()=>{ 605 document.getElementById('nrj-offline-modal')?.remove() 606 }) 607 608 let runAction=async el=>{ 609 let rawSlot=el.dataset.slot 610 let slot=rawSlot===NRJ_DRAFT_SLOT 611 ? NRJ_DRAFT_SLOT 612 : parseInt(rawSlot,10) 613 614 let act=el.dataset.act 615 616 if(act==='delete'){ 617 document.getElementById('nrj-offline-modal')?.remove() 618 await nrj_wait(50) 619 620 let ok=window.confirm( 621 'Are you sure you wish to delete this saved submission (slot '+slot+')?' 622 ) 623 624 if(ok){ 625 await nrj_manage_delete_slot(formId,slot) 626 nrj_toast( 627 'Deleted saved submission (slot '+slot+')', 628 true, 629 null 630 ) 631 } 632 } 633 634 if(act==='restore'){
635 let form=nrj_find_form() 636 if(!form)return 637 638 document.getElementById('nrj-offline-modal')?.remove() 639 640 let dummy=document.createElement('button') 641 await nrj_restore_from_slot(formId,slot,form,dummy) 642 } 643 644 if(act==='restore-draft'){ 645 if(!NRJ_ENABLE_DRAFTS)return 646 647 let form=nrj_find_form() 648 if(!form)return 649 650 document.getElementById('nrj-offline-modal')?.remove() 651 652 let dummy=document.createElement('button') 653 await nrj_restore_from_slot( 654 formId, 655 NRJ_DRAFT_SLOT, 656 form, 657 dummy 658 ) 659 } 660 661 if(act==='save-draft-slot'){ 662 if(!NRJ_ENABLE_DRAFTS)return 663 664 await nrj_save_draft_to_slot(formId,el) 665 document.getElementById('nrj-offline-modal')?.remove() 666 } 667 668 if(act==='details'){ 669 document.getElementById('nrj-offline-modal')?.remove() 670 nrj_open_details_modal(formId,slot) 671 } 672 } 673 674 box.querySelectorAll('[data-act]').forEach(el=>{ 675 el.addEventListener('click',async()=>{ 676 await runAction(el) 677 }) 678 679 el.addEventListener('keydown',async e=>{ 680 if(e.key!=='Enter'&&e.key!==' ')return 681 682 e.preventDefault() 683 await runAction(el) 684 }) 685 }) 686 687 const clearAllBtn=box.querySelector('#nrj-clear-all-storage') 688 689 if(clearAllBtn){ 690 clearAllBtn.addEventListener('click',async()=>{ 691 document.getElementById('nrj-offline-modal')?.remove() 692 await nrj_wait(50) 693 694 const ok=window.confirm( 695 'Clear ALL saved offline entries for this site (all forms and slots)?' 696 ) 697 698 if(ok){ 699 await nrj_clear_all_saved() 700 nrj_toast( 701 'Cleared all saved offline data', 702 true, 703 null 704 ) 705 } 706 }) 707 } 708 709 const isMobile= 710 (window.matchMedia&&window.matchMedia('(max-width: 640px)').matches) 711 ||/Android|iPhone|iPad|iPod|Windows Phone|Mobi/i.test( 712 navigator.userAgent||'' 713 ) 714 715 if(isMobile){ 716 let slotList=box.querySelector('#nrj-offline-slot-list') 717 718 if(slotList){ 719 slotList.style.maxHeight='55vh' 720 } 721 722 box.querySelectorAll('[data-act]').forEach(el=>{ 723 el.style.fontSize='11px' 724 }) 725 726 box.querySelectorAll( 727 '#nrj-clear-all-storage,#nrj-offline-close' 728 ).forEach(btn=>{ 729 btn.style.fontSize='10px' 730 btn.style.padding='5px 8px' 731 }) 732 } 733 734 nrj_update_storage_info() 735} 736 737function nrj_open_details_modal(formId,slot){ 738 let m=document.getElementById('nrj-offline-details-modal') 739 if(!m){ 740 m=document.createElement('div') 741 m.id='nrj-offline-details-modal' 742 m.style.position='fixed' 743 m.style.top='0';m.style.left='0';m.style.right='0';m.style.bottom='0' 744 m.style.background='rgba(0,0,0,0.4)' 745 m.style.zIndex='99999' 746 m.innerHTML='<div id="nrj-offline-details-box" style="background:#fff;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);min-width:340px;max-width:92vw;max-height:90vh;overflow:auto;border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.3)"></div>' 747 document.body.appendChild(m) 748 m.addEventListener('click',e=>{if(e.target===m)m.remove()}) 749 } 750 nrj_render_details_modal(formId,slot) 751} 752 753async function nrj_render_details_modal(formId,slot){ 754 let box=document.getElementById('nrj-offline-details-box') 755 if(!box)return 756 let idx=nrj_get_index(formId) 757 let info=idx[slot]||null 758 let savedAt=info?new Date(info.savedAt).toLocaleString():'â' 759 760 let fieldsStr=localStorage.getItem(nrj_ls_key(formId,slot,FIELDS_KEY))||'{}' 761 let fields={} 762 try{fields=JSON.parse(fieldsStr)||{}}catch(e){fields={}} 763 764 let files=await nrj_load_files(formId,slot) 765 766 let fieldRows='' 767 let fieldKeys=Object.keys(fields||{}) 768 if(fieldKeys.length){ 769 fieldKeys.forEach(k=>{ 770 let v=fields[k] 771 let txt=Array.isArray(v) 772 ? (v.length?v.join(', '):'No Data') 773 : ((v==null||String(v).trim()==='')?'No Data':String(v))
774 let label=nrj_field_label_by_key(k)||k 775 let left=nrj_htmlesc(label)+(label!==k?'<div style="font-size:10px;opacity:.7;margin-top:2px">'+nrj_htmlesc(k)+'</div>':'') 776 fieldRows+='<tr><td colspan="2" style="padding:8px 10px;background:rgba(0,16,46,1);color:#fff;font-weight:700">'+left+'</td></tr>' 777 + '<tr><td colspan="2" style="padding:10px;border-bottom:1px solid #eaeaea;word-break:break-word">'+nrj_htmlesc(txt)+'</td></tr>' 778 }) 779 }else{ 780 fieldRows+='<tr><td colspan="2" style="padding:8px 10px;background:rgba(0,16,46,1);color:#fff;font-weight:700">Fields</td></tr>' 781 +'<tr><td colspan="2" style="padding:10px;border-bottom:1px solid #eaeaea;word-break:break-word">No Data</td></tr>' 782 } 783 784 let fileRows='' 785 let fileKeys=Object.keys(files||{}) 786 if(fileKeys.length){ 787 fileKeys.forEach(k=>{ 788 let arr=(files[k]||[]) 789 let names=arr.length?arr.map(it=>nrj_htmlesc(it.name)).join(', '):'No Data' 790 let label=nrj_field_label_by_key(k)||k 791 let left=nrj_htmlesc(label)+(label!==k?'<div style="font-size:11px;opacity:.9;margin-top:2px">'+nrj_htmlesc(k)+'</div>':'') 792 fileRows+='<tr><td colspan="2" style="padding:8px 10px;background:rgba(0,16,46,1);color:#fff;font-weight:700">'+left+'</td></tr>' 793 + '<tr><td colspan="2" style="padding:10px;border-bottom:1px solid #eaeaea;word-break:break-word">'+names+'</td></tr>' 794 }) 795 }else{ 796 fileRows+='<tr><td colspan="2" style="padding:8px 10px;background:rgba(0,16,46,1);color:#fff;font-weight:700">Files</td></tr>' 797 +'<tr><td colspan="2" style="padding:10px;border-bottom:1px solid #eaeaea;word-break:break-word">No Data</td></tr>' 798 } 799 800 box.innerHTML='<div style="padding:14px 16px;font-weight:700;border-bottom:1px solid #eee">Entry Details · Form '+nrj_htmlesc(formId)+' · Slot '+slot+'</div>' 801 +'<div style="padding:14px 16px;display:grid;gap:12px">' 802 +'<div>Saved: '+nrj_htmlesc(savedAt)+' · Fields: '+fieldKeys.length+' · Files: '+fileKeys.length+'</div>' 803 +'<div>' 804 +'<div style="font-weight:700;margin:8px 0 6px">Fields</div>' 805 +'<div style="max-height:40vh;overflow:auto;border:1px solid #eee;border-radius:6px">' 806 +'<table style="width:100%;border-collapse:collapse;font-size:13px">'+fieldRows+'</table>' 807 +'</div>' 808 +'</div>' 809 +'<div>' 810 +'<div style="font-weight:700;margin:8px 0 6px">Files</div>' 811 +'<div style="max-height:30vh;overflow:auto;border:1px solid #eee;border-radius:6px">' 812 +'<table style="width:100%;border-collapse:collapse;font-size:13px">'+fileRows+'</table>' 813 +'</div>' 814 +'</div>' 815 +'</div>' 816 +'<div style="padding:12px 16px;border-top:1px solid #eee;text-align:right">' 817 +'<button type="button" id="nrj-details-back" style="margin-right:8px" class="viewbtn">Back</button>' 818 +'<button type="button" id="nrj-details-close" class="viewbtn">Close</button>' 819 +'</div>' 820 821 document.getElementById('nrj-details-close')?.addEventListener('click',()=>{document.getElementById('nrj-offline-details-modal')?.remove()}) 822 document.getElementById('nrj-details-back')?.addEventListener('click',()=>{document.getElementById('nrj-offline-details-modal')?.remove();nrj_open_manage_modal(formId)}) 823} 824 825function nrj_field_label_by_key(key){ 826 try{ 827 let parts=[...String(key).matchAll(/\[(\d+)\]/g)].map(m=>m[1]) 828 for(let i=parts.length-1;i>=0;i--){ 829 let id=parts[i] 830 let el=document.getElementById('frm_field_'+id+'_container')||document.querySelector('[id^="frm_field_'+id+'"][id$="_container"]') 831 if(el){ 832 let lbl=el.querySelector('.frm_primary_label') 833 if(lbl){ 834 let t=lbl.textContent||'' 835 return t.trim() 836 } 837 } 838 } 839 }catch(e){} 840 return null 841} 842 843 844function nrj_htmlesc(s){ 845 return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''') 846} 847 848 849function nrj_toast(msg,ok,btn){ 850 let el=document.createElement('div') 851 el.textContent=msg 852 el.style.position='absolute' 853 el.style.display='inline-block' 854 el.style.padding='6px 12px' 855 el.style.fontWeight='700' 856 el.style.borderRadius='6px' 857 el.style.border='1px solid #000' 858 el.style.background=ok?'#c6f6d5':'#fed7d7' 859 el.style.color='#000' 860 el.style.zIndex='99999' 861 el.style.whiteSpace='nowrap' 862 document.body.appendChild(el)
863 let anchor=document.getElementById('nrj-save-offline-buttons')||null 864 if(anchor){ 865 let r=anchor.getBoundingClientRect() 866 let targetX=r.left+window.scrollX+r.width*0.65 867 let w=el.offsetWidth 868 let minL=r.left+window.scrollX 869 let maxL=r.left+window.scrollX+r.width-w 870 let left=Math.max(minL,Math.min(targetX-w/2,maxL)) 871 el.style.left=left+'px' 872 el.style.top=(r.top+window.scrollY-8)+'px' 873 }else{ 874 let vw=document.documentElement.clientWidth 875 let vh=document.documentElement.clientHeight 876 el.style.left=(vw*0.65)+'px' 877 el.style.top=(vh*0.35)+'px' 878 el.style.transform='translate(-50%,-50%)' 879 } 880 setTimeout(()=>{el.style.transition='opacity .2s';el.style.opacity='0';setTimeout(()=>el.remove(),500)},2500) 881} 882 883function nrj_prompt_restore_file_mode(){ 884 let msg='Restore files as compressed images?\n\nRecommended: Compressed files are best when using cellular data and help reduce upload timeouts.\n\nOriginal files are not recommended over cellular networks because they may be large and can cause slow uploads or timeouts.\n\nYour original files remain safely stored in the Draft Slot either way.\n\nPress OK for compressed files.\nPress Cancel for original files.' 885 return window.confirm(msg)?'compressed':'original' 886} 887 888async function nrj_compress_image_file(file,maxWidth=1600,quality=0.72){ 889 if(!file||!file.type||!file.type.startsWith('image/'))return file 890 return new Promise(resolve=>{ 891 let img=new Image() 892 let reader=new FileReader() 893 reader.onload=function(e){ 894 img.src=e.target.result 895 } 896 reader.onerror=function(){ 897 resolve(file) 898 } 899 img.onload=function(){ 900 let scale=Math.min(1,maxWidth/img.width) 901 let canvas=document.createElement('canvas') 902 canvas.width=Math.round(img.width*scale) 903 canvas.height=Math.round(img.height*scale) 904 let ctx=canvas.getContext('2d') 905 ctx.drawImage(img,0,0,canvas.width,canvas.height) 906 canvas.toBlob(function(blob){ 907 if(!blob){ 908 resolve(file) 909 return 910 } 911 let name=String(file.name||'image').replace(/\.(png|jpg|jpeg|webp)$/i,'')+'.jpg' 912 resolve(new File([blob],name,{type:'image/jpeg',lastModified:Date.now()})) 913 },'image/jpeg',quality) 914 } 915 img.onerror=function(){ 916 resolve(file) 917 } 918 reader.readAsDataURL(file) 919 }) 920} 921 922function nrj_file_from_buf(it){ 923 let f=null 924 try{f=new File([new Uint8Array(it.buf)],it.name,{type:it.type||'',lastModified:it.lastModified||Date.now()})} 925 catch(e){try{f=new Blob([new Uint8Array(it.buf)],{type:it.type||''});f.name=it.name}catch(e2){}} 926 return f 927} 928 929async function nrj_buf_from_file(f){ 930 try{if(typeof f.arrayBuffer==='function')return await f.arrayBuffer()}catch(e){} 931 if(f.dataURL){ 932 try{let p=f.dataURL.indexOf(','),b=atob(f.dataURL.slice(p+1)),len=b.length,arr=new Uint8Array(len);for(let i=0;i<len;i++)arr[i]=b.charCodeAt(i);return arr.buffer}catch(e2){} 933 } 934 try{return await new Response(f).arrayBuffer()}catch(e3){return null} 935} 936 937function nrj_b64_from_buf(buf){ 938 let bin='',bytes=new Uint8Array(buf),len=bytes.byteLength 939 for(let i=0;i<len;i++)bin+=String.fromCharCode(bytes[i]) 940 return btoa(bin) 941} 942 943function nrj_buf_from_b64(b64){ 944 let bin=atob(b64),len=bin.length,bytes=new Uint8Array(len) 945 for(let i=0;i<len;i++)bytes[i]=bin.charCodeAt(i) 946 return bytes.buffer 947} 948 949function nrj_dz_instances_in_form(form){ 950 let out=[] 951 if(window.Dropzone&&Array.isArray(window.Dropzone.instances)){ 952 window.Dropzone.instances.forEach(function(d){if(d&&d.element&&form.contains(d.element))out.push(d)}) 953 } 954 if(out.length===0){ 955 let els=form.querySelectorAll('.frm_dropzone') 956 els.forEach(function(z){try{let d=z.dropzone||(window.Dropzone&&window.Dropzone.forElement?window.Dropzone.forElement(z):null);if(d)out.push(d)}catch(e){}}) 957 } 958 return out 959} 960 961function nrj_wait(ms){return new Promise(r=>setTimeout(r,ms))} 962 963async function nrj_clear_saved(formId,slot){ 964 if(!formId){ 965 localStorage.removeItem(FIELDS_KEY) 966 localStorage.removeItem(FILES_KEY) 967 try{await nrj_idb_del('files',FILES_KEY)}catch(e){} 968 return 969 } 970 let idx=nrj_get_index(formId) 971 let slots=slot?[slot]:[NRJ_DRAFT_SLOT,...NRJ_USER_SLOTS] 972 for(let s of slots){ 973 let fk=nrj_ls_key(formId,s,FIELDS_KEY) 974 let sk=nrj_ls_key(formId,s,FILES_KEY) 975 localStorage.removeItem(fk) 976 if(USE_IDB_FILES){ 977 try{await nrj_idb_del('files',sk)}catch(e){} 978 }else{ 979 localStorage.removeItem(sk) 980 } 981 idx[s]=null 982 } 983 nrj_set_index(formId,idx) 984} 985 986 987function nrj_apply_files(input,list){ 988 let applied=false,dt=null 989 try{dt=new DataTransfer()}catch(e){} 990 if(dt){ 991 list.forEach(it=>{let f=nrj_file_from_buf(it);if(f){try{dt.items.add(f)}catch(e){}}}) 992 try{input.files=dt.files;input.dispatchEvent(new Event('input',{bubbles:true}));input.dispatchEvent(new Event('change',{bubbles:true}));applied=true}catch(e){} 993 } 994 let dzEl=input.closest('.frm_dropzone')||input.parentElement?.querySelector?.('.frm_dropzone')||input.closest('.frm_form_field')?.querySelector?.('.frm_dropzone')||null 995 if(window.Dropzone&&dzEl&&dzEl.dropzone){ 996 try{dzEl.dropzone.removeAllFiles(true)}catch(e){} 997 list.forEach(it=>{let f=nrj_file_from_buf(it);if(f){try{dzEl.dropzone.addFile(f);applied=true}catch(e){}}}) 998 } 999 return applied 1000} 1001 1002function nrj_encode_files_for_ls(filesObj){ 1003 let out={} 1004 Object.keys(filesObj||{}
1004).forEach(name=>{ 1005 out[name]=(filesObj[name]||[]).map(it=>({name:it.name,type:it.type||'',lastModified:it.lastModified||Date.now(),b64:nrj_b64_from_buf(it.buf)})) 1006 }) 1007 return JSON.stringify(out) 1008} 1009 1010function nrj_decode_files_from_ls(str){ 1011 if(!str)return null 1012 let raw=JSON.parse(str),out={} 1013 Object.keys(raw||{}).forEach(name=>{ 1014 out[name]=(raw[name]||[]).map(it=>({name:it.name,type:it.type||'',lastModified:it.lastModified||Date.now(),buf:nrj_buf_from_b64(it.b64)})) 1015 }) 1016 return out 1017} 1018 1019function nrj_db(){ 1020 return new Promise((res,rej)=>{ 1021 let r=indexedDB.open('nrjFormCache',2) 1022 r.onupgradeneeded=e=>{ 1023 let db=e.target.result 1024 if(!db.objectStoreNames.contains('forms'))db.createObjectStore('forms') 1025 if(!db.objectStoreNames.contains('files'))db.createObjectStore('files') 1026 } 1027 r.onsuccess=e=>res(e.target.result) 1028 r.onerror=e=>rej(e) 1029 }) 1030} 1031 1032function nrj_idb_put(store,k,v){ 1033 return nrj_db().then(db=>new Promise((res,rej)=>{ 1034 let tx=db.transaction([store],'readwrite');let st=tx.objectStore(store);let q=st.put(v,k) 1035 q.onsuccess=()=>res(true);q.onerror=e=>rej(e);tx.oncomplete=()=>db.close() 1036 })) 1037} 1038 1039function nrj_idb_get(store,k){ 1040 return nrj_db().then(db=>new Promise((res,rej)=>{ 1041 let tx=db.transaction([store],'readonly');let st=tx.objectStore(store);let q=st.get(k) 1042 q.onsuccess=()=>res(q.result||null);q.onerror=e=>rej(e);tx.oncomplete=()=>db.close() 1043 })) 1044} 1045 1046function nrj_idb_del(store,k){ 1047 return nrj_db().then(db=>new Promise((res,rej)=>{ 1048 let tx=db.transaction([store],'readwrite');let st=tx.objectStore(store);let q=st.delete(k) 1049 q.onsuccess=()=>res(true);q.onerror=e=>rej(e);tx.oncomplete=()=>db.close() 1050 })) 1051} 1052 1053window.nrj_file_cache=window.nrj_file_cache||{} 1054function nrj_cache_put(name,arr){if(!name)return;window.nrj_file_cache[name]=(arr||[]).slice();} 1055function nrj_cache_add(name,item){if(!name||!item)return;let a=window.nrj_file_cache[name]||[];a.push(item);window.nrj_file_cache[name]=a;} 1056 1057function nrj_upload_error_log(eventType,data){ 1058 try{ 1059 let body=new URLSearchParams() 1060 body.append('action','nrj_upload_error_log') 1061 body.append('event',eventType||'unknown') 1062 body.append('form_id',data&&data.form_id?data.form_id:'') 1063 body.append('field_key',data&&data.field_key?data.field_key:'') 1064 body.append('file_name',data&&data.file_name?data.file_name:'') 1065 body.append('file_size',data&&data.file_size?String(data.file_size):'') 1066 body.append('message',data&&data.message?data.message:'') 1067 body.append('url',window.location.href) 1068 fetch((window.ajaxurl||'/wp-admin/admin-ajax.php'),{ 1069 method:'POST', 1070 credentials:'same-origin', 1071 headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'}, 1072 body:body.toString() 1073 }).catch(()=>{}) 1074 }catch(e){} 1075} 1076 1077function nrj_bind_file_sources(form){ 1078 let inputs=[...form.querySelectorAll('input[type="file"]')] 1079 1080 inputs.forEach(inp=>{ 1081 if(inp.dataset.nrjBound==='1')return 1082 1083 inp.dataset.nrjBound='1' 1084 1085 inp.addEventListener('change',async()=>{ 1086 if(NRJ_ENABLE_DRAFTS){ 1087 await nrj_save_draft(form) 1088 } 1089 }) 1090 }) 1091 1092 let dzs=nrj_dz_instances_in_form(form) 1093 1094 dzs.forEach(dz=>{ 1095 if(!dz)return 1096 if(dz.__nrjBound)return 1097 1098 dz.__nrjBound=true 1099 1100 let formId=nrj_get_form_id(form) 1101 let info=nrj_resolve_file_key(dz.element) 1102 let key=info&&info.key?info.key:null 1103 let originalTransform=dz.options.transformFile 1104 1105 dz.options.transformFile=function(file,done){ 1106 if(!file||!file.type||!file.type.startsWith('image/')){ 1107 if(typeof originalTransform==='function'){ 1108 originalTransform.call(dz,file,done) 1109 }else{ 1110 done(file) 1111 } 1112 1113 return 1114 } 1115 1116 nrj_compress_image_file(file).then(compressed=>{ 1117 nrj_logc( 1118 'compressed', 1119 file.name, 1120 file.size, 1121 '=>', 1122 compressed.size 1123 ) 1124 1125 done(compressed) 1126 }).catch(e=>{ 1127 let errorMessage=e&&e.message 1128 ? e.message 1129 : String(e||'Compression failed') 1130
1131 nrj_upload_error_log('compression_failed',{ 1132 form_id:formId, 1133 field_key:key, 1134 file_name:file&&file.name?file.name:'', 1135 file_size:file&&file.size?file.size:'', 1136 message:errorMessage 1137 }) 1138 1139 done(file) 1140 }) 1141 } 1142 1143 dz.on('addedfile',async file=>{ 1144 if(!key||!file)return 1145 1146 let buf=await nrj_buf_from_file(file) 1147 1148 if(buf){ 1149 nrj_cache_add(key,{ 1150 name:file.name, 1151 type:file.type||'', 1152 lastModified:file.lastModified||Date.now(), 1153 buf 1154 }) 1155 1156 if(NRJ_ENABLE_DRAFTS){ 1157 await nrj_save_draft(form) 1158 } 1159 } 1160 }) 1161 1162 dz.on('removedfile',async file=>{ 1163 if(!key||!file)return 1164 1165 let arr=(window.nrj_file_cache[key]||[]).filter(it=>!( 1166 it.name===file.name&& 1167 (it.lastModified||0)===(file.lastModified||0) 1168 )) 1169 1170 window.nrj_file_cache[key]=arr 1171 1172 if(NRJ_ENABLE_DRAFTS){ 1173 await nrj_save_draft(form) 1174 } 1175 }) 1176 1177 dz.on('error',function(file,message,xhr){ 1178 let errorMessage='' 1179 1180 if(typeof message==='string'){ 1181 errorMessage=message 1182 }else{ 1183 try{ 1184 errorMessage=JSON.stringify(message||{}) 1185 }catch(e){ 1186 errorMessage=String(message||'Upload failed') 1187 } 1188 } 1189 1190 if(xhr&&xhr.status){ 1191 errorMessage+=' | HTTP '+xhr.status 1192 } 1193 1194 nrj_upload_error_log('dropzone_error',{ 1195 form_id:formId, 1196 field_key:key, 1197 file_name:file&&file.name?file.name:'', 1198 file_size:file&&file.size?file.size:'', 1199 message:errorMessage 1200 }) 1201 }) 1202 1203 dz.on('canceled',function(file){ 1204 nrj_upload_error_log('dropzone_canceled',{ 1205 form_id:formId, 1206 field_key:key, 1207 file_name:file&&file.name?file.name:'', 1208 file_size:file&&file.size?file.size:'', 1209 message:'Upload canceled' 1210 }) 1211 }) 1212 1213 dz.on('timeout',function(file){ 1214 nrj_upload_error_log('dropzone_timeout',{ 1215 form_id:formId, 1216 field_key:key, 1217 file_name:file&&file.name?file.name:'', 1218 file_size:file&&file.size?file.size:'', 1219 message:'Upload timeout' 1220 }) 1221 }) 1222 }) 1223} 1224 1225function nrj_apply_subset(form,fields,pred){ 1226 Object.keys(fields||{}).forEach(name=>{ 1227 let nodes=form.querySelectorAll('[name="'+CSS.escape(name)+'"]') 1228 if(!nodes.length)return 1229 if(!pred(nodes[0]))return 1230 let v=fields[name] 1231 if(nodes[0].type==='radio')nodes.forEach(r=>nrj_set(r,v)) 1232 else if(nodes[0].type==='checkbox')nodes.forEach(c=>nrj_set(c,v)) 1233 else nrj_set(nodes[0],v) 1234 }) 1235} 1236 1237async function nrj_collect(form){ 1238 let fields={} 1239 form.querySelectorAll('input,select,textarea').forEach(el=>{ 1240 if(!el.name||el.disabled)return 1241 if(el.type==='file')return 1242 if(el.type==='checkbox'){fields[el.name]=fields[el.name]||[];if(el.checked)fields[el.name].push(el.value);return} 1243 if(el.type==='radio'){if(el.checked)fields[el.name]=el.value;return} 1244 if(el.tagName==='SELECT'&&el.multiple){fields[el.name]=[].slice.call(el.options).filter(o=>o.selected).map(o=>o.value);return} 1245 fields[el.name]=el.value 1246 }) 1247 let files={} 1248 let inputs=[...form.querySelectorAll('input[type="file"]')] 1249 let dzs=nrj_dz_instances_in_form(form) 1250 for(let input of inputs){ 1251 let key=input.name 1252 let cached=window.nrj_file_cache[key] 1253 if(cached&&cached.length){files[key]=cached.slice();continue} 1254 if(input.files&&input.files.length){ 1255 let arr=[] 1256 for(let f of input.files){ 1257 let buf=await nrj_buf_from_file(f) 1258 if(buf){arr.push({name:f.name,type:f.type||'',lastModified:f.lastModified||Date.now(),buf})} 1259 } 1260 if(arr.length)files[key]=arr 1261 } 1262 }
1263 dzs.forEach(dz=>{ 1264 let input=dz.element.closest('.frm_form_field')?.querySelector('input[type="file"]')||dz.element.querySelector('input[type="file"]')||null 1265 let key=input?input.name:null 1266 if(!key)return 1267 if(files[key])return 1268 let cached=window.nrj_file_cache[key] 1269 if(cached&&cached.length)files[key]=cached.slice() 1270 }) 1271 let n=Object.values(files).reduce((t,a)=>t+(a?a.length:0),0) 1272 return {fields,files,__counts:{total:n}} 1273} 1274 1275async function nrj_collect_fields(form){ 1276 let fields={} 1277 form.querySelectorAll('input,select,textarea').forEach(el=>{ 1278 if(!el.name||el.disabled)return 1279 if(el.type==='file')return 1280 if(el.type==='checkbox'){fields[el.name]=fields[el.name]||[];if(el.checked)fields[el.name].push(el.value);return} 1281 if(el.type==='radio'){if(el.checked)fields[el.name]=el.value;return} 1282 if(el.tagName==='SELECT'&&el.multiple){fields[el.name]=[].slice.call(el.options).filter(o=>o.selected).map(o=>o.value);return} 1283 fields[el.name]=el.value 1284 }) 1285 return fields 1286} 1287 1288 1289async function nrj_collect_files(form){ 1290 let files={} 1291 let hiddenKeys=[...form.querySelectorAll('input[type="hidden"][data-frmfile][name]')].map(h=>h.name) 1292 let uniqKeys=[...new Set(hiddenKeys)] 1293 for(let key of uniqKeys){ 1294 let cached=window.nrj_file_cache[key] 1295 if(cached&&cached.length){ 1296 files[key]=cached.slice() 1297 continue 1298 } 1299 let dz=nrj_find_dropzone_for_key(form,key) 1300 if(dz){ 1301 let list=typeof dz.getAcceptedFiles==='function'?dz.getAcceptedFiles():Array.isArray(dz.files)?dz.files:[] 1302 let arr=[] 1303 for(let f of list){ 1304 let buf=await nrj_buf_from_file(f) 1305 if(!buf&&f&&f.dataURL){ 1306 try{let p=f.dataURL.indexOf(',');buf=nrj_buf_from_b64(f.dataURL.slice(p+1))}catch(e){} 1307 } 1308 if(buf)arr.push({name:f.name,type:f.type||'',lastModified:f.lastModified||Date.now(),buf}) 1309 } 1310 if(arr.length){ 1311 files[key]=arr 1312 continue 1313 } 1314 } 1315 let hidden=form.querySelector('input[type="hidden"][data-frmfile][name="'+CSS.escape(key)+'"]') 1316 let field=hidden?hidden.closest('.frm_form_field'):null 1317 let native=field?field.querySelector('input[type="file"]'):null 1318 if(native&&native.files&&native.files.length){ 1319 let arr=[] 1320 for(let f of native.files){ 1321 let buf=await nrj_buf_from_file(f) 1322 if(buf)arr.push({name:f.name,type:f.type||'',lastModified:f.lastModified||Date.now(),buf}) 1323 } 1324 if(arr.length){ 1325 files[key]=arr 1326 } 1327 } 1328 } 1329 let total=Object.values(files).reduce((t,a)=>t+(a?a.length:0),0) 1330 return files 1331} 1332 1333 1334 1335async function nrj_save_files(form,formId,slot){ 1336 let files=await nrj_collect_files(form) 1337 try{ 1338 let payload=nrj_encode_files_for_ls(files) 1339 let key=nrj_ls_key(formId,slot,FILES_KEY) 1340 if(USE_IDB_FILES){ 1341 await nrj_idb_put('files',key,payload) 1342 }else{ 1343 localStorage.setItem(key,payload) 1344 } 1345 }catch(e){nrj_logc('save:files_error',e&&e.message?e.message:e)} 1346 let total=Object.values(files).reduce((t,a)=>t+(a?a.length:0),0) 1347 return total 1348} 1349 1350async function nrj_save_draft(form){ 1351 if(!NRJ_ENABLE_DRAFTS)return false 1352 let formId=nrj_get_form_id(form) 1353 if(!formId)return false 1354 let fields=await nrj_collect_fields(form) 1355 try{ 1356 localStorage.setItem(nrj_ls_key(formId,NRJ_DRAFT_SLOT,FIELDS_KEY),JSON.stringify(fields)) 1357 }catch(e){ 1358 nrj_logc('draft:fields_error',e&&e.message?e.message:e) 1359 } 1360 let total=await nrj_save_files(form,formId,NRJ_DRAFT_SLOT) 1361 let idx=nrj_get_index(formId) 1362 idx[NRJ_DRAFT_SLOT]={savedAt:Date.now(),fields:Object.keys(fields).length,files:total} 1363 nrj_set_index(formId,idx) 1364 return true 1365} 1366 1367async function nrj_save_draft_to_slot(formId,btn){ 1368 if(!NRJ_ENABLE_DRAFTS)return false 1369 let fieldsStr=localStorage.getItem(nrj_ls_key(formId,NRJ_DRAFT_SLOT,FIELDS_KEY)) 1370 let filesObj=await nrj_load_files(formId,NRJ_DRAFT_SLOT) 1371 if(!fieldsStr && (!filesObj||!Object.keys(filesObj).length)){ 1372 nrj_toast('Draft Slot is empty',false,btn) 1373 return false 1374 } 1375 let idx=nrj_get_index(formId) 1376 let slot=nrj_slot_pick_save(idx) 1377 if(!slot){ 1378 nrj_toast('Save canceled',false,btn) 1379 return false 1380 } 1381 if(fieldsStr){ 1382 localStorage.setItem(nrj_ls_key(formId,slot,FIELDS_KEY),fieldsStr) 1383 } 1384 let total=0 1385 if(filesObj&&Object.keys(filesObj).length){ 1386 let payload=nrj_encode_files_for_ls(filesObj) 1387 let key=nrj_ls_key(formId,slot,FILES_KEY) 1388 if(USE_IDB_FILES){ 1389 await nrj_idb_put('files',key,payload) 1390 }else{ 1391 localStorage.setItem(key,payload) 1392 } 1393 total=Object.values(filesObj).reduce((t,a)=>t+(a?a.length:0),0) 1394 } 1395 let fields={} 1396 try{fields=fieldsStr?JSON.parse(fieldsStr):{}}catch(e){} 1397 idx[slot]={savedAt:Date.now(),fields:Object.keys(fields).length,files:total} 1398 nrj_set_index(formId,idx) 1399 nrj_toast('Draft saved to slot '+slot,true,btn) 1400 return true 1401} 1402 1403async function nrj_check_existing_draft_on_load(form){ 1404 if(!NRJ_ENABLE_DRAFTS)return true 1405 if(!form||form.dataset.nrjDraftChecked==='1')return false 1406 form.dataset.nrjDraftChecked='1' 1407 let formId=nrj_get_form_id(form) 1408 if(!formId)return false 1409 let idx=nrj_get_index(formId) 1410 let draft=idx[NRJ_DRAFT_SLOT] 1411 if(!draft)return true
1412 let choice=window.prompt( 1413 'An unfinished draft was found on this device.\n\nType 1 to restore the draft.\nType 2 to save the draft to an offline slot and start a new form.\nType 3 to discard the draft and start a new form.\n\nThe draft will not be overwritten until you choose.', 1414 '1' 1415 ) 1416 if(choice==='1'){ 1417 await nrj_restore_from_slot(formId,NRJ_DRAFT_SLOT,form,null) 1418 return true 1419 } 1420 if(choice==='2'){ 1421 await nrj_save_draft_to_slot(formId,null) 1422 await nrj_clear_saved(formId,NRJ_DRAFT_SLOT) 1423 nrj_toast('Draft saved to slot. Starting new form.',true,null) 1424 return true 1425 } 1426 if(choice==='3'){ 1427 let ok=window.confirm('Discard this Draft Slot permanently?') 1428 if(ok){ 1429 await nrj_clear_saved(formId,NRJ_DRAFT_SLOT) 1430 nrj_toast('Draft discarded. Starting new form.',true,null) 1431 return true 1432 } 1433 } 1434 nrj_toast('Draft kept safe. Autosave is paused until refresh.',false,null) 1435 return false 1436} 1437 1438function nrj_bind_draft_autosave(form){ 1439 if(!NRJ_ENABLE_DRAFTS)return 1440 if(!form||form.dataset.nrjDraftAutosaveBound==='1')return 1441 form.dataset.nrjDraftAutosaveBound='1' 1442 let timer=null 1443 let queue=function(){ 1444 clearTimeout(timer) 1445 timer=setTimeout(async function(){ 1446 await nrj_save_draft(form) 1447 },1200) 1448 } 1449 form.addEventListener('input',function(e){ 1450 if(!e.target||!e.target.name)return 1451 if(e.target.type==='file')return 1452 queue() 1453 }) 1454 form.addEventListener('change',function(e){ 1455 if(!e.target||!e.target.name)return 1456 if(e.target.type==='file')return 1457 queue() 1458 }) 1459} 1460 1461 1462async function nrj_load_files(formId,slot){ 1463 let key=nrj_ls_key(formId,slot,FILES_KEY) 1464 if(USE_IDB_FILES){ 1465 let raw=await nrj_idb_get('files',key) 1466 if(!raw)return {} 1467 if(typeof raw==='string') return nrj_decode_files_from_ls(raw)||{} 1468 try{ return nrj_decode_files_from_ls(JSON.stringify(raw))||{} }catch(e){ return {} } 1469 }else{ 1470 let str=localStorage.getItem(key) 1471 return nrj_decode_files_from_ls(str)||{} 1472 } 1473} 1474 1475 1476function nrj_resolve_file_key(node){ 1477 let el=node&&node.element?node.element:node 1478 if(!(el instanceof HTMLElement))return null 1479 let field=el.closest('.frm_form_field')||el.closest('.frm_form_field_container')||el 1480 let hidden=field?field.querySelector('input[type="hidden"][data-frmfile][name]'):null 1481 if(hidden&&hidden.name)return {key:hidden.name,hidden,field} 1482 return null 1483} 1484 1485function nrj_find_dropzone_for_key(form,key){ 1486 if(!form||!key)return null 1487 let hidden=form.querySelector('input[type="hidden"][data-frmfile][name="'+CSS.escape(key)+'"]') 1488 if(!hidden)return null 1489 let field=hidden.closest('.frm_form_field') 1490 if(!field)return null 1491 let dzEl=field.querySelector('.frm_dropzone') 1492 if(!dzEl)return null 1493 if(dzEl.dropzone)return dzEl.dropzone 1494 if(window.Dropzone&&typeof window.Dropzone.forElement==='function'){ 1495 try{return window.Dropzone.forElement(dzEl)}catch(e){return null} 1496 } 1497 return null 1498} 1499 1500async function nrj_ensure_row_for_key(form,key){ 1501 let m=key.match(/^item_meta\[(\d+)\]\[(\d+)\]\[(\d+)\]/) 1502 if(!m)return 1503 let parent=m[1],row=parseInt(m[2],10) 1504 let sec=form.querySelector('#frm_section_'+parent+'-'+row) 1505 if(sec)return 1506 let btnAdd=form.querySelector('.frm_add_form_row[data-parent="'+parent+'"]') 1507 if(!btnAdd)return 1508 while(!form.querySelector('#frm_section_'+parent+'-'+row)){btnAdd.click();await nrj_wait(250)} 1509} 1510 1511 1512async function nrj_save(form,btn){ 1513 let formId=nrj_get_form_id(form) 1514 if(!formId){nrj_toast('Missing form_id',false,btn);return false} 1515 let idx=nrj_get_index(formId) 1516 let slot=nrj_slot_pick_save(idx) 1517 if(!slot){nrj_toast('Save canceled',false,btn);return false} 1518 let fields=await nrj_collect_fields(form) 1519 try{localStorage.setItem(nrj_ls_key(formId,slot,FIELDS_KEY),JSON.stringify(fields));}catch(e){nrj_logc('save:fields_error',e)} 1520 let total=await nrj_save_files(form,formId,slot) 1521 idx[slot]={savedAt:Date.now(),fields:Object.keys(fields).length,files:total} 1522 nrj_set_index(formId,idx) 1523 nrj_toast('Saved in slot '+slot+' · files '+total,true,btn) 1524 return true 1525} 1526 1527 1528function nrj_bind(){ 1529 if(nrj_bind._t)return 1530 nrj_bind._t=setTimeout(async function(){ 1531 nrj_bind._t=null 1532 await nrj_bind_now() 1533 },200) 1534} 1535 1536function nrj_set_pending_submit(formId){ 1537 if(!NRJ_ENABLE_DRAFTS)return 1538 if(!formId)return 1539 sessionStorage.setItem('nrj_pending_submit_form_id',formId) 1540 sessionStorage.setItem('nrj_pending_submit_time',String(Date.now())) 1541} 1542 1543async function nrj_check_pending_submit_success(){ 1544 if(!NRJ_ENABLE_DRAFTS)return 1545 let formId=sessionStorage.getItem('nrj_pending_submit_form_id') 1546 let submitTime=parseInt(sessionStorage.getItem('nrj_pending_submit_time')||'0',10) 1547 if(!formId||!submitTime)return 1548 let age=Date.now()-submitTime 1549 if(age>120000){ 1550 sessionStorage.removeItem('nrj_pending_submit_form_id') 1551 sessionStorage.removeItem('nrj_pending_submit_time') 1552 return 1553 } 1554 let hasErrors=document.querySelector('.frm_error,.frm_error_style,.frm_message_error,.frm_invalid') 1555 let hasSuccess=document.querySelector('.frm_message,.frm_success_style') 1556 if(hasSuccess&&!hasErrors){ 1557 await nrj_clear_saved(formId,NRJ_DRAFT_SLOT) 1558 sessionStorage.removeItem('nrj_pending_submit_form_id') 1559 sessionStorage.removeItem('nrj_pending_submit_time') 1560 nrj_toast('Submitted successfully. Draft Slot cleared.',true,null) 1561 } 1562} 1563 1564async function nrj_check_pending_submit_failure(){ 1565 if(!NRJ_ENABLE_DRAFTS)return 1566 let formId=sessionStorage.getItem('nrj_pending_submit_form_id') 1567 let submitTime=parseInt(sessionStorage.getItem('nrj_pending_submit_time')||'0',10) 1568 if(!formId||!submitTime)return 1569 let age=Date.now()-submitTime 1570 if(age>120000){ 1571 sessionStorage.removeItem('nrj_pending_submit_form_id') 1572 sessionStorage.removeItem('nrj_pending_submit_time') 1573 return 1574 } 1575 let hasErrors=document.querySelector('.frm_error,.frm_error_style,.frm_message_error,.frm_invalid') 1576 let hasSuccess=document.querySelector('.frm_message,.frm_success_style') 1577 if(hasErrors&&!hasSuccess){ 1578 let save=window.confirm('Upload failed. Your draft is still safely stored on this device.\n\nWould you also like to save a permanent copy to one of your offline slots?') 1579 if(save){ 1580 await nrj_save_draft_to_slot(formId,null) 1581 } 1582 sessionStorage.removeItem('nrj_pending_submit_form_id') 1583 sessionStorage.removeItem('nrj_pending_submit_time') 1584 } 1585} 1586 1587 1588async function nrj_bind_now(){ 1589 let saveBtn=document.getElementById('nrj-save-report-offline') 1590 let manageBtn=document.getElementById('nrj-manage-offline') 1591 if(!saveBtn&&!manageBtn)return 1592 let form=(saveBtn&&saveBtn.closest('form'))||(manageBtn&&manageBtn.closest('form'))||nrj_find_form() 1593 if(!form)return 1594 nrj_bind_file_sources(form) 1595 let allowAutosave=await nrj_check_existing_draft_on_load(form) 1596 if(allowAutosave)nrj_bind_draft_autosave(form)
1597 if(saveBtn&&!saveBtn.dataset.nrjBound){saveBtn.dataset.nrjBound='1';saveBtn.addEventListener('click',async()=>{await nrj_save(form,saveBtn)})} 1598 if(manageBtn&&!manageBtn.dataset.nrjBound){manageBtn.dataset.nrjBound='1';manageBtn.addEventListener('click',()=>{let fid=nrj_get_form_id(form);if(fid)nrj_open_manage_modal(fid)})} 1599} 1600 1601document.addEventListener('DOMContentLoaded',function(){ 1602 nrj_bind() 1603 nrj_check_pending_submit_success() 1604 nrj_check_pending_submit_failure() 1605}) 1606new MutationObserver(nrj_bind).observe(document.documentElement,{childList:true,subtree:true}) 1607window.nrj_offline_clear_saved=nrj_clear_saved
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.