- 新增 AKSRT\Attachment\UploadLimits:按用户类型解析图片/视频/文件 上传上限(取最大值+默认兜底,钳制到 PHP 上限)、允许/禁止格式过滤、 上传自动重命名、非管理员附件页 404;不含论坛角色 - 后台「附件配置」重做:大小限制/格式限制/上传行为三组配置, PHP Schema 与 SPA Schema 逐字段镜像(v1.2.0) - 定制页 AttachmentPermissions.vue 改为标准 schemaPage,清理失效的 API 包装、组件加载器与旧配置键(submit_image_max_size 等) - 投稿/评论上传、批量上传数量全部切换到新链路;AksField 数字组件 支持单位展示 - 更新 captcha harness 与 front-submit 契约测试;421 项测试通过
733 lines
40 KiB
JavaScript
733 lines
40 KiB
JavaScript
/**
|
||
* Front-submit TinyMCE tools matching the reference article editor.
|
||
*
|
||
* @package AKSRT
|
||
*/
|
||
(function (window, document) {
|
||
'use strict';
|
||
|
||
var cfg = window.aksrtSubmitData || {};
|
||
var editorCfg = cfg.editor || {};
|
||
var i18n = cfg.i18n || {};
|
||
var activeModal = null;
|
||
|
||
function notify(message, type, duration, id) {
|
||
if (typeof window.notyf === 'function') {
|
||
window.notyf(
|
||
message,
|
||
type,
|
||
duration === undefined ? 5 : duration,
|
||
id || 'aksrt-front-submit-editor-media-toast'
|
||
);
|
||
}
|
||
}
|
||
|
||
function escapeAttr(value) {
|
||
return String(value)
|
||
.replace(/&/g, '&')
|
||
.replace(/"/g, '"')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
}
|
||
|
||
function validHttpUrl(value) {
|
||
var raw = String(value).trim();
|
||
if (!/^https?:\/\//i.test(raw)) {
|
||
return '';
|
||
}
|
||
try {
|
||
var url = new URL(raw);
|
||
return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : '';
|
||
} catch (error) {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
function imageMarkup(url, attachmentId, alt, fullUrl) {
|
||
var safeUrl = validHttpUrl(url);
|
||
var safeFullUrl = validHttpUrl(fullUrl || url);
|
||
if (!safeUrl) {
|
||
return '';
|
||
}
|
||
var idAttr = attachmentId ? ' data-edit-file-id="' + Number(attachmentId) + '"' : '';
|
||
var altAttr = alt ? ' alt="' + escapeAttr(alt) + '"' : ' alt=""';
|
||
var fullAttr = safeFullUrl ? ' data-full-url="' + escapeAttr(safeFullUrl) + '"' : '';
|
||
return '<p><img src="' + escapeAttr(safeUrl) + '"' + idAttr + altAttr + fullAttr + '></p>';
|
||
}
|
||
|
||
function videoMarkup(url, attachmentId, name) {
|
||
var safeUrl = validHttpUrl(url);
|
||
if (!safeUrl) {
|
||
return '';
|
||
}
|
||
var idAttr = attachmentId ? ' data-edit-file-id="' + Number(attachmentId) + '"' : '';
|
||
return '<div contenteditable="false" data-video-url="' + escapeAttr(safeUrl) +
|
||
'" data-video-name="' + escapeAttr(name || safeUrl) + '"' + idAttr +
|
||
' class="new-dplayer post-dplayer dplayer"></div>';
|
||
}
|
||
|
||
function fileMarkup(url, attachmentId, name) {
|
||
var safeUrl = validHttpUrl(url);
|
||
if (!safeUrl) {
|
||
return '';
|
||
}
|
||
var filename = String(name || safeUrl.split('/').pop() || i18n.attachment || '附件');
|
||
var idAttr = attachmentId ? ' data-aksrt-file-id="' + Number(attachmentId) + '"' : '';
|
||
return '<div class="mb20 muted-box file-download-box" contenteditable="false">' +
|
||
'<div class="flex ac jsb"><div class="flex ac"><div class="mr10">' +
|
||
'<div class="file-download-icon"><i class="fa fas fa-file-alt" aria-hidden="true"></i></div></div>' +
|
||
'<div class="text-ellipsis-2 file-download-name" contenteditable="true">' + escapeAttr(filename) + '</div>' +
|
||
'</div><div class="flex0 ml20"><a href="' + escapeAttr(safeUrl) + '" data-mce-href="' +
|
||
escapeAttr(safeUrl) + '"' + idAttr + ' class="but c-blue file-download-btn">' +
|
||
'<i class="fa fas fa-download" aria-hidden="true"></i>' + escapeAttr(i18n.download || '下载') +
|
||
'</a></div></div></div>';
|
||
}
|
||
|
||
function embedVideoMarkup(value) {
|
||
var holder = document.createElement('div');
|
||
holder.innerHTML = String(value).trim();
|
||
var iframe = holder.querySelector('iframe');
|
||
var safeUrl = validHttpUrl(iframe ? iframe.getAttribute('src') : value);
|
||
if (!safeUrl) {
|
||
return '';
|
||
}
|
||
return '<div contenteditable="false" class="wp-block-embed is-type-video mb20">' +
|
||
'<div class="iframe-absbox" style="padding-bottom:56%;"><iframe src="' + escapeAttr(safeUrl) +
|
||
'" allowfullscreen="allowfullscreen" framespacing="0" border="0" width="100%" frameborder="no"></iframe>' +
|
||
'</div></div>';
|
||
}
|
||
|
||
function mediaUrl(type) {
|
||
var configured = cfg.media && cfg.media[type] && cfg.media[type].url;
|
||
if (configured) {
|
||
return String(configured).indexOf('{type}') !== -1
|
||
? String(configured).replace('{type}', encodeURIComponent(type))
|
||
: String(configured);
|
||
}
|
||
var base = cfg.mediaUrl || '';
|
||
if (String(base).indexOf('{type}') !== -1) return String(base).replace('{type}', encodeURIComponent(type));
|
||
if (base) return String(base).replace(/\/$/, '') + '/' + encodeURIComponent(type);
|
||
return cfg.restUrl ? cfg.restUrl.replace(/\/$/, '') + '/front-submit/media/' + encodeURIComponent(type) : ''; }
|
||
|
||
function currentPostId() {
|
||
var field = document.querySelector('#aksrt-submit-form input[name="posts_id"]');
|
||
return Number((field && field.value) || cfg.postId || 0) || 0;
|
||
}
|
||
|
||
function acceptsFile(file, type) {
|
||
if (!file) return false;
|
||
if (type === 'image') return /^image\//.test(file.type || '');
|
||
if (type === 'video') return /^video\//.test(file.type || '');
|
||
return true;
|
||
}
|
||
|
||
function uploadMedia(file, type) {
|
||
var uploadKey = type === 'file' ? 'resource' : type;
|
||
var upload = (cfg.uploads || {})[uploadKey] || null;
|
||
if (!upload || !upload.url) return Promise.reject(new Error(i18n.uploadUnavailable || '当前账号不能上传此类文件'));
|
||
if (!acceptsFile(file, type)) return Promise.reject(new Error(i18n.invalidFile || '请选择有效文件'));
|
||
if (upload.maxSize && file.size > upload.maxSize) return Promise.reject(new Error(i18n.fileTooLarge || '文件大小超过限制'));
|
||
var body = new FormData();
|
||
body.append('file', file);
|
||
var uploadUrl = String(upload.url);
|
||
if (currentPostId()) {
|
||
uploadUrl += (uploadUrl.indexOf('?') === -1 ? '?' : '&') + 'post_id=' + encodeURIComponent(String(currentPostId()));
|
||
}
|
||
return fetch(uploadUrl, { method: 'POST', credentials: 'same-origin', headers: { 'X-WP-Nonce': cfg.restNonce || '' }, body: body })
|
||
.then(function (response) {
|
||
return response.json().catch(function () { return {}; }).then(function (payload) {
|
||
if (!response.ok || !payload || !payload.success || !payload.url) {
|
||
throw new Error((payload && (payload.message || (payload.data && payload.data.message))) || i18n.error || '文件上传失败');
|
||
}
|
||
return payload;
|
||
});
|
||
});
|
||
}
|
||
|
||
function imageBatchLimit() {
|
||
// 0 = 不限制(沿用默认 100 的选取上限),1 = 不允许批量上传。
|
||
var configured = Number(cfg.imageUploadMultiple);
|
||
if (!isFinite(configured) || configured <= 0) {
|
||
return { multiple: true, maxItems: 100 };
|
||
}
|
||
if (configured === 1) {
|
||
return { multiple: false, maxItems: 1 };
|
||
}
|
||
return { multiple: true, maxItems: Math.min(configured, 100) };
|
||
}
|
||
|
||
function mediaDefinition(type) {
|
||
if (type === 'video') return {
|
||
title: i18n.insertVideo || '插入视频', mineTab: i18n.mineVideo || '我的视频', uploadTab: i18n.uploadVideo || '上传视频',
|
||
externalTab: i18n.externalVideo || '外链视频', embedTab: i18n.embedVideo || '嵌入视频', accept: 'video/*', multiple: false, maxItems: 1,
|
||
canUpload: !!editorCfg.canUploadVideo && !!((cfg.uploads || {}).video), canEmbed: !!editorCfg.canEmbedVideo,
|
||
select: i18n.selectVideo || '选择或拖入视频', hint: i18n.videoUploadHint || '上传完成后自动插入正文', externalHint: i18n.externalVideoHint || '请填写视频文件地址:',
|
||
};
|
||
if (type === 'file') return {
|
||
title: i18n.insertFile || '上传附件', mineTab: i18n.mineFile || '我的文件', uploadTab: i18n.uploadFile || '上传附件', externalTab: i18n.externalFile || '输入地址',
|
||
accept: '*/*', multiple: false, maxItems: 1, canUpload: !!editorCfg.canUploadFile && !!((cfg.uploads || {}).resource), canEmbed: false,
|
||
select: i18n.selectFile || '选择或拖入附件', hint: i18n.fileUploadHint || '上传完成后自动插入正文', externalHint: i18n.externalFileHint || '请填写附件地址:',
|
||
};
|
||
var batch = imageBatchLimit();
|
||
return {
|
||
title: i18n.insertImage || '插入图片', mineTab: i18n.mineImage || '我的图片', uploadTab: i18n.uploadImage || '上传图片', externalTab: i18n.externalImage || '外链图片',
|
||
accept: 'image/gif,image/jpeg,image/png,image/webp', multiple: batch.multiple, maxItems: batch.maxItems, canUpload: !!editorCfg.canUploadImage && !!((cfg.uploads || {}).image), canEmbed: false,
|
||
select: i18n.selectImages || '选择或拖入图片', hint: i18n.imageUploadHint || '可批量选择,上传完成后自动插入正文', externalHint: i18n.externalImageHint || '每行填写一个图片地址:',
|
||
};
|
||
}
|
||
|
||
function mediaMarkup(type, item) {
|
||
var id = item.attachment_id || item.id;
|
||
if (type === 'video') return videoMarkup(item.url, id, item.filename || item.name || '');
|
||
if (type === 'file') return fileMarkup(item.url, id, item.filename || item.name || '');
|
||
return imageMarkup(item.large_url || item.url, id, item.name || item.title || '', item.url);
|
||
}
|
||
|
||
function formatBytes(value) {
|
||
var size = Number(value) || 0, units = ['B', 'KB', 'MB', 'GB'], index = 0;
|
||
if (!size) return '';
|
||
while (size >= 1024 && index < units.length - 1) { size /= 1024; index += 1; }
|
||
return (index ? size.toFixed(size >= 10 ? 0 : 1) : String(Math.round(size))) + ' ' + units[index];
|
||
}
|
||
|
||
function extension(item) {
|
||
var name = String(item.filename || item.name || item.url || ''), match = name.match(/\.([a-z0-9]{1,8})(?:[?#].*)?$/i);
|
||
return match ? match[1].toUpperCase() : String(item.subtype || item.type || '').toUpperCase();
|
||
}
|
||
|
||
function closeMediaModal(force) {
|
||
if (!activeModal) return;
|
||
if (activeModal.uploading && !force) { notify(i18n.uploadingClose || '上传中,请稍候再关闭', 'warning'); return; }
|
||
document.removeEventListener('keydown', activeModal.onKeydown);
|
||
if (activeModal.searchTimer) window.clearTimeout(activeModal.searchTimer);
|
||
activeModal.root.remove();
|
||
if (activeModal.previousFocus && typeof activeModal.previousFocus.focus === 'function') activeModal.previousFocus.focus();
|
||
activeModal = null;
|
||
document.body.classList.remove('aksrt-editor-media-open');
|
||
}
|
||
|
||
function openMediaModal(options) {
|
||
options = options || {};
|
||
if (activeModal) {
|
||
closeMediaModal();
|
||
if (activeModal) return;
|
||
}
|
||
var type = ['image', 'video', 'file'].indexOf(options.type) === -1 ? 'image' : options.type;
|
||
var definition = mediaDefinition(type), multiple = options.multiple === undefined ? definition.multiple : !!options.multiple;
|
||
var maxItems = Math.max(1, Number(options.maxItems || (multiple ? definition.maxItems : 1)) || 1);
|
||
if (!multiple) maxItems = 1;
|
||
var allowExternal = options.allowExternal !== false;
|
||
var allowEmbed = definition.canEmbed && options.allowEmbed !== false;
|
||
var canUpload = definition.canUpload && options.canUpload !== false;
|
||
var root = document.createElement('div');
|
||
root.className = 'aksrt-editor-media';
|
||
root.setAttribute('data-editor-media-type', type);
|
||
root.setAttribute('role', 'dialog'); root.setAttribute('aria-modal', 'true'); root.setAttribute('aria-labelledby', 'aksrt-editor-media-title');
|
||
root.innerHTML =
|
||
'<button type="button" class="aksrt-editor-media__backdrop" data-editor-media-close tabindex="-1" aria-label="' + escapeAttr(i18n.close || '关闭') + '"></button>' +
|
||
'<div class="aksrt-editor-media__dialog" tabindex="-1"><button type="button" class="aksrt-editor-media__close" data-editor-media-close aria-label="' + escapeAttr(i18n.close || '关闭') + '">×</button>' +
|
||
'<h3 id="aksrt-editor-media-title" class="aksrt-editor-media__title">' + escapeAttr(options.title || definition.title) + '</h3>' +
|
||
'<div class="aksrt-editor-media__tabs" role="tablist"><button type="button" class="is-active" role="tab" aria-selected="true" data-editor-media-tab="mine">' + escapeAttr(definition.mineTab) + '</button>' +
|
||
(canUpload ? '<button type="button" role="tab" aria-selected="false" data-editor-media-tab="upload">' + escapeAttr(definition.uploadTab) + '</button>' : '') +
|
||
(allowExternal ? '<button type="button" role="tab" aria-selected="false" data-editor-media-tab="external">' + escapeAttr(definition.externalTab) + '</button>' : '') +
|
||
(allowEmbed && type === 'video' ? '<button type="button" role="tab" aria-selected="false" data-editor-media-tab="embed">' + escapeAttr(definition.embedTab) + '</button>' : '') + '</div>' +
|
||
'<div class="aksrt-editor-media__panels"><section class="aksrt-editor-media__panel is-active" data-editor-media-panel="mine">' +
|
||
'<div class="aksrt-editor-media__search"><label class="screen-reader-text" for="aksrt-editor-media-search">' + escapeAttr(i18n.search || '搜索') + '</label><input id="aksrt-editor-media-search" type="search" class="form-control" placeholder="' + escapeAttr(i18n.searchMedia || '搜索媒体(至少 2 个字符)') + '" data-editor-media-search><button type="button" class="but hollow" data-editor-media-clear>' + escapeAttr(i18n.clear || '清除') + '</button></div>' +
|
||
'<div class="aksrt-editor-media__status" data-editor-media-status aria-live="polite"></div><div class="aksrt-editor-media__list" data-editor-media-list></div><div class="aksrt-editor-media__mine-actions"><button type="button" class="but hollow aksrt-editor-media__load-more" data-editor-media-more hidden>' + escapeAttr(i18n.loadMore || '加载更多') + '</button><button type="button" class="but c-blue aksrt-editor-media__confirm" data-editor-media-confirm hidden>' + escapeAttr(i18n.confirmInsert || '确认插入') + '</button></div></section>' +
|
||
(canUpload ? '<section class="aksrt-editor-media__panel" data-editor-media-panel="upload"><label class="aksrt-editor-media__drop"><input type="file" accept="' + escapeAttr(definition.accept) + '"' + (multiple ? ' multiple' : '') + ' data-editor-media-files><i class="fa fas fa-cloud-upload-alt" aria-hidden="true"></i><strong>' + escapeAttr(definition.select) + '</strong><span>' + escapeAttr(definition.hint) + '</span></label><div class="aksrt-editor-media__selection" data-editor-media-selection></div><div class="aksrt-editor-media__progress" data-editor-media-progress hidden></div><button type="button" class="but c-blue aksrt-editor-media__submit" data-editor-media-upload disabled>' + escapeAttr(i18n.confirmUpload || '确认上传') + '</button></section>' : '') +
|
||
(allowExternal ? '<section class="aksrt-editor-media__panel" data-editor-media-panel="external"><p>' + escapeAttr(definition.externalHint) + '</p><textarea class="form-control" rows="5" placeholder="https://..." data-editor-media-urls></textarea><button type="button" class="but c-blue aksrt-editor-media__submit" data-editor-media-external>' + escapeAttr(i18n.confirmInsert || '确认插入') + '</button></section>' : '') +
|
||
(allowEmbed && type === 'video' ? '<section class="aksrt-editor-media__panel" data-editor-media-panel="embed"><p>' + escapeAttr(i18n.embedVideoHint || '请输入嵌入地址或者直接粘贴 iframe 嵌入代码:') + '</p><textarea class="form-control" rows="5" placeholder="<iframe src="https://..."></iframe>" data-editor-media-embed></textarea><button type="button" class="but c-blue aksrt-editor-media__submit" data-editor-media-embed-submit>' + escapeAttr(i18n.confirmEmbed || '确认嵌入') + '</button></section>' : '') + '</div></div>';
|
||
document.body.appendChild(root); document.body.classList.add('aksrt-editor-media-open');
|
||
|
||
var state = { root: root, uploading: false, loading: false, requestId: 0, searchTimer: null, previousFocus: document.activeElement, page: 0, totalPages: 0, search: '', items: [], pinned: [], selected: [], exclude: [] };
|
||
var list = root.querySelector('[data-editor-media-list]'), status = root.querySelector('[data-editor-media-status]'), more = root.querySelector('[data-editor-media-more]');
|
||
var confirm = root.querySelector('[data-editor-media-confirm]');
|
||
var search = root.querySelector('[data-editor-media-search]'), fileInput = root.querySelector('[data-editor-media-files]'), uploadButton = root.querySelector('[data-editor-media-upload]');
|
||
var selection = root.querySelector('[data-editor-media-selection]'), progress = root.querySelector('[data-editor-media-progress]'), drop = root.querySelector('.aksrt-editor-media__drop'), files = [];
|
||
function statusText(text, cls) { if (!status) return; status.className = 'aksrt-editor-media__status' + (cls ? ' is-' + cls : ''); status.textContent = text || ''; }
|
||
function itemId(item) { return Number(item.attachment_id || item.id || 0); }
|
||
function selectedIndex(id) { return state.selected.findIndex(function (item) { return itemId(item) === id; }); }
|
||
function renderSelection() { if (selection) selection.textContent = files.length ? (multiple ? (i18n.selectedFiles || '已选择 %d 个文件').replace('%d', String(files.length)) : files[0].name) : ''; if (uploadButton) uploadButton.disabled = !files.length || state.uploading; }
|
||
function renderItem(item) {
|
||
var id = itemId(item), index = selectedIndex(id), chosen = index !== -1, disabled = !chosen && state.selected.length >= maxItems;
|
||
var label = escapeAttr(item.filename || item.name || item.title || item.url || '');
|
||
var itemType = String(item.type || type);
|
||
var hasImagePreview = itemType === 'image' && (item.thumbnail_url || item.medium_url || item.url);
|
||
var thumb = hasImagePreview
|
||
? '<img src="' + escapeAttr(item.thumbnail_url || item.medium_url || item.url || '') + '" alt="' + label + '">'
|
||
: '<span class="aksrt-editor-media__file-icon"><i class="fa fas fa-' + (itemType === 'video' ? 'video' : 'file-alt') + '" aria-hidden="true"></i></span>';
|
||
var meta = type === 'image' ? label : label + (extension(item) ? ' · ' + escapeAttr(extension(item)) : '') + (formatBytes(item.filesizeInBytes) ? ' · ' + escapeAttr(formatBytes(item.filesizeInBytes)) : '');
|
||
return '<button type="button" class="aksrt-editor-media__item' + (chosen ? ' is-selected' : '') + '" data-editor-media-item="' + id + '"' + (disabled ? ' disabled' : '') + ' aria-pressed="' + (chosen ? 'true' : 'false') + '"><span class="aksrt-editor-media__check">' + (chosen ? String(index + 1) : '<i class="fa fas fa-check" aria-hidden="true"></i>') + '</span><span class="aksrt-editor-media__thumb">' + thumb + '</span><span class="aksrt-editor-media__meta">' + meta + '</span></button>';
|
||
}
|
||
function renderList() {
|
||
if (!list) return;
|
||
list.innerHTML = state.items.length ? state.items.map(renderItem).join('') : '';
|
||
if (more) more.hidden = state.loading || state.page >= state.totalPages || !state.items.length;
|
||
if (confirm) {
|
||
confirm.hidden = !multiple || !state.selected.length;
|
||
confirm.disabled = state.uploading;
|
||
confirm.textContent = (i18n.confirmSelected || '确认选择(%d)').replace('%d', String(state.selected.length));
|
||
}
|
||
}
|
||
function finish(value) { if (state.uploading) return; if (typeof options.onSelect === 'function') options.onSelect(multiple ? value.slice() : value[0]); closeMediaModal(true); }
|
||
function selectItem(item) { var id = itemId(item), index = selectedIndex(id); if (index !== -1) state.selected.splice(index, 1); else { if (!multiple) state.selected = []; if (state.selected.length >= maxItems) return; state.selected.push(item); } renderList(); if (!multiple && state.selected.length) finish(state.selected); }
|
||
function addItems(items) { (items || []).forEach(function (item) { var id = itemId(item); if (!id) return; if (state.exclude.indexOf(id) === -1) state.exclude.push(id); if (!state.items.some(function (old) { return itemId(old) === id; })) state.items.unshift(item); if (!state.pinned.some(function (old) { return itemId(old) === id; })) state.pinned.unshift(item); }); renderList(); }
|
||
function activateTab(name) { var tab = root.querySelector('[data-editor-media-tab="' + name + '"]'); if (tab) tab.click(); }
|
||
function request(page, replace) {
|
||
var url = mediaUrl(type); if (!url) { statusText(i18n.mediaUnavailable || '媒体库暂不可用', 'error'); return; }
|
||
if (replace) { state.items = state.search.length >= 2 ? [] : state.pinned.slice(); state.page = 0; }
|
||
state.loading = true; renderList(); statusText(i18n.loading || '加载中…', 'loading');
|
||
var requestId = ++state.requestId;
|
||
var params = new URLSearchParams(); params.set('paged', String(page)); params.set('per_page', '48'); if (currentPostId()) params.set('post_id', String(currentPostId())); if (state.search.length >= 2) params.set('search', state.search); if (state.exclude.length) params.set('exclude', state.exclude.join(','));
|
||
fetch(url + (url.indexOf('?') === -1 ? '?' : '&') + params.toString(), { credentials: 'same-origin', headers: { 'X-WP-Nonce': cfg.restNonce || '' } }).then(function (response) { return response.json().catch(function () { return {}; }).then(function (payload) { if (!response.ok) throw new Error((payload && (payload.message || (payload.data && payload.data.message))) || i18n.error || '加载失败'); return payload || {}; }); }).then(function (payload) {
|
||
if (requestId !== state.requestId) return;
|
||
var incoming = Array.isArray(payload.items) ? payload.items : []; if (replace) state.items = state.search.length >= 2 ? [] : state.pinned.slice(); incoming.forEach(function (item) { if (state.exclude.indexOf(itemId(item)) === -1) state.items.push(item); }); state.page = Number(payload.page || page); state.totalPages = Number(payload.total_pages || 0); state.loading = false; renderList(); statusText(state.items.length ? '' : (i18n.emptyMedia || '暂无媒体文件'), state.items.length ? '' : 'empty');
|
||
}).catch(function (error) { if (requestId !== state.requestId) return; state.loading = false; renderList(); statusText(error.message || i18n.mediaError || '媒体加载失败', 'error'); });
|
||
}
|
||
function setFiles(nextFiles) {
|
||
var candidates = Array.prototype.slice.call(nextFiles || []);
|
||
var uploadKey = type === 'file' ? 'resource' : type;
|
||
var upload = (cfg.uploads || {})[uploadKey] || {};
|
||
var rejected = false;
|
||
files = candidates.filter(function (file) {
|
||
var valid = acceptsFile(file, type) && (!upload.maxSize || file.size <= upload.maxSize);
|
||
rejected = rejected || !valid;
|
||
return valid;
|
||
});
|
||
if (rejected) notify(i18n.invalidFile || i18n.fileTooLarge || '部分文件类型或大小不符合要求', 'warning');
|
||
if (!multiple) files = files.slice(0, 1);
|
||
if (files.length > maxItems) {
|
||
files = files.slice(0, maxItems);
|
||
notify((i18n.maxMediaItems || '最多选择 %d 个文件').replace('%d', String(maxItems)), 'warning');
|
||
}
|
||
renderSelection();
|
||
}
|
||
function uploadSelected() {
|
||
if (!files.length || state.uploading) return;
|
||
state.uploading = true; renderSelection(); renderList();
|
||
if (uploadButton) uploadButton.textContent = i18n.uploading || '正在上传…';
|
||
if (progress) { progress.hidden = false; progress.textContent = i18n.uploading || '正在上传…'; progress.style.setProperty('--aksrt-upload-progress', '0%'); }
|
||
var complete = 0, total = files.length, nextIndex = 0, results = [];
|
||
var toastId = 'aksrt-front-submit-editor-media-toast';
|
||
notify(i18n.uploading || '正在上传…', 'load', '', toastId);
|
||
function worker() {
|
||
var index = nextIndex++;
|
||
if (index >= total) return Promise.resolve();
|
||
return uploadMedia(files[index], type).then(function (item) {
|
||
results[index] = { item: item };
|
||
}).catch(function (error) {
|
||
results[index] = { error: error };
|
||
}).finally(function () {
|
||
complete += 1;
|
||
if (progress) progress.style.setProperty('--aksrt-upload-progress', Math.round((complete / total) * 100) + '%');
|
||
}).then(worker);
|
||
}
|
||
var workers = Math.min(4, total);
|
||
Promise.all(Array.apply(null, Array(workers)).map(function () { return worker(); })).then(function () {
|
||
var items = results.map(function (result) { return result && result.item; }).filter(Boolean);
|
||
var failure = results.find(function (result) { return result && result.error; });
|
||
addItems(items);
|
||
state.selected = items.slice(0, maxItems);
|
||
state.uploading = false;
|
||
renderList();
|
||
files = [];
|
||
renderSelection();
|
||
if (items.length) notify(i18n.uploaded || '上传成功', 'success', 5, toastId);
|
||
if (failure) notify(failure.error.message || i18n.error || '部分文件上传失败', 'danger', 5, toastId);
|
||
if (!items.length) return;
|
||
if (!multiple) finish(state.selected);
|
||
else activateTab('mine');
|
||
}).finally(function () {
|
||
state.uploading = false;
|
||
renderList();
|
||
if (uploadButton) { uploadButton.disabled = !files.length; uploadButton.textContent = i18n.confirmUpload || '确认上传'; }
|
||
if (progress) progress.hidden = true;
|
||
});
|
||
}
|
||
function externalSelect() { var textarea = root.querySelector('[data-editor-media-urls]'), urls = String(textarea ? textarea.value : '').split(/\r?\n/).map(function (url) { var valid = validHttpUrl(url); return valid ? { url: valid } : null; }).filter(Boolean).slice(0, maxItems); if (!urls.length) { notify(i18n.invalidMediaUrl || '请填写有效地址', 'warning'); return; } finish(urls); }
|
||
function embedSelect() { var input = root.querySelector('[data-editor-media-embed]'), html = embedVideoMarkup(input ? input.value : ''); if (!html) { notify(i18n.invalidVideoUrl || '请填写有效的视频嵌入地址', 'warning'); return; } if (typeof options.onEmbed === 'function') options.onEmbed(html); else if (typeof options.onSelect === 'function') options.onSelect({ embed: html }); closeMediaModal(true); }
|
||
|
||
root.addEventListener('click', function (event) {
|
||
var close = event.target.closest('[data-editor-media-close]'); if (close) { event.preventDefault(); closeMediaModal(); return; }
|
||
var tab = event.target.closest('[data-editor-media-tab]'); if (tab) { var name = tab.getAttribute('data-editor-media-tab'); root.querySelectorAll('[data-editor-media-tab]').forEach(function (button) { var active = button === tab; button.classList.toggle('is-active', active); button.setAttribute('aria-selected', active ? 'true' : 'false'); }); root.querySelectorAll('[data-editor-media-panel]').forEach(function (panel) { panel.classList.toggle('is-active', panel.getAttribute('data-editor-media-panel') === name); }); if (name === 'mine' && state.page === 0) request(1, true); return; }
|
||
var itemButton = event.target.closest('[data-editor-media-item]'); if (itemButton && !itemButton.disabled) { var item = state.items.find(function (entry) { return itemId(entry) === Number(itemButton.getAttribute('data-editor-media-item')); }); if (item) selectItem(item); return; }
|
||
if (event.target.closest('[data-editor-media-clear]')) { if (search) search.value = ''; window.clearTimeout(state.searchTimer); state.search = ''; request(1, true); return; }
|
||
if (event.target.closest('[data-editor-media-confirm]')) { if (state.selected.length) finish(state.selected); return; }
|
||
if (event.target.closest('[data-editor-media-more]')) { if (!state.loading) request(state.page + 1, false); return; }
|
||
if (event.target.closest('[data-editor-media-external]')) { externalSelect(); return; }
|
||
if (event.target.closest('[data-editor-media-embed-submit]')) { embedSelect(); return; }
|
||
if (event.target.closest('[data-editor-media-upload]')) uploadSelected();
|
||
});
|
||
if (search) search.addEventListener('input', function () {
|
||
state.search = String(search.value || '').trim();
|
||
window.clearTimeout(state.searchTimer);
|
||
state.searchTimer = window.setTimeout(function () {
|
||
if (state.search.length === 1) {
|
||
state.items = [];
|
||
state.page = 0;
|
||
state.totalPages = 0;
|
||
state.loading = false;
|
||
renderList();
|
||
statusText(i18n.searchMin || '请输入至少 2 个字符进行搜索', 'empty');
|
||
return;
|
||
}
|
||
request(1, true);
|
||
}, 600);
|
||
});
|
||
if (fileInput) fileInput.addEventListener('change', function () { setFiles(fileInput.files); });
|
||
if (drop) { ['dragenter', 'dragover'].forEach(function (name) { drop.addEventListener(name, function (event) { event.preventDefault(); drop.classList.add('is-dragging'); }); }); ['dragleave', 'drop'].forEach(function (name) { drop.addEventListener(name, function (event) { event.preventDefault(); drop.classList.remove('is-dragging'); }); }); drop.addEventListener('drop', function (event) { setFiles(event.dataTransfer && event.dataTransfer.files); }); }
|
||
var onKeydown = function (event) { if (event.key === 'Escape') closeMediaModal(); }; state.onKeydown = onKeydown; activeModal = state; document.addEventListener('keydown', onKeydown);
|
||
var focusTarget = root.querySelector('.aksrt-editor-media__dialog'); if (focusTarget) focusTarget.focus({ preventScroll: true }); request(1, true);
|
||
}
|
||
|
||
window.AksrtSubmitMedia = { open: openMediaModal, close: function () { closeMediaModal(); }, upload: uploadMedia };
|
||
|
||
function selectedContent(editor) {
|
||
var content = String(editor.selection.getContent() || '').trim();
|
||
return content ? '<p>' + content + '</p>' : '<p>' + escapeAttr(i18n.hiddenPlaceholder || '隐藏内容') + '</p>';
|
||
}
|
||
|
||
function insertHiddenContent(editor, item) {
|
||
var node = editor.selection.getNode();
|
||
var content = selectedContent(editor);
|
||
var suffix = '<p></p>';
|
||
if (node && editor.dom.hasClass(node, 'tinymce-hide')) {
|
||
var editable = node.querySelector('[contenteditable="true"]');
|
||
content = editable ? editable.innerHTML : content;
|
||
editor.dom.remove(node);
|
||
suffix = '';
|
||
}
|
||
|
||
editor.insertContent(
|
||
'<div class="tinymce-hide" data-aksrt-hide-type="' + escapeAttr(item.type) + '" contenteditable="false">' +
|
||
'<p class="hide-before">' + escapeAttr(item.open) + '</p>' +
|
||
'<div contenteditable="true">' + content + '</div>' +
|
||
'<p class="hide-after">' + escapeAttr(item.close) + '</p>' +
|
||
'</div>' + suffix
|
||
);
|
||
editor.nodeChanged();
|
||
}
|
||
|
||
function insertQuote(editor, colorClass) {
|
||
var node = editor.selection.getNode();
|
||
var quote = editor.dom.getParent(node, '.quote_q');
|
||
if (quote) {
|
||
quote.className = 'quote_q quote-mce' + (colorClass ? ' ' + colorClass : '');
|
||
editor.nodeChanged();
|
||
return;
|
||
}
|
||
|
||
var content = String(editor.selection.getContent() || '').trim();
|
||
if (node && node.nodeName === 'P' && node.parentNode === editor.getBody()) {
|
||
content = node.outerHTML;
|
||
editor.dom.remove(node);
|
||
} else {
|
||
content = content ? '<p>' + content + '</p>' : '<p></p>';
|
||
}
|
||
editor.insertContent(
|
||
'<div class="quote_q quote-mce' + (colorClass ? ' ' + colorClass : '') +
|
||
'" contenteditable="false"><div contenteditable="true">' + content + '</div></div><p></p>'
|
||
);
|
||
editor.nodeChanged();
|
||
}
|
||
|
||
var codeLanguages = [
|
||
['yaml', 'yaml'],
|
||
['xml/html', 'xml'],
|
||
['visualbasic', 'visualbasic'],
|
||
['vhdl', 'vhdl'],
|
||
['typescript', 'typescript'],
|
||
['swift', 'swift'],
|
||
['sql', 'sql'],
|
||
['shell', 'shell'],
|
||
['scss/sass', 'scss'],
|
||
['rust', 'rust'],
|
||
['ruby', 'ruby'],
|
||
['python', 'python'],
|
||
['powershell', 'powershell'],
|
||
['php', 'php'],
|
||
['markdown', 'markdown'],
|
||
['lua', 'lua'],
|
||
['less', 'less'],
|
||
['kotlin', 'kotlin'],
|
||
['json', 'json'],
|
||
['javascript', 'javascript'],
|
||
['java', 'java'],
|
||
['ini/conf', 'ini'],
|
||
['go/golang', 'go'],
|
||
['docker', 'dockerfile'],
|
||
['diff', 'diff'],
|
||
['css', 'css'],
|
||
['csharp', 'csharp'],
|
||
['Cpp/C++/C', 'cpp'],
|
||
['assembly', 'assembly'],
|
||
[i18n.codeGeneric || '通用高亮', 'generic'],
|
||
];
|
||
|
||
var codeThemes = [
|
||
['enlighter', 'enlighter'],
|
||
['classic', 'classic'],
|
||
['beyond', 'beyond'],
|
||
['mowtwo', 'mowtwo'],
|
||
['eclipse', 'eclipse'],
|
||
['droide', 'droide'],
|
||
['minimal', 'minimal'],
|
||
['atomic', 'atomic'],
|
||
['dracula', 'dracula'],
|
||
['bootstrap4', 'bootstrap4'],
|
||
['rowhammer', 'rowhammer'],
|
||
['godzilla', 'godzilla'],
|
||
[i18n.codeFollowGlobal || '跟随全局设置', 'qj'],
|
||
];
|
||
|
||
function listboxValues(items) {
|
||
return items.map(function (item) {
|
||
return { text: item[0], value: item[1] };
|
||
});
|
||
}
|
||
|
||
function bindCharacterCount(editor) {
|
||
var counter = null;
|
||
|
||
function getCounter() {
|
||
var container = editor.getContainer();
|
||
var statusbar = container && container.querySelector('.mce-statusbar .mce-container-body');
|
||
if (!statusbar) {
|
||
return null;
|
||
}
|
||
|
||
counter = counter || statusbar.querySelector('[data-submit-word-count]');
|
||
if (!counter) {
|
||
counter = document.createElement('span');
|
||
counter.className = 'mce-wordcount aksrt-submit-word-count';
|
||
counter.setAttribute('data-submit-word-count', '');
|
||
counter.setAttribute('aria-live', 'polite');
|
||
statusbar.appendChild(counter);
|
||
}
|
||
return counter;
|
||
}
|
||
|
||
function updateCount() {
|
||
var count = String((editor.getBody() && editor.getBody().textContent) || '')
|
||
.replace(/\s+/g, '').length;
|
||
var element = getCounter();
|
||
if (element) {
|
||
element.textContent = '字数:' + count;
|
||
}
|
||
}
|
||
|
||
editor.on('init SetContent input keyup change Undo Redo', updateCount);
|
||
}
|
||
|
||
function openCodeModal(editor) {
|
||
var selected = editor.dom.getParent(editor.selection.getNode(), 'pre.enlighter-pre');
|
||
var code = selected ? selected.querySelector('code') : null;
|
||
var width = window.innerWidth > 800 ? Math.round(window.innerWidth * 0.5) :
|
||
(window.innerWidth > 640 ? Math.round(window.innerWidth * 0.7) : window.innerWidth - 20);
|
||
var height = Math.round(window.innerHeight * 0.6);
|
||
|
||
editor.windowManager.open({
|
||
title: (selected ? (i18n.edit || '编辑') : (i18n.insert || '插入')) +
|
||
(i18n.highlightCode || '高亮代码'),
|
||
width: width,
|
||
height: height,
|
||
body: [
|
||
{
|
||
type: 'listbox',
|
||
name: 'language',
|
||
label: i18n.selectLanguage || '选择语言',
|
||
value: code ? (code.getAttribute('data-enlighter-language') || 'generic') : 'generic',
|
||
values: listboxValues(codeLanguages),
|
||
},
|
||
{
|
||
type: 'listbox',
|
||
name: 'theme',
|
||
label: i18n.codeTheme || '主题',
|
||
value: code ? (code.getAttribute('data-enlighter-theme') || 'qj') : 'qj',
|
||
values: listboxValues(codeThemes),
|
||
},
|
||
{
|
||
type: 'textbox',
|
||
name: 'content',
|
||
label: i18n.codeLabel || '代码:',
|
||
value: code ? (code.textContent || '') : '',
|
||
multiline: true,
|
||
minHeight: Math.max(160, height - 115),
|
||
},
|
||
],
|
||
onsubmit: function (event) {
|
||
var content = String(event.data.content || '').replace(/\r\n/g, '\n');
|
||
if (!content) {
|
||
return;
|
||
}
|
||
var encoded = window.tinymce.html.Entities.encodeAllRaw(content);
|
||
var language = event.data.language === 'generic' ? '' :
|
||
' data-enlighter-language="' + escapeAttr(event.data.language) + '"';
|
||
var theme = event.data.theme === 'qj' ? '' :
|
||
' data-enlighter-theme="' + escapeAttr(event.data.theme) + '"';
|
||
var markup = '<pre contenteditable="false" class="enlighter-pre"><code class="gl"' +
|
||
language + theme + '>' + encoded + '</code></pre>';
|
||
if (selected) {
|
||
editor.dom.setOuterHTML(selected, markup);
|
||
} else {
|
||
editor.insertContent(markup + '<p></p>');
|
||
}
|
||
editor.nodeChanged();
|
||
editor.undoManager.add();
|
||
},
|
||
});
|
||
}
|
||
|
||
function registerEditorTools() {
|
||
if (!window.tinymce || !window.tinymce.PluginManager || window.aksrtSubmitEditorToolsRegistered) {
|
||
return false;
|
||
}
|
||
window.aksrtSubmitEditorToolsRegistered = true;
|
||
|
||
window.tinymce.PluginManager.add('aksrt_editor_tools', function (editor) {
|
||
bindCharacterCount(editor);
|
||
|
||
if (editorCfg.canUploadImage) {
|
||
var imageBatch = imageBatchLimit();
|
||
editor.addButton('aksrt_image', {
|
||
text: '',
|
||
icon: 'image',
|
||
tooltip: i18n.insertImage || '插入图片',
|
||
onclick: function () {
|
||
window.AksrtSubmitMedia.open({
|
||
type: 'image', multiple: imageBatch.multiple, maxItems: imageBatch.maxItems,
|
||
onSelect: function (items) {
|
||
editor.insertContent(items.map(function (item) { return mediaMarkup('image', item); }).join('') + '<p></p>');
|
||
editor.nodeChanged();
|
||
},
|
||
});
|
||
},
|
||
});
|
||
}
|
||
|
||
editor.addButton('aksrt_video', {
|
||
text: '',
|
||
icon: 'media',
|
||
tooltip: i18n.insertVideo || '插入视频',
|
||
onclick: function () {
|
||
window.AksrtSubmitMedia.open({
|
||
type: 'video', multiple: false,
|
||
onSelect: function (item) {
|
||
editor.insertContent(mediaMarkup('video', item) + '<p></p>');
|
||
editor.nodeChanged();
|
||
},
|
||
onEmbed: function (html) {
|
||
editor.insertContent(html + '<p></p>');
|
||
editor.nodeChanged();
|
||
},
|
||
});
|
||
},
|
||
});
|
||
|
||
if (editorCfg.canUploadFile) {
|
||
editor.addButton('aksrt_file', {
|
||
text: '',
|
||
icon: 'upload',
|
||
tooltip: i18n.insertFile || '上传附件',
|
||
onclick: function () {
|
||
window.AksrtSubmitMedia.open({
|
||
type: 'file', multiple: false,
|
||
onSelect: function (item) {
|
||
editor.insertContent(mediaMarkup('file', item) + '<p></p>');
|
||
editor.nodeChanged();
|
||
},
|
||
});
|
||
},
|
||
});
|
||
}
|
||
|
||
editor.addButton('aksrt_quote', {
|
||
text: '',
|
||
icon: 'blockquote',
|
||
tooltip: i18n.quote || '引言',
|
||
type: 'menubutton',
|
||
stateSelector: '.quote_q',
|
||
menu: [
|
||
{ text: i18n.quoteGray || '灰色', onclick: function () { insertQuote(editor, ''); } },
|
||
{ text: i18n.quoteRed || '红色', onclick: function () { insertQuote(editor, 'qe_wzk_c-red'); } },
|
||
{ text: i18n.quoteBlue || '蓝色', onclick: function () { insertQuote(editor, 'qe_wzk_lan'); } },
|
||
{ text: i18n.quoteGreen || '绿色', onclick: function () { insertQuote(editor, 'qe_wzk_lv'); } },
|
||
],
|
||
});
|
||
|
||
editor.addButton('aksrt_precode', {
|
||
text: '',
|
||
icon: 'code',
|
||
tooltip: i18n.highlightCode || '高亮代码',
|
||
stateSelector: 'pre.enlighter-pre',
|
||
onclick: function () {
|
||
openCodeModal(editor);
|
||
},
|
||
});
|
||
|
||
if (editorCfg.canHide) {
|
||
var menu = [
|
||
{
|
||
text: i18n.replyVisible || '回复可见',
|
||
type: 'reply',
|
||
open: '[reply]',
|
||
close: '[/reply]',
|
||
},
|
||
{
|
||
text: i18n.loginVisible || '登录可见',
|
||
type: 'login',
|
||
open: '[aksrt_hide mode="login"]',
|
||
close: '[/aksrt_hide]',
|
||
},
|
||
{
|
||
text: i18n.vipVisible || 'VIP 可见',
|
||
type: 'vip',
|
||
open: '[vip]',
|
||
close: '[/vip]',
|
||
},
|
||
];
|
||
if (editorCfg.canPay) {
|
||
menu.push({
|
||
text: i18n.payVisible || '付费可见',
|
||
type: 'payshow',
|
||
open: '[payshow]',
|
||
close: '[/payshow]',
|
||
});
|
||
}
|
||
menu = menu.map(function (item) {
|
||
return {
|
||
text: item.text,
|
||
onclick: function () {
|
||
insertHiddenContent(editor, item);
|
||
},
|
||
};
|
||
});
|
||
editor.addButton('aksrt_hide', {
|
||
text: '',
|
||
icon: 'preview',
|
||
tooltip: i18n.hiddenContent || '隐藏内容',
|
||
type: 'menubutton',
|
||
stateSelector: '.tinymce-hide',
|
||
menu: menu,
|
||
});
|
||
}
|
||
});
|
||
return true;
|
||
}
|
||
|
||
registerEditorTools();
|
||
if (!window.aksrtSubmitEditorToolsRegistered) {
|
||
var retryCount = 0;
|
||
var retryRegistration = function () {
|
||
if (registerEditorTools()) {
|
||
return;
|
||
}
|
||
retryCount += 1;
|
||
if (retryCount < 100) {
|
||
window.setTimeout(retryRegistration, 50);
|
||
}
|
||
};
|
||
retryRegistration();
|
||
}
|
||
})(window, document);
|