mirror of
https://github.com/zadam/trilium.git
synced 2026-01-11 17:14:26 +01:00
Refactor: Combine MathLive and LaTeX inputs into one single component
This commit is contained in:
parent
162c076a14
commit
827c8e0e72
@ -33,10 +33,8 @@ export default class MathUI extends Plugin {
|
||||
|
||||
public override destroy(): void {
|
||||
super.destroy();
|
||||
|
||||
this.formView?.destroy();
|
||||
|
||||
// Destroy preview element
|
||||
const previewEl = document.getElementById( this._previewUid );
|
||||
if ( previewEl ) {
|
||||
previewEl.parentNode?.removeChild( previewEl );
|
||||
@ -56,7 +54,7 @@ export default class MathUI extends Plugin {
|
||||
this._balloon.showStack( 'main' );
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.formView?.mathLiveInputView.focus();
|
||||
this.formView?.mathInputView.focus();
|
||||
});
|
||||
}
|
||||
|
||||
@ -87,15 +85,15 @@ export default class MathUI extends Plugin {
|
||||
mathConfig.popupClassName!
|
||||
);
|
||||
|
||||
formView.mathLiveInputView.bind( 'value' ).to( mathCommand, 'value' );
|
||||
formView.mathInputView.bind( 'value' ).to( mathCommand, 'value' );
|
||||
formView.displayButtonView.bind( 'isOn' ).to( mathCommand, 'display' );
|
||||
|
||||
// Form elements should be read-only when corresponding commands are disabled.
|
||||
formView.mathLiveInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', value => !value );
|
||||
formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', (value: boolean) => !value );
|
||||
formView.saveButtonView.bind( 'isEnabled' ).to(
|
||||
mathCommand,
|
||||
'isEnabled',
|
||||
formView.mathLiveInputView,
|
||||
formView.mathInputView,
|
||||
'value',
|
||||
( commandEnabled, equation ) => {
|
||||
const normalizedEquation = ( equation ?? '' ).trim();
|
||||
@ -104,24 +102,21 @@ export default class MathUI extends Plugin {
|
||||
);
|
||||
formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand, 'isEnabled' );
|
||||
|
||||
// Listen to submit button click
|
||||
this.listenTo( formView, 'submit', () => {
|
||||
editor.execute( 'math', formView.equation, formView.displayButtonView.isOn, mathConfig.outputType, mathConfig.forceOutputType );
|
||||
this._closeFormView();
|
||||
} );
|
||||
|
||||
// Listen to cancel button click
|
||||
this.listenTo( formView, 'cancel', () => {
|
||||
this._closeFormView();
|
||||
} );
|
||||
|
||||
// Close plugin ui, if esc is pressed (while ui is focused)
|
||||
formView.keystrokes.set( 'esc', ( _data, cancel ) => {
|
||||
this._closeFormView();
|
||||
cancel();
|
||||
} );
|
||||
|
||||
// Allow pressing Enter to submit changes, and use Shift+Enter to insert a new line
|
||||
// Enter to submit, Shift+Enter for newline
|
||||
formView.keystrokes.set('enter', (data, cancel) => {
|
||||
if (!data.shiftKey) {
|
||||
formView.fire('submit');
|
||||
@ -157,13 +152,11 @@ export default class MathUI extends Plugin {
|
||||
} );
|
||||
|
||||
if ( this._balloon.visibleView === this.formView ) {
|
||||
this.formView.mathLiveInputView.focus();
|
||||
this.formView.mathInputView.focus();
|
||||
}
|
||||
|
||||
// Show preview element
|
||||
const previewEl = document.getElementById( this._previewUid );
|
||||
if ( previewEl && this.formView.mathView ) {
|
||||
// Force refresh preview
|
||||
this.formView.mathView.updateMath();
|
||||
}
|
||||
|
||||
@ -171,9 +164,6 @@ export default class MathUI extends Plugin {
|
||||
this.formView.displayButtonView.isOn = mathCommand.display || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
public _hideUI(): void {
|
||||
if ( !this._isFormInPanel ) {
|
||||
return;
|
||||
@ -185,8 +175,6 @@ export default class MathUI extends Plugin {
|
||||
this.stopListening( this._balloon, 'change:visibleView' );
|
||||
|
||||
editor.editing.view.focus();
|
||||
|
||||
// Remove form first because it's on top of the stack.
|
||||
this._removeFormView();
|
||||
}
|
||||
|
||||
@ -202,10 +190,8 @@ export default class MathUI extends Plugin {
|
||||
private _removeFormView() {
|
||||
if ( this._isFormInPanel && this.formView ) {
|
||||
this.formView.saveButtonView.focus();
|
||||
|
||||
this._balloon.remove( this.formView );
|
||||
|
||||
// Hide preview element
|
||||
const previewEl = document.getElementById( this._previewUid );
|
||||
if ( previewEl ) {
|
||||
previewEl.style.visibility = 'hidden';
|
||||
|
||||
@ -15,8 +15,7 @@ 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, { type MathViewOptions } from './mathview.js';
|
||||
import MathLiveInputView from './mathliveinputview.js';
|
||||
import RawLatexInputView from './rawlatexinputview.js';
|
||||
import MathInputView from './mathinputview.js';
|
||||
import '../../theme/mathform.css';
|
||||
|
||||
export default class MainFormView extends View {
|
||||
@ -24,8 +23,7 @@ export default class MainFormView extends View {
|
||||
public cancelButtonView: ButtonView;
|
||||
public displayButtonView: SwitchButtonView;
|
||||
|
||||
public mathLiveInputView: MathLiveInputView;
|
||||
public rawLatexInputView: RawLatexInputView;
|
||||
public mathInputView: MathInputView;
|
||||
public mathView?: MathView;
|
||||
|
||||
public focusTracker = new FocusTracker();
|
||||
@ -42,24 +40,17 @@ export default class MainFormView extends View {
|
||||
super( locale );
|
||||
const t = locale.t;
|
||||
|
||||
// --- 1. View Initialization ---
|
||||
|
||||
this.mathLiveInputView = new MathLiveInputView( locale );
|
||||
this.rawLatexInputView = new RawLatexInputView( locale );
|
||||
this.rawLatexInputView.label = t( 'LaTeX' );
|
||||
|
||||
// Create views
|
||||
this.mathInputView = new MathInputView( locale );
|
||||
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' );
|
||||
|
||||
this.displayButtonView = this._createDisplayButton( t );
|
||||
|
||||
// --- 2. Construct Children & Preview ---
|
||||
// Build children
|
||||
|
||||
const children: View[] = [
|
||||
this.mathLiveInputView,
|
||||
this.rawLatexInputView,
|
||||
this.mathInputView,
|
||||
this.displayButtonView
|
||||
];
|
||||
|
||||
@ -67,19 +58,14 @@ export default class MainFormView extends View {
|
||||
const previewLabel = new LabelView( locale );
|
||||
previewLabel.text = t( 'Equation preview' );
|
||||
|
||||
// Clean instantiation using the options object
|
||||
this.mathView = new MathView( locale, mathViewOptions );
|
||||
|
||||
// Bind display mode: When button flips, preview updates automatically
|
||||
this.mathView.bind( 'display' ).to( this.displayButtonView, 'isOn' );
|
||||
|
||||
children.push( previewLabel, this.mathView );
|
||||
}
|
||||
|
||||
// --- 3. Sync Logic ---
|
||||
this._setupInputSync( previewEnabled );
|
||||
this._setupSync( previewEnabled );
|
||||
|
||||
// --- 4. Template Setup ---
|
||||
this.setTemplate( {
|
||||
tag: 'form',
|
||||
attributes: {
|
||||
@ -101,7 +87,6 @@ export default class MainFormView extends View {
|
||||
]
|
||||
} );
|
||||
|
||||
// --- 5. Accessibility ---
|
||||
this._focusCycler = new FocusCycler( {
|
||||
focusables: this._focusables,
|
||||
focusTracker: this.focusTracker,
|
||||
@ -117,8 +102,7 @@ export default class MainFormView extends View {
|
||||
|
||||
// Register focusables
|
||||
[
|
||||
this.mathLiveInputView,
|
||||
this.rawLatexInputView,
|
||||
this.mathInputView,
|
||||
this.displayButtonView,
|
||||
this.saveButtonView,
|
||||
this.cancelButtonView
|
||||
@ -133,14 +117,12 @@ export default class MainFormView extends View {
|
||||
}
|
||||
|
||||
public get equation(): string {
|
||||
return this.mathLiveInputView.value ?? '';
|
||||
return this.mathInputView.value ?? '';
|
||||
}
|
||||
|
||||
public set equation( equation: string ) {
|
||||
const norm = equation.trim();
|
||||
// Direct updates to the "source of truth"
|
||||
this.mathLiveInputView.value = norm.length ? norm : null;
|
||||
this.rawLatexInputView.value = norm;
|
||||
this.mathInputView.value = norm.length ? norm : null;
|
||||
if ( this.mathView ) this.mathView.value = norm;
|
||||
}
|
||||
|
||||
@ -148,28 +130,10 @@ export default class MainFormView extends View {
|
||||
this._focusCycler.focusFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a view currently has focus.
|
||||
*/
|
||||
private _isViewFocused(view: View): boolean {
|
||||
const el = view.element;
|
||||
const active = document.activeElement;
|
||||
return !!(el && active && el.contains(active));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
/** Handle delimiter stripping and preview updates. */
|
||||
private _setupSync(previewEnabled: boolean): void {
|
||||
this.mathInputView.on('change:value', () => {
|
||||
let eq = (this.mathInputView.value ?? '').trim();
|
||||
|
||||
// Strip delimiters if present (e.g. pasted content)
|
||||
if (hasDelimiters(eq)) {
|
||||
@ -177,31 +141,16 @@ export default class MainFormView extends View {
|
||||
eq = params.equation;
|
||||
this.displayButtonView.isOn = params.display;
|
||||
|
||||
// Only strip delimiters if not actively editing
|
||||
if (!this._isViewFocused(this.mathLiveInputView) && this.mathLiveInputView.value !== eq) {
|
||||
this.mathLiveInputView.value = eq;
|
||||
// Update the input with stripped delimiters
|
||||
if (this.mathInputView.value !== eq) {
|
||||
this.mathInputView.value = eq.length ? eq : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Sync to Raw LaTeX only if user isn't typing there
|
||||
if (!this._isViewFocused(this.rawLatexInputView) && this.rawLatexInputView.value !== eq) {
|
||||
this.rawLatexInputView.value = eq;
|
||||
// Update preview
|
||||
if (previewEnabled && this.mathView && this.mathView.value !== eq) {
|
||||
this.mathView.value = eq;
|
||||
}
|
||||
|
||||
updatePreview(eq);
|
||||
});
|
||||
|
||||
// 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 only if user isn't interacting with it
|
||||
if (!this._isViewFocused(this.mathLiveInputView) && this.mathLiveInputView.value !== normalized) {
|
||||
this.mathLiveInputView.value = normalized;
|
||||
}
|
||||
|
||||
updatePreview(eq);
|
||||
});
|
||||
}
|
||||
|
||||
@ -220,7 +169,6 @@ export default class MainFormView extends View {
|
||||
|
||||
btn.on( 'execute', () => {
|
||||
btn.isOn = !btn.isOn;
|
||||
// mathView updates automatically via bind()
|
||||
} );
|
||||
return btn;
|
||||
}
|
||||
|
||||
198
packages/ckeditor5-math/src/ui/mathinputview.ts
Normal file
198
packages/ckeditor5-math/src/ui/mathinputview.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import { View, type Locale } from 'ckeditor5';
|
||||
|
||||
interface MathFieldElement extends HTMLElement {
|
||||
value: string;
|
||||
readOnly: boolean;
|
||||
mathVirtualKeyboardPolicy: string;
|
||||
inlineShortcuts?: Record<string, string>;
|
||||
setValue(value: string, options?: { silenceNotifications?: boolean }): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined math input with MathLive visual editor and raw LaTeX textarea.
|
||||
*/
|
||||
export default class MathInputView extends View {
|
||||
public declare value: string | null;
|
||||
public declare isReadOnly: boolean;
|
||||
|
||||
public mathfield: MathFieldElement | null = null;
|
||||
private _textarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
constructor(locale: Locale) {
|
||||
super(locale);
|
||||
const t = locale.t;
|
||||
|
||||
this.set('value', null);
|
||||
this.set('isReadOnly', false);
|
||||
|
||||
this.setTemplate({
|
||||
tag: 'div',
|
||||
attributes: {
|
||||
class: ['ck', 'ck-math-input']
|
||||
},
|
||||
children: [
|
||||
// MathLive container
|
||||
{
|
||||
tag: 'div',
|
||||
attributes: { class: ['ck-mathlive-container'] }
|
||||
},
|
||||
// LaTeX label (outside wrapper)
|
||||
{
|
||||
tag: 'label',
|
||||
attributes: { class: ['ck-latex-label'] },
|
||||
children: [t('LaTeX')]
|
||||
},
|
||||
// Raw LaTeX wrapper (just textarea now)
|
||||
{
|
||||
tag: 'div',
|
||||
attributes: { class: ['ck-latex-wrapper'] },
|
||||
children: [
|
||||
{
|
||||
tag: 'textarea',
|
||||
attributes: {
|
||||
class: ['ck', 'ck-textarea', 'ck-latex-textarea'],
|
||||
autocapitalize: 'off',
|
||||
autocomplete: 'off',
|
||||
autocorrect: 'off',
|
||||
spellcheck: 'false'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
public override render(): void {
|
||||
super.render();
|
||||
|
||||
this._textarea = this.element!.querySelector('.ck-latex-textarea') as HTMLTextAreaElement;
|
||||
this._textarea.value = this.value ?? '';
|
||||
this._textarea.readOnly = this.isReadOnly;
|
||||
|
||||
this._loadMathLive();
|
||||
|
||||
// Textarea -> observable (and sync to mathfield)
|
||||
this._textarea.addEventListener('input', () => {
|
||||
const val = this._textarea!.value;
|
||||
if (this.mathfield) {
|
||||
this.mathfield.setValue(val, { silenceNotifications: true });
|
||||
}
|
||||
this.value = val.length ? val : null;
|
||||
});
|
||||
|
||||
// Observable -> textarea and mathfield
|
||||
this.on('change:value', (_evt, _name, newValue) => {
|
||||
const val = newValue ?? '';
|
||||
if (this._textarea && this._textarea.value !== val) {
|
||||
this._textarea.value = val;
|
||||
}
|
||||
if (this.mathfield && this.mathfield.value !== val) {
|
||||
this.mathfield.setValue(val, { silenceNotifications: true });
|
||||
}
|
||||
});
|
||||
|
||||
this.on('change:isReadOnly', (_evt, _name, newValue) => {
|
||||
if (this._textarea) this._textarea.readOnly = newValue;
|
||||
if (this.mathfield) this.mathfield.readOnly = newValue;
|
||||
});
|
||||
}
|
||||
|
||||
private async _loadMathLive(): Promise<void> {
|
||||
try {
|
||||
await import('mathlive');
|
||||
await customElements.whenDefined('math-field');
|
||||
|
||||
// Disable MathLive sounds
|
||||
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);
|
||||
const container = this.element?.querySelector('.ck-mathlive-container');
|
||||
if (container) {
|
||||
container.textContent = 'Math editor unavailable';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _createMathField(): void {
|
||||
const container = this.element?.querySelector('.ck-mathlive-container');
|
||||
if (!container) return;
|
||||
|
||||
const mathfield = document.createElement('math-field') as MathFieldElement;
|
||||
mathfield.mathVirtualKeyboardPolicy = 'manual';
|
||||
|
||||
// Add common shortcuts
|
||||
mathfield.addEventListener('mount', () => {
|
||||
mathfield.inlineShortcuts = {
|
||||
...mathfield.inlineShortcuts,
|
||||
dx: 'dx',
|
||||
dy: 'dy',
|
||||
dt: 'dt'
|
||||
};
|
||||
}, { once: true });
|
||||
|
||||
// Set initial value (may have been set before MathLive loaded)
|
||||
try {
|
||||
mathfield.value = this.value ?? '';
|
||||
} catch { /* MathLive may not be ready */ }
|
||||
mathfield.readOnly = this.isReadOnly;
|
||||
|
||||
if (this._textarea && this.value) {
|
||||
this._textarea.value = this.value;
|
||||
}
|
||||
|
||||
// MathLive -> textarea and observable
|
||||
mathfield.addEventListener('input', () => {
|
||||
try {
|
||||
const val = mathfield.value;
|
||||
if (this._textarea) this._textarea.value = val;
|
||||
this.value = val.length ? val : null;
|
||||
} catch { /* MathLive may not be ready */ }
|
||||
});
|
||||
|
||||
// Observable -> MathLive
|
||||
this.on('change:value', (_evt, _name, newValue) => {
|
||||
try {
|
||||
const val = newValue ?? '';
|
||||
if (mathfield.value !== val) {
|
||||
mathfield.setValue(val, { silenceNotifications: true });
|
||||
}
|
||||
} catch { /* MathLive may not be ready */ }
|
||||
});
|
||||
|
||||
container.appendChild(mathfield);
|
||||
this.mathfield = mathfield;
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.mathfield?.focus();
|
||||
}
|
||||
|
||||
public hideKeyboard(): void {
|
||||
if (this.mathfield) {
|
||||
try {
|
||||
this.mathfield.blur();
|
||||
(this.mathfield as any).executeCommand?.('hideVirtualKeyboard');
|
||||
} catch { /* MathLive may already be disposed */ }
|
||||
}
|
||||
}
|
||||
|
||||
public override destroy(): void {
|
||||
if (this.mathfield) {
|
||||
try {
|
||||
this.mathfield.blur();
|
||||
this.mathfield.remove();
|
||||
} catch { /* MathLive cleanup error */ }
|
||||
this.mathfield = null;
|
||||
}
|
||||
this._textarea = null;
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
import { View, type Locale } from 'ckeditor5';
|
||||
|
||||
interface MathFieldElement extends HTMLElement {
|
||||
value: string;
|
||||
readOnly: boolean;
|
||||
mathVirtualKeyboardPolicy: string;
|
||||
inlineShortcuts?: Record<string, string>;
|
||||
}
|
||||
|
||||
export default class MathLiveInputView extends View {
|
||||
public declare value: string | null;
|
||||
public declare isReadOnly: boolean;
|
||||
public mathfield: MathFieldElement | null = null;
|
||||
|
||||
constructor(locale: Locale) {
|
||||
super(locale);
|
||||
this.set('value', null);
|
||||
this.set('isReadOnly', false);
|
||||
|
||||
this.setTemplate({
|
||||
tag: 'div',
|
||||
attributes: {
|
||||
class: ['ck', 'ck-mathlive-input']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override render(): void {
|
||||
super.render();
|
||||
this._loadMathLive();
|
||||
}
|
||||
|
||||
private async _loadMathLive(): Promise<void> {
|
||||
try {
|
||||
await import('mathlive');
|
||||
await customElements.whenDefined('math-field');
|
||||
|
||||
// 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';
|
||||
|
||||
// Configure shortcuts after mount
|
||||
mathfield.addEventListener('mount', () => {
|
||||
mathfield.inlineShortcuts = {
|
||||
...mathfield.inlineShortcuts,
|
||||
dx: 'dx',
|
||||
dy: 'dy',
|
||||
dt: 'dt'
|
||||
};
|
||||
}, { once: true });
|
||||
|
||||
// Initial state
|
||||
mathfield.value = this.value ?? '';
|
||||
mathfield.readOnly = this.isReadOnly;
|
||||
|
||||
// DOM -> Observable
|
||||
mathfield.addEventListener('input', () => {
|
||||
const val = mathfield.value;
|
||||
this.value = val.length ? val : null;
|
||||
});
|
||||
|
||||
// Observable -> DOM
|
||||
this.on('change:value', (_evt, _name, nextValue) => {
|
||||
if (mathfield.value !== nextValue) {
|
||||
mathfield.value = nextValue ?? '';
|
||||
}
|
||||
});
|
||||
|
||||
this.on('change:isReadOnly', (_evt, _name, nextValue) => {
|
||||
mathfield.readOnly = nextValue;
|
||||
});
|
||||
|
||||
this.element.appendChild(mathfield);
|
||||
this.mathfield = mathfield;
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.mathfield?.focus();
|
||||
}
|
||||
|
||||
public override destroy(): void {
|
||||
if (this.mathfield) {
|
||||
this.mathfield.remove();
|
||||
this.mathfield = null;
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
import { LabeledFieldView, createLabeledTextarea, type Locale, type TextareaView } from 'ckeditor5';
|
||||
|
||||
/**
|
||||
* A labeled textarea view for direct LaTeX code editing.
|
||||
*/
|
||||
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;
|
||||
|
||||
constructor( locale: Locale ) {
|
||||
super( locale, createLabeledTextarea );
|
||||
|
||||
this.set( 'value', '' );
|
||||
this.set( 'isReadOnly', false );
|
||||
|
||||
const fieldView = this.fieldView;
|
||||
|
||||
// 1. Sync: DOM (Textarea) -> Observable
|
||||
fieldView.on( 'input', () => {
|
||||
// We cast strictly to HTMLTextAreaElement to access '.value' safely
|
||||
const textarea = fieldView.element as HTMLTextAreaElement;
|
||||
if ( textarea ) {
|
||||
this.value = textarea.value;
|
||||
}
|
||||
} );
|
||||
|
||||
// 2. Sync: Observable -> DOM (Textarea)
|
||||
this.on( 'change:value', () => {
|
||||
const textarea = fieldView.element as HTMLTextAreaElement;
|
||||
// Check for difference to avoid cursor jumping
|
||||
if ( textarea && textarea.value !== this.value ) {
|
||||
textarea.value = this.value;
|
||||
}
|
||||
} );
|
||||
|
||||
// 3. Sync: ReadOnly State
|
||||
this.on( 'change:isReadOnly', ( _evt, _name, nextValue ) => {
|
||||
fieldView.isReadOnly = nextValue;
|
||||
} );
|
||||
}
|
||||
|
||||
public override render(): void {
|
||||
super.render();
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,20 @@ import Math from '../src/math';
|
||||
import AutoformatMath from '../src/autoformatmath';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
// Suppress MathLive errors during async cleanup in tests
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('unhandledrejection', event => {
|
||||
if (event.reason?.message?.includes('options') || event.reason?.message?.includes('mathlive')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
window.addEventListener('error', event => {
|
||||
if (event.message?.includes('options') || event.message?.includes('mathlive')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe( 'CKEditor5 Math DLL', () => {
|
||||
it( 'exports Math', () => {
|
||||
expect( MathDll ).to.equal( Math );
|
||||
|
||||
@ -2,6 +2,20 @@ import { ClassicEditor, type EditorConfig } from 'ckeditor5';
|
||||
import MathUI from '../src/mathui';
|
||||
import { describe, beforeEach, it, afterEach, expect } from "vitest";
|
||||
|
||||
// Suppress MathLive errors during async cleanup
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('unhandledrejection', event => {
|
||||
if (event.reason?.message?.includes('options') || event.reason?.message?.includes('mathlive')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
window.addEventListener('error', event => {
|
||||
if (event.message?.includes('options') || event.message?.includes('mathlive')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe( 'Lazy load', () => {
|
||||
let editorElement: HTMLDivElement;
|
||||
let editor: ClassicEditor;
|
||||
@ -24,11 +38,14 @@ describe( 'Lazy load', () => {
|
||||
beforeEach( () => {
|
||||
editorElement = document.createElement( 'div' );
|
||||
document.body.appendChild( editorElement );
|
||||
|
||||
lazyLoadInvoked = false;
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
afterEach( async () => {
|
||||
if ( mathUIFeature?.formView ) {
|
||||
mathUIFeature._hideUI();
|
||||
}
|
||||
await new Promise( resolve => setTimeout( resolve, 50 ) );
|
||||
editorElement.remove();
|
||||
return editor.destroy();
|
||||
} );
|
||||
|
||||
@ -168,13 +168,13 @@ describe( 'MathUI', () => {
|
||||
|
||||
command.isEnabled = true;
|
||||
|
||||
expect( formView!.mathLiveInputView.isReadOnly ).to.be.false;
|
||||
expect( formView!.mathInputView.isReadOnly ).to.be.false;
|
||||
expect( formView!.saveButtonView.isEnabled ).to.be.false;
|
||||
expect( formView!.cancelButtonView.isEnabled ).to.be.true;
|
||||
|
||||
command.isEnabled = false;
|
||||
|
||||
expect( formView!.mathLiveInputView.isReadOnly ).to.be.true;
|
||||
expect( formView!.mathInputView.isReadOnly ).to.be.true;
|
||||
expect( formView!.saveButtonView.isEnabled ).to.be.false;
|
||||
expect( formView!.cancelButtonView.isEnabled ).to.be.true;
|
||||
} );
|
||||
@ -407,30 +407,30 @@ describe( 'MathUI', () => {
|
||||
setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );
|
||||
} );
|
||||
|
||||
it( 'should bind mainFormView.mathLiveInputView#value to math command value', () => {
|
||||
it( 'should bind mainFormView.mathInputView#value to math command value', () => {
|
||||
const command = editor.commands.get( 'math' );
|
||||
|
||||
expect( formView!.mathLiveInputView.value ).to.be.null;
|
||||
expect( formView!.mathInputView.value ).to.be.null;
|
||||
|
||||
command!.value = 'x^2';
|
||||
expect( formView!.mathLiveInputView.value ).to.equal( 'x^2' );
|
||||
expect( formView!.mathInputView.value ).to.equal( 'x^2' );
|
||||
} );
|
||||
|
||||
it( 'should execute math command on mainFormView#submit event', () => {
|
||||
const executeSpy = vi.spyOn( editor, 'execute' );
|
||||
|
||||
formView!.mathLiveInputView.value = 'x^2';
|
||||
formView!.mathInputView.value = 'x^2';
|
||||
formView!.fire( 'submit' );
|
||||
|
||||
expect( executeSpy.mock.lastCall?.slice( 0, 2 ) ).toMatchObject( [ 'math', 'x^2' ] );
|
||||
} );
|
||||
|
||||
it( 'should sync mathLiveInputView and rawLatexInputView', () => {
|
||||
formView!.mathLiveInputView.value = 'x^2';
|
||||
expect( formView!.rawLatexInputView.value ).to.equal( 'x^2' );
|
||||
it( 'should update equation value when mathInputView changes', () => {
|
||||
formView!.mathInputView.value = 'x^2';
|
||||
expect( formView!.equation ).to.equal( 'x^2' );
|
||||
|
||||
formView!.rawLatexInputView.value = '\\frac{1}{2}';
|
||||
expect( formView!.mathLiveInputView.value ).to.equal( '\\frac{1}{2}' );
|
||||
formView!.mathInputView.value = '\\frac{1}{2}';
|
||||
expect( formView!.equation ).to.equal( '\\frac{1}{2}' );
|
||||
} );
|
||||
|
||||
it( 'should hide the balloon on mainFormView#cancel if math command does not have a value', () => {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
/**
|
||||
* Math equation editor dialog styles
|
||||
* Supports MathLive input, raw LaTeX textarea, and equation preview
|
||||
*/
|
||||
|
||||
/* ============================================================================
|
||||
@ -14,8 +13,7 @@
|
||||
box-sizing: border-box;
|
||||
max-width: 80vw;
|
||||
max-height: 80vh;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@ -34,41 +32,12 @@
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
gap: var(--ck-spacing-standard);
|
||||
min-height: fit-content;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* LaTeX section heading */
|
||||
.ck-math-view > .ck-labeled-field-view::before {
|
||||
content: "LaTeX";
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--ck-color-text, #333);
|
||||
margin-bottom: 4px;
|
||||
padding-left: 2px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Equation preview section heading */
|
||||
.ck-math-view > math-field::before {
|
||||
content: "Equation preview";
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--ck-color-text, #333);
|
||||
margin-bottom: 4px;
|
||||
padding-left: 2px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Add spacing between preview and action buttons */
|
||||
.ck-math-view > math-field {
|
||||
margin-bottom: var(--ck-spacing-large, 16px);
|
||||
}
|
||||
|
||||
/* Action buttons row (Save/Cancel) */
|
||||
.ck-math-button-row {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
@ -87,14 +56,34 @@
|
||||
.ck.ck-math-form math-field,
|
||||
.ck.ck-math-form textarea {
|
||||
box-sizing: border-box;
|
||||
padding: var(--ck-spacing-small);
|
||||
background: var(--ck-color-input-background) !important;
|
||||
color: var(--ck-color-input-text, inherit);
|
||||
font-size: var(--ck-font-size-base);
|
||||
color: var(--ck-color-input-text, inherit);
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
border-radius: var(--ck-border-radius, 6px);
|
||||
outline: 3px solid transparent;
|
||||
outline-offset: 6px;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
outline-offset: 0 !important;
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.ck.ck-math-input .ck-textarea,
|
||||
.ck.ck-math-input .ck-textarea:focus,
|
||||
.ck.ck-math-input .ck-textarea:hover {
|
||||
transition: none !important;
|
||||
box-shadow: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.ck.ck-math-form math-field:focus-within,
|
||||
.ck.ck-math-form textarea:focus {
|
||||
outline: none !important;
|
||||
outline-offset: 0 !important;
|
||||
box-shadow: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* MathLive-specific configuration */
|
||||
@ -111,21 +100,9 @@
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
MathLive Visual Editor (Top Input)
|
||||
MathLive Configuration
|
||||
========================================================================= */
|
||||
|
||||
.ck.ck-mathlive-input {
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: fit-content;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
padding-bottom: var(--ck-spacing-small);
|
||||
resize: none;
|
||||
}
|
||||
|
||||
/* Configure MathLive shadow DOM layout */
|
||||
.ck.ck-math-form math-field::part(container),
|
||||
.ck.ck-math-form math-field::part(content),
|
||||
@ -156,62 +133,88 @@
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Raw LaTeX Textarea (Middle Input)
|
||||
Combined Math Input (MathLive + LaTeX Textarea)
|
||||
========================================================================= */
|
||||
|
||||
.ck-math-view .ck-labeled-field-view {
|
||||
.ck.ck-math-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ck-spacing-standard);
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ck.ck-math-input .ck-mathlive-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
flex-shrink: 0;
|
||||
padding: var(--ck-spacing-small);
|
||||
border: 1px solid var(--ck-color-input-border, #ccc);
|
||||
border-radius: var(--ck-border-radius, 6px);
|
||||
background: var(--ck-color-input-background) !important;
|
||||
transition: border-color 120ms ease;
|
||||
}
|
||||
|
||||
.ck.ck-math-input .ck-mathlive-container:focus-within {
|
||||
border-color: var(--ck-color-focus-border, #1a73e8);
|
||||
}
|
||||
|
||||
.ck.ck-math-input .ck-latex-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto;
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 60px;
|
||||
max-height: 65vh;
|
||||
flex: 1 1 auto;
|
||||
padding: var(--ck-spacing-small);
|
||||
border: 1px solid var(--ck-color-input-border, #ccc);
|
||||
border-radius: var(--ck-border-radius, 6px);
|
||||
background: var(--ck-color-input-background) !important;
|
||||
transition: border-color 120ms ease;
|
||||
}
|
||||
|
||||
.ck.ck-math-input .ck-latex-wrapper:focus-within {
|
||||
border-color: var(--ck-color-focus-border, #1a73e8);
|
||||
}
|
||||
|
||||
.ck.ck-math-input .ck-latex-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--ck-color-text, #333);
|
||||
opacity: 0.8;
|
||||
margin: 0 0 var(--ck-spacing-small) 0;
|
||||
}
|
||||
|
||||
.ck.ck-math-input .ck-latex-textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
min-height: 60px;
|
||||
max-height: 100%;
|
||||
flex: 1 1 auto;
|
||||
resize: both;
|
||||
overflow: auto;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Hide the default label (we use ::before for custom heading) */
|
||||
.ck-math-view .ck-labeled-field-view .ck-label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Textarea wrapper */
|
||||
.ck-math-view .ck-labeled-field-view .ck-labeled-field-view__input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
height: auto;
|
||||
font-family: monospace;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
box-shadow: none !important;
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
/* Raw LaTeX textarea styling */
|
||||
.ck-math-view .ck-labeled-field-view textarea {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
min-height: 140px;
|
||||
resize: none !important;
|
||||
border-radius: 0 !important;
|
||||
.ck.ck-math-input .ck-latex-textarea:focus {
|
||||
outline: none;
|
||||
box-shadow: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* Textarea hover and focus states */
|
||||
.ck-math-view .ck-labeled-field-view textarea:hover,
|
||||
.ck-math-view .ck-labeled-field-view textarea:focus {
|
||||
background: var(--ck-color-input-background) !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
/* ============================================================================
|
||||
Error State
|
||||
========================================================================= */
|
||||
|
||||
.ck-math-render-error {
|
||||
color: var(--ck-color-error-text, #db1d1d);
|
||||
@ -222,3 +225,13 @@
|
||||
border-radius: 2px;
|
||||
background: var(--ck-color-base-background, #fff);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Preview Section (always at bottom)
|
||||
========================================================================= */
|
||||
|
||||
.ck-math-preview {
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
@ -22,6 +22,9 @@ export default defineConfig( {
|
||||
include: [
|
||||
'tests/**/*.[jt]s'
|
||||
],
|
||||
exclude: [
|
||||
'tests/setup.ts'
|
||||
],
|
||||
globals: true,
|
||||
watch: false,
|
||||
coverage: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user