mirror of
https://github.com/zadam/trilium.git
synced 2025-12-04 22:44:25 +01:00
405 lines
10 KiB
TypeScript
405 lines
10 KiB
TypeScript
import { ButtonView, FocusCycler, LabelView, submitHandler, SwitchButtonView, View, ViewCollection, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5';
|
|
import IconCheck from '../../theme/icons/check.svg?raw';
|
|
import IconCancel from '../../theme/icons/cancel.svg?raw';
|
|
import { extractDelimiters, hasDelimiters } from '../utils.js';
|
|
import MathView 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 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 = new Locale();
|
|
|
|
constructor(
|
|
locale: Locale,
|
|
engine:
|
|
| 'mathjax'
|
|
| 'katex'
|
|
| ( (
|
|
equation: string,
|
|
element: HTMLElement,
|
|
display: boolean,
|
|
) => void ),
|
|
lazyLoad: undefined | ( () => Promise<void> ),
|
|
previewEnabled = false,
|
|
previewUid: string,
|
|
previewClassName: Array<string>,
|
|
popupClassName: Array<string>,
|
|
katexRenderOptions: KatexOptions
|
|
) {
|
|
super( locale );
|
|
|
|
const t = locale.t;
|
|
|
|
// Submit button
|
|
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', null );
|
|
this.saveButtonView.type = 'submit';
|
|
|
|
// MathLive visual equation editor
|
|
this.mathLiveInputView = this._createMathLiveInput();
|
|
|
|
// Raw LaTeX input
|
|
this.rawLatexInputView = this._createRawLatexInput();
|
|
|
|
// Raw LaTeX label
|
|
this.rawLatexLabel = new LabelView( locale );
|
|
this.rawLatexLabel.text = t( 'LaTeX' );
|
|
|
|
// Display button
|
|
this.displayButtonView = this._createDisplayButton();
|
|
|
|
// Cancel button
|
|
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel', 'cancel' );
|
|
|
|
this.previewEnabled = previewEnabled;
|
|
|
|
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 );
|
|
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
|
|
];
|
|
}
|
|
|
|
// Add UI elements to template
|
|
this.setTemplate( {
|
|
tag: 'form',
|
|
attributes: {
|
|
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
|
|
}
|
|
]
|
|
},
|
|
{
|
|
tag: 'div',
|
|
attributes: {
|
|
class: [ 'ck-math-button-row' ]
|
|
},
|
|
children: [
|
|
this.saveButtonView,
|
|
this.cancelButtonView
|
|
]
|
|
}
|
|
]
|
|
} );
|
|
}
|
|
|
|
public override render(): void {
|
|
super.render();
|
|
|
|
// Prevent default form submit event & trigger custom 'submit'
|
|
submitHandler( {
|
|
view: this
|
|
} );
|
|
|
|
// Register form elements to focusable elements
|
|
const childViews = [
|
|
this.mathLiveInputView,
|
|
this.rawLatexInputView,
|
|
this.displayButtonView,
|
|
this.saveButtonView,
|
|
this.cancelButtonView
|
|
];
|
|
|
|
childViews.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 );
|
|
}
|
|
|
|
this._initResizeSync();
|
|
}
|
|
|
|
public override destroy(): void {
|
|
super.destroy();
|
|
this._resizeObserver?.disconnect();
|
|
document.removeEventListener( 'mouseup', this._onMouseUp );
|
|
this.mathLiveInputView.element?.removeEventListener( 'mousedown', this._onMouseDown );
|
|
this.rawLatexInputView.element?.removeEventListener( 'mousedown', this._onMouseDown );
|
|
}
|
|
|
|
public focus(): void {
|
|
this._focusCycler.focusFirst();
|
|
}
|
|
|
|
public get equation(): string {
|
|
return this.mathLiveInputView.value ?? '';
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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'
|
|
}
|
|
} );
|
|
|
|
private _resizeObserver: ResizeObserver | null = null;
|
|
private _activeResizeTarget: HTMLElement | null = null;
|
|
|
|
private _onMouseUp = () => {
|
|
this._activeResizeTarget = null;
|
|
// Re-observe everything to ensure state is reset
|
|
if ( this.mathLiveInputView.element ) this._resizeObserver?.observe( this.mathLiveInputView.element );
|
|
if ( this.rawLatexInputView.element ) this._resizeObserver?.observe( this.rawLatexInputView.element );
|
|
};
|
|
|
|
private _onMouseDown = ( evt: Event ) => {
|
|
const target = evt.currentTarget as HTMLElement;
|
|
this._activeResizeTarget = target;
|
|
|
|
// Stop observing the OTHER element to prevent loops and errors while resizing
|
|
if ( target === this.mathLiveInputView.element ) {
|
|
if ( this.rawLatexInputView.element ) {
|
|
this._resizeObserver?.unobserve( this.rawLatexInputView.element );
|
|
}
|
|
} else if ( target === this.rawLatexInputView.element ) {
|
|
if ( this.mathLiveInputView.element ) {
|
|
this._resizeObserver?.unobserve( this.mathLiveInputView.element );
|
|
}
|
|
}
|
|
};
|
|
|
|
private _initResizeSync() {
|
|
if ( this.mathLiveInputView.element ) {
|
|
this.mathLiveInputView.element.addEventListener( 'mousedown', this._onMouseDown );
|
|
}
|
|
if ( this.rawLatexInputView.element ) {
|
|
this.rawLatexInputView.element.addEventListener( 'mousedown', this._onMouseDown );
|
|
}
|
|
document.addEventListener( 'mouseup', this._onMouseUp );
|
|
|
|
// Synchronize width between MathLive and Raw LaTeX inputs
|
|
this._resizeObserver = new ResizeObserver( entries => {
|
|
if ( !this._activeResizeTarget ) {
|
|
return;
|
|
}
|
|
|
|
for ( const entry of entries ) {
|
|
if ( entry.target === this._activeResizeTarget ) {
|
|
// Use style.width directly to avoid box-sizing issues causing infinite growth
|
|
const width = ( entry.target as HTMLElement ).style.width;
|
|
|
|
if ( !width ) continue;
|
|
|
|
const other = entry.target === this.mathLiveInputView.element
|
|
? this.rawLatexInputView.element
|
|
: this.mathLiveInputView.element;
|
|
|
|
if ( other && other.style.width !== width ) {
|
|
window.requestAnimationFrame( () => {
|
|
other.style.width = width;
|
|
} );
|
|
}
|
|
}
|
|
}
|
|
} );
|
|
|
|
if ( this.mathLiveInputView.element ) {
|
|
this._resizeObserver.observe( this.mathLiveInputView.element );
|
|
}
|
|
if ( this.rawLatexInputView.element ) {
|
|
this._resizeObserver.observe( this.rawLatexInputView.element );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates the MathLive visual equation editor.
|
|
*
|
|
* Handles bidirectional synchronization with the raw LaTeX textarea and preview.
|
|
*/
|
|
private _createMathLiveInput() {
|
|
const mathLiveInput = new MathLiveInputView( this.locale );
|
|
|
|
const onInput = () => {
|
|
const rawValue = mathLiveInput.value ?? '';
|
|
let equationInput = rawValue.trim();
|
|
|
|
// If input has delimiters
|
|
if ( hasDelimiters( equationInput ) ) {
|
|
// Get equation without delimiters
|
|
const params = extractDelimiters( equationInput );
|
|
|
|
// Remove delimiters from input field
|
|
mathLiveInput.value = params.equation;
|
|
|
|
equationInput = params.equation;
|
|
|
|
// Update display button and preview
|
|
this.displayButtonView.isOn = params.display;
|
|
}
|
|
|
|
const normalizedEquation = equationInput.length ? equationInput : null;
|
|
if ( mathLiveInput.value !== normalizedEquation ) {
|
|
mathLiveInput.value = normalizedEquation;
|
|
}
|
|
|
|
// Sync to raw LaTeX textarea
|
|
this.rawLatexInputView.value = equationInput;
|
|
|
|
if ( this.previewEnabled && this.mathView ) {
|
|
// Update preview
|
|
this.mathView.value = equationInput;
|
|
}
|
|
};
|
|
|
|
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,
|
|
eventName: string | null
|
|
) {
|
|
const button = new ButtonView( this.locale );
|
|
|
|
button.set( {
|
|
label,
|
|
icon,
|
|
tooltip: true
|
|
} );
|
|
|
|
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;
|
|
}
|
|
}
|