From 0113d2e9c9ff34a6ffb10707cc76d34b67a0c367 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 19 Jan 2026 16:29:41 +0000
Subject: [PATCH] =Refactored window.getTemplate into a full templating class window.jvbTemplates. Refactored CRUD.js, UploadManager.js, FormController.js, PopulateForm.js with that in mind

---
 assets/js/min/uploader.min.js |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diff --git a/assets/js/min/uploader.min.js b/assets/js/min/uploader.min.js
index 94f817d..7f8dabc 100644
--- a/assets/js/min/uploader.min.js
+++ b/assets/js/min/uploader.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.notifications=window.jvbNotifications,this.initDB(),this.fields=new Map,this.uploads=new Map,this.uploadBlobs=new Map,this.timeouts=new Map,this.selected=new Map,this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}},this.touch={x:null,y:null},this.hasBulkContext=null!==document.querySelector("details.uploader"),this.isTouching=!1,this.groups=new Map,this.subscribers=new Set,this.settings={allowedTypes:["image/jpeg","image/png","image/gif","image/webp","image/avif"],maxFileSize:5242880,maxProcessingTime:12e4,processingCheckInterval:5e3,smartCompression:!0,fieldTypes:{single:{maxFiles:1,allowMultiple:!1},gallery:{maxFiles:20,allowMultiple:!0},groupable:{maxFiles:20,allowMultiple:!0}}},this.statusMapping={received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"},this.init()}async init(){this.initElements(),this.initListeners(),this.initCompressionWorker(),this.queue.subscribe(((e,t)=>{if(console.log("Operation Endpoint: ",t.endpoint),"uploads"===t.endpoint)switch(e){case"cancel-operation":this.clearField(t.data.get("field_key"));break;case"operation-status":console.log("Operation Data: ",t.data);const e=t.data?.field_key||(t.data instanceof FormData?t.data.get("field_key"):null);e&&(console.log("Updating field status:",e,t.status),this.updateFieldStatus(e,t.status))}})),await this.checkPendingUploads(),this.scanFields()}initElements(){this.selectors={field:{field:".field.image",dropZone:".file-upload-container",preview:".item-grid.preview",hiddenValue:'input[type="hidden"]',progress:{progress:".progress",details:".progress .details",fill:".progress .fill",count:".progress .count"}},item:{img:"img",progress:{progress:".progress",details:".progress .details",fill:".progress .fill",count:".progress .count"},status:".status",select:'[name*="select-item"]',actions:".item-actions",featured:'[name="featured"]',meta:".upload-meta"},groups:{container:".item-grid.groups",display:".group-display",selectAll:"#select-all-uploads",actions:".selection-actions",info:".selection-controls .info",count:".selection-count",group:".upload-group",empty:".empty-group"}},this.ui={}}scanFields(){document.querySelectorAll(this.selectors.field.field).forEach((e=>{this.registerUploader(e)}))}registerUploader(e,t={}){let a=e.dataset.uploader??this.determineKey(e);if(e.dataset.uploader=a,!this.fields.has(a)){let s=e.dataset.type,i=this.settings.fieldTypes[s]??this.settings.fieldTypes.single,r={key:a,name:e.dataset.field,ui:{},type:s,maxFiles:i.maxFiles,multiple:i.allowMultiple,content:e.dataset.content??e.closest("dialog")?.dataset.content??e.closest("form").dataset.save??!1,itemID:e.dataset.itemID??e.closest("dialog")?.dataset.itemID??!1,context:e.dataset.context??e.closest("dialog")?.dataset.context??!1,mode:e.dataset.mode??"direct",...t};r.ui=window.uiFromSelectors(this.selectors,e),r.ui.groups.groups=new Map,this.selected.set(a,new Set),this.fields.set(a,r),"groupable"!==r.type||this.hasGroups||this.initGroupListeners()}return a}determineKey(e){return`${e.dataset.content??e.closest("dialog")?.dataset.content??e.closest("form").dataset.save??""}_${e.dataset.itemID??e.closest("dialog")?.dataset.itemID??""}_${e.dataset.field}`}getFieldIdFromElement(e){let t=e.closest(".field.image");if(t)return t.dataset.uploader??this.determineKey(t)}getFieldFromElement(e){let t=this.getFieldIdFromElement(e);return!!this.fields.has(t)&&this.fields.get(t)}getUploadFromElement(e){let t=this.getUploadIdFromElement(e);return!!this.uploads.has(t)&&this.uploads.get(t)}getUploadIdFromElement(e){let t=e.closest("[data-upload-id]");return t?.dataset.uploadId||null}getGroupFromElement(e){let t=this.getGroupIdFromElement(e);return!!this.groups.has(t)&&this.groups.get(t)}getGroupIdFromElement(e){return e.dataset.groupId??e.closest("[data-group-id]")?.dataset.groupId??e.closest(":has([data-group-id])")?.querySelector("[data-group-id]")?.dataset.groupId??null}getModalType(e){if(!(e&&e.ui&&e.ui.field&&e.ui.field.field))return null;const t=e.ui.field.field.closest("dialog");return t?t.classList.contains("edit")?"edit":t.classList.contains("create")?"create":t.classList.contains("bulkEdit")?"bulkEdit":t.className:null}getStatusText(e){return this.statusMapping[e]||e}getStatusIcon(e){return window.getIcon(this.queue.icons[e])}getStatusProgress(e){switch(console.log("Getting status progress for: ",e),e){case"local_processing":return 28;case"queued":return 50;case"uploading":return 66;case"pending":return 75;case"processing":return 89;case"completed":return 100;default:return 0}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.hasBulkContext&&(this.pasteHandler=this.handlePaste.bind(this),document.addEventListener("paste",this.pasteHandler)),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),window.addEventListener("beforeunload",this.handleBeforeUnload.bind(this))}clearListeners(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),this.hasBulkContext&&document.removeEventListener("paste",this.pasteHandler)}initGroupListeners(){this.hasGroups=!0,this.dragStartHandler=this.handleDragStart.bind(this),this.dragEndHandler=this.handleDragEnd.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dropHandler=this.handleDrop.bind(this),this.touchStartHandler=this.handleTouchStart.bind(this),this.touchMoveHandler=this.handleTouchMove.bind(this),this.touchEndHandler=this.handleTouchEnd.bind(this),this.touchCancelHandler=this.handleTouchCancel.bind(this),document.addEventListener("dragstart",this.dragStartHandler),document.addEventListener("dragend",this.dragEndHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("drop",this.dropHandler),document.addEventListener("touchstart",this.touchStartHandler),document.addEventListener("touchmove",this.touchMoveHandler),document.addEventListener("touchend",this.touchEndHandler),document.addEventListener("touchcancel",this.touchCancelHandler)}clearGroupListeners(){document.removeEventListener("dragstart",this.dragStartHandler),document.removeEventListener("dragend",this.dragEndHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("drop",this.dropHandler),document.removeEventListener("touchstart",this.touchStartHandler),document.removeEventListener("touchmove",this.touchMoveHandler),document.removeEventListener("touchend",this.touchEndHandler),document.removeEventListener("touchcancel",this.touchCancelHandler)}handleClick(e){if(e.target.closest(this.selectors.field.field))if(window.targetCheck(e,".restart-uploads")){e.preventDefault();const t=this.getFieldIdFromElement(e.target);this.restartUploads(t)}else if(window.targetCheck(e,".dismiss-cache-restore")){e.preventDefault();const t=e.target.closest(".upload-recovery-notification");t&&t.remove()}else if(window.targetCheck(e,"#select-all-uploads"))e.preventDefault(),this.handleSelectAll(e.target);else if(window.targetCheck(e,".upload-select")){e.shiftKey&&this.lastClickedUpload?(e.preventDefault(),this.handleRangeSelection(e.target,e)):this.updateSelection(e)}else if(window.targetCheck(e,".create-from-selection")){e.preventDefault();let t=this.createGroup(this.getFieldFromElement(e.target));this.addSelectionToGroup(t)}else if(window.targetCheck(e,".remove-selection"))e.preventDefault(),this.removeSelection(e.target);else if(window.targetCheck(e,".add-to-group, .add-selection-to-group"))e.preventDefault(),this.addSelectionToGroup(e.target);else if(window.targetCheck(e,".remove-group")){e.preventDefault();const t=e.target.closest(".upload-group");if(t){this.getFieldFromElement(t);this.removeGroup(t,!0)}}else if(window.targetCheck(e,".remove")){e.preventDefault();const t=this.getUploadIdFromElement(e.target),a=this.getFieldIdFromElement(e.target);t&&a&&this.removeUpload(a,t)}else if(window.targetCheck(e,".submit-uploads")){e.preventDefault();const t=this.getFieldIdFromElement(e.target);this.submitUploads(t)}else if(window.targetCheck(e,".retry-upload")){e.preventDefault();const t=this.getUploadIdFromElement(e.target);this.retryUpload(t)}}handleChange(e){if(e.target.closest(this.selectors.field.field)&&!e.target.classList.contains(this.selectors.field.hiddenValue))if(e.preventDefault(),window.targetCheck(e,'[type="file"]')){console.log(this.fields);let t=this.getFieldFromElement(e.target);if(console.log(t),!t)return void console.warn("File change on unregistered field: ",t.key);const a=Array.from(e.target.files);if(0===a.length)return;this.processFiles(t.key,a),e.target.value=""}else if(e.target.name.includes("select-"))this.updateSelection(e);else if(e.target.closest(".upload-meta")){e.preventDefault();let t=e.target.name,a=e.target.value,s=this.getUploadFromElement(e.target);s.changes[t]=a,this.uploads.set(s.id,s),this.persistFieldState(s.fieldId)}else if(e.target.closest(".group.fields")){let t=this.getGroupFromElement(e.target),a=e.target.name;t.changes[a]=e.target.value,this.persistFieldState(t.fieldId),this.groups.set(t.id,t)}}handlePaste(e){window.debouncer.schedule("imagePaste",(()=>{const t=Array.from(e.clipboardData.items).filter((e=>e.type.startsWith("image/")));if(0===t.length)return;e.preventDefault();const a=this.getFieldIdFromElement(e.target);if(!a)return;const s=[];t.forEach(((e,t)=>{const a=e.getAsFile();if(a){const e=new File([a],`pasted_image_${t+1}.png`,{type:a.type,lastModified:Date.now()});s.push(e)}})),s.length>0&&this.processFiles(a,s)}),100)}isTouchOnFormElement(e){return["input","button","label","select","textarea"].some((t=>e.matches(t)||e.closest(t)))}startDragOperation(e){const{primaryElement:t,sourceType:a,startPosition:s,event:i}=e,r=this.getUploadIdFromElement(t),o=this.getFieldIdFromElement(t),n=this.getDraggedItems(t);this.dragState={primaryItem:r,draggedItems:n,isDragging:!0,isMultiDrag:n.length>1,fieldId:o,sourceType:a,startTime:Date.now(),startPosition:s,currentPosition:s,currentTarget:null,validTarget:null,dragPreview:null,touchId:"touch"===a?i.touches[0]?.identifier:null,touchMoved:!1},this.createDragPreview(t),this.applyDraggingState(!0);const l=this.dragState.isMultiDrag?`Started dragging ${n.length} items`:"Started dragging item";return this.a11y.announce(l),this.provideDragFeedback("start"),!0}updateDragOperation(e,t){if(!this.dragState.isDragging)return;const{sourceType:a,startPosition:s}=this.dragState;if(this.dragState.currentPosition=e,"touch"===a&&!this.dragState.touchMoved){const t=Math.abs(e.x-s.x),a=Math.abs(e.y-s.y);(t>10||a>10)&&(this.dragState.touchMoved=!0)}this.updateDragPreview(e),this.updateDropTarget(t)}endDragOperation(e=null){if(!this.dragState.isDragging)return;const t=("drag"===this.dragState.sourceType||this.dragState.touchMoved)&&this.dragState.validTarget;t&&this.dragState.validTarget&&this.processItemDrop({itemIds:this.dragState.draggedItems,targetElement:this.dragState.validTarget,fieldId:this.dragState.fieldId,dropType:this.dragState.isMultiDrag?"multiple":"single",sourceType:this.dragState.sourceType}),this.cleanupDragOperation();const a=t?this.dragState.isMultiDrag?`Moved ${this.dragState.draggedItems.length} items`:"Item moved":"Drag cancelled";this.a11y.announce(a)}processItemDrop(e){const{itemIds:t,targetElement:a,fieldId:s,dropType:i,sourceType:r}=e;if(!t?.length||!a||!s)return!1;let o=a.classList.contains("item-grid")&&a.classList.contains("preview"),n=a;if(a.classList.contains("empty-group")){let e=this.createGroup(s);n=e.querySelector(".item-grid"),o=!1}if(t.forEach((e=>{this.addImageToGroup(e,n,o)})),"multiple"===i){const e=this.fields.get(s);this.clearAllSelections(e)}const l="multiple"===i?`Moved ${t.length} images to ${o?"main area":"group"}`:"Image moved to "+(o?"main area":"group");return this.a11y.announce(l),this.provideFeedback(r,"success",{count:t.length,isMultiple:"multiple"===i}),!0}clearAllSelections(e){if(e.container.querySelectorAll('[name*="select-item"]').forEach((e=>{e.checked=!1})),e.selectAll){e.selectAll.checked=!1;const t=e.selectAll.nextElementSibling;t&&(t.textContent="Select All")}e.selectActions&&(e.selectActions.hidden=!0),e.selectInfo&&(e.selectInfo.hidden=!0)}cleanupDragOperation(){this.dragState.dragPreview&&this.dragState.dragPreview.remove(),this.applyDraggingState(!1),this.clearDropTargetStates(),this.dragState.isDragging=!1,this.dragState.dragPreview=null,this.dragState.draggedItems=[]}getDraggedItems(e){const t=this.getSelectedUploads(e),a=e.dataset.uploadId;return t.length>1&&t.includes(a)?t:[a]}applyDraggingState(e){this.dragState.draggedItems.forEach((t=>{const a=document.querySelector(`[data-upload-id="${t}"]`);a&&a.classList.toggle("dragging",e)}))}createDragPreview(e){const{isMultiDrag:t,draggedItems:a}=this.dragState;this.dragState.dragPreview=t?this.createMultiDragPreview(e,a):this.createSingleDragPreview(e),this.updateDragPreview(this.dragState.startPosition),document.body.appendChild(this.dragState.dragPreview)}createSingleDragPreview(e){const t=e.cloneNode(!0);return t.dataset.uploadId=t.dataset.uploadId+"-dragging",this.styleDragPreview(t,!1),t}styleDragPreview(e,t=!1){e.style.cssText=`\n        position: fixed;\n        z-index: 10000;\n        pointer-events: none;\n        opacity: 0.9;\n        transform: scale(1.05);\n        transition: transform 0.2s ease;\n        ${t?"\n            width: 120px;\n            height: 120px;\n            background: white;\n            border-radius: 8px;\n            box-shadow: 0 8px 32px rgba(0,0,0,0.3);\n            padding: 4px;\n        ":"\n            border-radius: 4px;\n            box-shadow: 0 4px 16px rgba(0,0,0,0.2);\n        "}\n    `,e.classList.add("drag-preview","is-dragging"),t&&e.classList.add("multi-item")}createMultiDragPreview(e,t){const a=document.createElement("div");a.className="drag-preview multi-item";const s=Math.min(t.length,3);for(let e=0;e<s;e++){const s=t[e],i=document.querySelector(`[data-upload-id="${s}"]`);if(i){const t=i.cloneNode(!0);t.dataset.uploadId=s+"_dragging",t.style.cssText=`\n\t\t\t\tposition: absolute;\n\t\t\t\ttop: ${4*e}px;\n\t\t\t\tleft: ${4*e}px;\n\t\t\t\twidth: calc(100% - ${4*e}px);\n\t\t\t\theight: calc(100% - ${4*e}px);\n\t\t\t\topacity: ${1-.15*e};\n\t\t\t\ttransform: rotate(${2*(e-1)}deg);\n\t\t\t\tz-index: ${10-e};\n\t\t\t\tborder-radius: 4px;\n\t\t\t\toverflow: hidden;\n\t\t\t`,a.appendChild(t)}}if(t.length>1){const e=this.createCountBadge(t.length);a.appendChild(e)}return this.styleDragPreview(a,!0),a}updateDragPreview(e){if(!this.dragState.dragPreview)return;let t;t="touch"===this.dragState.sourceType?this.dragState.isMultiDrag?{x:-60,y:-80}:{x:-50,y:-60}:this.dragState.isMultiDrag?{x:15,y:15}:{x:10,y:10};const a=e.x-this.dragState.startPosition.x,s=e.y-this.dragState.startPosition.y;this.dragState.dragPreview.style.transform=`translate(${a+t.x}px, ${s+t.y}px) scale(1.05)`}updateDropTarget(e){this.dragState.currentTarget&&this.clearDropTargetState(this.dragState.currentTarget);const t=this.findValidDropTarget(e);if(this.dragState.currentTarget=e,this.dragState.validTarget=t,t&&(this.applyDropTargetState(t),"touch"===this.dragState.sourceType&&navigator.vibrate)){const e=this.dragState.isMultiDrag?[25,10,25]:[25];navigator.vibrate(e)}}findValidDropTarget(e){if(!e)return null;const t=e.closest(".item-grid.group, .empty-group, .item-grid.preview");if(t){if(this.getFieldIdFromElement(t)===this.dragState.fieldId)return t}return null}applyDropTargetState(e){e.classList.add("dragover"),this.dragState.isMultiDrag&&(e.classList.add("multi-drop"),e.setAttribute("data-item-count",this.dragState.draggedItems.length))}clearDropTargetState(e){e.classList.remove("dragover","multi-drop"),e.removeAttribute("data-item-count")}clearDropTargetStates(){document.querySelectorAll(".dragover").forEach((e=>{e.classList.remove("dragover","multi-drop"),e.removeAttribute("data-item-count")}))}createCountBadge(e){const t=document.createElement("div");return t.className="selection-count-badge",t.textContent=e.toString(),t.style.cssText="\n\t\t\tposition: absolute;\n\t\t\ttop: -8px;\n\t\t\tright: -8px;\n\t\t\tbackground: var(--accent-primary);\n\t\t\tcolor: white;\n\t\t\tborder-radius: 50%;\n\t\t\twidth: 24px;\n\t\t\theight: 24px;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tfont-size: 12px;\n\t\t\tfont-weight: bold;\n\t\t\tbox-shadow: 0 2px 8px rgba(0,0,0,0.3);\n\t\t\tz-index: 20;\n\t\t",t}provideDragFeedback(e){const t={start:[50],success:this.dragState.isMultiDrag?[50,25,50,25,50]:[50,25,50],cancel:[100]};"touch"===this.dragState.sourceType&&navigator.vibrate&&t[e]&&navigator.vibrate(t[e])}provideFeedback(e,t,a={}){const s={success:a.isMultiple?[50,25,50,25,50]:[50,25,50],error:[100,50,100]};"touch"===e&&navigator.vibrate&&s[t]&&navigator.vibrate(s[t])}clearDragoverStates(){document.querySelectorAll(".dragover").forEach((e=>{e.classList.remove("dragover","multi-drop"),e.removeAttribute("data-item-count")}))}handleDragEnter(e){if(window.targetCheck(e,".image.field")&&e.dataTransfer.types.includes("Files")){e.preventDefault();const t=e.target.closest(".file-upload-container");t&&t.classList.add("dragover")}}handleDragLeave(e){if(!window.targetCheck(e,".image.field"))return;const t=e.target.closest(".file-upload-container");t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragStart(e){if(!window.targetCheck(e,".image.field"))return;const t=e.target.closest("[data-upload-id]");if(!t)return;this.startDragOperation({primaryElement:t,sourceType:"drag",startPosition:{x:e.clientX,y:e.clientY},event:e})?(e.dataTransfer.setData("text/plain",this.dragState.primaryItem),e.dataTransfer.effectAllowed="move"):e.preventDefault()}handleDragOver(e){this.dragState.isDragging&&window.targetCheck(e,".image.field")&&(e.preventDefault(),this.updateDragOperation({x:e.clientX,y:e.clientY},e.target))}handleDrop(e){if(!window.targetCheck(e,".image.field"))return;e.preventDefault(),this.clearDragoverStates();const t=e.target.closest(".file-upload-container");if(t){const a=Array.from(e.dataTransfer.files);if(a.length>0){const e=this.getFieldIdFromElement(t);e&&(this.processFiles(e,a),this.a11y.announce(`${a.length} file(s) dropped for upload`))}}}handleDragEnd(e){if(!this.dragState.isDragging)return;const t=document.elementFromPoint(this.dragState.currentPosition?.x||e.clientX,this.dragState.currentPosition?.y||e.clientY);this.endDragOperation(t)}handleTouchStart(e){if(!window.targetCheck(e,".image.field"))return;if(this.isTouchOnFormElement(e.target))return;const t=e.target.closest("[data-upload-id]");if(!t)return;const a=e.touches[0];this.startDragOperation({primaryElement:t,sourceType:"touch",startPosition:{x:a.clientX,y:a.clientY},event:e})&&e.preventDefault()}handleTouchMove(e){if(!this.dragState.isDragging)return;e.preventDefault();const t=e.touches[0],a=document.elementFromPoint(t.clientX,t.clientY);this.updateDragOperation({x:t.clientX,y:t.clientY},a)}handleTouchEnd(e){if(!this.dragState.isDragging)return;e.preventDefault();const t=e.changedTouches[0],a=document.elementFromPoint(t.clientX,t.clientY);this.endDragOperation(a)}handleTouchCancel(e){this.dragState.isDragging&&(this.cleanupDragOperation(),this.a11y.announce("Drag cancelled"))}async submitUploads(e){const t=this.fields.get(e);if(!t)return;const a=Array.from(t.uploads||[]).map((e=>this.uploads.get(e))).filter((e=>e&&("processed"===e.status||"processed-original"===e.status)));if(0!==a.length)try{await this.queueUpload(e),this.notifications.add(`Submitting ${a.length} upload(s)`,"info")}catch(t){this.error.log(t,{component:"UploadManager",action:"submitUploads",fieldId:e}),this.notifications.add("Failed to submit uploads","error")}else this.notifications.add("No uploads ready to submit","warning")}async retryUpload(e){const t=this.uploads.get(e);if(!t)return;if(this.fields.get(t.fieldId))try{if(this.updateUploadStatus(e,"received"),t.processedFile)this.updateUploadStatus(e,"processed"),await this.queueUpload(t.fieldId);else{if(!t.originalFile)throw new Error("No file data available for retry");await this.processFile(t.fieldId,t.originalFile)&&await this.queueUpload(t.fieldId)}this.notifications.add("Retrying upload...","info")}catch(t){this.error.log(t,{component:"UploadManager",action:"retryUpload",uploadId:e}),this.notifications.add("Failed to retry upload","error")}}async restartUploads(e){const t=this.fields.get(e);if(!t?.uploads)return;const a=Array.from(t.uploads).map((e=>this.uploads.get(e))).filter((e=>e&&"failed"===e.status));if(0!==a.length){for(const e of a)await this.retryUpload(e.id);this.notifications.add(`Restarting ${a.length} upload(s)`,"info")}else this.notifications.add("No failed uploads to restart","info")}async queueUpload(e){const t=this.fields.get(e);if(!t?.uploads)return;const a=Array.from(t.uploads);if(0===a.length)return;const s=this.prepareUploadData(t,a);this.a11y.announce("Queuing for upload");let i=1===a.length?"image":"images";const r={endpoint:"uploads",method:"POST",data:s,title:`Uploading ${a.length} ${i} to server...`,popup:`Uploading ${a.length} ${i}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(r);return a.forEach((t=>{let a=this.uploads.get(t);a&&(a.operationId=e,this.updateUploadStatus(t,"queued"))})),t.operationId=e,e}catch(e){throw e}finally{this.persistFieldState(t.key)}}prepareUploadData(e,t){console.log("Preparing Upload:",e);const a=new FormData;a.append("content",e.content),a.append("mode",e.mode),a.append("field_name",e.name),a.append("field_key",e.key),a.append("field_type",e.type),a.append("item_id",e.itemID),a.append("context",e.context);let s=[];t.forEach((e=>{let t=this.uploads.get(e);if(t){const i=t.processedFile||t.originalFile;i?(a.append("files[]",i),s.push(t.id)):console.warn(`No file for upload ${e}`)}else console.warn(`Upload ${e} not found in uploads map`)})),a.append("upload_map",s),console.log("Final FormData:");for(let e of a.entries())console.log(e[0],e[1]);return a}async queueImageMeta(e){const t=this.getUploadFromElement(element);if(!t)return;const a=this.fields.get(t.fieldId);if(!a)return;const s=element.closest(".upload-meta");if(!s)return;const i={title:s.querySelector('[name="title"]')?.value||"",alt_text:s.querySelector('[name="alt_text"]')?.value||"",caption:s.querySelector('[name="caption"]')?.value||"",description:s.querySelector('[name="description"]')?.value||""};t.meta={...t.meta,...i},this.uploads.set(t.id,t),this.hasMetaChanges=!0;"completed"===t.status&&t.attachmentId?await this.sendMetaUpdate(t):t.operationId?this.queueDependentMetaUpdate(t):this.persistFieldState(a.key)}async sendMetaUpdate(e){const t=new FormData;t.append("attachment_id",e.attachmentId),t.append("title",e.meta.title),t.append("alt_text",e.meta.alt_text),t.append("caption",e.meta.caption),t.append("description",e.meta.description);const a={endpoint:"uploads/meta",method:"POST",data:t,title:`Updating metadata for ${e.meta.originalName}`,canMerge:!0,headers:{action_nonce:jvbSettings.dash}};try{await this.queue.addToQueue(a),this.notifications.add("Metadata updated","success")}catch(t){this.error.log(t,{component:"UploadManager",action:"sendMetaUpdate",uploadId:e.id})}}queueDependentMetaUpdate(e){const t={endpoint:"uploads/meta",method:"POST",dependencies:[e.operationId],data:()=>{const t=new FormData;return t.append("operation_id",e.operationId),t.append("upload_id",e.id),t.append("title",e.meta.title),t.append("alt_text",e.meta.alt_text),t.append("caption",e.meta.caption),t.append("description",e.meta.description),t},title:"Updating metadata after upload",canMerge:!0,headers:{action_nonce:jvbSettings.dash}};this.queue.addToQueue(t)}async processFiles(e,t){const a=this.fields.get(e);if(!a)return;const s=t.filter((e=>this.validateFile(e,a)));if(0===s.length)return;if(!this.checkFieldLimits(e,s.length))return;const i=await this.processBatch(e,s);this.maybeLockUploads(e),a.groupDisplay&&(a.groupDisplay.hidden=!1),i.length>0&&await this.queueUpload(e),this.hideUploadProgress(e),this.a11y.announce(`Processed ${i.length} of ${s.length} files`)}checkFieldLimits(e,t){const a=this.fields.get(e);if(!a)return!1;const s=a.uploads?.size||0;return!(s+t>a.maxFiles)||(this.notifications.add(`Cannot add ${t} files. Max ${a.maxFiles} allowed, currently have ${s}.`,"warning"),!1)}generateUploadId(){return`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`}validateFile(e,t){return this.settings.allowedTypes.includes(e.type)?!(e.size>this.settings.maxFileSize)||(this.notify(`File too large: ${this.formatBytes(e.size)}`,"error"),!1):(this.notify(`Invalid file type: ${e.type}`,"error"),!1)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const a=t<0?0:t,s=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,s)).toFixed(a))+" "+["Bytes","KB","MB","GB"][s]}async processBatch(e,t){const a=[],s=[],i=this.worker.settings.maxConcurrent;let r=t.length;for(let o=0;o<t.length;o++){this.updateUploadProgress(e,o,r),s.length>=i&&await Promise.race(s);const n=this.processFile(e,t[o]).then((e=>{const t=s.indexOf(n);return t>-1&&s.splice(t,1),e&&a.push(e),e})).catch((e=>{console.error(`Failed to process ${t[o].name}:`,e);const a=s.indexOf(n);return a>-1&&s.splice(a,1),null}));s.push(n)}return await Promise.all(s),a}async processFile(e,t){const a=this.fields.get(e),s=await this.setUpload(e,t),i=s.id;try{this.addImageToGroup(i),this.updateUploadStatus(i,"local_processing");let e=null,r=!1;try{e=await this.processImage(t,i)}catch(a){console.warn(`Processing failed for ${t.name}, using original:`,a),r=!0,e=t}s.processedFile=e,s.processingFailed=r,this.updateUploadStatus(i,"processed"),this.uploads.set(i,s),a&&a.key&&await this.persistFieldState(a.key);const o=r?`${t.name} added (original format)`:`${t.name} processed and ready`;return this.a11y.announce(o),s}catch(e){return this.cleanupFailedUpload(i,a.key),this.error.log(e,{component:"UploadManager",action:"processFile",uploadId:i,fileName:t.name}),null}}async processImage(e,t){const a=this.worker.settings.timeout;return new Promise(((s,i)=>{let r,o=!1;r=setTimeout((()=>{o||(o=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),i(new Error(`Processing timeout for ${e.name}`)))}),a),this.worker.tasks.set(t,{file:e,timeoutId:r}),this.handleProcess(e,t).then((e=>{o||(o=!0,clearTimeout(r),this.worker.tasks.delete(t),s(e))})).catch((e=>{o||(o=!0,clearTimeout(r),this.worker.tasks.delete(t),i(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const a=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,a,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,a,.85)}async processOnMainThread(e,t,a){return new Promise(((s,i)=>{const r=new Image,o=document.createElement("canvas"),n=o.getContext("2d");let l=null;const d=()=>{r.onload=null,r.onerror=null,l&&(URL.revokeObjectURL(l),l=null),o.width=1,o.height=1,n.clearRect(0,0,1,1)};r.onload=()=>{try{const{width:l,height:c}=this.calculateOptimalDimensions(r,t);o.width=l,o.height=c,n.imageSmoothingEnabled=!0,n.imageSmoothingQuality="high",n.drawImage(r,0,0,l,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,a);o.toBlob((t=>{if(d(),t){const a=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});s(a)}else i(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),i(new Error(`Canvas processing failed: ${e.message}`))}},r.onerror=()=>{d(),i(new Error(`Failed to load image: ${e.name}`))};try{l=URL.createObjectURL(e),r.src=l}catch(e){d(),i(new Error(`Failed to create object URL: ${e.message}`))}}))}getOptimalFormat(e){return"image/gif"===e.type||"image/svg+xml"===e.type?e.type:this.supportsWebP()?"image/webp":"image/jpeg"}getOptimalQuality(e,t){return e.size<512e3?Math.max(t,.9):e.size<2097152?t:Math.min(t,.8)}getProcessedFileName(e,t){return e.name.replace(/\.[^/.]+$/,"")+({"image/webp":".webp","image/jpeg":".jpg","image/png":".png","image/gif":".gif"}[t]||".jpg")}getMaxDimension(){const e=window.screen.width,t=window.devicePixelRatio||1;return e*t>2560?2400:e*t>1920?1920:1200}shouldUseWorker(e){return this.worker.worker&&e.size>1048576&&"undefined"!=typeof OffscreenCanvas}async processWithWorker(e,t,a,s){return new Promise(((i,r)=>{if(!this.worker.worker)return void r(new Error("Worker not available"));const o=`${t}_${Date.now()}`,n=t=>{if(t.data.messageId===o)if(this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),t.data.success){const a=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});i(a)}else r(new Error(t.data.error||"Worker processing failed"))},l=e=>{this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),r(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",n),this.worker.worker.addEventListener("error",l),this.worker.worker.postMessage({messageId:o,file:e,maxDimension:a,quality:s,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){console.log("Restarting compression worker..."),this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n            self.onmessage = async function(e) {\n                const { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\n                try {\n                    // Create ImageBitmap from file\n                    const bitmap = await createImageBitmap(file);\n\n                    // Calculate dimensions\n                    const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n                    const width = Math.round(bitmap.width * scale);\n                    const height = Math.round(bitmap.height * scale);\n\n                    // Create OffscreenCanvas\n                    const canvas = new OffscreenCanvas(width, height);\n                    const ctx = canvas.getContext('2d');\n\n                    // Draw and resize\n                    ctx.imageSmoothingEnabled = true;\n                    ctx.imageSmoothingQuality = 'high';\n                    ctx.drawImage(bitmap, 0, 0, width, height);\n\n                    // Clean up bitmap\n                    bitmap.close();\n\n                    // Convert to blob\n                    const blob = await canvas.convertToBlob({\n                        type: outputFormat,\n                        quality: quality\n                    });\n\n                    self.postMessage({\n                        messageId,\n                        success: true,\n                        blob: blob,\n                        format: outputFormat\n                    });\n\n                } catch (error) {\n                    self.postMessage({\n                        messageId,\n                        success: false,\n                        error: error.message\n                    });\n                }\n            };\n        "],{type:"application/javascript"});this.worker.worker=new Worker(URL.createObjectURL(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:a,height:s}=e;if(a<=t&&s<=t)return{width:a,height:s};const i=Math.min(t/a,t/s);return{width:Math.round(a*i),height:Math.round(s*i)}}supportsWebP(){return 0===document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp")}cleanupFailedUpload(e,t){const a=this.fields.get(t);a?.uploads&&a.uploads.delete(e);const s=this.uploads.get(e);s&&(s.preview?.startsWith("blob:")&&URL.revokeObjectURL(s.preview),s.element?.remove(),this.uploads.delete(e)),this.worker.tasks.delete(e)}updateUploadStatus(e,t){console.log("Updating upload status for: ",e);let a=this.uploads.get(e);a&&(a.status=t,this.updateImageUI(a.id),this.persistFieldState(a.fieldId))}updateImageUI(e){console.log("Updating image UI: ",e);const t=this.uploads.get(e);if(console.log(t),!t?.element)return;const a=t.element.querySelector(".progress"),s=t.element;if(console.log("Updating Upload UI:",t),s&&(s.className=s.className.replace(/status-[\w-]+/g,""),s.classList.add(`status-${t.status}`)),a){let e=this.getStatusIcon(t.status),s=this.getStatusText(t.status),i=this.getStatusProgress(t.status);const r=a.querySelector(".fill"),o=a.querySelector("span.icon"),n=a.querySelector("span.details");r&&(r.style.width=`${i}%`),n&&(n.textContent=s),o&&(window.removeChildren(o),o.append(e)),"completed"===t.status&&setTimeout((()=>{a&&window.fade(a,!1)}),1e3)}}maybeLockUploads(e){const t=this.fields.get(e);t&&t.ui.field.dropZone&&(t.ui.field.dropZone.hidden=t.uploads&&t.uploads.size>=t.maxFiles)}createImageElement(e,t=!1){let a=window.getTemplate("uploadItem");if(!a)return void console.error("Image template not found");a.dataset.uploadId=e.id,a.querySelector('[name="featured"]').value=e.id;let[s,i,r]=[a.querySelector('[name="featured"]'),a.querySelector("img"),a.querySelector("details")];if([s.value,i.src,i.alt]=[e.id,e.preview,e.originalFile?.name??e.meta?.originalName??""],r){let e=window.getTemplate("uploadMeta");e&&r.append(e)}return a.draggable=t,a.querySelectorAll("input").forEach((t=>{let a=t.id;if(a){let s=a+e.id,i=t.parentNode.querySelector(`label[for="${a}"]`);t.id=s,i&&(i.htmlFor=s)}})),a}updateUploadProgress(e,t,a,s){const i=this.fields.get(e);if(!i)return;let r=i.ui.field.progress.progress;if(!r){r=window.getTemplate("imageProgress");const e=i.dropZone||i.container.firstElementChild;e?e.insertAdjacentElement("afterend",r):i.container.prepend(r)}const o=a>0?Math.round(t/a*100):0,n=i.ui.field.progress.fill,l=i.ui.field.progress.details,d=i.ui.field.progress.count;n&&(n.style.width=`${o}%`),l&&(l.textContent=s),d&&(d.textContent=`${t}/${a}`),t===a&&r.classList.add("completed")}hideUploadProgress(e){const t=this.fields.get(e);if(!t)return;const a=t.ui.field.progress.progress;a&&window.fade(a,!1)}async initDB(){if(!("indexedDB"in window))return;const e=indexedDB.open("jvb_uploads_db",1);e.onupgradeneeded=e=>{const t=e.target.result;if(!t.objectStoreNames.contains("fieldStates")){const e=t.createObjectStore("fieldStates",{keyPath:"fieldId"});e.createIndex("timestamp","timestamp",{unique:!1}),e.createIndex("content","content",{unique:!1}),e.createIndex("itemId","itemId",{unique:!1})}t.objectStoreNames.contains("uploadBlobs")||t.createObjectStore("uploadBlobs",{keyPath:"uploadId"})},e.onsuccess=e=>{this.db=e.target.result,this.loadFields()},e.onerror=e=>{console.error("IndexedDB error:",e)}}async loadFields(){if(this.db)return new Promise((e=>{const t=this.db.transaction(["fieldStates","uploadBlobs"],"readonly"),a=t.objectStore("fieldStates"),s=t.objectStore("uploadBlobs");a.getAll().onsuccess=t=>{t.target.result.forEach((e=>{let t=e.uploads,a=t.map((e=>e.id));e.uploads=new Set(a),this.fields.set(e.key,e),t.forEach((e=>{this.uploads.set(e.id,e)}))})),this.notify("uploads-loaded",{items:Array.from(this.uploads.values())}),e()};s.getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.uploadBlobs.set(e.id,e)})),this.notify("blobs-loaded",{items:Array.from(this.uploadBlobs.values())}),e()}}))}getUpload(e){return this.uploads.get(e)}clearField(e){let t=Array.from(this.fields.uploads);if(t.forEach((e=>{this.uploads.delete(e)})),this.fields.delete(e),this.db){const a=this.db.transaction(["fieldStates","uploadBlobs"],"readwrite");a.objectStore("fieldStates").delete(e),t.forEach((e=>{a.objectStore("uploadBlobs").delete(e)}))}}updateFieldStatus(e,t){const a=this.fields.get(e);if(!a)return;a.uploads.forEach((e=>{console.log("Attempting to set upload to status: ",t),this.updateUploadStatus(e,t)}));const s=a.ui.field.field;if(s){s.dataset.uploadStatus=t;const e=s.querySelector(".submit-uploads");e&&(e.disabled="uploading"===t||"processing"===t)}}handleUploadComplete(e){const t=e.response;if(!t?.uploads)return;t.uploads.forEach((e=>{const t=this.uploads.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,this.updateUploadStatus(e.upload_id,"completed"),this.uploads.set(t.id,t),this.clearUpload(t.id))}));const a=e.data.get("field_key");a&&this.persistFieldState(a)}async clearUpload(e){const t=this.uploads.get(e);if(t&&(t.preview?.startsWith("blob:")&&URL.revokeObjectURL(t.preview),this.persistFieldState(t.fieldId),this.uploads.delete(e),this.uploadBlobs.delete(e),this.db)){const t=this.db.transaction(["uploadBlobs"],"readwrite");await t.objectStore("uploadBlobs").delete(e)}}async setUpload(e,t,a=null){a||(a=this.generateUploadId());const s={id:a,fieldId:e,groupId:null,originalFile:t,processedFile:null,status:"received",progress:{percent:0,message:"Received..."},preview:URL.createObjectURL(t),createdAt:Date.now(),meta:{title:"",alt_text:"",caption:"",originalName:t.name,originalType:t.type,originalSize:t.size},changes:{}},i=this.fields.get(e);return i?(i.uploads||(i.uploads=new Set),i.uploads.add(a),s.element=this.createImageElement(s,"groupable"===i.type),s.ui=window.uiFromSelectors(this.selectors.item,s.element),this.uploads.set(a,s),this.updateImageUI(a),await this.persistFieldState(e),s):(console.error(`Field ${e} not found`),null)}getFieldUploads(e,t){const a=this.fields.get(e);return console.log("Got field uploads: ",a),a?.uploads?Array.from(a.uploads).map((e=>{let a=this.uploads.get(e);if(!a)return null;if(t){const{element:e,ui:t,...s}=a;a=s}return a})).filter(Boolean):[]}async persistFieldState(e){if(!this.db)return;const t=this.fields.get(e);if(!t)return;const{ui:a,container:s,dropZone:i,previewGrid:r,selectAll:o,selectActions:n,selectInfo:l,selectCount:d,groupDisplay:c,...u}=t,p={fieldId:e,timestamp:Date.now(),config:{key:u.key,id:u.id,name:u.name,type:u.type,content:u.content,itemID:u.itemID,context:u.context,mode:u.mode,maxFiles:u.maxFiles,multiple:u.multiple},context:{url:window.location.href,modalType:this.getModalType(t),formId:t.formId},uploads:this.getFieldUploads(e,!0),groups:Array.from(this.groups.entries()).filter((([t,a])=>a.fieldId===e&&a.uploads.size>0)).map((([e,t])=>({id:t.id,uploads:Array.from(t.uploads),meta:t.meta,changes:t.changes})))},h=this.db.transaction(["fieldStates"],"readwrite");await h.objectStore("fieldStates").put(p)}async checkPendingUploads(){if(!this.db)return;const e=this.db.transaction(["fieldStates"],"readonly").objectStore("fieldStates"),t=(await new Promise((t=>{const a=e.getAll();a.onsuccess=()=>t(a.result)}))).filter((e=>e.uploads.some((e=>"processing"===e.status||"processed"===e.status||"pending"===e.status))));0!==t.length&&this.showRecoveryNotification(t)}showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0);let a=window.getTemplate("restoreNotification");[a.querySelector(".restore-details").textContent]=[`${t} upload(s) from ${e.length} field(s) can be recovered.`],e.forEach((e=>{console.log(e);let t=window.getTemplate("restoreField");e.uploads.forEach((e=>{let a=window.getTemplate("restoreItem");[a.querySelector("img").src]=[e.preview],t.append(a)})),a.append(t)})),a.querySelector('[data-action="restore"]').addEventListener("click",(()=>{this.restoreFieldStates(e),a.remove()})),a.querySelector('[data-action="dismiss"]').addEventListener("click",(()=>{this.notifications.add("Uploads saved for later restoration","info"),a.remove()})),a.querySelector('[data-action="clear"]').addEventListener("click",(()=>{this.clearCachedUploads(e),a.remove()})),document.body.appendChild(a)}async restoreFieldStates(e){const t=new Map;if(e.forEach((e=>{t.has(e.context.url)||t.set(e.context.url,[]),t.get(e.context.url).push(e)})),1===t.size&&t.has(window.location.href)){for(const t of e)await this.restoreField(t);this.notifications.add(`Restored ${e.length} field(s)`,"success")}else{sessionStorage.setItem("jvb_restore_uploads",JSON.stringify(e));const a=t.keys().next().value;window.location.href!==a&&(window.location.href=a)}}async restoreField(e){const{config:t,context:a,uploads:s,groups:i}=e;a.modalType&&await this.openModalForRestore(a);const r=document.querySelector(`.field.image[data-field-id="${t.id}"]`);if(!r)return void console.warn(`Field ${t.id} not found for restoration`);const o=this.registerUploader(r,t),n=this.fields.get(o);for(const e of s)await this.restoreUpload(n,e);i&&i.length>0&&await this.restoreGroups(n,i,s),this.maybeLockUploads(o),"direct"===t.mode&&await this.queueUpload(o)}async restoreUpload(e,t){const a=await this.getBlobData(t.id);let s=null;a&&(s=new File([a.data],a.name,{type:a.type,lastModified:a.lastModified}),t.processedFile=s),e.uploads||(e.uploads=new Set),e.uploads.add(t.id),t.element=this.createImageElement(t,"groupable"===e.type);const i=t.groupId?e.ui.groups.groups.get(t.groupId):e.ui.field.preview;i&&(i.append(t.element),t.location=i),this.uploads.set(t.id,t)}async restoreGroups(e,t,a){for(const s of t){const t=this.createGroupElement(s.id,e.key);e.ui.groups.groups.set(s.id,t),e.ui.groups.container.insertBefore(t,e.ui.groups.empty);const i=new Set(s.uploadIds);this.groups.set(s.id,i),s.meta&&this.groupsMeta.set(s.id,s.meta),s.uploadIds.forEach((e=>{const i=a.find((t=>t.id===e));i&&i.element&&(t.querySelector(".item-grid").append(i.element),i.location=t.querySelector(".item-grid"),i.groupId=s.id)}))}}async getBlobData(e){if(!this.db)return null;const t=this.db.transaction(["uploadBlobs"],"readonly").objectStore("uploadBlobs").get(e);return new Promise((e=>{t.onsuccess=()=>e(t.result),t.onerror=()=>e(null)}))}async openModalForRestore(e){const{modalType:t,formId:a}=e;let s=null;switch(t){case"create":s=document.querySelector('[data-action="create"]');break;case"edit":s=document.querySelector(`[data-action="edit"][data-id="${e.itemId}"]`);break;case"bulkEdit":s=document.querySelector('[data-action="bulk-edit"]')}s&&(s.click(),await new Promise((e=>setTimeout(e,300))))}async clearCachedUploads(e){if(!this.db)return;const t=this.db.transaction(["fieldStates","uploadBlobs"],"readwrite");for(const a of e){await t.objectStore("fieldStates").delete(a.fieldId);for(const e of a.uploads)await t.objectStore("uploadBlobs").delete(e.id),e.preview?.startsWith("blob:")&&URL.revokeObjectURL(e.preview)}this.notifications.add("Cached uploads cleared","info")}async checkRestorationIntent(){const e=sessionStorage.getItem("jvb_restore_uploads");if(!e)return;const t=JSON.parse(e),a=t.filter((e=>e.context.url===window.location.href));if(a.length>0){for(const e of a)await this.restoreField(e);const e=t.filter((e=>e.context.url!==window.location.href));e.length>0?sessionStorage.setItem("jvb_restore_uploads",JSON.stringify(e)):sessionStorage.removeItem("jvb_restore_uploads"),this.notifications.add(`Restored ${a.length} field(s)`,"success")}}addImageToGroup(e,t=null,a=!0){let s=this.getUpload(e);if(!s)return;let i=this.fields.get(s.fieldId);if(i&&(t||s.location!==i.ui.field.preview)&&t!==s.location){if(s.location){let t=s.location.dataset.groupId;if(t){let a=this.groups.get(t);a&&(a.delete(e),0===a.size&&this.removeGroup(t))}}if(s.element.querySelector('[name="featured"]').hidden=!t,t){let a=t.dataset.groupId,i=this.groups.get(a);i||(i=this.createGroup(s.fieldId)),i.uploads.add(e)}else t=i.ui.field.preview;t.append(s.element),a&&this.persistFieldState(i.key)}}addSelectionToGroup(e){let t=this.getFieldFromElement(e);if(!t)return;if(0===this.selected.get(t.key).size)return;let a=this.getGroupFromElement(e);a||(a=this.createGroup(t.key)),Array.from(this.selected).forEach((e=>{this.addImageToGroup(e,a.grid,!1)})),this.persistFieldState(a.fieldId)}removeGroup(e,t=!1){let a=this.groups.get(e);if(!a)return;if(t&&!window.confirm("This will delete this group. Any uploads in this group will return to the main grid. Are you sure?"))return;a.uploads.size>0&&Array.from(a.uploads).forEach((e=>{this.addImageToGroup(e)}));let s=a.element;s&&(window.fade(s,!1),this.a11y.announce("Empty group removed")),this.persistFieldState(a.fieldId)}createGroup(e){let t=this.fields.get(e);if(!t)return;let a=t.ui.groups.size;t.ui.groups.groups.set(`group-${a}`,this.createGroupElement(`group-${a}`,e));let s=t.ui.groups.groups.get(`group-${a}`);t.ui.groups.container.insertAfter(s,t.ui.groups.empty);let i={fieldId:t.key,id:`group-${a}`,element:s,grid:s.querySelector(".item-grid"),uploads:new Set,meta:{post_title:"",post_excerpt:""},changes:{}};return this.groups.set(`group-${a}`,i),i}createGroupElement(e,t){let a=window.getTemplate("imageGroup");if(!a)return;a.dataset.groupId=e,a.dataset.fieldId=t;let s=window.getTemplate("groupMetaData");return a.querySelector(".fields")?.append(s),a}handleSelectAll(e,t=null){const a=this.getFieldFromElement(e);if(!a)return;null===t&&(t=e.checked);(a.previewGrid.querySelectorAll("[data-upload-id]")||[]).forEach((e=>{const a=e.querySelector('[name*="select-item"]');a&&(a.checked=t)})),this.updateSelectAll(e),this.a11y.announce(t?"All uploads selected":"All uploads deselected"),this.lastClickedUpload=null}updateSelection(e){let t=this.getFieldFromElement(e.target),a=this.getUploadFromElement(e.target);t&&a?(this.lastClickedUpload=a.id,e.target.checked?this.selected.get(t.key).add(a.id):this.selected.get(t.key).delete(a.id)):console.log("No field or upload found...")}updateSelectAll(e){const t=this.getFieldFromElement(e);if(!t)return;const a=this.getSelectedUploads(e);a.length>0?(t.selectActions.hidden=!1,t.selectInfo.hidden=!1,t.selectCount.textContent=`${a.length}`):(t.selectActions.hidden=!0,t.selectInfo.hidden=!0);let s=a.length===t.container.querySelectorAll(".item-grid.preview .upload-item").length;t.selectAll.checked=s,t.selectAll.nextElementSibling.textContent=s?"Clear Selection":"Select All"}getSelectedUploads(e){let t=this.getFieldFromElement(e);if(t)return Array.from(this.selected.get(t.key)??[])}handleRangeSelection(e,t){if(!this.getFieldFromElement(e))return;const a=this.getUploadIdFromElement(e);if(!a||!this.lastClickedUpload)return;const s=e.closest(".item-grid"),i=Array.from(s.querySelectorAll("[data-upload-id]")),r=i.findIndex((e=>e.dataset.uploadId===this.lastClickedUpload)),o=i.findIndex((e=>e.dataset.uploadId===a));if(-1===r||-1===o)return;const n=Math.min(r,o),l=Math.max(r,o);for(let e=n;e<=l;e++){const t=i[e].querySelector('[name*="select-item"]');t&&(t.checked=!0)}e.checked=!0,this.updateSelectAll(e);const d=l-n+1;this.a11y.announce(`Selected ${d} items in range`),this.lastClickedUpload=a}removeSelection(e){let t=this.getFieldIdFromElement(e);const a=this.getSelectedUploads(e);0!==a.length?a.forEach((e=>{this.removeUpload(t,e)})):this.notify("No uploads selected","warning")}removeUpload(e,t){const a=this.fields.get(e),s=this.uploads.get(t);if(a&&s){if(a.uploads?.delete(t),s.groupId){const e=this.groups.get(s.groupId);e?.delete(t)}s.element?.remove(),this.clearUpload(t),this.maybeLockUploads(e),this.updateSelectAll(a.ui.field.field),this.a11y.announce("Upload removed")}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((a=>a(e,t)))}handleBeforeUnload(e){if(Array.from(this.uploads.values()).filter((e=>"processing"===e.status||"pending"===e.status||"uploading"===e.status)).length>0){const t="You have uploads in progress. Are you sure you want to leave?";return e.preventDefault(),e.returnValue=t,t}}cleanup(){this.clearListeners(),this.hasGroups&&this.clearGroupListeners(),this.compressionWorker=null,this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbUploads=new e}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.queue=window.jvbQueue,this.error=window.jvbError,this.templates=window.jvbTemplates,this.subscribers=new Set,this.initStores(),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.sortables=new Map,this.previewUrls=new Set,this.initElements(),this.initListeners(),this.defineTemplates()}defineTemplates(){const e=this.templates,t=this;e.define("uploadItem",{refs:{select:'[name="select-item"]',featured:'[name="featured"]',img:"img",video:"video",file:"label > span",details:"details"},manyRefs:{inputs:"input"},setup({el:s,refs:r,manyRefs:i,data:o}){let a,l,n,d=!1;switch(Object.hasOwn(o,"file")?(s.dataset.uploadId=o.uploadId,a=t.getSubtypeFromMime(o.file.type)||"image",l="document"!==a&&t.createPreviewUrl(o.file),d=l,n=o.file.name||""):(s.dataset.id=o.id,a=t.getSubtypeFromURL(o.medium??o.src),l=o.medium??o.src,n=o["image-alt-text"]??""),s.dataset.subtype=a,r.featured&&(r.featured.value=o.uploadId),a){case"image":r.img&&(r.img.src=l,r.img.alt=n,d&&(r.img.dataset.previewUrl=d)),r.video&&r.video.remove(),r.file&&r.file.remove();break;case"video":r.video&&(r.video.src=l,r.video.alt=n,d&&(r.video.dataset.previewUrl=d)),r.img&&r.img.remove(),r.file&&r.file.remove();break;case"document":if(r.preview){let e=o.file.name.split(".").pop()?.toLowerCase()??"",t={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},s=window.getIcon(t[e]??"file");r.preview.innerText=o.file.name??o.title,r.preview.prepend(s)}r.img&&r.img.remove(),r.video&&r.video.remove()}if(r.details&&r.details.append(e.create("uploadMeta")),s.draggable="single"!==s.dataset.mode,i.inputs)for(let e of i.inputs)window.prefixInput(e,`${o.uploadId}-`)}}),e.define("uploadMeta",{refs:{alt:'[name="alt_text"]',title:'[name="image-title"]',description:'[name="image-caption"]'},setup({el:e,refs:t,manyRefs:s,data:r}){Object.hasOwn(r,"alt")&&t.alt&&(t.alt.value=r.alt),Object.hasOwn(r,"title")&&t.title&&(t.title.value=r.title),Object.hasOwn(r,"description")&&t.description&&(t.description.value=r.description)}}),e.define("imageGroup",{refs:{selectAll:"[data-select-all]",fields:".fields",details:"details",grid:".item-grid"},setup({el:t,refs:s,manyRefs:r,data:i}){t.dataset.groupId=i.groupId,s.selectAll&&window.prefixInput(s.selectAll,`select-all-${i.groupId}`,!0);let o=e.create("groupMetadata",{groupId:i.groupId});o?s.fields.append(o):s.details.remove(),s.grid&&(s.grid.dataset.groupId=i.groupId)}}),e.define("groupMetadata",{manyRefs:{inputs:"input,textarea,select"},setup({el:e,refs:t,manyRefs:s,data:r}){t.inputs&&t.inputs.forEach((e=>{window.prefixInput(e,`${r.groupId}-`)}))}}),e.define("restoreNotification",{refs:{details:".details",wrap:".wrap"},setup({el:t,refs:s,manyRefs:r,data:i}){if(s.details){let e=i.bySource.size>1?` across ${i.bySource.size} pages`:"",t=i.pendingUploads.length>1?"uploads":"upload";s.details.textContent=`${i.pendingUploads.length} ${t} can be recovered${e}`}if(!s.wrap)return void console.warn("No wrap element in template");let o=1;for(const[t,r]of i.bySource){let i={index:o,isCurrent:t===window.location.href,src:t,uploads:r};s.wrap.append(e.create("restoreField",i)),o++}}}),e.define("restoreField",{refs:{h3:"h3",a:"h3 a",grid:".item-grid"},async setup({el:e,refs:s,manyRefs:r,data:i}){let o=t.registerField(e,!1,`recovery_${i.index}`);i.isCurrent?(e.open=!0,s.a?.remove(),s.h3&&(s.h3.textContent="From this page:")):s.a&&(s.a.href=i.src,s.a.title="Navigate to page and restore",s.a.textContent=i.src);let a=[...new Set(i.uploads.map((e=>e.group??"preview")))];for(let e of a){let r="preview"===e||t.stores.groups.get(e);if(!r)continue;let a=await t.createGroupElement(e,o),l=a.querySelector(".item-grid"),n=i.uploads.filter((t=>t.group===("preview"===e)?null:e));for(const[e,t]of Object.entries(r.fields??{})){let s=a.querySelector(`input[name*="${e}"]`);s&&(s.value=t)}for(let e of n){let s=await t.createUpload(e.id,t.formatFile(e),o);l.append(s)}s.grid.append(a)}}})}initStores(){const{uploads:e,groups:t}=window.jvbStore.register("uploads",[{storeName:"uploads",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"status",keyPath:"status"},{name:"group",keyPath:"group"},{name:"src",keyPath:"src"}]},{storeName:"groups",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"src",keyPath:"src"}]}]);this.stores={uploads:e,groups:t,ready:[]},this.stores.uploads.subscribe(this.handleStores.bind(this,"uploads")),this.stores.groups.subscribe(this.handleStores.bind(this,"groups")),this.queue.subscribe(((e,t)=>{if(("operation-status"===e||"cancel-operation"===e)&&["image_upload","video_upload","document_upload"].includes(t.type)){let s=(t.data instanceof FormData?this.stores.uploads.formDataToObject(t.data):t.data).upload_ids;if(!s||0===s.length)return;if("cancel-operation"===e)return this.handleOperationCancelled(s);this.setBulkUpload(s,"status",t.status).then((()=>{})),"completed"===t.status&&s.forEach((e=>{this.removeUpload(e).then((()=>{}))}))}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery().then((()=>{})))}initWorker(){this.worker=null,this.workerState={worker:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:3e3,maxConcurrent:3,restartAfterTimeout:!0}}}initElements(){this.selectors={fields:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-container",preview:".preview-wrap",grid:".item-grid.preview",progress:{progress:".file-upload-container .progress",fill:".file-upload-container .progress .fill",details:".file-upload-container .progress .details",icon:".file-upload-container .progress .icon"},selectAll:"[data-select-all]",actions:".selection-actions",count:".selected .info",hidden:'input[type="hidden"]'},groups:{container:".group-display",grid:".item-grid.groups",empty:".empty-group",header:".sidebar .header"},group:{item:".upload-group",actions:".selection-actions",selectAll:'[name="select-all-group"]',count:".group-header .info",fields:"details .fields",grid:".item-grid.group",total:".group-content .group-count"},items:{item:".item.upload",checkbox:'[name="select-item"]',featured:'[name="featured"]',image:"img",details:"details",progress:{progress:".progress",fill:".fill",details:".details",icon:".icon"}}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dropHandler=this.handleDrop.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}async setUpload(e,t){const s={...{id:e,attachment:null,group:null,field:null,src:window.location.href,blob:null,status:"local_processing",operationId:null,fields:{}},...t};return Object.preventExtensions(s),await this.stores.uploads.save(s),s}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls.delete(e))}formatFile(e){return e.blob?new File([e.blob],e.fields.originalName||"file",{type:e.fields.type||e.blob.type,lastModified:e.fields.lastModified||Date.now()}):null}handleClick(e){let t=window.targetCheck(e,this.selectors.fields.dropZone);t&&!e.target.matches("input, button, a")&&t.querySelector(this.selectors.fields.input)?.click();const s=window.targetCheck(e,"[data-action]");s&&this.handleAction(s)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(s).then((()=>{}));break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e).then((()=>{}));break;case"upload":this.queueUploads("uploads/groups",s).then((()=>{}));break;case"restore":this.handleRestoreSelected().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":this.handleClearCache().then((()=>{}))}}handleChange(e){let t=this.getFieldIdFromElement(e.target);if(!t)return;if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);return void(s.length>0&&this.processFiles(t,s).then((()=>{})))}if(e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]'))return;let s=this.fields.get(t);s&&s.config.autoUpload&&("post_group"===s.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e).then((()=>{})))}handleGroupMetaChange(e){const t=e.dataset.groupId;if(!t)return;const s=e.name,r=e.value,i=s.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");window.debouncer.schedule(`group-meta-${t}-${i}`,(async()=>{const e=this.stores.groups.get(t);e&&(e.fields||(e.fields={}),e.fields[i]=r,await this.setGroup(t,e))}),300)}handleDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.fields.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleDragLeave(e){const t=e.target.closest(this.selectors.fields.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.fields.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleDrop(e){const t=e.target.closest(this.selectors.fields.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover"),t.classList.add("uploading");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const r=this.getFieldIdFromElement(t);r&&(this.processFiles(r,s).then((()=>{this.updateHandlerItems(r)})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t){let s=new FormData;const r=this.fields.get(t);if(!r)return;let i=this.stores.uploads.filterByIndex({field:t});if(0===i.length)return;const[o,a]=["uploads"===e,"uploads/groups"===e];let l,n,d,u,p;s.append("fieldId",r.id),s.append("content",r.config.content),o&&(s.append("mode",r.config.mode),s.append("field_name",r.config.name),s.append("fieldId",r.id),s.append("field_type",r.config.type),s.append("subtype",r.config.subtype),s.append("item_id",r.config.itemID),s.append("destination",r.config.destination)),a?({posts:l,uploadMap:n,files:d}=this.collectGroups(t)):o&&({uploadMap:n,files:d}=this.collectUploads(t)),a&&s.append("posts",JSON.stringify(l)),d.forEach((e=>{s.append("files[]",e)})),s.append("upload_ids",JSON.stringify(n)),o?(u=`Uploading ${i.length} file${i.length>1?"s":""} to server...`,p=`Uploading ${i.length} file${i.length>1?"s":""}...`):a&&(u=`Creating ${l.length} ${r.config.content}${l.length>1?"s":""} from uploads...`,p=`Creating ${l.length} post${l.length>1?"s":""}...`),await this.setBulkUpload(i,"status","queued");let c=this.sendToQueue(e,s,u,p);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1)}return c?(r.operationId=c,await this.setBulkUpload(i,"operationId",c),await this.setBulkUpload(i,"status","uploading"),await this.setBulkGroup(t,"operationId",c),this.fields.set(r.id,r)):await this.setBulkUpload(i,"status","failed"),this.notify("sent-to-queue",t),c}async sendToQueue(e,t,s="",r="",i=!1){""===r&&(r=s);const o={endpoint:e,method:"POST",data:t,title:s,popup:r,canMerge:i,sendNow:"uploads/groups"===e,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(o)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e}),r=[],i=[],o=[];for(const e of s){const t=this.groups.get(e.id)?.element,s={images:[],fields:this.collectGroupFieldsFromDOM(t,e.id)},a=this.getGroupUploadsInOrder(e);for(const t of a){const r=this.formatFile(t);if(r){o.push(r);const a={upload_id:t.id,index:i.length},l=this.uploads.get(t.id),n=l?.element?.querySelector(`input[name="${e.id}_featured"]`);n?.checked&&(s.fields.featured=t.id),s.images.push(a),i.push(t.id)}}r.push(s)}const a=t.filter((e=>!e.group));for(const e of a){const t={images:[],fields:{}},s=this.formatFile(e);if(s){o.push(s);const r={upload_id:e.id,index:i.length};t.images.push(r),i.push(e.id)}r.push(t)}return{posts:r,uploadMap:i,files:o}}getGroupUploadsInOrder(e){return e.uploads&&0!==e.uploads.length?e.uploads.map((e=>this.stores.uploads.get(e))).filter(Boolean):[]}collectGroupFieldsFromDOM(e,t){if(!e)return{};const s={};return e.querySelectorAll("input, textarea, select").forEach((e=>{const r=e.name.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");["featured","select-all"].some((e=>r.includes(e)))||e.value&&(s[r]=e.value)})),s}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],r=[];for(const e of t){const t=this.formatFile(e);t&&(r.push(t),s.push(e.id))}return{uploadMap:s,files:r}}async queueUploadMeta(e){const t=e.target.closest(this.selectors.items.item)?.dataset.uploadId,s=this.stores.uploads.get(t);if(!t||!s)return;if(!this.fields.get(s.field))return;let r={};r[e.target.name]=e.target.value,s.fields={...s.fields,...r},await this.setUpload(s.id,s);let i={};return i[s.attachmentId??s.id]=s.fields,await this.sendToQueue("uploads/meta",i,"Uploading Meta","",!0)}scanFields(e,t=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t)))}registerField(e,t=!0,s=null){const r={element:e,id:s||this.determineFieldId(e),config:this.extractFieldConfig(e,t),uploads:new Set,operationId:null,groups:[],ui:window.uiFromSelectors(this.selectors.fields,e),groupUI:window.uiFromSelectors(this.selectors.groups,e)};return this.fields.set(r.id,r),e.dataset.uploader=r.id,this.getSelectionHandler(r.id),"single"!==r.config.type&&this.initSortable(r.id),r.id}extractFieldConfig(e,t){return{autoUpload:t,destination:e.dataset.destination||"meta",content:this.extractFieldContent(e),mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:this.extractFieldItemId(e)??0,maxFiles:parseInt(e.dataset.maxFiles)??25,subType:e.dataset.subtype??"image"}}extractFieldContent(e){return e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||null}extractFieldItemId(e){return e.dataset.itemId||e.closest("dialog")?.dataset.itemId||null}determineFieldId(e){let t=this.extractFieldContent(e);t=null===t?"":t+"_";let s=this.extractFieldItemId(e);s=null===s?"":s+"_";return`${t}${s}${e.dataset.field||""}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,r){const i=this.fields.get(e);i&&window.showProgress(i.ui.progress,t,s,r)}getWorker(){return this.workerState.worker||"undefined"==typeof OffscreenCanvas||(this.workerState.worker=new Worker("worker.js"),this.workerState.worker.onmessage=e=>this.handleWorkerMessage(e),this.workerState.worker.onerror=e=>this.handleWorkerError(e)),this.workerState.worker}handleWorkerMessage(e){const{id:t,blob:s}=e.data,r=this.workerState.tasks.get(t);r&&(clearTimeout(r.timeoutId),r.resolve(s),this.workerState.tasks.delete(t))}handleWorkerError(e){this.workerState.tasks.forEach((t=>{clearTimeout(t.timeoutId),t.reject(e)})),this.workerState.tasks.clear(),this.restartWorker()}restartWorker(){this.workerState.worker&&(this.workerState.worker.terminate(),this.workerState.worker=null),this.workerState.restart.count++}async processImages(e,t=2200,s=2200){const r=[],i=[...e],o=this.workerState.settings.maxConcurrent,a=async()=>{for(;i.length>0;){const e=i.shift();r.push(await this.processImage(e,t,s))}};return await Promise.all(Array.from({length:Math.min(o,e.length)},(()=>a()))),r}async processImage(e,t=2200,s=2200,r=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),r)}catch(r){return this.resizeImage(e,t,s)}}withTimeout(e,t){return Promise.race([e,new Promise(((e,s)=>setTimeout((()=>s(new Error("Timeout"))),t)))])}async workerImage(e,t=2200,s=2200){const{settings:r,restart:i}=this.workerState;if(i.count>=i.max)throw new Error("Worker max restarts exceeded");const o=await createImageBitmap(e);let{width:a,height:l}=o;if(a>t||l>s){const e=Math.min(t/a,s/l);a=Math.round(a*e),l=Math.round(l*e)}const n=this.getWorker(),d=crypto.randomUUID();return new Promise(((t,s)=>{const i=setTimeout((()=>{this.workerState.tasks.delete(d),r.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),r.timeout);this.workerState.tasks.set(d,{resolve:t,reject:s,timeoutId:i}),n.postMessage({id:d,imageBitmap:o,width:a,height:l,type:e.type,quality:.9},[o])}))}resizeImage(e,t,s){return new Promise((r=>{const i=new Image;i.onload=()=>{URL.revokeObjectURL(i.src);let{width:o,height:a}=i;if(o>t||a>s){const e=Math.min(t/o,s/a);o=Math.round(o*e),a=Math.round(a*e)}const l=document.createElement("canvas");l.width=o,l.height=a,l.getContext("2d").drawImage(i,0,0,o,a),l.toBlob(r,e.type,.9)},i.src=URL.createObjectURL(e)}))}async processFiles(e,t){let s=this.fields.get(e);if(!s)return;s.groupUI.container&&(s.groupUI.container.hidden=!1);const r=t.length;let i=0;this.updateFieldProgress(e,0,r,"Processing files...");const o=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),r=await this.setUpload(s,{id:s,field:e,status:"local_processing",blob:null,fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),i=await this.createUpload(s,t,e);return this.uploads.set(s,{element:i,ui:window.uiFromSelectors(this.selectors.items,i)}),await this.addToGroup(s,null),{uploadId:s,upload:r,file:t}}))),a=o.filter((e=>e.file.type.startsWith("image/"))),l=o.filter((e=>!e.file.type.startsWith("image/"))),n=await this.processImages(a.map((e=>e.file)));for(let t=0;t<a.length;t++){const{uploadId:s,upload:o}=a[t];o.blob=n[t],o.fields.size=n[t].size,o.status="queued",await this.setUpload(s,o),i++,this.updateFieldProgress(e,i,r,"Processing files...")}for(const{uploadId:t,upload:s,file:o}of l)s.blob=o,s.status="queued",await this.setUpload(t,s),i++,this.updateFieldProgress(e,i,r,"Processing files...");this.maybeLockUploads(e),s.config.autoUpload&&"post_group"!==s.config.destination&&await this.queueUploads("uploads",e)}async checkRecovery(){const e=this.stores.uploads.filterByIndex({status:["local_processing","queued","uploading"]});if(0===e.length)return;const t=new Map;e.forEach((e=>{const s=e.src||"unknown";t.has(s)||t.set(s,[]),t.get(s).push(e)}));let s={bySource:t,pendingUploads:e};document.body.append(this.templates.create("restoreNotification",s));let r=document.querySelector("dialog.restore-uploads");this.restoreModal=new window.jvbModal(r),this.restoreSelection=new window.jvbHandleSelection(r,{wrapper:{wrapper:".restore-field",id:"selection"},items:".item-grid.restore",selectAll:{bulkControls:".selection-actions",checkbox:"#select-all-restore",count:".selection-count"}}),this.restoreModal.handleOpen()}async handleRestoreSelected(){if(!this.restoreSelection)return;let e=Array.from(this.restoreSelection.selectedItems);0!==e.length&&await this.restoreSelectedUploads(e)}async handleRestoreAll(){if(!this.restoreModal)return;const e=Array.from(this.restoreModal.modal.querySelectorAll(".item.upload")).map((e=>e.dataset.uploadId));await this.restoreSelectedUploads(e)}async restoreSelectedUploads(e){let t=window.location.href,s=Array.from(this.stores.uploads.data.values()).filter((s=>e.includes(s.id)&&s.src===t)),r=[...new Set(s.map((e=>e.group)))].filter(Boolean),i=s[0].field;if(!document.querySelector(`[data-uploader="${i}"]`))return void console.log("No field found for "+i);let o=this.fields.get(i);o.groupUI.container&&(o.groupUI.container.hidden=!1);let a=[];for(let e of r){let t=this.stores.groups.get(e);await this.createGroup(i,e);let r=this.groups.get(e),o=s.filter((t=>t.group===e));if(t&&this.groups.has(e)){let e=t.fields;for(const[t,s]of Object.entries(e)){let e=r.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of o){let s=await this.createUpload(t.id,this.formatFile(t),i);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),a.push(t.id)}}let l=s.filter((e=>!a.includes(e.id)));for(let e of l){let t=await this.createUpload(e.id,this.formatFile(e),i);this.uploads.set(e.id,{element:t,ui:window.uiFromSelectors(this.selectors.items,t)}),await this.addToGroup(e.id,null)}this.cleanupRestore()}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}getStatusText(e){return{received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"}[e]||e}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]??0}async createUpload(e,t,s){let r=this.fields.get(s);if(!r)return null;let i={uploadId:e,file:t,field:r};return this.templates.create("uploadItem",i)}getSubtypeFromURL(e){const t=e.split("?")[0].toLowerCase();return[".webp",".jpg",".jpeg",".png",".gif",".svg"].some((e=>t.endsWith(e)))?"image":[".mp4",".ogg",".mov",".webm",".avi"].some((e=>t.endsWith(e)))?"video":"document"}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}async handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId;confirm("Remove this item?")&&(await this.removeUpload(s),this.a11y.announce("Item removed"))}async setBulkUpload(e,t,s){const r=Array.from(e).map((async e=>{if("string"==typeof e&&(e=await this.stores.uploads.get(e)),e)return"status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e)}));await Promise.all(r)}async setUploadStatus(e,t){"string"==typeof e&&(e=await this.stores.uploads.get(e)),e&&e.progress&&window.showProgress(e.progress,this.getStatusProgress(t),100,this.getStatusText(t),this.queue.icons[t]??"")}async removeUpload(e){let t=this.stores.uploads.get(e);if(!t)return;if(t.group){let s=this.stores.groups.get(t.group);s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length?await this.removeGroup(s.id,!1):await this.stores.groups.save(s)}await this.clearUpload(e),this.maybeLockUploads(t.field);let s=this.selectionHandlers.get(t.field);s&&s.deselect(e),this.a11y.announce("Upload removed")}async clearUpload(e){const t=this.uploads.get(e);if(t&&(this.revokePreviewUrl(t.preview),t.element)){const e=t.element.dataset.previewUrl;this.revokePreviewUrl(e),t.element.remove()}this.uploads.delete(e),await this.stores.uploads.delete(e)}async handleAddToGroup(e){const t=this.selected.get(e);if(!t||0===t.size)return;let s=await this.createGroup(e);s&&(await Promise.all(Array.from(t).map((e=>this.addToGroup(e,s)))),this.selectionHandlers.get(e)?.clearSelection(),this.a11y.announce(`Created group with ${t.size} items`))}async createGroup(e,t=null){let s=this.fields.get(e);if(!s)return;t||(t=window.generateID("group"));const r=this.createGroupElement(t,e);if(!r)return null;const i=s.groupUI.empty;i?.nextSibling?s.groupUI.grid.insertBefore(r,i.nextSibling):s.groupUI.grid.append(r);const o=r.querySelector(".item-grid");o&&(o.dataset.groupId=t,this.createSortable(e,o,t));let a=this.stores.groups.data.has(t)?this.stores.groups.data.get(t):{};return await this.setGroup(t,{...a,id:t,field:e}),t}createGroupElement(e,t=null){let s={groupId:e,fieldId:t},r=this.templates.create("imageGroup",s);return this.groups.set(e,{element:r,ui:window.uiFromSelectors(this.selectors.group,r)}),this.getSelectionHandler(t)?.addWrapper(r),r}async setGroup(e,t){const s={...{id:e,src:window.location.href,uploads:[],operationId:null,field:null,fields:{}},...t};Object.preventExtensions(s),await this.stores.groups.save(s)}async setBulkGroup(e,t,s){let r=this.stores.groups.filterByIndex({field:e});if(0===r.length)return;let i=r.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(i)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),r=this.uploads.get(e);if(!s||!r)return;const i=this.fields.get(s.field);if(!i)return;if(null!==r.element?.parentElement&&(!t&&null===s.group||t===s.group))return void this.handleReorder(s.field,t);if(s.group){const t=this.stores.groups.get(s.group);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.removeGroup(t.id,!1):await this.stores.groups.save(t))}r.ui.checkbox&&(r.ui.checkbox.checked=!1);const o=this.selectionHandlers.get(s.field);if(o&&o.isSelected(e)&&o.deselect(e),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),r.ui.featured&&(r.ui.featured.hidden=!t),t){r.ui.featured&&(r.ui.featured.name=`${t}_featured`);let i=this.stores.groups.get(t);i&&(i.uploads.push(e),s.group=t,await this.stores.groups.save(i))}else s.group=null;let a=t?this.groups.get(t)?.ui.grid:i.ui.grid;a&&(a.append(r.element),t&&await this.handleReorder(s.field,t)),await this.stores.uploads.save(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.group.item);if(!t)return;let s=t.dataset.groupId;if(!confirm("Delete this group? Items will be moved back to the upload area."))return;let r=this.stores.uploads.filterByIndex({group:s});Promise.all(r.map((e=>this.addToGroup(e.id,null)))).then((()=>{this.removeGroup(s,!1).then((()=>{})),this.a11y.announce("Group deleted. Items returned to upload area")}))}async removeGroup(e,t=!0){let s=this.groups.get(e),r=this.stores.groups.get(e);if(!r)return;let i=!0;t&&r.uploads.length>0&&(i=window.confirm("Keep uploads in this group?")),await Promise.all(r.uploads.map((e=>i?this.addToGroup(e,null):this.removeUpload(e))));if(this.fields.get(r.field)){const t=this.getGroupKey(r.field,e),i=this.selectionHandlers.get(t);i?.destroy&&i.destroy(),this.selectionHandlers.get(r.field)?.removeWrapper(s.element);const o=this.sortables.get(t);o?.destroy&&o.destroy(),this.sortables.delete(t)}s?.element&&s.element.remove(),this.groups.delete(e),await this.stores.groups.delete(e),this.a11y.announce("Group removed")}maybeLockUploads(e){let t=this.fields.get(e);if(!t||!t.ui.dropZone)return;let s=this.stores.uploads.filterByIndex({field:e}).length,r=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=r}async handleOperationCancelled(e){0!==e.length&&e.forEach((e=>{this.removeUpload(e)}))}getGroupKey(e,t=null){return t?`${e}_${t}`:`${e}`}getSelectionHandler(e){let t=this.getGroupKey(e);if(!this.selectionHandlers.has(t)){let s=this.fields.get(e);if(!s)return;if("post_group"!==s.config.destination)return;let r=new window.jvbHandleSelection(s.element,{selectAll:{checkbox:this.selectors.fields.selectAll,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions},item:{item:this.selectors.items.item,checkbox:this.selectors.items.checkbox,idAttribute:"uploadId"},wrapper:{wrapper:".preview-wrap, .upload-group",id:"groupId"}});r.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems)})),this.selectionHandlers.set(t,r)}return this.selectionHandlers.get(t)}updateHandlerItems(e){let t=this.getSelectionHandler(e);t&&t.collectItems()}initSortable(e){if(!window.Sortable)return;const t=this.fields.get(e);t&&(!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0),this.createSortable(e,t.ui.grid,null),this.initEmptyGroupDropZone(e))}createSortable(e,t,s){if(!t)return null;const r=this.getGroupKey(e,s);if(this.sortables.has(r))return this.sortables.get(r);const i=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},dragClass:"dragging",onStart:t=>{const s=t.item,r=s?.dataset.uploadId,i=this.selected.get(e);if(r&&(!i||!i.has(r))){const t=this.selectionHandlers.get(e);t&&t.select(r)}},onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(r,i),i}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),e.dataTransfer.dropEffect="move",s.classList.add("drag-over")})),s.addEventListener("dragleave",(e=>{s.contains(e.relatedTarget)||s.classList.remove("drag-over")})),s.addEventListener("drop",(async t=>{t.preventDefault(),s.classList.remove("drag-over");const r=this.selected.get(e);if(!r||0===r.size)return;const i=await this.createGroup(e);i&&(await Promise.all(Array.from(r).map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,r=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===r.length)return;const i=s.dataset.groupId||null;for(const e of r)await this.addToGroup(e,i);await this.handleReorder(t,i),this.selectionHandlers.get(t)?.clearSelection()}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(!s)return void console.log("Couldn't Reorder items...");let r=Array.from(s.children).filter((e=>e.matches(this.selectors.items.item)&&!e.classList.contains("ghost"))).map((e=>e.dataset.uploadId)).filter((e=>e));if(t){let e=this.stores.groups.get(t);e&&(e.uploads=r,this.stores.groups.save(e).then((()=>{})))}else{let t=this.fields.get(e)?.ui.hidden;t&&(t.value=r.join(","))}this.a11y.announce("Items reordered")}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.previewUrls.forEach((e=>{this.revokePreviewUrl(e)})),this.previewUrls.clear()}cleanupAllPreviewUrls(){this.previewUrls.forEach((e=>this.revokePreviewUrl(e))),this.previewUrls.clear()}async handleClearCache(){const e=window.location.href,t=this.stores.uploads.filterByIndex({src:e}),s=this.stores.groups.filterByIndex({src:e});await Promise.all([...t.map((e=>this.clearUpload(e.id))),...s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id))))]),this.restoreModal&&this.cleanupRestore(),this.a11y.announce("Cache cleared for this page")}async getFilesForForm(e){const t=e.querySelectorAll(this.selectors.fields.field),s=[];for(const e of t){const t=this.determineFieldId(e),r=e.dataset.field,i=this.stores.uploads.filterByIndex({field:t});for(const e of i){const t=this.formatFile(e);t&&s.push({file:t,fieldName:r,uploadId:e.id,meta:e.fields||{}})}}return s}async clearFieldFromStores(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all(t.map((e=>this.clearUpload(e.id)))),await Promise.all(s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id)))))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUploads=new e)}))}))})();
\ No newline at end of file

--
Gitblit v1.10.0