Files
jiapu/public/js/media-display.js
T
2026-08-29 19:06:07 +08:00

245 lines
9.1 KiB
JavaScript

(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(root);
return;
}
root.MediaDisplay = factory(root);
if (root.document) {
root.document.addEventListener('DOMContentLoaded', function () {
root.MediaDisplay.init(root.document);
});
}
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
'use strict';
function stringValue(value) {
return value === undefined || value === null ? '' : String(value);
}
function escapeHtml(value) {
return stringValue(value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function normalizeId(value) {
var text;
if (value === undefined || value === null || value === '') return '';
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
text = String(value).trim();
return /^[1-9][0-9]*$/.test(text) ? text : '';
}
function normalizeAccessUrl(value) {
var url;
try {
url = new URL(stringValue(value).trim());
} catch (error) {
return '';
}
return url.protocol === 'https:' || url.protocol === 'http:' ? url.href : '';
}
function normalizeFileAccess(value) {
var source = value || {};
var accessUrl = normalizeAccessUrl(source.accessUrl);
var fileId = normalizeId(source.fileId);
var fileSize;
var normalized;
if (!accessUrl) return null;
if (source.fileId !== undefined && source.fileId !== null && source.fileId !== '' && !fileId) return null;
normalized = {
fileId: fileId,
fileName: stringValue(source.fileName).trim(),
mediaType: stringValue(source.mediaType).trim().toLowerCase(),
accessUrl: accessUrl,
expiresAt: stringValue(source.expiresAt).trim()
};
fileSize = Number(source.fileSize);
if (source.fileSize !== undefined && source.fileSize !== null && source.fileSize !== '' &&
Number.isSafeInteger(fileSize) && fileSize >= 0) {
normalized.fileSize = fileSize;
}
if (!normalized.fileId) delete normalized.fileId;
if (!normalized.fileName) delete normalized.fileName;
if (!normalized.mediaType) delete normalized.mediaType;
if (!normalized.expiresAt) delete normalized.expiresAt;
return normalized;
}
function normalizeFileList(values) {
if (!Array.isArray(values)) return [];
return values.map(normalizeFileAccess).filter(Boolean);
}
function classNames(base, value) {
var additions = stringValue(value).split(/\s+/).filter(function (name) {
return /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name);
});
return [base].concat(additions).join(' ');
}
function renderImage(value, options) {
var file = normalizeFileAccess(value);
var settings = options || {};
var alt;
var loading;
var caption;
if (!file) return '';
alt = stringValue(settings.alt || file.fileName || '图片').trim();
loading = settings.eager === true ? 'eager' : 'lazy';
caption = stringValue(settings.caption).trim();
return '<figure class="' + escapeHtml(classNames('media-frame', settings.className)) +
'" data-media-kind="image"><button class="media-preview" type="button" data-media-preview-url="' +
escapeHtml(file.accessUrl) + '" data-media-preview-alt="' + escapeHtml(alt) + '">' +
'<img src="' + escapeHtml(file.accessUrl) + '" alt="' + escapeHtml(alt) +
'" loading="' + loading + '" decoding="async" /></button>' +
'<span class="media-fallback" hidden>图片暂时无法显示</span>' +
(caption ? '<figcaption>' + escapeHtml(caption) + '</figcaption>' : '') + '</figure>';
}
function renderAvatar(value, options) {
var file = normalizeFileAccess(value);
var settings = options || {};
var name = stringValue(settings.name || '用户').trim() || '用户';
var initial = Array.from(name)[0] || '家';
var classes = classNames('media-avatar', settings.className);
if (!file) {
return '<span class="' + escapeHtml(classes) + '" aria-label="' +
escapeHtml(name + '的头像') + '">' + escapeHtml(initial) + '</span>';
}
return '<span class="' + escapeHtml(classes) + '"><img src="' + escapeHtml(file.accessUrl) +
'" alt="' + escapeHtml(name + '的头像') + '" loading="lazy" decoding="async" />' +
'<span class="media-avatar-fallback" aria-hidden="true">' + escapeHtml(initial) + '</span></span>';
}
function renderVideo(value, options) {
var file = normalizeFileAccess(value);
var settings = options || {};
var poster = normalizeFileAccess(settings.posterFile);
var title;
if (!file) return '';
title = stringValue(settings.title || file.fileName || '家族视频').trim();
return '<figure class="' + escapeHtml(classNames('media-frame media-video', settings.className)) +
'" data-media-kind="video"><video controls preload="metadata" src="' + escapeHtml(file.accessUrl) + '"' +
(poster ? ' poster="' + escapeHtml(poster.accessUrl) + '"' : '') +
' aria-label="' + escapeHtml(title) + '"></video>' +
'<span class="media-fallback" hidden>视频暂时无法播放</span>' +
(settings.caption ? '<figcaption>' + escapeHtml(settings.caption) + '</figcaption>' : '') + '</figure>';
}
function mediaKind(file) {
var type = stringValue(file && file.mediaType).toLowerCase();
if (type.indexOf('image/') === 0) return 'image';
if (type.indexOf('video/') === 0) return 'video';
return 'file';
}
function renderDownload(file) {
var label = file.fileName || '附件';
return '<a class="media-download" href="' + escapeHtml(file.accessUrl) +
'" target="_blank" rel="noopener noreferrer" download>下载 ' + escapeHtml(label) + '</a>';
}
function renderGallery(values, options) {
var files = normalizeFileList(values);
var settings = options || {};
var label = stringValue(settings.label || '媒体附件').trim();
var content;
if (!files.length) return '';
content = files.map(function (file) {
var kind = mediaKind(file);
if (kind === 'image') return renderImage(file, { alt: file.fileName || label });
if (kind === 'video') return renderVideo(file, { title: file.fileName || label });
return renderDownload(file);
}).join('');
return '<div class="' + escapeHtml(classNames('media-gallery', settings.className)) +
'" role="group" aria-label="' + escapeHtml(label) + '">' + content + '</div>';
}
function ensurePreviewDialog(documentRef) {
var dialog = documentRef.querySelector('[data-media-preview-dialog]');
var close;
if (dialog) return dialog;
dialog = documentRef.createElement('dialog');
dialog.className = 'media-dialog';
dialog.setAttribute('data-media-preview-dialog', '');
dialog.setAttribute('aria-label', '图片预览');
dialog.innerHTML = '<button class="media-dialog-close" type="button" aria-label="关闭图片预览">关闭</button>' +
'<img alt="" data-media-dialog-image />';
documentRef.body.appendChild(dialog);
close = dialog.querySelector('.media-dialog-close');
close.addEventListener('click', function () {
if (typeof dialog.close === 'function') dialog.close();
else dialog.removeAttribute('open');
});
dialog.addEventListener('click', function (event) {
if (event.target !== dialog) return;
if (typeof dialog.close === 'function') dialog.close();
else dialog.removeAttribute('open');
});
return dialog;
}
function init(documentRef) {
if (!documentRef || documentRef.documentElement.getAttribute('data-media-display-ready') === 'true') return;
documentRef.documentElement.setAttribute('data-media-display-ready', 'true');
documentRef.addEventListener('error', function (event) {
var target = event.target;
var frame;
var fallback;
if (!target || !/^(IMG|VIDEO)$/.test(target.tagName)) return;
frame = target.closest && target.closest('.media-frame, .media-avatar');
if (!frame) return;
frame.classList.add('is-media-error');
target.hidden = true;
fallback = frame.querySelector('.media-fallback, .media-avatar-fallback');
if (fallback) fallback.hidden = false;
}, true);
documentRef.addEventListener('click', function (event) {
var trigger = event.target.closest && event.target.closest('[data-media-preview-url]');
var dialog;
var image;
var url;
if (!trigger) return;
url = normalizeAccessUrl(trigger.getAttribute('data-media-preview-url'));
if (!url) return;
dialog = ensurePreviewDialog(documentRef);
image = dialog.querySelector('[data-media-dialog-image]');
image.src = url;
image.alt = trigger.getAttribute('data-media-preview-alt') || '图片预览';
if (typeof dialog.showModal === 'function') dialog.showModal();
else dialog.setAttribute('open', '');
});
}
return {
normalizeFileAccess: normalizeFileAccess,
normalizeFileList: normalizeFileList,
renderImage: renderImage,
renderAvatar: renderAvatar,
renderVideo: renderVideo,
renderGallery: renderGallery,
init: init
};
});