数字公式优化

This commit is contained in:
2026-05-29 16:11:23 +08:00
parent 6288c3e2ea
commit 4b1a10b20c
11 changed files with 826 additions and 114 deletions

View File

@@ -3,6 +3,9 @@ import Vue from 'vue';
import katex from 'katex';
import JSZip from 'jszip';
import mammoth from "mammoth";
import { MathfieldElement } from 'mathlive';
import 'mathlive/dist/mathlive-static.css';
import 'mathlive/dist/mathlive-fonts.css';
import { importWordDocumentWithMath as parseWordDocumentWithMath, parseHtmlToLatex as convertHtmlToLatex } from '@/utils/wordMathImport';
import api from '../../api/index.js';
import Common from '@/components/common/common'
@@ -26,6 +29,499 @@ const replaceNegativeSign = function (text) {
return text;
};
let activeInlineMathOverlay = null;
function escapeLatexForWmathAttr(latex) {
return String(latex || '')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/</g, '&lt;');
}
function buildStoredLatex(rawLatex) {
const t = String(rawLatex || '').trim();
if (!t) return '';
if (/^\$\$[\s\S]+\$\$$/.test(t)) return t;
if (/^\$[\s\S]+\$$/.test(t)) return `$$${t.replace(/^\$|\$$/g, '')}$$`;
return `$$${t}$$`;
}
function resolveWmathWrapMode(latex) {
const t = String(latex || '')
.trim()
.replace(/^\$\$/, '')
.replace(/\$\$$/, '');
if (!t) return 'block';
if (/^\\begin\{/.test(t) || /\n\s*\n/.test(t)) return 'block';
if (/\\frac|\\dfrac|\\tfrac|\\sum|\\int|\\prod|\\lim|\\sqrt\{|\\displaystyle/.test(t)) {
return 'block';
}
return 'inline';
}
function createWmathElementHtml(rawLatex, uid, wrapMode) {
const storedLatex = buildStoredLatex(rawLatex);
const safeLatex = escapeLatexForWmathAttr(storedLatex);
const mode = normalizeWmathWrapMode(wrapMode);
return (
'<wmath contenteditable="false" data-id="' +
uid +
'" data-latex="' +
safeLatex +
'" data-wrap="' +
mode +
'">' +
storedLatex +
'</wmath>'
);
}
function parseLatexFromWmath(wmathElement) {
const raw = (wmathElement && wmathElement.getAttribute('data-latex')) || '';
return raw
.replace(/^\$\$/, '')
.replace(/\$\$$/, '')
.replace(/^\$/, '')
.replace(/\$$/, '')
.trim();
}
function getEventElementTarget(target) {
if (!target) return null;
if (target.nodeType === 3) return target.parentNode;
if (target.nodeType === 9) return null;
return target;
}
function findWmathAncestor(node, root) {
let el = getEventElementTarget(node);
while (el && el !== root) {
if (el.nodeType === 1 && el.nodeName && el.nodeName.toLowerCase() === 'wmath') {
return el;
}
el = el.parentNode;
}
return null;
}
function stripMceSelectedAttr(html) {
return String(html || '').replace(/\s*data-mce-selected="[^"]*"/gi, '');
}
function normalizeWmathWrapMode(wrap) {
return wrap === 'inline' ? 'inline' : 'block';
}
function getOppositeWrapLabel(wrapMode) {
const target = wrapMode === 'inline' ? 'Text above and below' : 'Inline with text';
return 'Change to ' + target;
}
function clearEditorMathSelection(ed) {
if (!ed || !ed.getBody) return;
const body = ed.getBody();
if (!body) return;
body.querySelectorAll('[data-mce-selected]').forEach((el) => {
el.removeAttribute('data-mce-selected');
});
try {
ed.selection.collapse(false);
} catch (err) {
// ignore
}
}
function closeWmathContextMenu() {
const menu = document.getElementById('tinymce-wmath-context-menu');
if (menu) menu.remove();
}
function copyTextToClipboard(text, onSuccess) {
const value = String(text || '').trim();
if (!value) return;
const done = typeof onSuccess === 'function' ? onSuccess : () => {};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(value).then(done).catch(() => {
const ta = document.createElement('textarea');
ta.value = value;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
done();
});
} else {
const ta = document.createElement('textarea');
ta.value = value;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
done();
}
}
function showWmathContextMenu(ed, wmathElement, event) {
if (!wmathElement || !event) return;
if (document.getElementById('tinymce-inline-math-overlay')) return;
closeWmathContextMenu();
event.preventDefault();
event.stopPropagation();
const latex = parseLatexFromWmath(wmathElement);
const wrapMode = wmathElement.getAttribute('data-wrap') || 'block';
const uid = wmathElement.getAttribute('data-id') || 'wmath-' + Math.random().toString(36).substr(2, 9);
const wrapLabel = getOppositeWrapLabel(wrapMode);
const nextWrap = wrapMode === 'inline' ? 'block' : 'inline';
const menu = document.createElement('div');
menu.id = 'tinymce-wmath-context-menu';
menu.className = 'tinymce-wmath-context-menu';
menu.innerHTML =
'<button type="button" class="tinymce-wmath-context-item" data-action="copy">Copy LaTeX code</button>' +
'<button type="button" class="tinymce-wmath-context-item" data-action="wrap">' +
wrapLabel +
'</button>';
menu.style.left = Math.min(event.clientX, window.innerWidth - 220) + 'px';
menu.style.top = Math.min(event.clientY, window.innerHeight - 100) + 'px';
document.body.appendChild(menu);
const onMenuAction = (e) => {
const item = e.target.closest('[data-action]');
if (!item || !menu.contains(item)) return;
e.preventDefault();
e.stopPropagation();
const action = item.getAttribute('data-action');
if (action === 'copy') {
copyTextToClipboard(latex);
} else if (action === 'wrap' && latex) {
ed.dom.setOuterHTML(wmathElement, createWmathElementHtml(latex, uid, nextWrap));
setTimeout(() => {
if (typeof window.renderMathJax === 'function') {
window.renderMathJax(ed.id);
}
clearEditorMathSelection(ed);
}, 10);
}
closeWmathContextMenu();
document.removeEventListener('mousedown', onOutside, true);
};
const onOutside = (e) => {
if (menu.contains(e.target)) return;
closeWmathContextMenu();
document.removeEventListener('mousedown', onOutside, true);
};
menu.addEventListener('mousedown', (e) => e.stopPropagation());
menu.addEventListener('click', onMenuAction);
setTimeout(() => document.addEventListener('mousedown', onOutside, true), 0);
}
function openInlineMathEditor(ed, options) {
try {
openInlineMathEditorCore(ed, options);
} catch (err) {
console.error('openInlineMathEditor failed:', err);
closeInlineMathOverlay();
}
}
function openInlineMathEditorCore(ed, options) {
const opts = options || {};
const mode = opts.mode === 'edit' ? 'edit' : 'insert';
const wmathElement = opts.wmathElement || null;
closeInlineMathOverlay();
const iframe = ed.iframeElement;
const iframeDoc = ed.getDoc();
if (!iframe || !iframeDoc) return;
const placeholderId = 'math-ph-' + Date.now();
let uid = 'wmath-' + Math.random().toString(36).substr(2, 9);
let initialLatex = '';
let originalWmathHtml = null;
let preservedWrapMode = null;
let currentWrapMode = 'block';
let anchorRect = null;
if (mode === 'edit' && wmathElement) {
uid = wmathElement.getAttribute('data-id') || uid;
initialLatex = parseLatexFromWmath(wmathElement);
preservedWrapMode = wmathElement.getAttribute('data-wrap') || null;
originalWmathHtml = stripMceSelectedAttr(wmathElement.outerHTML);
anchorRect = wmathElement.getBoundingClientRect();
const phWidth = Math.max(Math.round(anchorRect.width), 4);
const phHeight = Math.max(Math.round(anchorRect.height), 4);
ed.dom.setOuterHTML(
wmathElement,
`<span id="${placeholderId}" class="math-placeholder" style="display:inline-block;min-width:${phWidth}px;min-height:${phHeight}px;vertical-align:middle;">&#8203;</span>`
);
} else {
ed.insertContent(
`<span id="${placeholderId}" class="math-placeholder" style="display:inline-block;min-width:4px;">&#8203;</span>`
);
}
const placeholder = ed.dom.get(placeholderId);
if (!placeholder) return;
currentWrapMode = normalizeWmathWrapMode(preservedWrapMode);
const phRect = placeholder.getBoundingClientRect();
const iframeRect = iframe.getBoundingClientRect();
const anchor = anchorRect || phRect;
const overlayTop = iframeRect.top + anchor.top;
const overlayLeft = iframeRect.left + anchor.left;
const overlayMinWidth = Math.min(Math.max(anchor.width, phRect.width, 360), 560);
let overlay = document.getElementById('tinymce-inline-math-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'tinymce-inline-math-overlay';
overlay.className = 'tinymce-inline-math-overlay';
document.body.appendChild(overlay);
}
overlay.style.display = 'block';
overlay.style.top = overlayTop + 'px';
overlay.style.left = overlayLeft + 'px';
overlay.style.minWidth = overlayMinWidth + 'px';
overlay.innerHTML = `
<div class="tinymce-inline-math-panel">
<div class="tinymce-inline-math-field-wrap"></div>
<div class="tinymce-inline-math-footer">
<div class="tinymce-inline-math-tools">
<button type="button" class="tinymce-inline-math-copy">Copy LaTeX code</button>
<span class="tinymce-inline-math-tool-divider"></span>
<button type="button" class="tinymce-inline-math-wrap-toggle"></button>
</div>
<div class="tinymce-inline-math-actions">
<button type="button" class="tinymce-inline-math-cancel">Cancel</button>
<button type="button" class="tinymce-inline-math-confirm">OK</button>
</div>
</div>
</div>
`;
const fieldWrap = overlay.querySelector('.tinymce-inline-math-field-wrap');
const confirmBtn = overlay.querySelector('.tinymce-inline-math-confirm');
const cancelBtn = overlay.querySelector('.tinymce-inline-math-cancel');
const copyBtn = overlay.querySelector('.tinymce-inline-math-copy');
const wrapToggleBtn = overlay.querySelector('.tinymce-inline-math-wrap-toggle');
const updateWrapToggleLabel = () => {
wrapToggleBtn.textContent = getOppositeWrapLabel(currentWrapMode);
};
updateWrapToggleLabel();
wrapToggleBtn.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
});
wrapToggleBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
currentWrapMode = currentWrapMode === 'inline' ? 'block' : 'inline';
updateWrapToggleLabel();
});
copyBtn.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
});
const mf = new MathfieldElement();
fieldWrap.appendChild(mf);
copyBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
copyTextToClipboard(mf.value, () => {
copyBtn.textContent = 'Copied!';
copyBtn.classList.add('is-copied');
setTimeout(() => {
copyBtn.textContent = 'Copy LaTeX code';
copyBtn.classList.remove('is-copied');
}, 2000);
});
});
let mfInitialized = false;
const initMathfield = () => {
if (mfInitialized) return;
mfInitialized = true;
mf.virtualKeyboardMode = 'onfocus';
mf.placeholder = 'Type formula here.';
mf.menuItems = [];
try {
if (initialLatex) {
mf.value = initialLatex;
}
} catch (valueErr) {
console.warn('Mathfield set value failed:', valueErr);
}
requestAnimationFrame(() => {
mf.focus();
});
};
mf.addEventListener('mount', initMathfield, { once: true });
mf.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
setTimeout(initMathfield, 0);
let finalized = false;
let ignoreOutsideClick = true;
const openedAt = Date.now();
const cleanupOutsideListeners = () => {
document.removeEventListener('mousedown', onDocMouseDown, true);
iframeDoc.removeEventListener('mousedown', onIframeMouseDown, true);
mf.removeEventListener('blur', onMfBlur);
};
const canCancelOutside = () => !ignoreOutsideClick && Date.now() - openedAt > 400;
const finalize = (save) => {
if (finalized) return;
finalized = true;
cleanupOutsideListeners();
const latex = (mf.value || '').trim();
closeInlineMathOverlay();
const ph = ed.dom.get(placeholderId);
if (!ph) return;
const afterUpdate = () => {
setTimeout(() => {
if (typeof window.renderMathJax === 'function') {
window.renderMathJax(ed.id);
}
clearEditorMathSelection(ed);
}, 10);
};
if (save && latex) {
ed.dom.setOuterHTML(ph, createWmathElementHtml(latex, uid, currentWrapMode));
afterUpdate();
} else if (mode === 'edit' && originalWmathHtml) {
ed.dom.setOuterHTML(ph, originalWmathHtml);
afterUpdate();
} else {
ed.dom.remove(ph);
clearEditorMathSelection(ed);
}
};
const onDocMouseDown = (e) => {
if (!canCancelOutside()) return;
if (overlay.contains(e.target)) return;
if (isMathliveUiTarget(e.target)) return;
finalize(false);
};
const onIframeMouseDown = (e) => {
if (!canCancelOutside()) return;
if (overlay.contains(e.target)) return;
if (isMathliveUiTarget(e.target)) return;
finalize(false);
};
const onMfBlur = () => {
setTimeout(() => {
if (finalized) return;
if (!canCancelOutside()) return;
const activeEl = document.activeElement;
const iframeActiveEl = iframeDoc.activeElement;
if (overlay.contains(activeEl)) return;
if (isMathliveUiTarget(activeEl) || isMathliveUiTarget(iframeActiveEl)) return;
finalize(false);
}, 150);
};
confirmBtn.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
});
confirmBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
finalize(true);
});
cancelBtn.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
});
cancelBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
finalize(false);
});
activeInlineMathOverlay = {
overlay,
iframeDoc,
mf,
finalize,
cleanupOutsideListeners
};
mf.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
e.preventDefault();
finalize(false);
}
});
mf.addEventListener('blur', onMfBlur);
setTimeout(() => {
ignoreOutsideClick = false;
document.addEventListener('mousedown', onDocMouseDown, true);
iframeDoc.addEventListener('mousedown', onIframeMouseDown, true);
}, 500);
}
function insertInlineMathWithMathlive(ed) {
openInlineMathEditor(ed, { mode: 'insert' });
}
function editInlineMathWithMathlive(ed, wmathElement) {
if (!wmathElement) return;
if (activeInlineMathOverlay) return;
openInlineMathEditor(ed, { mode: 'edit', wmathElement });
}
function isMathliveUiTarget(target) {
if (!target || !target.closest) return false;
return !!(
target.closest('#tinymce-inline-math-overlay') ||
target.closest('.ML__keyboard') ||
target.closest('.ML__virtual-keyboard') ||
target.closest('.MLK__plate') ||
target.closest('math-field')
);
}
function closeInlineMathOverlay() {
if (!activeInlineMathOverlay) return;
const { overlay, cleanupOutsideListeners } = activeInlineMathOverlay;
if (typeof cleanupOutsideListeners === 'function') {
cleanupOutsideListeners();
}
overlay.style.display = 'none';
overlay.innerHTML = '';
activeInlineMathOverlay = null;
}
// 首字母大写的方法
// const capitalizeFirstLetter = function (text) {
@@ -1837,6 +2333,19 @@ str = str.replace(regex, function (match, content, offset, fullString) {
},
insertInlineMathWithMathlive(ed) {
openInlineMathEditor(ed, { mode: 'insert' });
},
editInlineMathWithMathlive(ed, wmathElement) {
if (!wmathElement) return;
openInlineMathEditor(ed, { mode: 'edit', wmathElement });
},
findWmathFromTarget(target, root) {
return findWmathAncestor(target, root);
},
openWmathContextMenu(ed, wmathElement, event) {
showWmathContextMenu(ed, wmathElement, event);
},
initEditorButton(vueInstance, ed) {
ed.ui.registry.addMenuButton('customDropdown', {
@@ -2189,28 +2698,37 @@ str = str.replace(regex, function (match, content, offset, fullString) {
let latexEditorBookmark = null; // 用于记录插入点
let activeEditorId = null; // 当前激活的编辑器 ID
// 在编辑器工具栏中添加 "LateX" 按钮
// // 在编辑器工具栏中添加 "LateX" 按钮
// ed.ui.registry.addButton('LateX', {
// text: 'LateX', // 按钮文本
// onAction: function () {
// // 1. 获取当前光标位置
// const latexEditorBookmark = ed.selection.getBookmark(2); // 获取光标位置
// const editorId = ed.id; // 保存当前编辑器 ID
// // 2. 生成一个随机的 ID用于 wmath 标签
// const uid = 'wmath-' + Math.random().toString(36).substr(2, 9);
// // 3. 创建一个 <wmath> 标签并插入到光标处
// const wmathHtml = `<wmath contenteditable="false" data-id="${uid}" data-latex="" data-wrap="block"></wmath>`;
// ed.insertContent(wmathHtml); // 在光标位置插入 wmath 标签
// // 4. 打开公式编辑器窗口,并传递光标位置、编辑器 ID 和 wmathId
// const url = `/LateX?editorId=${editorId}&wmathId=${uid}`;
// // vueInstance.openLatexEditor({
// // editorId:editorId,
// // wmathId:uid,
// // });
// window.open(url, '_blank', 'width=1000,height=800,scrollbars=no,resizable=no');
// }
// });
// 行内公式编辑MathLive类似 Word「在此处键入公式」
ed.ui.registry.addButton('LateX', {
text: 'LateX', // 按钮文本
text: 'LateX',
tooltip: 'Type formula here',
onAction: function () {
// 1. 获取当前光标位置
const latexEditorBookmark = ed.selection.getBookmark(2); // 获取光标位置
const editorId = ed.id; // 保存当前编辑器 ID
// 2. 生成一个随机的 ID用于 wmath 标签
const uid = 'wmath-' + Math.random().toString(36).substr(2, 9);
// 3. 创建一个 <wmath> 标签并插入到光标处
const wmathHtml = `<wmath contenteditable="false" data-id="${uid}" data-latex="" data-wrap="block"></wmath>`;
ed.insertContent(wmathHtml); // 在光标位置插入 wmath 标签
// 4. 打开公式编辑器窗口,并传递光标位置、编辑器 ID 和 wmathId
const url = `/LateX?editorId=${editorId}&wmathId=${uid}`;
// vueInstance.openLatexEditor({
// editorId:editorId,
// wmathId:uid,
// });
window.open(url, '_blank', 'width=1000,height=800,scrollbars=no,resizable=no');
insertInlineMathWithMathlive(ed);
}
});