mirror of
https://github.com/zadam/trilium.git
synced 2025-12-04 22:44:25 +01:00
Improve MathLive integration and lazy loading
This commit is contained in:
parent
9386465de7
commit
162c076a14
@ -1,104 +1,98 @@
|
|||||||
import { View, type Locale } from 'ckeditor5';
|
import { View, type Locale } from 'ckeditor5';
|
||||||
import 'mathlive'; // Import side-effects only (registers the <math-field> tag)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface describing the custom <math-field> element.
|
|
||||||
*/
|
|
||||||
interface MathFieldElement extends HTMLElement {
|
interface MathFieldElement extends HTMLElement {
|
||||||
value: string;
|
value: string;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
mathVirtualKeyboardPolicy: string;
|
mathVirtualKeyboardPolicy: string;
|
||||||
// Interface includes the shortcuts property
|
inlineShortcuts?: Record<string, string>;
|
||||||
inlineShortcuts: Record<string, string>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A wrapper for the MathLive <math-field> component.
|
|
||||||
*/
|
|
||||||
export default class MathLiveInputView extends View {
|
export default class MathLiveInputView extends View {
|
||||||
/**
|
|
||||||
* The current LaTeX value.
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
public declare value: string | null;
|
public declare value: string | null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Read-only state.
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
public declare isReadOnly: boolean;
|
public declare isReadOnly: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* Reference to the DOM element.
|
|
||||||
* Typed as MathFieldElement | null for proper TS support.
|
|
||||||
*/
|
|
||||||
public mathfield: MathFieldElement | null = null;
|
public mathfield: MathFieldElement | null = null;
|
||||||
|
|
||||||
constructor( locale: Locale ) {
|
constructor(locale: Locale) {
|
||||||
super( locale );
|
super(locale);
|
||||||
|
this.set('value', null);
|
||||||
|
this.set('isReadOnly', false);
|
||||||
|
|
||||||
this.set( 'value', null );
|
this.setTemplate({
|
||||||
this.set( 'isReadOnly', false );
|
|
||||||
|
|
||||||
this.setTemplate( {
|
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
attributes: {
|
attributes: {
|
||||||
class: [ 'ck', 'ck-mathlive-input' ]
|
class: ['ck', 'ck-mathlive-input']
|
||||||
}
|
}
|
||||||
} );
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override render(): void {
|
public override render(): void {
|
||||||
super.render();
|
super.render();
|
||||||
|
this._loadMathLive();
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Create element with the specific type
|
private async _loadMathLive(): Promise<void> {
|
||||||
const mathfield = document.createElement( 'math-field' ) as MathFieldElement;
|
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';
|
mathfield.mathVirtualKeyboardPolicy = 'manual';
|
||||||
|
|
||||||
//Disable differential D
|
// Configure shortcuts after mount
|
||||||
mathfield.addEventListener( 'mount', () => {
|
mathfield.addEventListener('mount', () => {
|
||||||
mathfield.inlineShortcuts = {
|
mathfield.inlineShortcuts = {
|
||||||
...mathfield.inlineShortcuts, // Safe to read now
|
...mathfield.inlineShortcuts,
|
||||||
dx: 'dx',
|
dx: 'dx',
|
||||||
dy: 'dy',
|
dy: 'dy',
|
||||||
dt: 'dt'
|
dt: 'dt'
|
||||||
};
|
};
|
||||||
} );
|
}, { once: true });
|
||||||
|
|
||||||
|
// Initial state
|
||||||
// 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
|
|
||||||
mathfield.value = this.value ?? '';
|
mathfield.value = this.value ?? '';
|
||||||
mathfield.readOnly = this.isReadOnly;
|
mathfield.readOnly = this.isReadOnly;
|
||||||
|
|
||||||
// 4. Bind Events (DOM -> Observable)
|
// DOM -> Observable
|
||||||
mathfield.addEventListener( 'input', () => {
|
mathfield.addEventListener('input', () => {
|
||||||
const val = mathfield.value;
|
const val = mathfield.value;
|
||||||
this.value = val.length ? val : null;
|
this.value = val.length ? val : null;
|
||||||
} );
|
});
|
||||||
|
|
||||||
// 5. Bind Events (Observable -> DOM)
|
// Observable -> DOM
|
||||||
this.on( 'change:value', ( _evt, _name, nextValue ) => {
|
this.on('change:value', (_evt, _name, nextValue) => {
|
||||||
if ( mathfield.value !== nextValue ) {
|
if (mathfield.value !== nextValue) {
|
||||||
mathfield.value = nextValue ?? '';
|
mathfield.value = nextValue ?? '';
|
||||||
}
|
}
|
||||||
} );
|
});
|
||||||
|
|
||||||
this.on( 'change:isReadOnly', ( _evt, _name, nextValue ) => {
|
this.on('change:isReadOnly', (_evt, _name, nextValue) => {
|
||||||
mathfield.readOnly = nextValue;
|
mathfield.readOnly = nextValue;
|
||||||
} );
|
});
|
||||||
|
|
||||||
// 6. Mount to the wrapper view
|
this.element.appendChild(mathfield);
|
||||||
this.element?.appendChild( mathfield );
|
|
||||||
this.mathfield = mathfield;
|
this.mathfield = mathfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +101,7 @@ export default class MathLiveInputView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override destroy(): void {
|
public override destroy(): void {
|
||||||
if ( this.mathfield ) {
|
if (this.mathfield) {
|
||||||
this.mathfield.remove();
|
this.mathfield.remove();
|
||||||
this.mathfield = null;
|
this.mathfield = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,23 +55,39 @@ export default class MathView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public updateMath(): void {
|
public updateMath(): void {
|
||||||
if ( this.element ) {
|
if (!this.element) {
|
||||||
|
return;
|
||||||
// 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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
public override render(): void {
|
||||||
|
|||||||
@ -37,6 +37,7 @@ describe( 'Lazy load', () => {
|
|||||||
await buildEditor( {
|
await buildEditor( {
|
||||||
math: {
|
math: {
|
||||||
engine: 'katex',
|
engine: 'katex',
|
||||||
|
enablePreview: true,
|
||||||
lazyLoad: async () => {
|
lazyLoad: async () => {
|
||||||
lazyLoadInvoked = true;
|
lazyLoadInvoked = true;
|
||||||
}
|
}
|
||||||
@ -44,6 +45,15 @@ describe( 'Lazy load', () => {
|
|||||||
} );
|
} );
|
||||||
|
|
||||||
mathUIFeature._showUI();
|
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;
|
expect( lazyLoadInvoked ).to.be.true;
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user