Jake Vanderwerf
2026-02-04 2127b1bdd73ecd2423e443992da4b442f5a3c1a3
1
(()=>{class e{constructor(e){this.manager=e,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.groupElements=new Map,this.groupStoreReady=!1,this.selectors={container:".upload-group",grid:".item-grid.group",header:".group-header",selectAll:'[name="select-all-group"]',actions:".group-actions",count:".selection-controls .info",display:".group-display",empty:".empty-group"},this.init()}async init(){const{groups:e}=window.jvbStore.register("uploads",[{storeName:"groups",keyPath:"id",indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"pageUrl",keyPath:"pageUrl"}],TTL:6048e5,delayFetch:!0}]);this.groupStore=e,this.groupStore.subscribe((e=>{"data-loaded"===e&&(this.groupStoreReady=!0,this.checkForRecovery())})),this.manager.subscribe(this.handleManagerEvent.bind(this)),this.queue.subscribe(this.handleQueueEvent.bind(this)),this.initListeners(),this.manager.groups=this}handleManagerEvent(e,t){switch(e){case"field-registered":this.enhanceField(t.fieldId);break;case"files-processed":this.onFilesProcessed(t.fieldId);break;case"upload-removed":this.onUploadRemoved(t.uploadId,t.fieldId);break;case"field-state-updated":this.updateGroupCount(t.fieldId)}}handleQueueEvent(e,t){if("uploads/groups"!==t.endpoint)return;const o=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;switch(e){case"operation-complete":this.handleOperationComplete(t,o);break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,o)}}enhanceField(e){const t=this.manager.fieldElements.get(e);if(!t)return;const{element:o,config:a}=t;if("post_group"!==a.destination)return;const r=o.querySelector(this.selectors.display);r&&(t.ui.groups={display:r,container:o.querySelector(".item-grid.groups"),empty:o.querySelector(this.selectors.empty),groups:new Map}),this.initEmptyGroupDropZone(e),a.maxFiles=20}initEmptyGroupDropZone(e){const t=this.manager.fieldElements.get(e),o=t?.ui?.groups?.empty;o&&!o.sortableInstance&&(o.sortableInstance=new Sortable(o,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:e,pull:!1,put:!0},ghostClass:"sortable-ghost",onEnd:t=>this.handleDrop(t,e)}))}onFilesProcessed(e){const t=this.manager.fieldElements.get(e);t&&("post_group"===t.config.destination&&t.ui.groups?.display&&(t.ui.groups.display.hidden=!1),this.initGroupSortables(e))}initGroupSortables(e){const t=this.manager.fieldElements.get(e);if(!t?.element)return;t.element.querySelectorAll(this.selectors.grid).forEach((t=>{if(t.sortableInstance)return;const o=t.closest(this.selectors.container)?.dataset.groupId;this.createSortableForGrid(t,e,o)}))}async onUploadRemoved(e,t){const o=this.manager.uploadStore.get(e);if(o?.groupId){const t=this.groupStore.get(o.groupId);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.deleteGroup(o.groupId,!1):await this.groupStore.save(t))}}initListeners(){document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this))}handleClick(e){const t=e.target.closest("[data-action]");if(!t)return;const o=t.dataset.action,a=this.manager.getFieldIdFromElement(t);switch(o){case"add-to-group":this.handleAddToGroup(a);break;case"delete-group":this.handleDeleteGroup(t);break;case"remove-from-group":this.handleRemoveFromGroup(t);break;case"upload":const o=this.manager.fieldElements.get(a);"post_group"===o?.config.destination&&(e.stopImmediatePropagation(),this.submitGroupedUploads(a))}}handleChange(e){const t=this.manager.getFieldIdFromElement(e.target);if(!t)return;const o=this.manager.fieldElements.get(t);if("post_group"!==o?.config.destination)return;const a=e.target.closest(this.selectors.container);a&&this.handleGroupMetaChange(e.target,a)}createSortableForGrid(e,t,o=null){if(!e||e.sortableInstance)return;const a=new Sortable(e,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:t,pull:!0,put:!0},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:e=>this.handleDrop(e,t),onSelect:e=>this.manager.syncCheckboxToSortable(e.item,!0),onDeselect:e=>this.manager.syncCheckboxToSortable(e.item,!1),onAdd:e=>this.manager.updateSortableState(e.to),onRemove:e=>this.manager.updateSortableState(e.from)});e.sortableInstance=a;const r=o?`${t}-group-${o}`:`${t}-preview`;return this.manager.sortableInstances.set(r,a),a}async handleDrop(e,t){const o=e.to,a=e.from,r=e.items?.length>0?e.items:[e.item],s=r.map((e=>e.dataset.uploadId));if(o===a)return void this.handleReorder(e);const i=this.getDropTargetType(o);try{switch(i){case"empty-group":const e=await this.createGroup(t);if(!e)throw new Error("Group creation failed");for(const t of s)await this.addToGroup(t,e.grid);break;case"preview":for(const e of s)await this.removeFromGroup(e);break;case"group":for(const e of s)await this.addToGroup(e,o);break;default:throw new Error("Unknown drop target")}this.finalizeDrop(t,r.length,i)}catch(e){console.error("Drop error:",e);const o=this.manager.fieldElements.get(t);o?.ui?.preview&&r.forEach((e=>o.ui.preview.appendChild(e))),this.a11y.announce("An error occurred. Items returned to preview.")}this.manager.updateSortableState(o),a!==o&&this.manager.updateSortableState(a)}getDropTargetType(e){return e.classList.contains("empty-group")?"empty-group":e.classList.contains("preview")?"preview":e.classList.contains("group")?"group":"unknown"}finalizeDrop(e,t,o){const a={"empty-group":t>1?`Created group with ${t} items`:"Created group with item",preview:t>1?`Moved ${t} items to preview`:"Moved item to preview",group:t>1?`Moved ${t} items to group`:"Moved item to group"};this.a11y.announce(a[o]||"Items moved"),this.manager.selectionHandlers.get(e)?.clearSelection()}handleReorder(e){const t=e.to,o=t.dataset.groupId;if(o){const e=this.groupStore.get(o);if(e){const o=Array.from(t.querySelectorAll(".item:not(.sortable-ghost)")).map((e=>e.dataset.uploadId)).filter(Boolean);e.uploads=o,this.groupStore.save(e)}}this.a11y.announce("Item reordered")}getFieldGroups(e){return this.groupStore.getAll().filter((t=>t.fieldId===e))}async createGroup(e,t=null){const o=this.manager.fieldElements.get(e);if(!o)return null;const a={id:t=t||this.manager.generateId("group"),fieldId:e,pageUrl:window.location.href,uploads:[],fields:{}};await this.groupStore.save(a);const r=this.createGroupElement(t,e);if(!r)return null;const s=r.querySelector(this.selectors.grid);return this.groupElements.set(t,{element:r,grid:s,fieldId:e}),o.ui.groups?.container&&o.ui.groups.empty?o.ui.groups.container.insertBefore(r,o.ui.groups.empty):o.ui.groups?.container&&o.ui.groups.container.appendChild(r),this.addGroupSelectionHandler(e,t),s&&this.createSortableForGrid(s,e,t),this.updateGroupCount(e),{id:t,element:r,grid:s}}createGroupElement(e,t){const o=window.getTemplate("imageGroup");if(!o)return null;o.dataset.groupId=e,o.dataset.fieldId=t;const a=window.getTemplate("groupMetadata"),r=o.querySelector(".fields");if(r&&a){r.append(a);const s=r.querySelector('[name="post_title"]'),i=r.querySelector('[name="post_excerpt"]');s&&(s.id=`${e}_title`,s.name=`${e}[post_title]`),i&&(i.id=`${e}_excerpt`,i.name=`${e}[post_excerpt]`);const n=this.manager.fieldElements.get(t);if(n?.config.content){const e=o.querySelector("summary");e&&(e.textContent=n.config.content+" Fields")}}else o.querySelector("details")?.remove();const s=o.querySelector(this.selectors.grid);return s&&(s.dataset.groupId=e),o}async deleteGroup(e,t=!0){const o=this.groupElements.get(e);if(!o)return;const a=this.groupStore.get(e);if(t&&a?.uploads)for(const e of a.uploads)await this.removeFromGroup(e);await this.groupStore.delete(e),o.element?.remove(),this.groupElements.delete(e);const r=`${o.fieldId}-group-${e}`;this.manager.sortableInstances.get(r)?.destroy(),this.manager.sortableInstances.delete(r);const s=`${o.fieldId}_${e}`;this.manager.selectionHandlers.get(s)?.destroy(),this.manager.selectionHandlers.delete(s),this.updateGroupCount(o.fieldId),this.a11y.announce("Group removed")}async addToGroup(e,t){const o=this.manager.uploadStore.get(e),a=this.manager.uploadElements.get(e);if(!o||!a)return;const r=t.dataset.groupId,s=this.groupStore.get(r);if(o.groupId&&o.groupId!==r){const t=this.groupStore.get(o.groupId);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.deleteGroup(o.groupId,!1):await this.groupStore.save(t))}o.groupId=r,s&&!s.uploads.includes(e)&&(s.uploads.push(e),await this.groupStore.save(s)),await this.manager.uploadStore.save(o),t.appendChild(a.element),a.location=t;const i=a.element.querySelector('[name="featured"]');i&&(i.hidden=!1,i.name=`${r}_featured`);const n=a.element.querySelector('[name*="select-item"]');n&&(n.checked=!1),this.manager.updateSortableState(t)}async removeFromGroup(e){const t=this.manager.uploadStore.get(e),o=this.manager.uploadElements.get(e);if(!t||!o)return;const a=this.manager.fieldElements.get(t.fieldId);if(!a?.ui?.preview)return;if(t.groupId){const o=this.groupStore.get(t.groupId);o&&(o.uploads=o.uploads.filter((t=>t!==e)),0===o.uploads.length?await this.deleteGroup(t.groupId,!1):await this.groupStore.save(o)),t.groupId=null,await this.manager.uploadStore.save(t)}a.ui.preview.appendChild(o.element),o.location=a.ui.preview;const r=o.element.querySelector('[name="featured"]');r&&(r.hidden=!0,r.checked=!1),this.manager.updateSortableState(a.ui.preview)}async handleGroupMetaChange(e,t){const o=t.dataset.groupId,a=this.groupStore.get(o);if(!a)return;let r=e.name;r.includes(o)&&(r=r.replace(`${o}_`,"").replace(`${o}[`,"").replace("]","")),a.fields[r]=e.value,await this.groupStore.save(a)}updateGroupCount(e){const t=this.manager.fieldElements.get(e);if(!t)return;const o=this.getFieldGroups(e).length;t.element.dataset.hasGroups=o>0}async handleAddToGroup(e){const t=this.manager.selected.get(e);if(t&&0!==t.size){const o=await this.createGroup(e);if(!o)return;for(const e of t)await this.addToGroup(e,o.grid);this.manager.selectionHandlers.get(e)?.clearSelection(),this.a11y.announce(`Created group with ${t.size} items`)}else await this.createGroup(e)}async handleDeleteGroup(e){const t=e.closest(this.selectors.container);t&&confirm("Delete this group? Items will be moved back to the upload area.")&&await this.deleteGroup(t.dataset.groupId,!0)}async handleRemoveFromGroup(e){const t=e.closest("[data-upload-id]");t&&(await this.removeFromGroup(t.dataset.uploadId),this.a11y.announce("Item moved to preview"))}addGroupSelectionHandler(e,t){const o=`${e}_${t}`;if(this.manager.selectionHandlers.has(o))return;const a=this.groupElements.get(t);if(!a?.element)return;const r=new window.jvbHandleSelection({container:a.element,ui:{selectAll:a.element.querySelector(this.selectors.selectAll),bulkControls:a.element.querySelector(this.selectors.actions),count:a.element.querySelector(this.selectors.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});r.subscribe(((t,o)=>{["item-selected","item-deselected","range-selected"].includes(t)&&this.manager.selected.set(e,o.selectedItems)})),this.manager.selectionHandlers.set(o,r)}async submitGroupedUploads(e){const t=this.manager.getFieldUploads(e),o=this.getFieldGroups(e),a=this.manager.fieldElements.get(e);if(0===t.length||!a)return;a.element.closest("details")?.removeAttribute("open"),document.body.classList.add("uploading");const r=new FormData,s=[],i=[];for(const e of o){const o=t.filter((t=>t.groupId===e.id)),a={images:[],fields:{...e.fields}};for(const e of o){const t=await this.manager.getBlobData(e.id);if(t){r.append("files[]",t),a.images.push({upload_id:e.id,index:i.length}),i.push(e.id);const o=this.manager.uploadElements.get(e.id),s=o?.element?.querySelector('[name*="featured"]');s?.checked&&(a.fields.featured=e.id)}}a.images.length>0&&s.push(a)}const n=t.filter((e=>!e.groupId));for(const e of n){const t=await this.manager.getBlobData(e.id);t&&(r.append("files[]",t),s.push({images:[{upload_id:e.id,index:i.length}],fields:{}}),i.push(e.id))}r.append("content",a.config.content),r.append("user",a.config.itemID),r.append("fieldId",e),r.append("posts",JSON.stringify(s)),r.append("upload_ids",JSON.stringify(i));const d={endpoint:"uploads/groups",method:"POST",data:r,title:`Creating ${s.length} ${a.config.content}${s.length>1?"s":""}...`,popup:`Creating ${s.length} post${s.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{const e=await this.queue.addToQueue(d);for(const o of t)o.operationId=e,o.status="queued",await this.manager.uploadStore.save(o),this.manager.updateUploadUI(o.id);return this.a11y.announce(`Creating ${s.length} post${s.length>1?"s":""}`),e}catch(t){throw this.error.log(t,{component:"UploadGroups",action:"submitGroupedUploads",fieldId:e}),t}}async handleOperationComplete(e,t){const o=e.result?.data||e.serverData?.data||[];for(const e of o){const t=this.manager.uploadStore.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",await this.manager.uploadStore.save(t),this.manager.updateUploadUI(e.upload_id))}if(!t)return;const a=this.manager.getFieldUploads(t),r=new Set;for(const e of a)"completed"===e.status&&(e.groupId&&r.add(e.groupId),await this.manager.clearUpload(e.id));for(const e of r)await this.groupStore.delete(e),this.groupElements.get(e)?.element?.remove(),this.groupElements.delete(e);this.manager.updateFieldState(t),this.a11y.announce("All uploads completed successfully"),document.body.classList.remove("uploading")}async handleOperationFailed(e,t){const o=e.data instanceof FormData?JSON.parse(e.data.get("upload_ids")||"[]"):e.data?.upload_ids||[],a="operation-failed-permanent"===e.status?"failed_permanent":"failed";for(const e of o)await this.manager.updateUploadStatus(e,a);t&&this.manager.updateFieldState(t),document.body.classList.remove("uploading")}checkForRecovery(){if(!this.manager.storesReady||!this.groupStoreReady)return;if(this.hasCheckedForRecovery)return;const e=this.manager.uploadStore.getAll();if(!e.some((e=>e.groupId))&&this.manager.hasCheckedForRecovery)return void(this.hasCheckedForRecovery=!0);this.hasCheckedForRecovery=!0,this.manager.hasCheckedForRecovery=!0;const t=e.filter((e=>!e.operationId&&["processed","processing","queued"].includes(e.status)));if(0===t.length)return;const o=this.groupUploadsByField(t);this.showRecoveryNotification(o)}groupUploadsByField(e){const t=new Map;return e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,{fieldId:e.fieldId,pageUrl:e.pageUrl,uploads:[],groups:new Map});const o=t.get(e.fieldId);if(o.uploads.push(e),e.groupId){const t=this.groupStore.get(e.groupId);t&&o.groups.set(e.groupId,t)}})),t}async showRecoveryNotification(e){let t=0,o=0;e.forEach((e=>{t+=e.uploads.length,o+=e.groups.size}));const a=window.getTemplate("restoreNotification");if(!a)return;const r=o>0?`${o} group${o>1?"s":""} with ${t} upload${t>1?"s":""} can be restored.`:`${t} upload${t>1?"s":""} can be recovered.`,s=a.querySelector(".restore-details");s&&(s.textContent=r);for(const[t,o]of e){const e=window.getTemplate("restoreField");if(!e)continue;const r=e.querySelector("h3");r&&(r.textContent=t);const s=e.querySelector(".item-grid.restore");for(const e of o.uploads){const o=await this.manager.getBlobData(e.id);if(!o)continue;const a=this.manager.createUploadElement({id:e.id,preview:this.manager.createPreviewUrl(o),meta:e.meta,subtype:this.manager.getSubtypeFromMime(o.type)});a.dataset.fieldId=t,s?.appendChild(a)}a.querySelector(".wrap")?.appendChild(e)}document.querySelector(".field.upload")?.appendChild(a);const i=document.querySelector("dialog.restore-uploads");i&&(this.restoreModal=new window.jvbModal(i),this.restoreSelection=new window.jvbHandleSelection({container:i,ui:{selectAll:i.querySelector("#select-all-restore"),count:i.querySelector(".selection-count")}}),this.restoreModal.handleOpen(),i.addEventListener("click",this.handleRestoreAction.bind(this)))}handleRestoreAction(e){const t=e.target.closest("[data-action]")?.dataset.action;switch(t){case"restore":this.handleRestoreSelected();break;case"restore-all":this.handleRestoreAll();break;case"clear-cache":this.handleClearCache()}}async handleRestoreSelected(){const e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=[];e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const o=e.closest(".item");o&&t.push({uploadId:o.dataset.uploadId,fieldId:o.dataset.fieldId})})),t.length>0&&await this.restoreUploads(t),this.cleanupRestoreModal()}async handleRestoreAll(){const e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=[];e.querySelectorAll(".item.upload").forEach((e=>{t.push({uploadId:e.dataset.uploadId,fieldId:e.dataset.fieldId})})),await this.restoreUploads(t),this.cleanupRestoreModal()}async restoreUploads(e){const t=new Map;e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)}));for(const[e,o]of t)await this.restoreFieldUploads(e,o)}async restoreFieldUploads(e,t){let o=this.manager.fieldElements.get(e);if(!o){const t=document.querySelector(`[data-uploader="${e}"]`);t&&(this.manager.registerUploader(t),o=this.manager.fieldElements.get(e),this.enhanceField(e))}if(!o)return void console.warn(`Field ${e} not found for restoration`);o.ui.dropZone&&(o.ui.dropZone.hidden=!0),o.ui.groups?.display&&(o.ui.groups.display.hidden=!1);const a=this.getFieldGroups(e);for(const t of a)await this.restoreGroup(e,t);for(const o of t){const t=this.manager.uploadStore.get(o);t&&await this.restoreUploadElement(e,t)}this.manager.updateFieldState(e),this.manager.refreshSortable(e),this.manager.maybeLockUploads(e)}async restoreGroup(e,t){const o=await this.createGroup(e,t.id);if(o&&t.fields){const e=o.element.querySelector('[name*="post_title"]'),a=o.element.querySelector('[name*="post_excerpt"]');e&&t.fields.post_title&&(e.value=t.fields.post_title),a&&t.fields.post_excerpt&&(a.value=t.fields.post_excerpt)}}async restoreUploadElement(e,t){const o=this.manager.fieldElements.get(e);if(!o)return;const a=await this.manager.getBlobData(t.id);if(!a)return;const r=this.manager.createPreviewUrl(a),s=this.manager.createUploadElement({id:t.id,preview:r,meta:t.meta,subtype:this.manager.getSubtypeFromMime(a.type)});let i;if(t.groupId){const e=this.groupElements.get(t.groupId);if(e?.grid){i=e.grid;const o=s.querySelector('[name="featured"]');o&&(o.hidden=!1,o.name=`${t.groupId}_featured`)}else i=o.ui.preview}else i=o.ui.preview;i.appendChild(s),this.manager.uploadElements.set(t.id,{element:s,preview:r,location:i}),t.status="processed",await this.manager.uploadStore.save(t)}handleClearCache(){confirm("Discard these uploads?")&&(this.manager.uploadStore.clear(),this.groupStore.clear(),this.cleanupRestoreModal())}cleanupRestoreModal(){this.restoreModal&&(this.restoreModal.handleClose(),this.restoreSelection?.destroy(),this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null,this.restoreSelection=null)}destroy(){this.groupElements.forEach(((e,t)=>{const o=`${e.fieldId}-group-${t}`;this.manager.sortableInstances.get(o)?.destroy(),this.manager.sortableInstances.delete(o)})),this.groupElements.clear()}}document.addEventListener("DOMContentLoaded",(()=>{const t=()=>{window.jvbUploads&&!window.jvbUploadGroups&&(window.jvbUploadGroups=new e(window.jvbUploads))};window.jvbUploads?t():window.auth?.subscribe((e=>{"auth-loaded"===e&&setTimeout(t,50)}))}))})();