mirror of
https://github.com/zadam/trilium.git
synced 2025-12-04 22:44:25 +01:00
Compare commits
4 Commits
b3188b7e45
...
26e06956d6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26e06956d6 | ||
|
|
162c076a14 | ||
|
|
9386465de7 | ||
|
|
acca22f3a1 |
@ -48,8 +48,7 @@ export default class MainFormView extends View {
|
||||
this.rawLatexInputView = new RawLatexInputView( locale );
|
||||
this.rawLatexInputView.label = t( 'LaTeX' );
|
||||
|
||||
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save' );
|
||||
this.saveButtonView.type = 'submit';
|
||||
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', 'submit' );
|
||||
|
||||
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel' );
|
||||
this.cancelButtonView.delegate( 'execute' ).to( this, 'cancel' );
|
||||
@ -150,58 +149,67 @@ export default class MainFormView extends View {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up split handlers for synchronization.
|
||||
* Checks if a view currently has focus.
|
||||
*/
|
||||
private _setupInputSync( previewEnabled: boolean ): void {
|
||||
// Handler 1: MathLive -> Raw LaTeX
|
||||
this.mathLiveInputView.on( 'change:value', () => {
|
||||
let eq = ( this.mathLiveInputView.value ?? '' ).trim();
|
||||
private _isViewFocused(view: View): boolean {
|
||||
const el = view.element;
|
||||
const active = document.activeElement;
|
||||
return !!(el && active && el.contains(active));
|
||||
}
|
||||
|
||||
// Delimiter Normalization
|
||||
if ( hasDelimiters( eq ) ) {
|
||||
const params = extractDelimiters( eq );
|
||||
/**
|
||||
* Sets up synchronization with Focus Gating.
|
||||
*/
|
||||
private _setupInputSync(previewEnabled: boolean): void {
|
||||
const updatePreview = (eq: string) => {
|
||||
if (previewEnabled && this.mathView && this.mathView.value !== eq) {
|
||||
this.mathView.value = eq;
|
||||
}
|
||||
};
|
||||
|
||||
// Handler 1: MathLive -> Raw LaTeX + Preview
|
||||
this.mathLiveInputView.on('change:value', () => {
|
||||
let eq = (this.mathLiveInputView.value ?? '').trim();
|
||||
|
||||
// Strip delimiters if present (e.g. pasted content)
|
||||
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 ) {
|
||||
// Only strip delimiters if not actively editing
|
||||
if (!this._isViewFocused(this.mathLiveInputView) && this.mathLiveInputView.value !== eq) {
|
||||
this.mathLiveInputView.value = eq;
|
||||
}
|
||||
}
|
||||
|
||||
// Sync to Raw LaTeX
|
||||
if ( this.rawLatexInputView.value !== eq ) {
|
||||
// Sync to Raw LaTeX only if user isn't typing there
|
||||
if (!this._isViewFocused(this.rawLatexInputView) && this.rawLatexInputView.value !== eq) {
|
||||
this.rawLatexInputView.value = eq;
|
||||
}
|
||||
|
||||
// Sync to Preview
|
||||
if ( previewEnabled && this.mathView && this.mathView.value !== eq ) {
|
||||
this.mathView.value = eq;
|
||||
}
|
||||
} );
|
||||
updatePreview(eq);
|
||||
});
|
||||
|
||||
// Handler 2: Raw LaTeX -> MathLive
|
||||
this.rawLatexInputView.on( 'change:value', () => {
|
||||
const eq = ( this.rawLatexInputView.value ?? '' ).trim();
|
||||
// Handler 2: Raw LaTeX -> MathLive + Preview
|
||||
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 ) {
|
||||
// Sync to MathLive only if user isn't interacting with it
|
||||
if (!this._isViewFocused(this.mathLiveInputView) && this.mathLiveInputView.value !== normalized) {
|
||||
this.mathLiveInputView.value = normalized;
|
||||
}
|
||||
|
||||
// Sync to Preview
|
||||
if ( previewEnabled && this.mathView && this.mathView.value !== eq ) {
|
||||
this.mathView.value = eq;
|
||||
}
|
||||
} );
|
||||
updatePreview(eq);
|
||||
});
|
||||
}
|
||||
|
||||
private _createButton( label: string, icon: string, className: string ): ButtonView {
|
||||
private _createButton( label: string, icon: string, className: string, type?: 'submit' | 'button' ): ButtonView {
|
||||
const btn = new ButtonView( this.locale );
|
||||
btn.set( { label, icon, tooltip: true } );
|
||||
btn.extendTemplate( { attributes: { class: className } } );
|
||||
if (type) btn.type = type;
|
||||
return btn;
|
||||
}
|
||||
|
||||
|
||||
@ -1,104 +1,98 @@
|
||||
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 {
|
||||
value: string;
|
||||
readOnly: boolean;
|
||||
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 {
|
||||
/**
|
||||
* 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<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
} );
|
||||
} );
|
||||
|
||||
@ -212,3 +212,13 @@
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.ck-math-render-error {
|
||||
color: var(--ck-color-error-text, #db1d1d);
|
||||
padding: var(--ck-spacing-small);
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
border: 1px dashed var(--ck-color-error-text, #db1d1d);
|
||||
border-radius: 2px;
|
||||
background: var(--ck-color-base-background, #fff);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user