From 162c076a145194b80f3275b4f85fbe790d0a332b Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Tue, 2 Dec 2025 22:30:37 +0100 Subject: [PATCH] Improve MathLive integration and lazy loading --- .../src/ui/mathliveinputview.ts | 114 +++++++++--------- packages/ckeditor5-math/src/ui/mathview.ts | 48 +++++--- packages/ckeditor5-math/tests/lazyload.ts | 10 ++ 3 files changed, 96 insertions(+), 76 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts index f761be787..a2d540228 100644 --- a/packages/ckeditor5-math/src/ui/mathliveinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathliveinputview.ts @@ -1,104 +1,98 @@ import { View, type Locale } from 'ckeditor5'; -import 'mathlive'; // Import side-effects only (registers the tag) -/** - * Interface describing the custom element. - */ interface MathFieldElement extends HTMLElement { value: string; readOnly: boolean; mathVirtualKeyboardPolicy: string; - // Interface includes the shortcuts property - inlineShortcuts: Record; + inlineShortcuts?: Record; } -/** - * A wrapper for the MathLive component. - */ export default class MathLiveInputView extends View { - /** - * The current LaTeX value. - * @observable - */ public declare value: string | null; - - /** - * Read-only state. - * @observable - */ public declare isReadOnly: boolean; - - /** - * Reference to the DOM element. - * Typed as MathFieldElement | null for proper TS support. - */ public mathfield: MathFieldElement | null = null; - constructor( locale: Locale ) { - super( locale ); + constructor(locale: Locale) { + super(locale); + this.set('value', null); + this.set('isReadOnly', false); - this.set( 'value', null ); - this.set( 'isReadOnly', false ); - - this.setTemplate( { + this.setTemplate({ tag: 'div', attributes: { - class: [ 'ck', 'ck-mathlive-input' ] + class: ['ck', 'ck-mathlive-input'] } - } ); + }); } public override render(): void { super.render(); + this._loadMathLive(); + } - // 1. Create element with the specific type - const mathfield = document.createElement( 'math-field' ) as MathFieldElement; + private async _loadMathLive(): Promise { + try { + await import('mathlive'); + await customElements.whenDefined('math-field'); - // 2. Configure Options + // Configure global MathLive settings + const MathfieldClass = customElements.get( 'math-field' ) as any; + if ( MathfieldClass ) { + MathfieldClass.soundsDirectory = null; + MathfieldClass.plonkSound = null; + } + + if (!this.element) return; + + this._createMathField(); + } catch (error) { + console.error('MathLive load failed:', error); + if (this.element) { + this.element.textContent = 'Math editor unavailable'; + } + } + } + + private _createMathField(): void { + if (!this.element) return; + + const mathfield = document.createElement('math-field') as MathFieldElement; + + // Instance-level config (no prototype pollution) mathfield.mathVirtualKeyboardPolicy = 'manual'; - //Disable differential D - mathfield.addEventListener( 'mount', () => { + // Configure shortcuts after mount + mathfield.addEventListener('mount', () => { mathfield.inlineShortcuts = { - ...mathfield.inlineShortcuts, // Safe to read now + ...mathfield.inlineShortcuts, dx: 'dx', dy: 'dy', dt: 'dt' }; - } ); + }, { once: true }); - - // Disable sounds safely - const MathfieldConstructor = customElements.get( 'math-field' ); - if ( MathfieldConstructor ) { - const proto = MathfieldConstructor as any; - if ( proto.soundsDirectory !== null ) proto.soundsDirectory = null; - if ( proto.plonkSound !== null ) proto.plonkSound = null; - } - - // 3. Set Initial State + // Initial state mathfield.value = this.value ?? ''; mathfield.readOnly = this.isReadOnly; - // 4. Bind Events (DOM -> Observable) - mathfield.addEventListener( 'input', () => { + // DOM -> Observable + mathfield.addEventListener('input', () => { const val = mathfield.value; this.value = val.length ? val : null; - } ); + }); - // 5. Bind Events (Observable -> DOM) - this.on( 'change:value', ( _evt, _name, nextValue ) => { - if ( mathfield.value !== nextValue ) { + // Observable -> DOM + this.on('change:value', (_evt, _name, nextValue) => { + if (mathfield.value !== nextValue) { mathfield.value = nextValue ?? ''; } - } ); + }); - this.on( 'change:isReadOnly', ( _evt, _name, nextValue ) => { + this.on('change:isReadOnly', (_evt, _name, nextValue) => { mathfield.readOnly = nextValue; - } ); + }); - // 6. Mount to the wrapper view - this.element?.appendChild( mathfield ); + this.element.appendChild(mathfield); this.mathfield = mathfield; } @@ -107,7 +101,7 @@ export default class MathLiveInputView extends View { } public override destroy(): void { - if ( this.mathfield ) { + if (this.mathfield) { this.mathfield.remove(); this.mathfield = null; } diff --git a/packages/ckeditor5-math/src/ui/mathview.ts b/packages/ckeditor5-math/src/ui/mathview.ts index 42a4c0df1..87af15d08 100644 --- a/packages/ckeditor5-math/src/ui/mathview.ts +++ b/packages/ckeditor5-math/src/ui/mathview.ts @@ -55,23 +55,39 @@ export default class MathView extends View { } public updateMath(): void { - if ( this.element ) { - - // This prevents the new render from appending to the old one. - this.element.textContent = ''; - - void renderEquation( - this.value, - this.element, - this.options.engine, - this.options.lazyLoad, - this.display, - true, // isPreview - this.options.previewUid, - this.options.previewClassName, - this.options.katexRenderOptions - ); + if (!this.element) { + return; } + + // Handle empty equations + if (!this.value || !this.value.trim()) { + this.element.textContent = ''; + this.element.classList.remove('ck-math-render-error'); + return; + } + + // Clear previous render + this.element.textContent = ''; + this.element.classList.remove('ck-math-render-error'); + + renderEquation( + this.value, + this.element, + this.options.engine, + this.options.lazyLoad, + this.display, + true, // isPreview + this.options.previewUid, + this.options.previewClassName, + this.options.katexRenderOptions + ).catch(error => { + console.error('Math rendering failed:', error); + + if (this.element) { + this.element.textContent = 'Error rendering equation'; + this.element.classList.add('ck-math-render-error'); + } + }); } public override render(): void { diff --git a/packages/ckeditor5-math/tests/lazyload.ts b/packages/ckeditor5-math/tests/lazyload.ts index 126507850..573984368 100644 --- a/packages/ckeditor5-math/tests/lazyload.ts +++ b/packages/ckeditor5-math/tests/lazyload.ts @@ -37,6 +37,7 @@ describe( 'Lazy load', () => { await buildEditor( { math: { engine: 'katex', + enablePreview: true, lazyLoad: async () => { lazyLoadInvoked = true; } @@ -44,6 +45,15 @@ describe( 'Lazy load', () => { } ); mathUIFeature._showUI(); + + // Trigger render with a non-empty value to bypass empty check optimization + if ( mathUIFeature.formView ) { + mathUIFeature.formView.equation = 'x^2'; + } + + // Wait for async rendering and lazy loading + await new Promise( resolve => setTimeout( resolve, 100 ) ); + expect( lazyLoadInvoked ).to.be.true; } ); } );