(function (root, factory) { if (typeof module === 'object' && module.exports) { module.exports = factory(root); return; } root.AttachmentEditor = factory(root); })(typeof globalThis !== 'undefined' ? globalThis : window, function (root) { 'use strict'; var editors = new Map(); function normalizeFiles(files) { var seen = new Set(); return (files || []).map(function (file) { var ossId = file && file.ossId; if ((typeof ossId === 'number' && !Number.isSafeInteger(ossId)) || !/^[1-9][0-9]*$/.test(String(ossId))) { throw new Error('原附件缺少有效的上传标识,暂不能保存,请刷新后重试'); } return { ossId: String(ossId), fileName: String(file.fileName || '附件'), mediaType: String(file.mediaType || '').toLowerCase(), accessUrl: root.MediaDisplay ? (root.MediaDisplay.normalizeFileAccess(file) || {}).accessUrl || '' : '' }; }).filter(function (file) { if (seen.has(file.ossId)) return false; seen.add(file.ossId); return true; }); } function release(files) { files.forEach(function (file) { if (file.objectUrl) root.URL.revokeObjectURL(file.objectUrl); }); } function getEditor(target) { if (typeof target === 'string') target = root.document.querySelector(target); return editors.get(target); } function sync(editor) { editor.target.value = editor.files.map(function (file) { return file.ossId; }).join(','); editor.preview.replaceChildren(); editor.files.forEach(function (file) { var card = root.document.createElement('div'); var url = file.objectUrl || file.accessUrl; var name = root.document.createElement('span'); var remove = root.document.createElement('button'); var media; card.className = 'attachment-edit-item'; if (url && /^(image|video)(\/|$)/.test(file.mediaType)) { media = root.document.createElement(/^image/.test(file.mediaType) ? 'img' : 'video'); media.src = url; if (media.tagName === 'IMG') media.alt = file.fileName; else { media.controls = true; media.preload = 'metadata'; } media.addEventListener('error', function () { media.hidden = true; name.textContent = file.fileName + '(预览不可用,附件仍保留)'; }); card.appendChild(media); } name.className = 'attachment-edit-name'; name.textContent = file.fileName; card.appendChild(name); remove.type = 'button'; remove.className = 'pill attachment-edit-remove'; remove.textContent = '移除'; remove.setAttribute('aria-label', '移除 ' + file.fileName); remove.disabled = editor.busy || editor.input.disabled; // 相册接口尚不支持可靠清空封面,仅允许替换。 if (editor.input.hasAttribute('data-attachment-replace-only')) { remove.hidden = true; } remove.addEventListener('click', function () { if (editor.busy || editor.input.disabled) return; editor.files = editor.files.filter(function (existing) { return existing !== file; }); release([file]); editor.cleared = true; sync(editor); }); card.appendChild(remove); editor.preview.appendChild(card); }); if (editor.status && !editor.busy) { editor.status.textContent = editor.files.length ? '已选择 ' + editor.files.length + ' 个文件;移除或替换将在保存后生效' : (editor.cleared ? '附件已移除,保存后生效' : '未选择文件'); } } function init() { root.document.querySelectorAll('[data-attachment-editor]').forEach(function (input) { var target = root.document.querySelector(input.getAttribute('data-upload-target')); var preview; var editor; if (!target || editors.has(target)) return; preview = root.document.createElement('div'); preview.className = 'attachment-edit-list'; preview.setAttribute('aria-label', '已选择的附件'); input.insertAdjacentElement('afterend', preview); editor = { target: target, input: input, preview: preview, status: root.document.querySelector(input.getAttribute('data-upload-status')), multiple: input.getAttribute('data-upload-multiple') === 'true', files: [], cleared: false, busy: false }; editors.set(target, editor); new root.MutationObserver(function () { preview.querySelectorAll('button').forEach(function (button) { button.disabled = input.disabled || editor.busy; }); }).observe(input, { attributes: true, attributeFilter: ['disabled'] }); }); } function setFiles(target, files) { init(); var editor = getEditor(target); if (!editor) throw new Error('附件编辑控件未初始化'); editor.invalid = true; var normalized = normalizeFiles(files); release(editor.files); editor.files = normalized; editor.cleared = false; editor.invalid = false; sync(editor); } function addUploaded(target, upload, file) { var editor = getEditor(target); if (!editor) return false; var attachment = normalizeFiles([{ ossId: upload.ossId, fileName: file.name, mediaType: file.type }])[0]; if (editor.files.some(function (existing) { return existing.ossId === attachment.ossId; })) return true; attachment.objectUrl = root.URL.createObjectURL(file); if (!editor.multiple) { release(editor.files); editor.files = []; } editor.files.push(attachment); editor.cleared = false; sync(editor); return true; } function clear(target) { var editor = getEditor(target); if (!editor || editor.busy || editor.input.disabled) return; release(editor.files); editor.files = []; editor.cleared = true; sync(editor); } function readField(field) { var editor = getEditor(field); // nullable 单封面 PATCH:空值仅在用户主动移除时提交 null。 if (editor && !editor.multiple && editor.cleared && !editor.files.length) return null; return field.value; } function setBusy(target, busy) { var editor = getEditor(target); if (!editor) return; editor.busy = busy; editor.preview.querySelectorAll('button').forEach(function (button) { button.disabled = busy || editor.input.disabled; }); } if (root.document) { root.document.addEventListener('DOMContentLoaded', init); root.document.addEventListener('submit', function (event) { var pending = Array.from(editors.values()).some(function (editor) { return editor.input.form === event.target && editor.busy; }); var invalid = Array.from(editors.values()).some(function (editor) { return editor.input.form === event.target && editor.invalid; }); if (!pending && !invalid) return; event.preventDefault(); event.stopImmediatePropagation(); var status = event.target.querySelector('.upload-status'); if (status) status.textContent = invalid ? '原附件读取不完整,请刷新后重试,暂不能保存' : '文件正在上传,请等待上传完成后再保存'; }, true); root.addEventListener('pagehide', function (event) { if (!event.persisted) editors.forEach(function (editor) { release(editor.files); }); }); } return { init: init, normalizeFiles: normalizeFiles, setFiles: setFiles, addUploaded: addUploaded, clear: clear, readField: readField, setBusy: setBusy }; });