Jake Vanderwerf
2025-11-10 e9967fa22781d922ba4eb8fb44fe72d200ac4b14
assets/js/concise/UploadManager.js
@@ -38,6 +38,8 @@
         ],
      });
      window.jvbUploadBlobs = this.uploadStore;
      // Subscribe to store events
      this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this));
      this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this));
@@ -48,7 +50,6 @@
      // Core data structures
      this.fields = new Map();
      this.uploads = new Map();
      this.uploadBlobs = new Map();
      this.groups = new Map();
      this.selected = new Map();
      this.selectionHandlers = new Map();
@@ -98,6 +99,20 @@
         'failed_permanent': 'Upload failed permanently'
      };
      // Sortable configuration
      this.sortableInstances = new Map();
      this.sortableConfig = {
         animation: 150,
         draggable: '.item',
         handle: '.select-item-label, img', // Can drag by image or checkbox label
         ghostClass: 'sortable-ghost',
         chosenClass: 'sortable-chosen',
         dragClass: 'sortable-drag',
         onEnd: (evt) => {
            this.handleReorder(evt);
         }
      };
      this.init();
   }
@@ -209,6 +224,9 @@
      if (config.destination === 'post_group' && !this.dragController) {
         this.initGroupFeatures();
      }
      if (config.type !== 'single') {
         this.initSortable(field);
      }
      return fieldId;
   }
@@ -355,6 +373,76 @@
      });
   }
   initSortable(field) {
      if (!window.Sortable) return;
      // Main grid
      const mainGrid = field.element.querySelector('.item-grid:not(.group)');
      if (mainGrid) {
         this.sortableInstances.set(`${field.id}-main`,
            new Sortable(mainGrid, {
               ...this.sortableConfig,
               group: {
                  name: field.id,
                  pull: true,
                  put: true
               }
            })
         );
      }
      // Group grids (for selection mode with grouping)
      const groupGrids = field.element.querySelectorAll('.item-grid.group');
      groupGrids.forEach((grid, index) => {
         this.sortableInstances.set(`${field.id}-group-${index}`,
            new Sortable(grid, {
               ...this.sortableConfig,
               group: {
                  name: field.id,
                  pull: true,
                  put: true
               }
            })
         );
      });
   }
// Add reorder handler
   handleReorder(evt) {
      const grid = evt.to;
      const fieldWrapper = grid.closest('.field, .upload');
      if (!fieldWrapper) return;
      const form = fieldWrapper.closest('form');
      if (!form) return;
      // Get form config if available
      const formId = form.dataset.formId;
      if (formId && window.jvbForms) {
         const formConfig = window.jvbForms.forms?.get(formId);
         if (formConfig?.options.autosave) {
            // Trigger autosave after reordering
            window.jvbForms.scheduleSave(formConfig, 1000);
         }
      }
      // Announce for accessibility
      if (window.jvbA11y) {
         window.jvbA11y.announce('Item reordered');
      }
      // Trigger custom event
      fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
         detail: {
            from: evt.from,
            to: evt.to,
            oldIndex: evt.oldIndex,
            newIndex: evt.newIndex
         },
         bubbles: true
      }));
   }
   /*******************************************************************************
    * EXTERNAL FILE DROP HANDLERS (for new uploads from desktop)
    *******************************************************************************/
@@ -1750,6 +1838,9 @@
      formData.append('posts', JSON.stringify(posts));
      formData.append('upload_ids', JSON.stringify(uploadMap));
      for (const [key, value] of formData.entries()) {
         console.log(key, value);
      }
      const operation = {
         endpoint: 'uploads/groups',
         method: 'POST',
@@ -2861,11 +2952,14 @@
      }
   }
   async saveUpload(upload) {
      // Handle blob data separately
      if (upload.file instanceof File || upload.file instanceof Blob) {
         await this.uploadStore.saveBlob(upload.id, upload.file);
         // Don't store the file in the main store
         const { file, originalFile, ...cleanUpload } = upload;
      // Use the processed file if available, otherwise original
      const fileToStore = upload.processedFile || upload.originalFile || upload.file;
      if (fileToStore instanceof File || fileToStore instanceof Blob) {
         await this.uploadStore.saveBlob(upload.id, fileToStore);
         // Don't store file objects in main store
         const { file, originalFile, processedFile, ...cleanUpload } = upload;
         await this.uploadStore.save(cleanUpload);
      } else {
         await this.uploadStore.save(upload);
@@ -2929,6 +3023,12 @@
      this.selectionHandlers.clear();
      this.cleanupAllPreviewUrls();
      this.sortableInstances.forEach(instance => {
         if (instance?.destroy) {
            instance.destroy();
         }
      });
      this.sortableInstances.clear();
      // Clear data
      this.fields.clear();
@@ -2938,6 +3038,20 @@
      this.subscribers.clear();
   }
   destroySortable(fieldName) {
      // Destroy all sortable instances for this field
      const instances = Array.from(this.sortableInstances.keys())
         .filter(key => key.startsWith(fieldName));
      instances.forEach(key => {
         const instance = this.sortableInstances.get(key);
         if (instance?.destroy) {
            instance.destroy();
         }
         this.sortableInstances.delete(key);
      });
   }
   cleanupRestore() {
      this.restoreModal.handleClose();
      this.restoreSelection.destroy();