245 lines
9.1 KiB
JavaScript
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, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
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
|
|
};
|
|
});
|