Files
jiapu/public/js/attachment-editor.js
T
2026-09-13 19:41:18 +08:00

183 lines
7.3 KiB
JavaScript

(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 };
});