Merge pull request #2003 from TriliumNext/math-edit

feat(math): support multi-line formula editing
This commit is contained in:
Elian Doran 2025-05-26 16:43:16 +03:00 committed by GitHub
commit 6a29fae7c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 39 additions and 50 deletions

View File

@ -27,6 +27,18 @@ export default class MathEditing extends Plugin {
public init(): void {
const editor = this.editor;
const originalProcessor = editor.data.processor;
const originalToView = originalProcessor.toView.bind(originalProcessor);
const mathSpanRegex = /<span class="math-tex">([\s\S]*?)<\/span>/g;
originalProcessor.toView = (data: string) => {
// Preprocessing: preserve line breaks inside math formulas by replacing \n with <!--LF-->
const processedData = data.replace(mathSpanRegex, (_, content) =>
`<span class="math-tex">${content.replace(/\n/g, '___MATH_TEX_LF___')}</span>`
);
return originalToView(processedData);
};
editor.commands.add( 'math', new MathCommand( editor ) );
this._defineSchema();
@ -120,8 +132,7 @@ export default class MathEditing extends Plugin {
model: ( viewElement, { writer } ) => {
const child = viewElement.getChild( 0 );
if ( child?.is( '$text' ) ) {
const equation = child.data.trim();
const equation = child.data.trim().replace(/___MATH_TEX_LF___/g, '\n');
const params = Object.assign( extractDelimiters( equation ), {
type: mathConfig.forceOutputType ?
mathConfig.outputType :

View File

@ -110,6 +110,27 @@ export default class MathUI extends Plugin {
cancel();
} );
// Allow pressing Enter to submit changes, and use Shift+Enter to insert a new line
formView.keystrokes.set('enter', (data, cancel) => {
if (!data.shiftKey) {
formView.fire('submit');
cancel();
}
});
// Allow the textarea to be resizable
formView.mathInputView.fieldView.once('render', () => {
const textarea = formView.mathInputView.fieldView.element;
if (!textarea) return;
textarea.focus();
Object.assign(textarea.style, {
resize: 'both',
height: '100px',
width: '400px',
minWidth: '100%',
});
});
return formView;
}

View File

@ -1,16 +1,16 @@
import { ButtonView, createLabeledInputText, FocusCycler, LabelView, LabeledFieldView, submitHandler, SwitchButtonView, View, ViewCollection, type InputTextView, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5';
import { ButtonView, createLabeledTextarea, FocusCycler, LabelView, LabeledFieldView, submitHandler, SwitchButtonView, View, ViewCollection, type TextareaView, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5';
import { IconCheck, IconCancel } from "@ckeditor/ckeditor5-icons";
import { extractDelimiters, hasDelimiters } from '../utils.js';
import MathView from './mathview.js';
import '../../theme/mathform.css';
import type { KatexOptions } from '../typings-external.js';
class MathInputView extends LabeledFieldView<InputTextView> {
class MathInputView extends LabeledFieldView<TextareaView> {
public value: null | string = null;
public isReadOnly = false;
constructor( locale: Locale ) {
super( locale, createLabeledInputText );
super( locale, createLabeledTextarea );
}
}

View File

@ -49,7 +49,7 @@ export default class MathView extends View {
this.setTemplate( {
tag: 'div',
attributes: {
class: [ 'ck', 'ck-math-preview' ]
class: [ 'ck', 'ck-math-preview', 'ck-reset_all-excluded' ]
}
} );
}

View File

@ -94,7 +94,6 @@ export async function renderEquation(
el => {
renderMathJax3( equation, el, display, () => {
if ( preview ) {
moveAndScaleElement( element, el );
el.style.visibility = 'visible';
}
} );
@ -115,7 +114,6 @@ export async function renderEquation(
if ( preview && isMathJaxVersion2( MathJax ) ) {
// eslint-disable-next-line new-cap
MathJax.Hub.Queue( () => {
moveAndScaleElement( element, el );
el.style.visibility = 'visible';
} );
}
@ -139,7 +137,6 @@ export async function renderEquation(
} );
}
if ( preview ) {
moveAndScaleElement( element, el );
el.style.visibility = 'visible';
}
}
@ -295,47 +292,7 @@ function getPreviewElement(
previewEl.setAttribute( 'id', previewUid );
previewEl.classList.add( ...previewClassName );
previewEl.style.visibility = 'hidden';
document.body.appendChild( previewEl );
let ticking = false;
const renderTransformation = () => {
if ( !ticking ) {
window.requestAnimationFrame( () => {
if ( previewEl ) {
moveElement( element, previewEl );
ticking = false;
}
} );
ticking = true;
}
};
// Create scroll listener for following
window.addEventListener( 'resize', renderTransformation );
window.addEventListener( 'scroll', renderTransformation );
element.appendChild( previewEl );
}
return previewEl;
}
function moveAndScaleElement( parent: HTMLElement, child: HTMLElement ) {
// Move to right place
moveElement( parent, child );
// Scale parent element same as preview
const domRect = child.getBoundingClientRect();
parent.style.width = domRect.width + 'px';
parent.style.height = domRect.height + 'px';
}
function moveElement( parent: HTMLElement, child: HTMLElement ) {
const domRect = parent.getBoundingClientRect();
const left = window.scrollX + domRect.left;
const top = window.scrollY + domRect.top;
child.style.position = 'absolute';
child.style.left = left + 'px';
child.style.top = top + 'px';
child.style.zIndex = 'var(--ck-z-panel)';
child.style.pointerEvents = 'none';
}