mirror of
https://github.com/zadam/trilium.git
synced 2025-12-04 14:34:24 +01:00
Improve and simplify Mathfield integration
This commit is contained in:
parent
d2052ad236
commit
51db729546
@ -71,22 +71,20 @@ export default class MathUI extends Plugin {
|
||||
throw new CKEditorError( 'math-command' );
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
||||
const mathConfig = editor.config.get( 'math' )!;
|
||||
|
||||
const formView = new MainFormView(
|
||||
editor.locale,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
mathConfig.engine!,
|
||||
mathConfig.lazyLoad,
|
||||
{
|
||||
engine: mathConfig.engine!,
|
||||
lazyLoad: mathConfig.lazyLoad,
|
||||
previewUid: this._previewUid,
|
||||
previewClassName: mathConfig.previewClassName!,
|
||||
katexRenderOptions: mathConfig.katexRenderOptions!
|
||||
},
|
||||
mathConfig.enablePreview,
|
||||
this._previewUid,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
mathConfig.previewClassName!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
mathConfig.popupClassName!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
mathConfig.katexRenderOptions!
|
||||
mathConfig.popupClassName!
|
||||
);
|
||||
|
||||
formView.mathLiveInputView.bind( 'value' ).to( mathCommand, 'value' );
|
||||
@ -164,9 +162,9 @@ export default class MathUI extends Plugin {
|
||||
|
||||
// Show preview element
|
||||
const previewEl = document.getElementById( this._previewUid );
|
||||
if ( previewEl && this.formView.previewEnabled ) {
|
||||
if ( previewEl && this.formView.mathView ) {
|
||||
// Force refresh preview
|
||||
this.formView.mathView?.updateMath();
|
||||
this.formView.mathView.updateMath();
|
||||
}
|
||||
|
||||
this.formView.equation = mathCommand.value ?? '';
|
||||
|
||||
@ -1,174 +1,136 @@
|
||||
import { ButtonView, FocusCycler, LabelView, submitHandler, SwitchButtonView, View, ViewCollection, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5';
|
||||
import {
|
||||
ButtonView,
|
||||
FocusCycler,
|
||||
LabelView,
|
||||
submitHandler,
|
||||
SwitchButtonView,
|
||||
View,
|
||||
ViewCollection,
|
||||
type FocusableView,
|
||||
Locale,
|
||||
FocusTracker,
|
||||
KeystrokeHandler
|
||||
} from 'ckeditor5';
|
||||
import IconCheck from '@ckeditor/ckeditor5-icons/theme/icons/check.svg?raw';
|
||||
import IconCancel from '@ckeditor/ckeditor5-icons/theme/icons/cancel.svg?raw';
|
||||
import { extractDelimiters, hasDelimiters } from '../utils.js';
|
||||
import MathView from './mathview.js';
|
||||
import MathView, { type MathViewOptions } from './mathview.js';
|
||||
import MathLiveInputView from './mathliveinputview.js';
|
||||
import RawLatexInputView from './rawlatexinputview.js';
|
||||
import '../../theme/mathform.css';
|
||||
import type { KatexOptions } from '../typings-external.js';
|
||||
|
||||
export default class MainFormView extends View {
|
||||
public saveButtonView: ButtonView;
|
||||
public cancelButtonView: ButtonView;
|
||||
public displayButtonView: SwitchButtonView;
|
||||
|
||||
public mathLiveInputView: MathLiveInputView;
|
||||
public rawLatexInputView: RawLatexInputView;
|
||||
public rawLatexLabel: LabelView;
|
||||
public displayButtonView: SwitchButtonView;
|
||||
public cancelButtonView: ButtonView;
|
||||
public previewEnabled: boolean;
|
||||
public previewLabel?: LabelView;
|
||||
public mathView?: MathView;
|
||||
public override locale: Locale;
|
||||
|
||||
public focusTracker = new FocusTracker();
|
||||
public keystrokes = new KeystrokeHandler();
|
||||
private _focusables = new ViewCollection<FocusableView>();
|
||||
private _focusCycler: FocusCycler;
|
||||
|
||||
constructor(
|
||||
locale: Locale,
|
||||
engine:
|
||||
| 'mathjax'
|
||||
| 'katex'
|
||||
| ( (
|
||||
equation: string,
|
||||
element: HTMLElement,
|
||||
display: boolean,
|
||||
) => void ),
|
||||
lazyLoad: undefined | ( () => Promise<void> ),
|
||||
mathViewOptions: MathViewOptions,
|
||||
previewEnabled = false,
|
||||
previewUid: string,
|
||||
previewClassName: Array<string>,
|
||||
popupClassName: Array<string>,
|
||||
katexRenderOptions: KatexOptions
|
||||
popupClassName: string[] = []
|
||||
) {
|
||||
super( locale );
|
||||
this.locale = locale;
|
||||
|
||||
const t = locale.t;
|
||||
|
||||
// Submit button
|
||||
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', null );
|
||||
// --- 1. View Initialization ---
|
||||
|
||||
this.mathLiveInputView = new MathLiveInputView( locale );
|
||||
this.rawLatexInputView = new RawLatexInputView( locale );
|
||||
this.rawLatexInputView.label = t( 'LaTeX' );
|
||||
|
||||
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save' );
|
||||
this.saveButtonView.type = 'submit';
|
||||
|
||||
// MathLive visual equation editor
|
||||
this.mathLiveInputView = this._createMathLiveInput();
|
||||
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel' );
|
||||
this.cancelButtonView.delegate( 'execute' ).to( this, 'cancel' );
|
||||
|
||||
// Raw LaTeX input
|
||||
this.rawLatexInputView = this._createRawLatexInput();
|
||||
this.displayButtonView = this._createDisplayButton( t );
|
||||
|
||||
// Raw LaTeX label
|
||||
this.rawLatexLabel = new LabelView( locale );
|
||||
this.rawLatexLabel.text = t( 'LaTeX' );
|
||||
// --- 2. Construct Children & Preview ---
|
||||
|
||||
// Display button
|
||||
this.displayButtonView = this._createDisplayButton();
|
||||
const children: View[] = [
|
||||
this.mathLiveInputView,
|
||||
this.rawLatexInputView,
|
||||
this.displayButtonView
|
||||
];
|
||||
|
||||
// Cancel button
|
||||
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel', 'cancel' );
|
||||
if ( previewEnabled ) {
|
||||
const previewLabel = new LabelView( locale );
|
||||
previewLabel.text = t( 'Equation preview' );
|
||||
|
||||
this.previewEnabled = previewEnabled;
|
||||
// Clean instantiation using the options object
|
||||
this.mathView = new MathView( locale, mathViewOptions );
|
||||
|
||||
let children = [];
|
||||
if ( this.previewEnabled ) {
|
||||
// Preview label
|
||||
this.previewLabel = new LabelView( locale );
|
||||
this.previewLabel.text = t( 'Equation preview' );
|
||||
|
||||
// Math element
|
||||
this.mathView = new MathView( engine, lazyLoad, locale, previewUid, previewClassName, katexRenderOptions );
|
||||
// Bind display mode: When button flips, preview updates automatically
|
||||
this.mathView.bind( 'display' ).to( this.displayButtonView, 'isOn' );
|
||||
|
||||
children = [
|
||||
this.mathLiveInputView,
|
||||
this.rawLatexLabel,
|
||||
this.rawLatexInputView,
|
||||
this.displayButtonView,
|
||||
this.previewLabel,
|
||||
this.mathView
|
||||
];
|
||||
} else {
|
||||
children = [
|
||||
this.mathLiveInputView,
|
||||
this.rawLatexLabel,
|
||||
this.rawLatexInputView,
|
||||
this.displayButtonView
|
||||
];
|
||||
children.push( previewLabel, this.mathView );
|
||||
}
|
||||
|
||||
// Add UI elements to template
|
||||
// --- 3. Sync Logic ---
|
||||
this._setupInputSync( previewEnabled );
|
||||
|
||||
// --- 4. Template Setup ---
|
||||
this.setTemplate( {
|
||||
tag: 'form',
|
||||
attributes: {
|
||||
class: [
|
||||
'ck',
|
||||
'ck-math-form',
|
||||
...popupClassName
|
||||
],
|
||||
class: [ 'ck', 'ck-math-form', ...popupClassName ],
|
||||
tabindex: '-1',
|
||||
spellcheck: 'false'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
tag: 'div',
|
||||
attributes: {
|
||||
class: [ 'ck-math-scroll' ]
|
||||
},
|
||||
children: [
|
||||
{
|
||||
tag: 'div',
|
||||
attributes: {
|
||||
class: [ 'ck-math-view' ]
|
||||
},
|
||||
children
|
||||
}
|
||||
]
|
||||
attributes: { class: [ 'ck-math-scroll' ] },
|
||||
children: [ { tag: 'div', attributes: { class: [ 'ck-math-view' ] }, children } ]
|
||||
},
|
||||
{
|
||||
tag: 'div',
|
||||
attributes: {
|
||||
class: [ 'ck-math-button-row' ]
|
||||
},
|
||||
children: [
|
||||
this.saveButtonView,
|
||||
this.cancelButtonView
|
||||
]
|
||||
attributes: { class: [ 'ck-math-button-row' ] },
|
||||
children: [ this.saveButtonView, this.cancelButtonView ]
|
||||
}
|
||||
]
|
||||
} );
|
||||
|
||||
// --- 5. Accessibility ---
|
||||
this._focusCycler = new FocusCycler( {
|
||||
focusables: this._focusables,
|
||||
focusTracker: this.focusTracker,
|
||||
keystrokeHandler: this.keystrokes,
|
||||
actions: { focusPrevious: 'shift + tab', focusNext: 'tab' }
|
||||
} );
|
||||
}
|
||||
|
||||
public override render(): void {
|
||||
super.render();
|
||||
|
||||
// Prevent default form submit event & trigger custom 'submit'
|
||||
submitHandler( {
|
||||
view: this
|
||||
} );
|
||||
submitHandler( { view: this } );
|
||||
|
||||
// Register form elements to focusable elements
|
||||
const childViews = [
|
||||
// Register focusables
|
||||
[
|
||||
this.mathLiveInputView,
|
||||
this.rawLatexInputView,
|
||||
this.displayButtonView,
|
||||
this.saveButtonView,
|
||||
this.cancelButtonView
|
||||
];
|
||||
|
||||
childViews.forEach( v => {
|
||||
].forEach( v => {
|
||||
if ( v.element ) {
|
||||
this._focusables.add( v );
|
||||
this.focusTracker.add( v.element );
|
||||
}
|
||||
} );
|
||||
|
||||
// Listen to keypresses inside form element
|
||||
if ( this.element ) {
|
||||
this.keystrokes.listenTo( this.element );
|
||||
}
|
||||
}
|
||||
|
||||
public override destroy(): void {
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this._focusCycler.focusFirst();
|
||||
if ( this.element ) this.keystrokes.listenTo( this.element );
|
||||
}
|
||||
|
||||
public get equation(): string {
|
||||
@ -176,151 +138,82 @@ export default class MainFormView extends View {
|
||||
}
|
||||
|
||||
public set equation( equation: string ) {
|
||||
const normalizedEquation = equation.trim();
|
||||
this.mathLiveInputView.value = normalizedEquation.length ? normalizedEquation : null;
|
||||
this.rawLatexInputView.value = normalizedEquation;
|
||||
if ( this.previewEnabled && this.mathView ) {
|
||||
this.mathView.value = normalizedEquation;
|
||||
}
|
||||
const norm = equation.trim();
|
||||
// Direct updates to the "source of truth"
|
||||
this.mathLiveInputView.value = norm.length ? norm : null;
|
||||
this.rawLatexInputView.value = norm;
|
||||
if ( this.mathView ) this.mathView.value = norm;
|
||||
}
|
||||
|
||||
public focusTracker: FocusTracker = new FocusTracker();
|
||||
public keystrokes: KeystrokeHandler = new KeystrokeHandler();
|
||||
private _focusables = new ViewCollection<FocusableView>();
|
||||
private _focusCycler: FocusCycler = new FocusCycler( {
|
||||
focusables: this._focusables,
|
||||
focusTracker: this.focusTracker,
|
||||
keystrokeHandler: this.keystrokes,
|
||||
actions: {
|
||||
focusPrevious: 'shift + tab',
|
||||
focusNext: 'tab'
|
||||
}
|
||||
} );
|
||||
public focus(): void {
|
||||
this._focusCycler.focusFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the MathLive visual equation editor.
|
||||
*
|
||||
* Handles bidirectional synchronization with the raw LaTeX textarea and preview.
|
||||
* Sets up split handlers for synchronization.
|
||||
*/
|
||||
private _createMathLiveInput() {
|
||||
const mathLiveInput = new MathLiveInputView( this.locale );
|
||||
private _setupInputSync( previewEnabled: boolean ): void {
|
||||
// Handler 1: MathLive -> Raw LaTeX
|
||||
this.mathLiveInputView.on( 'change:value', () => {
|
||||
let eq = ( this.mathLiveInputView.value ?? '' ).trim();
|
||||
|
||||
const onInput = () => {
|
||||
let equationInput = ( mathLiveInput.value ?? '' ).trim();
|
||||
|
||||
// If input has delimiters, strip them and update the display mode.
|
||||
if ( hasDelimiters( equationInput ) ) {
|
||||
const params = extractDelimiters( equationInput );
|
||||
equationInput = params.equation;
|
||||
// Delimiter Normalization
|
||||
if ( hasDelimiters( eq ) ) {
|
||||
const params = extractDelimiters( eq );
|
||||
eq = params.equation;
|
||||
this.displayButtonView.isOn = params.display;
|
||||
|
||||
// UX Fix: If we stripped delimiters, update the source
|
||||
// so the visual editor doesn't show them.
|
||||
if ( this.mathLiveInputView.value !== eq ) {
|
||||
this.mathLiveInputView.value = eq;
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedEquation = equationInput.length ? equationInput : null;
|
||||
|
||||
// Update self if needed.
|
||||
if ( mathLiveInput.value !== normalizedEquation ) {
|
||||
mathLiveInput.value = normalizedEquation;
|
||||
// Sync to Raw LaTeX
|
||||
if ( this.rawLatexInputView.value !== eq ) {
|
||||
this.rawLatexInputView.value = eq;
|
||||
}
|
||||
|
||||
// Sync to raw LaTeX textarea if its value is different.
|
||||
if ( this.rawLatexInputView.value !== equationInput ) {
|
||||
this.rawLatexInputView.value = equationInput;
|
||||
// Sync to Preview
|
||||
if ( previewEnabled && this.mathView && this.mathView.value !== eq ) {
|
||||
this.mathView.value = eq;
|
||||
}
|
||||
} );
|
||||
|
||||
// Handler 2: Raw LaTeX -> MathLive
|
||||
this.rawLatexInputView.on( 'change:value', () => {
|
||||
const eq = ( this.rawLatexInputView.value ?? '' ).trim();
|
||||
const normalized = eq.length ? eq : null;
|
||||
|
||||
// Sync to MathLive
|
||||
if ( this.mathLiveInputView.value !== normalized ) {
|
||||
this.mathLiveInputView.value = normalized;
|
||||
}
|
||||
|
||||
// Update preview if enabled and its value is different.
|
||||
if ( this.previewEnabled && this.mathView && this.mathView.value !== equationInput ) {
|
||||
this.mathView.value = equationInput;
|
||||
// Sync to Preview
|
||||
if ( previewEnabled && this.mathView && this.mathView.value !== eq ) {
|
||||
this.mathView.value = eq;
|
||||
}
|
||||
};
|
||||
|
||||
mathLiveInput.on( 'change:value', onInput );
|
||||
|
||||
return mathLiveInput;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the raw LaTeX textarea editor.
|
||||
*
|
||||
* Provides direct LaTeX code editing and synchronizes changes with the MathLive visual editor.
|
||||
*/
|
||||
private _createRawLatexInput() {
|
||||
const t = this.locale.t;
|
||||
const rawLatexInput = new RawLatexInputView( this.locale );
|
||||
rawLatexInput.label = t( 'LaTeX' );
|
||||
|
||||
// Sync raw LaTeX textarea changes to MathLive visual editor
|
||||
rawLatexInput.on( 'change:value', () => {
|
||||
const rawValue = rawLatexInput.value ?? '';
|
||||
const equationInput = rawValue.trim();
|
||||
|
||||
// Update MathLive visual editor
|
||||
const normalizedEquation = equationInput.length ? equationInput : null;
|
||||
if ( this.mathLiveInputView.value !== normalizedEquation ) {
|
||||
this.mathLiveInputView.value = normalizedEquation;
|
||||
}
|
||||
|
||||
// Update preview
|
||||
if ( this.previewEnabled && this.mathView ) {
|
||||
this.mathView.value = equationInput;
|
||||
}
|
||||
} );
|
||||
|
||||
return rawLatexInput;
|
||||
private _createButton( label: string, icon: string, className: string ): ButtonView {
|
||||
const btn = new ButtonView( this.locale );
|
||||
btn.set( { label, icon, tooltip: true } );
|
||||
btn.extendTemplate( { attributes: { class: className } } );
|
||||
return btn;
|
||||
}
|
||||
|
||||
private _createButton(
|
||||
label: string,
|
||||
icon: string,
|
||||
className: string,
|
||||
eventName: string | null
|
||||
) {
|
||||
const button = new ButtonView( this.locale );
|
||||
private _createDisplayButton( t: ( str: string ) => string ): SwitchButtonView {
|
||||
const btn = new SwitchButtonView( this.locale );
|
||||
btn.set( { label: t( 'Display mode' ), withText: true } );
|
||||
btn.extendTemplate( { attributes: { class: 'ck-button-display-toggle' } } );
|
||||
|
||||
button.set( {
|
||||
label,
|
||||
icon,
|
||||
tooltip: true
|
||||
btn.on( 'execute', () => {
|
||||
btn.isOn = !btn.isOn;
|
||||
// mathView updates automatically via bind()
|
||||
} );
|
||||
|
||||
button.extendTemplate( {
|
||||
attributes: {
|
||||
class: className
|
||||
}
|
||||
} );
|
||||
|
||||
if ( eventName ) {
|
||||
button.delegate( 'execute' ).to( this, eventName );
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private _createDisplayButton() {
|
||||
const t = this.locale.t;
|
||||
|
||||
const switchButton = new SwitchButtonView( this.locale );
|
||||
|
||||
switchButton.set( {
|
||||
label: t( 'Display mode' ),
|
||||
withText: true
|
||||
} );
|
||||
|
||||
switchButton.extendTemplate( {
|
||||
attributes: {
|
||||
class: 'ck-button-display-toggle'
|
||||
}
|
||||
} );
|
||||
|
||||
switchButton.on( 'execute', () => {
|
||||
// Toggle state
|
||||
switchButton.isOn = !switchButton.isOn;
|
||||
|
||||
if ( this.previewEnabled && this.mathView ) {
|
||||
// Update preview view
|
||||
this.mathView.display = switchButton.isOn;
|
||||
}
|
||||
} );
|
||||
|
||||
return switchButton;
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,33 +1,27 @@
|
||||
import { View, type Locale } from 'ckeditor5';
|
||||
import { MathfieldElement } from 'mathlive';
|
||||
import 'mathlive'; // Import side-effects only (registers the <math-field> tag)
|
||||
|
||||
/**
|
||||
* A view that wraps the MathLive `<math-field>` web component for interactive LaTeX equation editing.
|
||||
*
|
||||
* MathLive provides a rich math input experience with live rendering, virtual keyboard support,
|
||||
* and various accessibility features.
|
||||
*
|
||||
* @see https://cortexjs.io/mathlive/
|
||||
* A wrapper for the MathLive <math-field> component.
|
||||
* Uses 'any' typing to avoid TypeScript module resolution errors.
|
||||
*/
|
||||
export default class MathLiveInputView extends View {
|
||||
/**
|
||||
* The current LaTeX value of the math field.
|
||||
*
|
||||
* The current LaTeX value.
|
||||
* @observable
|
||||
*/
|
||||
public declare value: string | null;
|
||||
|
||||
/**
|
||||
* Whether the input is in read-only mode.
|
||||
*
|
||||
* Read-only state.
|
||||
* @observable
|
||||
*/
|
||||
public declare isReadOnly: boolean;
|
||||
|
||||
/**
|
||||
* Reference to the `<math-field>` DOM element.
|
||||
* Reference to the DOM element (typed as any to prevent TS errors).
|
||||
*/
|
||||
public mathfield: HTMLElement | null = null;
|
||||
public mathfield: any = null;
|
||||
|
||||
constructor( locale: Locale ) {
|
||||
super( locale );
|
||||
@ -43,68 +37,53 @@ export default class MathLiveInputView extends View {
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public override render(): void {
|
||||
super.render();
|
||||
// Disable sounds before creating mathfield
|
||||
if (typeof MathfieldElement !== 'undefined') {
|
||||
MathfieldElement.soundsDirectory = null;
|
||||
}
|
||||
|
||||
// (Removed global area click-to-focus logic; focusing now relies on direct field interaction.)
|
||||
|
||||
// Create the MathLive math-field custom element
|
||||
// 1. Create element using DOM API instead of Class constructor
|
||||
// This avoids "Module has no exported member" errors.
|
||||
const mathfield = document.createElement( 'math-field' ) as any;
|
||||
this.mathfield = mathfield;
|
||||
|
||||
// Configure the virtual keyboard to be manually controlled (shown by user interaction)
|
||||
mathfield.setAttribute( 'virtual-keyboard-mode', 'manual' );
|
||||
// 2. Configure Options
|
||||
mathfield.mathVirtualKeyboardPolicy = 'manual';
|
||||
|
||||
// Set initial value
|
||||
const initialValue = this.value ?? '';
|
||||
if ( initialValue ) {
|
||||
( mathfield as any ).value = initialValue;
|
||||
// Disable sounds
|
||||
const MathfieldElement = customElements.get( 'math-field' );
|
||||
if ( MathfieldElement ) {
|
||||
( MathfieldElement as any ).soundsDirectory = null;
|
||||
( MathfieldElement as any ).plonkSound = null;
|
||||
}
|
||||
|
||||
// Bind readonly state
|
||||
if ( this.isReadOnly ) {
|
||||
( mathfield as any ).readOnly = true;
|
||||
}
|
||||
// 3. Set Initial State
|
||||
mathfield.value = this.value ?? '';
|
||||
mathfield.readOnly = this.isReadOnly;
|
||||
|
||||
// Sync math-field changes to observable value
|
||||
// 4. Bind Events (DOM -> Observable)
|
||||
mathfield.addEventListener( 'input', () => {
|
||||
const nextValue: string = ( mathfield as any ).value;
|
||||
this.value = nextValue.length ? nextValue : null;
|
||||
const val = mathfield.value;
|
||||
this.value = val.length ? val : null;
|
||||
} );
|
||||
|
||||
// Sync observable value changes back to math-field
|
||||
this.on( 'change:value', () => {
|
||||
const nextValue = this.value ?? '';
|
||||
if ( ( mathfield as any ).value !== nextValue ) {
|
||||
( mathfield as any ).value = nextValue;
|
||||
// 5. Bind Events (Observable -> DOM)
|
||||
this.on( 'change:value', ( _evt, _name, nextValue ) => {
|
||||
if ( mathfield.value !== nextValue ) {
|
||||
mathfield.value = nextValue ?? '';
|
||||
}
|
||||
} );
|
||||
|
||||
// Sync readonly state to math-field
|
||||
this.on( 'change:isReadOnly', () => {
|
||||
( mathfield as any ).readOnly = this.isReadOnly;
|
||||
this.on( 'change:isReadOnly', ( _evt, _name, nextValue ) => {
|
||||
mathfield.readOnly = nextValue;
|
||||
} );
|
||||
|
||||
// 6. Mount
|
||||
this.element?.appendChild( mathfield );
|
||||
this.mathfield = mathfield;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses the math-field element.
|
||||
*/
|
||||
public focus(): void {
|
||||
this.mathfield?.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public override destroy(): void {
|
||||
if ( this.mathfield ) {
|
||||
this.mathfield.remove();
|
||||
|
||||
@ -2,44 +2,44 @@ import { View, type Locale } from 'ckeditor5';
|
||||
import type { KatexOptions } from '../typings-external.js';
|
||||
import { renderEquation } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Configuration options for the MathView.
|
||||
*/
|
||||
export interface MathViewOptions {
|
||||
engine: 'mathjax' | 'katex' | ( ( equation: string, element: HTMLElement, display: boolean ) => void );
|
||||
lazyLoad: undefined | ( () => Promise<void> );
|
||||
previewUid: string;
|
||||
previewClassName: Array<string>;
|
||||
katexRenderOptions: KatexOptions;
|
||||
}
|
||||
|
||||
export default class MathView extends View {
|
||||
/**
|
||||
* The LaTeX equation value to render.
|
||||
* @observable
|
||||
*/
|
||||
public declare value: string;
|
||||
|
||||
/**
|
||||
* Whether to render in display mode (centered) or inline.
|
||||
* @observable
|
||||
*/
|
||||
public declare display: boolean;
|
||||
public previewUid: string;
|
||||
public previewClassName: Array<string>;
|
||||
public katexRenderOptions: KatexOptions;
|
||||
public engine:
|
||||
| 'mathjax'
|
||||
| 'katex'
|
||||
| ( ( equation: string, element: HTMLElement, display: boolean ) => void );
|
||||
public lazyLoad: undefined | ( () => Promise<void> );
|
||||
|
||||
constructor(
|
||||
engine:
|
||||
| 'mathjax'
|
||||
| 'katex'
|
||||
| ( (
|
||||
equation: string,
|
||||
element: HTMLElement,
|
||||
display: boolean,
|
||||
) => void ),
|
||||
lazyLoad: undefined | ( () => Promise<void> ),
|
||||
locale: Locale,
|
||||
previewUid: string,
|
||||
previewClassName: Array<string>,
|
||||
katexRenderOptions: KatexOptions
|
||||
) {
|
||||
/**
|
||||
* Configuration options passed during initialization.
|
||||
*/
|
||||
private options: MathViewOptions;
|
||||
|
||||
constructor( locale: Locale, options: MathViewOptions ) {
|
||||
super( locale );
|
||||
|
||||
this.engine = engine;
|
||||
this.lazyLoad = lazyLoad;
|
||||
this.previewUid = previewUid;
|
||||
this.katexRenderOptions = katexRenderOptions;
|
||||
this.previewClassName = previewClassName;
|
||||
this.options = options;
|
||||
|
||||
this.set( 'value', '' );
|
||||
this.set( 'display', false );
|
||||
|
||||
// Update rendering when state changes.
|
||||
// Checking isRendered prevents errors during initialization.
|
||||
this.on( 'change', () => {
|
||||
if ( this.isRendered ) {
|
||||
this.updateMath();
|
||||
@ -59,13 +59,13 @@ export default class MathView extends View {
|
||||
void renderEquation(
|
||||
this.value,
|
||||
this.element,
|
||||
this.engine,
|
||||
this.lazyLoad,
|
||||
this.options.engine,
|
||||
this.options.lazyLoad,
|
||||
this.display,
|
||||
true,
|
||||
this.previewUid,
|
||||
this.previewClassName,
|
||||
this.katexRenderOptions
|
||||
true, // isPreview
|
||||
this.options.previewUid,
|
||||
this.options.previewClassName,
|
||||
this.options.katexRenderOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,21 +2,16 @@ import { LabeledFieldView, createLabeledTextarea, type Locale, type TextareaView
|
||||
|
||||
/**
|
||||
* A labeled textarea view for direct LaTeX code editing.
|
||||
*
|
||||
* This provides a plain text input for users who prefer to write LaTeX syntax directly
|
||||
* or need to paste/edit raw LaTeX code.
|
||||
*/
|
||||
export default class RawLatexInputView extends LabeledFieldView<TextareaView> {
|
||||
/**
|
||||
* The current LaTeX value.
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
public declare value: string;
|
||||
|
||||
/**
|
||||
* Whether the input is in read-only mode.
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
public declare isReadOnly: boolean;
|
||||
@ -29,21 +24,23 @@ export default class RawLatexInputView extends LabeledFieldView<TextareaView> {
|
||||
|
||||
const fieldView = this.fieldView;
|
||||
|
||||
// Sync textarea input to observable value
|
||||
// 1. Sync: DOM (Textarea) -> Observable
|
||||
// We listen to the native 'input' event on the child view
|
||||
fieldView.on( 'input', () => {
|
||||
if ( fieldView.element ) {
|
||||
this.value = fieldView.element.value;
|
||||
}
|
||||
} );
|
||||
|
||||
// Sync observable value changes back to textarea
|
||||
// 2. Sync: Observable -> DOM (Textarea)
|
||||
this.on( 'change:value', () => {
|
||||
// Check for difference to avoid cursor jumping or unnecessary updates
|
||||
if ( fieldView.element && fieldView.element.value !== this.value ) {
|
||||
fieldView.element.value = this.value;
|
||||
}
|
||||
} );
|
||||
|
||||
// Sync readonly state (manual binding to avoid CKEditor observable rebind error)
|
||||
// 3. Sync: ReadOnly State
|
||||
this.on( 'change:isReadOnly', () => {
|
||||
if ( fieldView.element ) {
|
||||
fieldView.element.readOnly = this.isReadOnly;
|
||||
@ -51,12 +48,7 @@ export default class RawLatexInputView extends LabeledFieldView<TextareaView> {
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public override render(): void {
|
||||
super.render();
|
||||
// All styling is handled via CSS in mathform.css
|
||||
// (Removed obsolete mousedown propagation; no longer needed after resize & gray-area click removal.)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user