| | |
| | | |
| | | if (manyRefs.inputs) { |
| | | for (let input of manyRefs.inputs) { |
| | | let wrapper = input.closest('[data-field]')??el; |
| | | let wrapper = input.closest('[data-field]')??input.closest('.radio-button')??el; |
| | | |
| | | window.prefixInput(input, `${data.id??data.uploadId}-`, wrapper); |
| | | } |
| | | } |
| | |
| | | inputs: 'input,textarea,select' |
| | | }, |
| | | setup({el, refs, manyRefs, data}) { |
| | | if (refs.inputs) { |
| | | refs.inputs.forEach(input => { |
| | | if (manyRefs.inputs) { |
| | | manyRefs.inputs.forEach(input => { |
| | | let wrapper = input.closest('[data-field]'); |
| | | input.dataset.groupId = data.groupId; |
| | | window.prefixInput(input, `${data.groupId}-`, wrapper); |
| | | }); |
| | | } |
| | |
| | | if (event === 'data-ready') { |
| | | this.stores.ready.push(storeName); |
| | | if (this.storesReady()) { |
| | | this.checkRecovery().then(()=>{}); |
| | | this.checkRecovery().then(() => {}); |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | Object.preventExtensions(upload); |
| | | await this.stores.uploads.save(upload); |
| | | |
| | | if (this.fields.has(upload.field)) { |
| | | let field = this.fields.get(upload.field); |
| | | switch (upload.status) { |
| | | case 'local_processing': |
| | | this.notify('upload-received', { |
| | | field: field.element, |
| | | id: upload.id |
| | | }); |
| | | } |
| | | } |
| | | |
| | | |
| | | return upload; |
| | | } |
| | | |
| | |
| | | LISTENERS |
| | | *********************************************************************/ |
| | | handleClick(e) { |
| | | if (!window.targetCheck(e, this.selectors.fields.field)) return; |
| | | |
| | | //Open the file input if it's a dropzone |
| | | let dropZone = window.targetCheck(e, this.selectors.fields.dropZone); |
| | | if (dropZone && !e.target.matches('input, button, a')){ |
| | |
| | | } |
| | | |
| | | let field = this.fields.get(fieldId); |
| | | |
| | | if (field.config.destination === 'post_group') { |
| | | this.handleGroupMetaChange(e.target); |
| | | } else { |
| | |
| | | const groupId = input.dataset.groupId; |
| | | if (!groupId) return; |
| | | |
| | | // Capture values immediately (before debouncer) |
| | | // Capture values immediately |
| | | const inputName = input.name; |
| | | if (!inputName) return; |
| | | const inputValue = input.value; |
| | |
| | | |
| | | if (isUpload) { |
| | | data.append('mode', field.config.mode); |
| | | data.append('field_name', field.config.name); |
| | | |
| | | data.append('field_name', field.config.repeaterPath || field.config.name); |
| | | data.append('fieldId', field.id); |
| | | data.append('field_type', field.config.type); |
| | | data.append('subtype', field.config.subtype); |
| | |
| | | if (data.config.type !== 'single') { |
| | | this.initSortable(data.id); |
| | | } |
| | | this.maybeLockUploads(data.id); |
| | | |
| | | return data.id; |
| | | } |
| | | |
| | | extractFieldConfig(fieldElement, autoUpload, imageMeta) { |
| | | return { |
| | | extractFieldConfig(el, autoUpload, imageMeta) { |
| | | const config = { |
| | | autoUpload: autoUpload, |
| | | showMeta: imageMeta, |
| | | destination: fieldElement.dataset.destination || 'meta', //TODO: why do we need this? |
| | | content: this.extractFieldContent(fieldElement), |
| | | mode: fieldElement.dataset.mode || 'direct', |
| | | type: fieldElement.dataset.type || 'single', |
| | | name: fieldElement.dataset.field, |
| | | itemID: this.extractFieldItemId(fieldElement)??0, |
| | | maxFiles: parseInt(fieldElement.dataset.maxFiles)??25, |
| | | subType: fieldElement.dataset.subtype?? 'image' |
| | | destination: el.dataset.destination || 'meta', |
| | | content: this.extractFieldContent(el), |
| | | mode: el.dataset.mode || 'direct', |
| | | type: el.dataset.type || 'single', |
| | | name: el.dataset.field, |
| | | itemID: this.extractFieldItemId(el) ?? 0, |
| | | maxFiles: ('max-files' in el.dataset) ? parseInt(el.dataset.maxFiles) : 0, |
| | | subType: el.dataset.subtype ?? 'image', |
| | | repeaterPath: null |
| | | }; |
| | | |
| | | const repeaterRow = el.closest('[data-index]'); |
| | | const repeater = repeaterRow?.closest('[data-field][data-repeater-id]'); |
| | | if (repeater && repeaterRow) { |
| | | config.repeaterPath = `${repeater.dataset.field}:${repeaterRow.dataset.index}:${config.name}`; |
| | | } |
| | | |
| | | return config; |
| | | } |
| | | |
| | | extractFieldContent(fieldElement) { |
| | |
| | | determineFieldId(fieldElement) { |
| | | let content = this.extractFieldContent(fieldElement); |
| | | content = (content === null) ? '' : content+'_'; |
| | | |
| | | let itemID = this.extractFieldItemId(fieldElement); |
| | | itemID = (itemID === null) ? '' : itemID+'_'; |
| | | |
| | | const field = fieldElement.dataset.field || ''; |
| | | |
| | | // If inside a repeater row, include repeater name + index for uniqueness |
| | | const repeaterRow = fieldElement.closest('[data-index]'); |
| | | const repeater = repeaterRow?.closest('[data-field][data-repeater-id]'); |
| | | if (repeater && repeaterRow) { |
| | | return `${content}${itemID}${repeater.dataset.field}_${repeaterRow.dataset.index}_${field}`; |
| | | } |
| | | |
| | | return `${content}${itemID}${field}`; |
| | | } |
| | | |
| | |
| | | RECOVERY |
| | | *************************************************************/ |
| | | async checkRecovery() { |
| | | const pendingUploads = this.stores.uploads.filterByIndex({status: ['local_processing', 'queued', 'uploading']}); |
| | | const allGroups = Array.from(this.stores.groups.data.values()); |
| | | for (const group of allGroups) { |
| | | const hasUploads = this.stores.uploads.filterByIndex({group: group.id}).length > 0; |
| | | if (!hasUploads) { |
| | | await this.stores.groups.delete(group.id); |
| | | } |
| | | const hasUploads = this.stores.uploads.filterByIndex({ group: group.id }).length > 0; |
| | | if (!hasUploads) await this.stores.groups.delete(group.id); |
| | | } |
| | | if (pendingUploads.length === 0) return; |
| | | |
| | | // Group by source page |
| | | const bySource = new Map(); |
| | | pendingUploads.forEach(upload => { |
| | | const src = upload.src || 'unknown'; |
| | | if (!bySource.has(src)) bySource.set(src, []); |
| | | bySource.get(src).push(upload); |
| | | }); |
| | | |
| | | let data = { |
| | | bySource: bySource, |
| | | pendingUploads: pendingUploads |
| | | }; |
| | | |
| | | document.body.append(this.templates.create('restoreNotification', data)); |
| | | let notification = document.querySelector('dialog.restore-uploads'); |
| | | this.restoreModal = new window.jvbModal(notification); |
| | | this.restoreSelection = new window.jvbHandleSelection(notification, |
| | | { |
| | | wrapper: { |
| | | wrapper: '.restore-field', |
| | | id: 'selection' |
| | | }, |
| | | items: '.item-grid.restore', |
| | | selectAll: { |
| | | bulkControls: '.selection-actions', |
| | | checkbox: '#select-all-restore', |
| | | count: '.selection-count' |
| | | } |
| | | }); |
| | | this.restoreModal.handleOpen(); |
| | | } |
| | | //TODO: Old method of checkRecovery. All recovery logic has moved to the FormController.js |
| | | // async checkRecovery() { |
| | | // const pendingUploads = this.stores.uploads.filterByIndex({status: ['local_processing', 'queued', 'uploading']}); |
| | | // const allGroups = Array.from(this.stores.groups.data.values()); |
| | | // for (const group of allGroups) { |
| | | // const hasUploads = this.stores.uploads.filterByIndex({group: group.id}).length > 0; |
| | | // if (!hasUploads) { |
| | | // await this.stores.groups.delete(group.id); |
| | | // } |
| | | // } |
| | | // if (pendingUploads.length === 0) return; |
| | | // |
| | | // // Group by source page |
| | | // const bySource = new Map(); |
| | | // pendingUploads.forEach(upload => { |
| | | // const src = upload.src || 'unknown'; |
| | | // if (!bySource.has(src)) bySource.set(src, []); |
| | | // bySource.get(src).push(upload); |
| | | // }); |
| | | // |
| | | // let data = { |
| | | // bySource: bySource, |
| | | // pendingUploads: pendingUploads |
| | | // }; |
| | | // |
| | | // document.body.append(this.templates.create('restoreNotification', data)); |
| | | // let notification = document.querySelector('dialog.restore-uploads'); |
| | | // this.restoreModal = new window.jvbModal(notification); |
| | | // this.restoreSelection = new window.jvbHandleSelection(notification, |
| | | // { |
| | | // 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 selected = Array.from(this.restoreSelection.selectedItems); |
| | | if (selected.length === 0) { |
| | | return; |
| | | } |
| | | |
| | | await this.restoreSelectedUploads(selected); |
| | | } |
| | | async handleRestoreAll() { |
| | | if (!this.restoreModal) return; |
| | | const allUploads = Array.from(this.restoreModal.modal.querySelectorAll('.item.upload')).map(item => item.dataset.uploadId); |
| | | |
| | | await this.restoreSelectedUploads(allUploads); |
| | | } |
| | | |
| | | // async handleRestoreSelected() { |
| | | // if (!this.restoreSelection) return; |
| | | // |
| | | // let selected = Array.from(this.restoreSelection.selectedItems); |
| | | // if (selected.length === 0) { |
| | | // return; |
| | | // } |
| | | // |
| | | // await this.restoreSelectedUploads(selected); |
| | | // } |
| | | // async handleRestoreAll() { |
| | | // if (!this.restoreModal) return; |
| | | // const allUploads = Array.from(this.restoreModal.modal.querySelectorAll('.item.upload')).map(item => item.dataset.uploadId); |
| | | // |
| | | // await this.restoreSelectedUploads(allUploads); |
| | | // } |
| | | // |
| | | async restoreSelectedUploads(selectedUploads) { |
| | | let currentPage = window.location.href; |
| | | |
| | |
| | | let fieldId = uploads[0].field; |
| | | let field = document.querySelector(`[data-uploader="${fieldId}"]`); |
| | | if (!field) { |
| | | console.log('No field found for '+fieldId); |
| | | return; |
| | | if ('crudManager' in window && fieldId.startsWith(window.crudManager.content)) { |
| | | let [content, itemId, fieldName] = fieldId.split('_'); |
| | | if (parseInt(itemId) > 0) { |
| | | window.crudManager.openEditModal(itemId); |
| | | field = document.querySelector(`[data-uploader="${fieldId}"]`); |
| | | } else { |
| | | console.log('No field found for '+fieldId); |
| | | return; |
| | | } |
| | | } else { |
| | | console.log('No field found for '+fieldId); |
| | | return; |
| | | } |
| | | |
| | | } |
| | | let fieldData = this.fields.get(fieldId); |
| | | if (fieldData.groupUI.container) { |
| | |
| | | }); |
| | | await this.addToGroup(upload.id, null); |
| | | } |
| | | } |
| | | // |
| | | // cleanupRestore() { |
| | | // this.restoreModal.handleClose(); |
| | | // this.restoreSelection.destroy(); |
| | | // this.restoreSelection = null; |
| | | // this.restoreModal.destroy(); |
| | | // this.restoreModal.modal.remove(); |
| | | // this.restoreModal = null; |
| | | // } |
| | | |
| | | this.cleanupRestore(); |
| | | async restoreUploads(uploadIds) { |
| | | const uploads = uploadIds.map(id => this.stores.uploads.get(id)).filter(Boolean); |
| | | if (uploads.length === 0) return; |
| | | await this.restoreSelectedUploads(uploads.map(u => u.id)); |
| | | } |
| | | |
| | | cleanupRestore() { |
| | | this.restoreModal.handleClose(); |
| | | this.restoreSelection.destroy(); |
| | | this.restoreSelection = null; |
| | | this.restoreModal.destroy(); |
| | | this.restoreModal.modal.remove(); |
| | | this.restoreModal = null; |
| | | async clearUploads(uploadIds) { |
| | | await Promise.all(uploadIds.map(id => this.clearUpload(id))); |
| | | } |
| | | /******************************************************************************* |
| | | STATUS MANAGEMENT |
| | |
| | | } |
| | | |
| | | getSubtypeFromURL(url) { |
| | | if (!url || url === '') { |
| | | return ''; |
| | | } |
| | | const imgs = ['.webp', '.jpg', '.jpeg', '.png', '.gif', '.svg']; |
| | | const videos = ['.mp4', '.ogg', '.mov', '.webm', '.avi']; |
| | | |
| | |
| | | * @param button |
| | | */ |
| | | async handleRemoveItem(button) { |
| | | console.log('Handling remove upload'); |
| | | const item = button.closest(this.selectors.items.item); |
| | | if (!item) return; |
| | | |
| | |
| | | if (!field?.ui.hidden) return; |
| | | |
| | | const remaining = Array.from(field.ui.grid?.querySelectorAll(this.selectors.items.item) || []) |
| | | .map(el => el.dataset.id || el.dataset.uploadId) |
| | | .map(el => { |
| | | if (Object.hasOwn(el.dataset, 'id') && el.dataset.id > 0) { |
| | | return el.dataset.id; |
| | | } |
| | | |
| | | if (Object.hasOwn(el.dataset, 'upload-id') && el.dataset.uploadId > 0) { |
| | | return el.dataset.uploadId; |
| | | } |
| | | //For timeline |
| | | return el.dataset.itemId; |
| | | }) |
| | | .filter(Boolean); |
| | | |
| | | field.ui.hidden.value = remaining.join(','); |
| | | const newValue = remaining.join(','); |
| | | if (field.ui.hidden.value === newValue) return; |
| | | |
| | | field.ui.hidden.value = newValue; |
| | | field.ui.hidden.dispatchEvent(new Event('change', { bubbles: true })); |
| | | } |
| | | async setBulkUpload(uploads, key, value) { |
| | |
| | | if (selectionHandler?.destroy) { |
| | | selectionHandler.destroy(); |
| | | } |
| | | this.selectionHandlers.get(group.field)?.removeWrapper(element.element); |
| | | if (this.selectionHandlers.get(group.field) && element && element.element) { |
| | | this.selectionHandlers.get(group.field).removeWrapper(element.element) |
| | | } |
| | | |
| | | // Existing sortable cleanup |
| | | const sortable = this.sortables.get(sortableKey); |
| | | if (sortable?.destroy) { |
| | | sortable.destroy(); |
| | | if (this.sortables.has(sortableKey)) { |
| | | const sortable = this.sortables.get(sortableKey); |
| | | if (sortable?.destroy) { |
| | | sortable.destroy(); |
| | | } |
| | | |
| | | this.sortables.delete(sortableKey); |
| | | } |
| | | this.sortables.delete(sortableKey); |
| | | |
| | | } |
| | | |
| | | if (element?.element) { |
| | |
| | | |
| | | let uploads = this.stores.uploads.filterByIndex({field: fieldId}); |
| | | let count = uploads.length; |
| | | let max = field.config.maxFiles??25; |
| | | let max = field.config.maxFiles??0; |
| | | |
| | | field.ui.dropZone.hidden = count >= max; |
| | | field.ui.dropZone.hidden = max > 0 && count >= max; |
| | | } |
| | | /******************************************************************************* |
| | | OPERATION METHODS |