/** * Front-submit TinyMCE tools matching the reference article editor. * * @package AKSRT */ (function (window, document) { 'use strict'; if (!window.tinymce || !window.tinymce.PluginManager) { return; } 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, '>'); } 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) { var safeUrl = validHttpUrl(url); if (!safeUrl) { return ''; } var idAttr = attachmentId ? ' data-edit-file-id="' + Number(attachmentId) + '"' : ''; var altAttr = alt ? ' alt="' + escapeAttr(alt) + '"' : ' alt=""'; return '

'; } function videoMarkup(url, attachmentId, name) { var safeUrl = validHttpUrl(url); if (!safeUrl) { return ''; } var idAttr = attachmentId ? ' data-edit-file-id="' + Number(attachmentId) + '"' : ''; return '
'; } 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-download-file="' + Number(attachmentId) + '"' : ''; return '
' + '
' + '
' + '
' + escapeAttr(filename) + '
' + '
'; } 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 '
' + '
' + '
'; } function closeMediaModal() { if (!activeModal) { return; } document.removeEventListener('keydown', activeModal.onKeydown); activeModal.root.remove(); activeModal = null; document.body.classList.remove('aksrt-editor-media-open'); } 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); return fetch(upload.url, { 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) || i18n.error || '文件上传失败'); } return payload; }); }); } function mediaDefinition(type) { if (type === 'video') { return { title: i18n.insertVideo || '插入视频', uploadTab: i18n.uploadVideo || '上传视频', externalTab: i18n.externalVideo || '外链视频', embedTab: i18n.embedVideo || '嵌入视频', accept: 'video/*', multiple: false, 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 || '上传附件', uploadTab: i18n.uploadFile || '上传附件', externalTab: i18n.externalFile || '输入地址', accept: '*/*', multiple: false, canUpload: !!editorCfg.canUploadFile && !!((cfg.uploads || {}).resource), canEmbed: false, select: i18n.selectFile || '选择或拖入附件', hint: i18n.fileUploadHint || '上传完成后自动插入正文', externalHint: i18n.externalFileHint || '请填写附件地址:', }; } return { title: i18n.insertImage || '插入图片', uploadTab: i18n.uploadImage || '上传图片', externalTab: i18n.externalImage || '外链图片', accept: 'image/gif,image/jpeg,image/png,image/webp', multiple: true, canUpload: !!editorCfg.canUploadImage && !!((cfg.uploads || {}).image), canEmbed: false, select: i18n.selectImages || '选择或拖入图片', hint: i18n.imageUploadHint || '可批量选择,上传完成后自动插入正文', externalHint: i18n.externalImageHint || '每行填写一个图片地址:', }; } function mediaMarkup(type, item) { if (type === 'video') { return videoMarkup(item.url, item.attachment_id, item.filename || item.name || ''); } if (type === 'file') { return fileMarkup(item.url, item.attachment_id, item.filename || item.name || ''); } return imageMarkup(item.url, item.attachment_id, item.name || ''); } function createMediaModal(editor, type) { closeMediaModal(); var definition = mediaDefinition(type); var canUpload = definition.canUpload; var firstTab = canUpload ? 'upload' : 'external'; 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 = '' + '
' + '' + '

' + escapeAttr(definition.title) + '

' + '
' + (canUpload ? '' : '') + '' + (definition.canEmbed ? '' : '') + '
' + '
' + (canUpload ? '
' + '' + '
' + '' + '
' : '') + '
' + '

' + escapeAttr(definition.externalHint) + '

' + '' + '' + '
' + (definition.canEmbed ? '
' + '

' + escapeAttr(i18n.embedVideoHint || '请输入嵌入地址或者直接粘贴 iframe 嵌入代码:') + '

' + '' + '' + '
' : '') + '
' + '
'; document.body.appendChild(root); document.body.classList.add('aksrt-editor-media-open'); var fileInput = root.querySelector('[data-editor-media-files]'); var uploadButton = root.querySelector('[data-editor-media-upload]'); var selection = root.querySelector('[data-editor-media-selection]'); var drop = root.querySelector('.aksrt-editor-media__drop'); var files = []; function showSelection() { if (!selection || !uploadButton) { return; } selection.textContent = files.length ? (definition.multiple ? (i18n.selectedFiles || '已选择 %d 个文件').replace('%d', String(files.length)) : files[0].name) : ''; uploadButton.disabled = files.length === 0; } function setFiles(nextFiles) { var candidates = Array.prototype.slice.call(nextFiles || []); files = candidates.filter(function (file) { return acceptsFile(file, type); }); if (candidates.length && !files.length) { notify(i18n.invalidFile || '请选择有效文件', 'warning'); } if (!definition.multiple && files.length > 1) { files = files.slice(0, 1); } showSelection(); } 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 (item) { var active = item === tab; item.classList.toggle('is-active', active); item.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); }); return; } if (event.target.closest('[data-editor-media-external]')) { var textarea = root.querySelector('[data-editor-media-urls]'); var urls = String(textarea ? textarea.value : '').split(/\r?\n/); if (!definition.multiple) { urls = urls.slice(0, 1); } var html = urls.map(function (url) { return mediaMarkup(type, { url: url }); }).join(''); if (!html) { notify(i18n.invalidMediaUrl || '请填写有效地址', 'warning'); return; } editor.insertContent(html + '

'); editor.nodeChanged(); closeMediaModal(); return; } if (event.target.closest('[data-editor-media-embed-submit]')) { var embedInput = root.querySelector('[data-editor-media-embed]'); var embedHtml = embedVideoMarkup(embedInput ? embedInput.value : ''); if (!embedHtml) { notify(i18n.invalidVideoUrl || '请填写有效的视频嵌入地址', 'warning'); return; } editor.insertContent(embedHtml + '

'); editor.nodeChanged(); closeMediaModal(); return; } if (event.target.closest('[data-editor-media-upload]')) { if (!files.length || !uploadButton) { return; } uploadButton.disabled = true; uploadButton.textContent = i18n.uploading || '正在上传…'; notify(i18n.uploading || '正在上传…', 'load', ''); Promise.all(files.map(function (file) { return uploadMedia(file, type); })).then(function (items) { var html = items.map(function (item) { return mediaMarkup(type, item); }).join(''); if (!html) { throw new Error(i18n.error || '文件上传失败'); } editor.insertContent(html + '

'); editor.nodeChanged(); notify(i18n.uploaded || '上传成功', 'success'); closeMediaModal(); }).catch(function (error) { notify(error.message || i18n.error || '文件上传失败', 'danger'); uploadButton.disabled = false; uploadButton.textContent = i18n.confirmUpload || '确认上传'; }); } }); 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(); } }; activeModal = { root: root, onKeydown: onKeydown }; document.addEventListener('keydown', onKeydown); var focusTarget = root.querySelector('.aksrt-editor-media__dialog'); if (focusTarget) { focusTarget.focus({ preventScroll: true }); } } function selectedContent(editor) { var content = String(editor.selection.getContent() || '').trim(); return content ? '

' + content + '

' : '

' + escapeAttr(i18n.hiddenPlaceholder || '隐藏内容') + '

'; } function insertHiddenContent(editor, item) { var node = editor.selection.getNode(); var content = selectedContent(editor); var suffix = '

'; 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( '
' + '

' + escapeAttr(item.open) + '

' + '
' + content + '
' + '

' + escapeAttr(item.close) + '

' + '
' + 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 ? '

' + content + '

' : '

'; } editor.insertContent( '
' + content + '

' ); 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 = '
' + encoded + '
'; if (selected) { editor.dom.setOuterHTML(selected, markup); } else { editor.insertContent(markup + '

'); } editor.nodeChanged(); editor.undoManager.add(); }, }); } window.tinymce.PluginManager.add('aksrt_editor_tools', function (editor) { bindCharacterCount(editor); if (editorCfg.canUploadImage) { editor.addButton('aksrt_image', { text: '', icon: 'image', tooltip: i18n.insertImage || '插入图片', onclick: function () { createMediaModal(editor, 'image'); }, }); } editor.addButton('aksrt_video', { text: '', icon: 'media', tooltip: i18n.insertVideo || '插入视频', onclick: function () { createMediaModal(editor, 'video'); }, }); if (editorCfg.canUploadFile) { editor.addButton('aksrt_file', { text: '', icon: 'upload', tooltip: i18n.insertFile || '上传附件', onclick: function () { createMediaModal(editor, 'file'); }, }); } 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, }); } }); })(window, document);