diff --git a/.gitignore b/.gitignore
index 4dd7bde624..44d9b523dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,4 +51,4 @@ upload
# docs
site/
apps/*/coverage
-scripts/translation/.language*.json
\ No newline at end of file
+scripts/translation/.language*.json
diff --git a/apps/client/src/translations/hu/translation.json b/apps/client/src/translations/hu/translation.json
index 042fbb281b..40e39bec35 100644
--- a/apps/client/src/translations/hu/translation.json
+++ b/apps/client/src/translations/hu/translation.json
@@ -21,7 +21,13 @@
},
"bundle-error": {
"title": "Nem sikerült betölteni az egyéni szkriptet",
- "message": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó szkript nem hajtható végre a következő ok miatt:\n\n{{message}}"
+ "message": "A skript nem hajtható végre a következő ok miatt:\n\n{{message}}"
+ },
+ "widget-list-error": {
+ "title": "A Widget-ek letöltése sikertelen volt"
+ },
+ "widget-render-error": {
+ "title": "Nem sikerült renderelni a React widget-et"
}
},
"add_link": {
diff --git a/apps/client/src/widgets/sql_result.tsx b/apps/client/src/widgets/sql_result.tsx
index e4fde650b9..7aaa5739d3 100644
--- a/apps/client/src/widgets/sql_result.tsx
+++ b/apps/client/src/widgets/sql_result.tsx
@@ -23,7 +23,7 @@ export default function SqlResults() {
{t("sql_result.no_rows")}
) : (
-
+
{results?.map(rows => {
// inserts, updates
if (typeof rows === "object" && !Array.isArray(rows)) {
diff --git a/apps/server/src/assets/translations/hi/server.json b/apps/server/src/assets/translations/hi/server.json
index 9b03fa2c37..c994dc43d1 100644
--- a/apps/server/src/assets/translations/hi/server.json
+++ b/apps/server/src/assets/translations/hi/server.json
@@ -10,6 +10,7 @@
"creating-and-moving-notes": "नोट्स बनाना और स्थानांतरित करना",
"move-note-up": "नोट को ऊपर ले जाएं",
"move-note-down": "नोट को नीचे ले जाएं",
- "note-clipboard": "नोट क्लिपबोर्ड"
+ "note-clipboard": "नोट क्लिपबोर्ड",
+ "duplicate-subtree": "डुप्लिकेट सबट्री"
}
}
diff --git a/apps/website/src/translations/hi/translation.json b/apps/website/src/translations/hi/translation.json
index f268e30ad7..4146c0ca0e 100644
--- a/apps/website/src/translations/hi/translation.json
+++ b/apps/website/src/translations/hi/translation.json
@@ -13,6 +13,7 @@
"note_structure_description": "नोटों को पदानुक्रमिक रूप से व्यवस्थित किया जा सकता है। फ़ोल्डर्स की कोई आवश्यकता नहीं है, क्योंकि प्रत्येक नोट में उप-नोट हो सकते हैं। एक एकल नोट को पदानुक्रम में कई स्थानों पर जोड़ा जा सकता है।"
},
"productivity_benefits": {
- "protected_notes_title": "संरक्षित नोट्स"
+ "protected_notes_title": "संरक्षित नोट्स",
+ "web_clipper_title": "वेब क्लिपर"
}
}
diff --git a/apps/website/src/translations/nb-NO/translation.json b/apps/website/src/translations/nb-NO/translation.json
index f9e3b5a586..9ad41c5527 100644
--- a/apps/website/src/translations/nb-NO/translation.json
+++ b/apps/website/src/translations/nb-NO/translation.json
@@ -10,13 +10,18 @@
"title": "Organiser tankene dine. Bygg din personlige kunnskapsbase.",
"github": "GitHub",
"get_started": "Kom i gang",
- "dockerhub": "Docker Hub"
+ "dockerhub": "Docker Hub",
+ "screenshot_alt": "Screenshot fra Trilium Notes skrivebordsprogram",
+ "subtitle": "Trilium er en open-source-løsning for å ta notater og organisere en personlig kunnskapsbase. Kan brukes lokalt på arbeidsstasjonen din eller synkroniseres med en selv-hostet løsning for å ha dine notater med deg overalt."
},
"organization_benefits": {
"title": "Organisering",
"note_structure_title": "Notatstruktur",
"hoisting_title": "Arbeidsflate og fokusering",
- "attributes_description": "Bruk relasjoner mellom notater eller legg til etiketter for enkel kategorisering. Bruk fremhevede attributter for å legge inn strukturert informasjon som kan brukes i tabeller og tavler."
+ "attributes_description": "Bruk relasjoner mellom notater eller legg til etiketter for enkel kategorisering. Bruk fremhevede attributter for å legge inn strukturert informasjon som kan brukes i tabeller og tavler.",
+ "note_structure_description": "Notater kan arrangeres herarkisk. Det trengs ikke mapper, siden alle notater kan inneholde undernotater. Ett notat kan legges inn flere steder i herarkiet.",
+ "attributes_title": "Notatetiketter og -relasjoner",
+ "hoisting_description": "Du kan enkelt skille personlige og arbeidsnotater ved å gruppere de under arbeidsrom, som fokuserer notat-treet ditt på kun ønskede notater."
},
"productivity_benefits": {
"sync_title": "Synkronisering",
@@ -26,7 +31,12 @@
"protected_notes_title": "Beskyttede notater",
"title": "Produktivitet og sikkerhet",
"sync_content": "Bruk en selv-hostet eller cloud-instans for å enkelt synkronisere notater på tvers av enheter, og ha de tilgjengelige fra din mobiltelefon ved hjelp av progressiv web-app.",
- "jump_to_content": "Hopp raskt til notater eller grensesnittkommandoer over hele hierarkiet ved å søke etter tittel, med \"fuzzy\" matching for å ta hensyn til skrivefeil eller små differanser."
+ "jump_to_content": "Hopp raskt til notater eller grensesnittkommandoer over hele hierarkiet ved å søke etter tittel, med \"fuzzy\" matching for å ta hensyn til skrivefeil eller små differanser.",
+ "revisions_content": "Notater lagres periodisk i bakgrunnen og revisjonshistorikk kan brukes for tilbakeblikk eller å omgjøre uønskede endringer. Revisjoner kan også lages manuelt.",
+ "protected_notes_content": "Beskytt sensitiv personlig informasjon ved å kryptere notater og låse de med en passordkryptert sesjon.",
+ "jump_to_title": "Hurtigsøk og kommandoer",
+ "search_content": "Eller søk etter tekst i notatene og finjuster søket ved å filtrere på foreldrenotat eller dybde.",
+ "web_clipper_content": "Hent nettsider (eller screenshots) og legg de direkte i Trilium ved hjelp av web clipper nettleserutvidelse."
},
"note_types": {
"canvas_title": "Kanvas",
@@ -34,7 +44,14 @@
"text_title": "Tekstnotat",
"code_title": "Kodenotat",
"file_title": "Filnotat",
- "mermaid_title": "Mermaid diagrammer"
+ "mermaid_title": "Mermaid diagrammer",
+ "title": "Flere måter å presentere informasjonen din",
+ "text_description": "Notatene redigeres med en visuell editor (WYSIWYG), som støtter tabeller, bilder, matematiske uttrykk og kodeblokker med syntaksutheving. Formater tekst hurtig med Markdown-inspirert syntaks eller \"slash-kommandoer\".",
+ "code_description": "Store samlinger med kildekode eller skript bruker en dedikert editor med syntaksfremheving for mange programmeringsspråk og med flere fargetema.",
+ "file_description": "Integrer multimediafiler som PDFer, bilder og video med forhåndsvisning i programmet.",
+ "mermaid_description": "Lag diagrammer som flytskjema, klasse- og sekvensdiagrammer, Ganttdiagrammer og mye mer ved hjelp av Mermaidsyntaks.",
+ "mindmap_description": "Organiser dine tanker visuelt eller gjør en brainstorming.",
+ "others_list": "og andre: <0>notatkart0>, <1>relasjonskart1>, <2>lagrede søk2>, <3>rendret notat3>, og <4>web view4>."
},
"extensibility_benefits": {
"import_export_title": "Import/eksport",
diff --git a/package.json b/package.json
index 7ff8c4155b..711ebbee52 100644
--- a/package.json
+++ b/package.json
@@ -66,7 +66,7 @@
"jiti": "2.6.1",
"jsonc-eslint-parser": "2.4.2",
"react-refresh": "0.18.0",
- "rollup-plugin-webpack-stats": "2.1.8",
+ "rollup-plugin-webpack-stats": "2.1.9",
"tslib": "2.8.1",
"tsx": "4.21.0",
"typescript": "~5.9.0",
diff --git a/packages/ckeditor5-math/package.json b/packages/ckeditor5-math/package.json
index 2992bbdae9..f362a54022 100644
--- a/packages/ckeditor5-math/package.json
+++ b/packages/ckeditor5-math/package.json
@@ -70,6 +70,7 @@
]
},
"dependencies": {
- "@ckeditor/ckeditor5-icons": "47.3.0"
+ "@ckeditor/ckeditor5-icons": "47.3.0",
+ "mathlive": "0.108.2"
}
}
diff --git a/packages/ckeditor5-math/src/index.ts b/packages/ckeditor5-math/src/index.ts
index 6d6982ae23..eac4d7b223 100644
--- a/packages/ckeditor5-math/src/index.ts
+++ b/packages/ckeditor5-math/src/index.ts
@@ -1,6 +1,9 @@
import ckeditor from './../theme/icons/math.svg?raw';
import './augmentation.js';
import "../theme/mathform.css";
+import 'mathlive';
+import 'mathlive/fonts.css';
+import 'mathlive/static.css';
export { default as Math } from './math.js';
export { default as MathUI } from './mathui.js';
diff --git a/packages/ckeditor5-math/src/mathui.ts b/packages/ckeditor5-math/src/mathui.ts
index 4c4a2794c5..3e3da2744d 100644
--- a/packages/ckeditor5-math/src/mathui.ts
+++ b/packages/ckeditor5-math/src/mathui.ts
@@ -55,9 +55,9 @@ export default class MathUI extends Plugin {
this._balloon.showStack( 'main' );
- requestAnimationFrame(() => {
- this.formView?.mathInputView.fieldView.element?.focus();
- });
+ requestAnimationFrame( () => {
+ this.formView?.mathInputView.focus();
+ } );
}
private _createFormView() {
@@ -71,31 +71,37 @@ export default class MathUI extends Plugin {
throw new CKEditorError( 'math-command' );
}
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const mathConfig = editor.config.get( 'math' )!;
const formView = new MainFormView(
editor.locale,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- mathConfig.engine!,
- mathConfig.lazyLoad,
+ {
+ engine: mathConfig.engine!,
+ lazyLoad: mathConfig.lazyLoad,
+ previewUid: this._previewUid,
+ previewClassName: mathConfig.previewClassName!,
+ katexRenderOptions: mathConfig.katexRenderOptions!
+ },
mathConfig.enablePreview,
- this._previewUid,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- mathConfig.previewClassName!,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- mathConfig.popupClassName!,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- mathConfig.katexRenderOptions!
+ mathConfig.popupClassName!
);
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.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', value => !value );
- formView.saveButtonView.bind( 'isEnabled' ).to( mathCommand );
- formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand );
+ formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', ( value: boolean ) => !value );
+ formView.saveButtonView.bind( 'isEnabled' ).to(
+ mathCommand,
+ 'isEnabled',
+ formView.mathInputView,
+ 'value',
+ ( commandEnabled, equation ) => {
+ const normalizedEquation = ( equation ?? '' ).trim();
+ return commandEnabled && normalizedEquation.length > 0;
+ }
+ );
+ formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand, 'isEnabled' );
// Listen to submit button click
this.listenTo( formView, 'submit', () => {
@@ -115,24 +121,12 @@ export default class MathUI extends Plugin {
} );
// Allow pressing Enter to submit changes, and use Shift+Enter to insert a new line
- formView.keystrokes.set('enter', (data, cancel) => {
- if (!data.shiftKey) {
- formView.fire('submit');
+ formView.keystrokes.set( 'enter', ( data, cancel ) => {
+ if ( !data.shiftKey ) {
+ formView.fire( 'submit' );
cancel();
}
- });
-
- // Allow the textarea to be resizable
- formView.mathInputView.fieldView.once('render', () => {
- const textarea = formView.mathInputView.fieldView.element;
- if (!textarea) return;
- Object.assign(textarea.style, {
- resize: 'both',
- height: '100px',
- width: '400px',
- minWidth: '100%',
- });
- });
+ } );
return formView;
}
@@ -162,14 +156,12 @@ export default class MathUI extends Plugin {
} );
if ( this._balloon.visibleView === this.formView ) {
- this.formView.mathInputView.fieldView.element?.select();
+ this.formView.mathInputView.focus();
}
- // Show preview element
const previewEl = document.getElementById( this._previewUid );
- if ( previewEl && this.formView.previewEnabled ) {
- // Force refresh preview
- this.formView.mathView?.updateMath();
+ if ( previewEl && this.formView.mathView ) {
+ this.formView.mathView.updateMath();
}
this.formView.equation = mathCommand.value ?? '';
@@ -206,8 +198,10 @@ export default class MathUI extends Plugin {
private _removeFormView() {
if ( this._isFormInPanel && this.formView ) {
- this.formView.saveButtonView.focus();
+ // Hide virtual keyboard before removing the form
+ this.formView.hideKeyboard();
+ this.formView.saveButtonView.focus();
this._balloon.remove( this.formView );
// Hide preview element
diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts
index 2d1c597938..52d3b05120 100644
--- a/packages/ckeditor5-math/src/ui/mainformview.ts
+++ b/packages/ckeditor5-math/src/ui/mainformview.ts
@@ -1,91 +1,59 @@
-import { ButtonView, createLabeledTextarea, FocusCycler, LabelView, LabeledFieldView, submitHandler, SwitchButtonView, View, ViewCollection, type TextareaView, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5';
+import { ButtonView, FocusCycler, FocusTracker, KeystrokeHandler, LabelView, submitHandler, SwitchButtonView, View, ViewCollection, type FocusableView, type Locale } from 'ckeditor5';
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 from './mathview.js';
+import MathView, { type MathViewOptions } from './mathview.js';
+import MathInputView from './mathinputview.js';
import '../../theme/mathform.css';
-import type { KatexOptions } from '../typings-external.js';
-
-class MathInputView extends LabeledFieldView {
- public value: null | string = null;
- public isReadOnly = false;
-
- constructor( locale: Locale ) {
- super( locale, createLabeledTextarea );
- }
-}
export default class MainFormView extends View {
public saveButtonView: ButtonView;
- public mathInputView: MathInputView;
- public displayButtonView: SwitchButtonView;
public cancelButtonView: ButtonView;
- public previewEnabled: boolean;
- public previewLabel?: LabelView;
+ public displayButtonView: SwitchButtonView;
+
+ public mathInputView: MathInputView;
public mathView?: MathView;
- public override locale: Locale = new Locale();
- public lazyLoad: undefined | ( () => Promise );
+
+ public focusTracker = new FocusTracker();
+ public keystrokes = new KeystrokeHandler();
+ private _focusables = new ViewCollection();
+ private _focusCycler: FocusCycler;
constructor(
locale: Locale,
- engine:
- | 'mathjax'
- | 'katex'
- | ( (
- equation: string,
- element: HTMLElement,
- display: boolean,
- ) => void ),
- lazyLoad: undefined | ( () => Promise ),
+ mathViewOptions: MathViewOptions,
previewEnabled = false,
- previewUid: string,
- previewClassName: Array,
- popupClassName: Array,
- katexRenderOptions: KatexOptions
+ popupClassName: Array = []
) {
super( locale );
-
const t = locale.t;
- // Submit button
- this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', null );
- this.saveButtonView.type = 'submit';
+ // 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 );
- // Equation input
- this.mathInputView = this._createMathInput();
+ // Build children
- // Display button
- this.displayButtonView = this._createDisplayButton();
+ const children: Array = [
+ this.mathInputView,
+ this.displayButtonView
+ ];
- // Cancel button
- this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel', 'cancel' );
+ if ( previewEnabled ) {
+ const previewLabel = new LabelView( locale );
+ previewLabel.text = t( 'Equation preview' );
- 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 = new MathView( locale, mathViewOptions );
this.mathView.bind( 'display' ).to( this.displayButtonView, 'isOn' );
- children = [
- this.mathInputView,
- this.displayButtonView,
- this.previewLabel,
- this.mathView
- ];
- } else {
- children = [
- this.mathInputView,
- this.displayButtonView
- ];
+ children.push( previewLabel, this.mathView );
}
- // Add UI elements to template
+ this._setupSync( previewEnabled );
+
this.setTemplate( {
tag: 'form',
attributes: {
@@ -107,10 +75,30 @@ export default class MainFormView extends View {
},
children
},
- this.saveButtonView,
- this.cancelButtonView
+ {
+ tag: 'div',
+ attributes: {
+ class: [
+ 'ck-math-button-row'
+ ]
+ },
+ children: [
+ this.saveButtonView,
+ this.cancelButtonView
+ ]
+ }
]
} );
+
+ this._focusCycler = new FocusCycler( {
+ focusables: this._focusables,
+ focusTracker: this.focusTracker,
+ keystrokeHandler: this.keystrokes,
+ actions: {
+ focusPrevious: 'shift + tab',
+ focusNext: 'tab'
+ }
+ } );
}
public override render(): void {
@@ -121,103 +109,73 @@ export default class MainFormView extends View {
view: this
} );
- // Register form elements to focusable elements
- const childViews = [
- this.mathInputView,
+ const focusableViews = [
+ this.mathInputView.latexTextAreaView,
this.displayButtonView,
this.saveButtonView,
this.cancelButtonView
];
- childViews.forEach( v => {
+ focusableViews.forEach( v => {
+ this._focusables.add( v );
if ( v.element ) {
- this._focusables.add( v );
this.focusTracker.add( v.element );
}
} );
- // Listen to keypresses inside form element
+ this.mathInputView.on( 'mathfieldReady', () => {
+ const mathfieldView = this.mathInputView.mathFieldFocusableView;
+ if ( mathfieldView.element ) {
+ if ( this._focusables.has( mathfieldView ) ) {
+ this._focusables.remove( mathfieldView );
+ }
+ this._focusables.add( mathfieldView, 0 );
+ this.focusTracker.add( mathfieldView.element );
+ }
+ } );
+
if ( this.element ) {
this.keystrokes.listenTo( this.element );
}
}
+ public get equation(): string {
+ return this.mathInputView.value ?? '';
+ }
+
+ public set equation( equation: string ) {
+ const norm = equation.trim();
+ this.mathInputView.value = norm.length ? norm : null;
+ if ( this.mathView ) {
+ this.mathView.value = norm;
+ }
+ }
+
public focus(): void {
this._focusCycler.focusFirst();
}
- public get equation(): string {
- return this.mathInputView.fieldView.element?.value ?? '';
- }
+ private _setupSync( previewEnabled: boolean ): void {
+ this.mathInputView.on( 'change:value', () => {
+ let eq = ( this.mathInputView.value ?? '' ).trim();
- public set equation( equation: string ) {
- if ( this.mathInputView.fieldView.element ) {
- this.mathInputView.fieldView.element.value = equation;
- }
- if ( this.previewEnabled && this.mathView ) {
- this.mathView.value = equation;
- }
- }
+ if ( hasDelimiters( eq ) ) {
+ const params = extractDelimiters( eq );
+ eq = params.equation;
+ this.displayButtonView.isOn = params.display;
- public focusTracker: FocusTracker = new FocusTracker();
- public keystrokes: KeystrokeHandler = new KeystrokeHandler();
- private _focusables = new ViewCollection();
- private _focusCycler: FocusCycler = new FocusCycler( {
- focusables: this._focusables,
- focusTracker: this.focusTracker,
- keystrokeHandler: this.keystrokes,
- actions: {
- focusPrevious: 'shift + tab',
- focusNext: 'tab'
- }
- } );
-
- private _createMathInput() {
- const t = this.locale.t;
-
- // Create equation input
- const mathInput = new MathInputView( this.locale );
- const fieldView = mathInput.fieldView;
- mathInput.infoText = t( 'Insert equation in TeX format.' );
-
- const onInput = () => {
- if ( fieldView.element != null ) {
- let equationInput = fieldView.element.value.trim();
-
- // If input has delimiters
- if ( hasDelimiters( equationInput ) ) {
- // Get equation without delimiters
- const params = extractDelimiters( equationInput );
-
- // Remove delimiters from input field
- fieldView.element.value = params.equation;
-
- equationInput = params.equation;
-
- // update display button and preview
- this.displayButtonView.isOn = params.display;
+ if ( this.mathInputView.value !== eq ) {
+ this.mathInputView.value = eq.length ? eq : null;
}
- if ( this.previewEnabled && this.mathView ) {
- // Update preview view
- this.mathView.value = equationInput;
- }
-
- this.saveButtonView.isEnabled = !!equationInput;
}
- };
- fieldView.on( 'render', onInput );
- fieldView.on( 'input', onInput );
-
- return mathInput;
+ if ( previewEnabled && this.mathView && this.mathView.value !== eq ) {
+ this.mathView.value = eq;
+ }
+ } );
}
- private _createButton(
- label: string,
- icon: string,
- className: string,
- eventName: string | null
- ) {
+ private _createButton( label: string, icon: string, className: string, type?: 'submit' | 'button' ): ButtonView {
const button = new ButtonView( this.locale );
button.set( {
@@ -232,16 +190,14 @@ export default class MainFormView extends View {
}
} );
- if ( eventName ) {
- button.delegate( 'execute' ).to( this, eventName );
+ if ( type ) {
+ button.type = type;
}
return button;
}
- private _createDisplayButton() {
- const t = this.locale.t;
-
+ private _createDisplayButton( t: ( str: string ) => string ): SwitchButtonView {
const switchButton = new SwitchButtonView( this.locale );
switchButton.set( {
@@ -256,15 +212,13 @@ export default class MainFormView extends View {
} );
switchButton.on( 'execute', () => {
- // Toggle state
switchButton.isOn = !switchButton.isOn;
-
- if ( this.previewEnabled && this.mathView ) {
- // Update preview view
- this.mathView.display = switchButton.isOn;
- }
} );
return switchButton;
}
+
+ public hideKeyboard(): void {
+ this.mathInputView.hideKeyboard();
+ }
}
diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts
new file mode 100644
index 0000000000..027b7aab9b
--- /dev/null
+++ b/packages/ckeditor5-math/src/ui/mathinputview.ts
@@ -0,0 +1,268 @@
+// Math input widget: wraps a MathLive and a LaTeX textarea
+// and keeps them in sync for the CKEditor 5 math dialog.
+import { View, type Locale, type FocusableView } from 'ckeditor5';
+import 'mathlive/fonts.css'; // Auto-bundles offline fonts
+
+declare global {
+ interface Window {
+ mathVirtualKeyboard?: {
+ visible: boolean;
+ show: () => void;
+ hide: () => void;
+ addEventListener: ( event: string, cb: () => void ) => void;
+ removeEventListener: ( event: string, cb: () => void ) => void;
+ };
+ }
+}
+
+interface MathFieldElement extends HTMLElement {
+ value: string;
+ readOnly: boolean;
+ mathVirtualKeyboardPolicy: string;
+ inlineShortcuts?: Record;
+ setValue?: ( value: string, options?: { silenceNotifications?: boolean } ) => void;
+}
+
+// Wrapper for the MathLive element to make it focusable in CKEditor's UI system
+export class MathFieldFocusableView extends View implements FocusableView {
+ public declare element: HTMLElement | null;
+ private _view: MathInputView;
+ constructor( locale: Locale, view: MathInputView ) {
+ super( locale );
+ this._view = view;
+ }
+ public focus(): void {
+ this._view.mathfield?.focus();
+ }
+ public setElement( el: HTMLElement ): void {
+ this.element = el;
+ }
+}
+
+// Wrapper for the LaTeX textarea to make it focusable in CKEditor's UI system
+export class LatexTextAreaView extends View implements FocusableView {
+ declare public element: HTMLTextAreaElement;
+ constructor( locale: Locale ) {
+ super( locale );
+ this.setTemplate( { tag: 'textarea', attributes: {
+ class: [ 'ck', 'ck-textarea', 'ck-latex-textarea' ], spellcheck: 'false', tabindex: 0
+ } } );
+ }
+ public focus(): void {
+ this.element?.focus();
+ }
+}
+
+// Main view class for the math input
+export default class MathInputView extends View {
+ public declare value: string | null;
+ public declare isReadOnly: boolean;
+ public mathfield: MathFieldElement | null = null;
+ public readonly latexTextAreaView: LatexTextAreaView;
+ public readonly mathFieldFocusableView: MathFieldFocusableView;
+ private _destroyed = false;
+ private _vkGeometryHandler?: () => void;
+ private _updating = false;
+ private static _configured = false;
+
+ constructor( locale: Locale ) {
+ super( locale );
+ this.latexTextAreaView = new LatexTextAreaView( locale );
+ this.mathFieldFocusableView = new MathFieldFocusableView( locale, this );
+ this.set( 'value', null );
+ this.set( 'isReadOnly', false );
+ this.setTemplate( {
+ tag: 'div', attributes: { class: [ 'ck', 'ck-math-input' ] },
+ children: [
+ { tag: 'div', attributes: { class: [ 'ck-mathlive-container' ] } },
+ { tag: 'label', attributes: { class: [ 'ck-latex-label' ] }, children: [ locale.t( 'LaTeX' ) ] },
+ { tag: 'div', attributes: { class: [ 'ck-latex-wrapper' ] }, children: [ this.latexTextAreaView ] }
+ ]
+ } );
+ }
+
+ public override render(): void {
+ super.render();
+ const textarea = this.latexTextAreaView.element;
+
+ // Sync changes from the LaTeX textarea to the mathfield and model
+ this.listenTo( textarea, 'input', () => {
+ if ( this._updating ) {
+ return;
+ }
+ this._updating = true;
+ const val = textarea.value;
+ this.value = val || null;
+ if ( this.mathfield ) {
+ if ( val === '' ) {
+ this.mathfield.remove();
+ this.mathfield = null;
+ this._initMathField( false );
+ } else if ( this.mathfield.value.trim() !== val.trim() ) {
+ this._setMathfieldValue( val );
+ }
+ }
+ this._updating = false;
+ } );
+
+ // Sync changes from the model (this.value) to the UI elements
+ this.on( 'change:value', ( _e, _n, val ) => {
+ if ( this._updating ) {
+ return;
+ }
+ this._updating = true;
+ const newVal = val ?? '';
+ if ( textarea.value !== newVal ) {
+ textarea.value = newVal;
+ }
+ if ( this.mathfield ) {
+ if ( this.mathfield.value.trim() !== newVal.trim() ) {
+ this._setMathfieldValue( newVal );
+ }
+ } else if ( newVal !== '' ) {
+ this._initMathField( false );
+ }
+ this._updating = false;
+ } );
+
+ // Handle read-only state changes
+ this.on( 'change:isReadOnly', ( _e, _n, val ) => {
+ textarea.readOnly = val;
+ if ( this.mathfield ) {
+ this.mathfield.readOnly = val;
+ }
+ } );
+
+ // Handle virtual keyboard geometry changes
+ const vk = window.mathVirtualKeyboard;
+ if ( vk && !this._vkGeometryHandler ) {
+ this._vkGeometryHandler = () => {
+ if ( vk.visible && this.mathfield ) {
+ this.mathfield.focus();
+ }
+ };
+ vk.addEventListener( 'geometrychange', this._vkGeometryHandler );
+ }
+
+ const initial = this.value ?? '';
+ if ( textarea.value !== initial ) {
+ textarea.value = initial;
+ }
+ this._loadMathLive();
+ }
+
+ // Loads the MathLive library dynamically
+ private async _loadMathLive(): Promise {
+ try {
+ await import( 'mathlive' );
+ await customElements.whenDefined( 'math-field' );
+ if ( this._destroyed ) {
+ return;
+ }
+ if ( !MathInputView._configured ) {
+ const MathfieldClass = customElements.get( 'math-field' ) as any;
+ if ( MathfieldClass ) {
+ MathfieldClass.soundsDirectory = null;
+ MathfieldClass.plonkSound = null;
+ MathInputView._configured = true;
+ }
+ }
+ if ( this.element && !this._destroyed ) {
+ this._initMathField( true );
+ }
+ } catch {
+ const c = this.element?.querySelector( '.ck-mathlive-container' );
+ if ( c ) {
+ c.textContent = 'Math editor unavailable';
+ }
+ }
+ }
+
+ // Initializes the element
+ private _initMathField( shouldFocus: boolean ): void {
+ const container = this.element?.querySelector( '.ck-mathlive-container' );
+ if ( !container ) {
+ return;
+ }
+ if ( this.mathfield ) {
+ this._setMathfieldValue( this.value ?? '' );
+ return;
+ }
+ const mf = document.createElement( 'math-field' ) as MathFieldElement;
+ mf.mathVirtualKeyboardPolicy = 'auto';
+ mf.setAttribute( 'tabindex', '0' );
+ mf.value = this.value ?? '';
+ mf.readOnly = this.isReadOnly;
+ container.appendChild( mf );
+ // Set shortcuts after mounting (accessing inlineShortcuts requires mounted element)
+ try {
+ if ( mf.inlineShortcuts ) {
+ mf.inlineShortcuts = { ...mf.inlineShortcuts, dx: 'dx', dy: 'dy', dt: 'dt' };
+ }
+ } catch {
+ // Inline shortcut configuration is optional; ignore failures to avoid breaking the math field.
+ }
+ mf.addEventListener( 'keydown', ev => {
+ if ( ev.key === 'Tab' ) {
+ if ( ev.shiftKey ) {
+ ev.preventDefault();
+ } else {
+ ev.preventDefault();
+ ev.stopImmediatePropagation();
+ this.latexTextAreaView.focus();
+ }
+ }
+ }, { capture: true } );
+ mf.addEventListener( 'input', () => {
+ if ( this._updating ) {
+ return;
+ }
+ this._updating = true;
+ const textarea = this.latexTextAreaView.element;
+ if ( textarea.value.trim() !== mf.value.trim() ) {
+ textarea.value = mf.value;
+ }
+ this.value = mf.value || null;
+ this._updating = false;
+ } );
+ this.mathfield = mf;
+ this.mathFieldFocusableView.setElement( mf );
+ this.fire( 'mathfieldReady' );
+ if ( shouldFocus ) {
+ requestAnimationFrame( () => mf.focus() );
+ }
+ }
+
+ // Updates the mathfield value without triggering loops
+ private _setMathfieldValue( value: string ): void {
+ if ( !this.mathfield ) {
+ return;
+ }
+ if ( this.mathfield.setValue ) {
+ this.mathfield.setValue( value, { silenceNotifications: true } );
+ } else {
+ this.mathfield.value = value;
+ }
+ }
+
+ public hideKeyboard(): void {
+ window.mathVirtualKeyboard?.hide();
+ }
+
+ public focus(): void {
+ this.mathfield?.focus();
+ }
+
+ public override destroy(): void {
+ this._destroyed = true;
+ const vk = window.mathVirtualKeyboard;
+ if ( vk && this._vkGeometryHandler ) {
+ vk.removeEventListener( 'geometrychange', this._vkGeometryHandler );
+ this._vkGeometryHandler = undefined;
+ }
+ this.hideKeyboard();
+ this.mathfield?.remove();
+ this.mathfield = null;
+ super.destroy();
+ }
+}
diff --git a/packages/ckeditor5-math/src/ui/mathview.ts b/packages/ckeditor5-math/src/ui/mathview.ts
index fab16262e9..aa1027329e 100644
--- a/packages/ckeditor5-math/src/ui/mathview.ts
+++ b/packages/ckeditor5-math/src/ui/mathview.ts
@@ -2,44 +2,44 @@ import { View, type Locale } from 'ckeditor5';
import type { KatexOptions } from '../typings-external.js';
import { renderEquation } from '../utils.js';
+/**
+ * Configuration options for the MathView.
+ */
+export interface MathViewOptions {
+ engine: 'mathjax' | 'katex' | ( ( equation: string, element: HTMLElement, display: boolean ) => void );
+ lazyLoad: undefined | ( () => Promise );
+ previewUid: string;
+ previewClassName: Array;
+ katexRenderOptions: KatexOptions;
+}
+
export default class MathView extends View {
+ /**
+ * The LaTeX equation value to render.
+ * @observable
+ */
public declare value: string;
+
+ /**
+ * Whether to render in display mode (centered) or inline.
+ * @observable
+ */
public declare display: boolean;
- public previewUid: string;
- public previewClassName: Array;
- public katexRenderOptions: KatexOptions;
- public engine:
- | 'mathjax'
- | 'katex'
- | ( ( equation: string, element: HTMLElement, display: boolean ) => void );
- public lazyLoad: undefined | ( () => Promise );
- constructor(
- engine:
- | 'mathjax'
- | 'katex'
- | ( (
- equation: string,
- element: HTMLElement,
- display: boolean,
- ) => void ),
- lazyLoad: undefined | ( () => Promise ),
- locale: Locale,
- previewUid: string,
- previewClassName: Array,
- katexRenderOptions: KatexOptions
- ) {
+ /**
+ * Configuration options passed during initialization.
+ */
+ private options: MathViewOptions;
+
+ constructor( locale: Locale, options: MathViewOptions ) {
super( locale );
-
- this.engine = engine;
- this.lazyLoad = lazyLoad;
- this.previewUid = previewUid;
- this.katexRenderOptions = katexRenderOptions;
- this.previewClassName = previewClassName;
+ this.options = options;
this.set( 'value', '' );
this.set( 'display', false );
+ // Update rendering when state changes.
+ // Checking isRendered prevents errors during initialization.
this.on( 'change', () => {
if ( this.isRendered ) {
this.updateMath();
@@ -55,19 +55,39 @@ export default class MathView extends View {
}
public updateMath(): void {
- if ( this.element ) {
- void renderEquation(
- this.value,
- this.element,
- this.engine,
- this.lazyLoad,
- this.display,
- true,
- this.previewUid,
- this.previewClassName,
- this.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/index.ts b/packages/ckeditor5-math/tests/index.ts
index 4493420e1d..b379ad8c68 100644
--- a/packages/ckeditor5-math/tests/index.ts
+++ b/packages/ckeditor5-math/tests/index.ts
@@ -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 );
diff --git a/packages/ckeditor5-math/tests/lazyload.ts b/packages/ckeditor5-math/tests/lazyload.ts
index 1265078502..173fae184d 100644
--- a/packages/ckeditor5-math/tests/lazyload.ts
+++ b/packages/ckeditor5-math/tests/lazyload.ts
@@ -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();
} );
@@ -37,6 +54,7 @@ describe( 'Lazy load', () => {
await buildEditor( {
math: {
engine: 'katex',
+ enablePreview: true,
lazyLoad: async () => {
lazyLoadInvoked = true;
}
@@ -44,6 +62,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;
} );
} );
diff --git a/packages/ckeditor5-math/tests/mathui.ts b/packages/ckeditor5-math/tests/mathui.ts
index 5a392c0db0..94becd000e 100644
--- a/packages/ckeditor5-math/tests/mathui.ts
+++ b/packages/ckeditor5-math/tests/mathui.ts
@@ -410,7 +410,7 @@ describe( 'MathUI', () => {
it( 'should bind mainFormView.mathInputView#value to math command value', () => {
const command = editor.commands.get( 'math' );
- expect( formView!.mathInputView.value ).to.null;
+ expect( formView!.mathInputView.value ).to.be.null;
command!.value = 'x^2';
expect( formView!.mathInputView.value ).to.equal( 'x^2' );
@@ -419,10 +419,18 @@ describe( 'MathUI', () => {
it( 'should execute math command on mainFormView#submit event', () => {
const executeSpy = vi.spyOn( editor, 'execute' );
- formView!.mathInputView.fieldView.element!.value = 'x^2';
+ formView!.mathInputView.value = 'x^2';
formView!.fire( 'submit' );
- expect(executeSpy.mock.lastCall?.slice(0, 2)).toMatchObject(['math', 'x^2']);
+ expect( executeSpy.mock.lastCall?.slice( 0, 2 ) ).toMatchObject( [ 'math', 'x^2' ] );
+ } );
+
+ it( 'should update equation value when mathInputView changes', () => {
+ formView!.mathInputView.value = 'x^2';
+ expect( formView!.equation ).to.equal( 'x^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', () => {
diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css
index 3b7b4047f9..a5d55f2f1b 100644
--- a/packages/ckeditor5-math/theme/mathform.css
+++ b/packages/ckeditor5-math/theme/mathform.css
@@ -1,35 +1,220 @@
+/**
+ * Math Equation Editor Dialog Styles - Compact & Readable
+ */
+
+/* === Z-INDEX: MathLive UI above CKEditor === */
+.ML__keyboard, .ML__popover, .ML__menu, .ML__suggestions, .ML__autocomplete,
+.ML__tooltip, .ML__sr-only, [data-ml-root], #mathlive-suggestion-popover,
+.mathlive-suggestions-popover, [data-ml-tooltip], .ML__base {
+ z-index: calc(var(--ck-z-panel) + 1000) !important;
+}
+.ML__tooltip, [role="tooltip"], .ML__popover[role="tooltip"], .popover, [data-ml-tooltip] {
+ z-index: calc(var(--ck-z-panel) + 2000) !important;
+ position: fixed !important;
+}
+.ck.ck-balloon-panel, .ck.ck-balloon-panel .ck-balloon-panel__content {
+ overflow: visible !important;
+}
+
+/* === MAIN DIALOG === */
.ck.ck-math-form {
- display: flex;
- align-items: flex-start;
- flex-direction: row;
- flex-wrap: nowrap;
- padding: var(--ck-spacing-standard);
-
- @media screen and (max-width: 600px) {
- flex-wrap: wrap;
-
- & .ck-math-view {
- flex-basis: 100%;
-
- & .ck-labeled-view {
- flex-basis: 100%;
- }
-
- & .ck-label {
- flex-basis: 100%;
- }
- }
-
- & .ck-button {
- flex-basis: 50%;
- }
- }
+ display: flex;
+ flex-direction: column;
+ padding: var(--ck-spacing-standard);
+ box-sizing: border-box;
+ max-width: 80vw;
+ max-height: 80vh;
+ overflow: visible;
+ user-select: text;
}
-.ck-math-tex.ck-placeholder::before {
- display: none !important;
+/* Scrollable content - vertical scroll, horizontal visible for tooltips */
+.ck-math-view {
+ overflow-y: auto;
+ overflow-x: visible;
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ gap: var(--ck-spacing-standard);
+ min-height: 0;
+ width: 100%;
}
-.ck.ck-toolbar-container {
- z-index: calc(var(--ck-z-panel) + 2);
+/* === MATH INPUT === */
+.ck.ck-math-input {
+ display: flex;
+ flex-direction: column;
+ gap: var(--ck-spacing-standard);
+ width: fit-content;
+ min-width: 100%;
+ max-width: 100%;
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow: visible !important;
+}
+
+/* === MATHLIVE EDITOR === */
+.ck.ck-math-input .ck-mathlive-container {
+ position: relative;
+ width: 100%;
+ min-height: 50px;
+ padding: var(--ck-spacing-small);
+ border: 1px solid var(--ck-color-input-border);
+ border-radius: var(--ck-border-radius);
+ background: var(--ck-color-input-background) !important;
+ transition: border-color 120ms ease;
+ overflow: visible !important;
+ clip-path: none !important;
+}
+.ck.ck-math-input .ck-mathlive-container:focus-within {
+ border-color: var(--ck-color-focus-border);
+}
+
+/* Position keyboard & menu buttons */
+.ck-mathlive-container math-field::part(virtual-keyboard-toggle),
+.ck-mathlive-container math-field::part(menu-toggle) {
+ position: absolute;
+ top: 8px;
+}
+.ck-mathlive-container math-field::part(virtual-keyboard-toggle) { right: 40px; }
+.ck-mathlive-container math-field::part(menu-toggle) {
+ right: 8px;
+ display: flex !important;
+ visibility: visible !important;
+}
+
+/* Math field element */
+.ck.ck-math-form math-field {
+ display: block !important;
+ width: 100%;
+ font-size: 1.5em;
+ background: transparent !important;
+ color: var(--ck-color-input-text);
+ border: none !important;
+ padding: 0;
+ outline: none !important;
+ --selection-background-color: rgba(33, 150, 243, 0.2);
+ --selection-color: inherit;
+ --contains-highlight-background-color: rgba(0, 0, 0, 0.05);
+}
+
+/* === LATEX TEXTAREA === */
+.ck.ck-math-input .ck-latex-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: fit-content;
+ min-width: 100%;
+ max-width: 100%;
+ padding: var(--ck-spacing-small);
+ border: 1px solid var(--ck-color-input-border);
+ border-radius: var(--ck-border-radius);
+ background: var(--ck-color-input-background) !important;
+ transition: border-color 120ms ease;
+ box-sizing: border-box;
+}
+.ck.ck-math-input .ck-latex-wrapper:focus-within {
+ border-color: var(--ck-color-focus-border);
+}
+.ck.ck-math-input .ck-latex-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--ck-color-text);
+ opacity: 0.8;
+ margin: 0 0 var(--ck-spacing-small) 0;
+ flex-shrink: 0;
+}
+.ck.ck-math-input .ck-latex-textarea {
+ width: fit-content;
+ min-width: 100%;
+ max-width: 100%;
+ min-height: 60px;
+ max-height: calc(80vh - 300px);
+ resize: both;
+ overflow: auto;
+ font-family: 'Courier New', monospace;
+ font-size: 0.95em;
+ background: transparent !important;
+ color: var(--ck-color-input-text);
+ border: none !important;
+ padding: 0;
+ outline: none !important;
+ box-sizing: border-box;
+}
+
+/* === DISPLAY TOGGLE === */
+.ck-button-display-toggle {
+ align-self: flex-start;
+ padding: var(--ck-spacing-small) var(--ck-spacing-standard);
+ background: var(--ck-color-input-background);
+ color: var(--ck-color-text);
+ border: 1px solid var(--ck-color-input-border);
+ border-radius: var(--ck-border-radius);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+.ck-button-display-toggle:hover { background: var(--ck-color-focus-border); }
+
+/* === PREVIEW === */
+.ck-math-preview,
+.ck.ck-math-preview {
+ width: 100%;
+ min-height: 40px;
+ max-height: none !important;
+ height: auto !important;
+ padding: var(--ck-spacing-small);
+ background: transparent !important;
+ border: none !important;
+ display: block;
+ text-align: left;
+ overflow-x: auto !important;
+ overflow-y: visible !important;
+ flex-shrink: 0;
+}
+
+/* Center equation when in display mode */
+.ck-math-preview[data-display="true"],
+.ck.ck-math-preview[data-display="true"] {
+ text-align: center;
+}
+
+.ck-math-preview.ck-error, .ck-math-render-error {
+ border-color: var(--ck-color-error-text);
+ background: var(--ck-color-base-background);
+ color: var(--ck-color-error-text);
+}
+
+/* === BUTTONS === */
+.ck-math-button-row {
+ display: flex;
+ gap: var(--ck-spacing-standard);
+ justify-content: flex-end;
+ margin-top: var(--ck-spacing-standard);
+}
+.ck-button-save, .ck-button-cancel {
+ padding: var(--ck-spacing-small) var(--ck-spacing-standard);
+ border: 1px solid var(--ck-color-input-border);
+ border-radius: var(--ck-border-radius);
+ cursor: pointer;
+ font-weight: 500;
+}
+.ck-button-save {
+ background: var(--ck-color-focus-border);
+ color: white;
+}
+.ck-button-cancel {
+ background: var(--ck-color-input-background);
+ color: var(--ck-color-text);
+}
+.ck-button-save:hover { opacity: 0.9; }
+.ck-button-cancel:hover { background: var(--ck-color-base-background); }
+
+/* === OVERFLOW FIX: Allow tooltips to escape === */
+.ck.ck-balloon-panel,
+.ck.ck-balloon-panel .ck-balloon-panel__content,
+.ck.ck-math-form,
+.ck-math-view,
+.ck.ck-math-input,
+.ck.ck-math-input .ck-mathlive-container {
+ overflow: visible !important;
+ clip-path: none !important;
}
diff --git a/packages/ckeditor5-math/vitest.config.ts b/packages/ckeditor5-math/vitest.config.ts
index fb7ccfff03..47a35ced82 100644
--- a/packages/ckeditor5-math/vitest.config.ts
+++ b/packages/ckeditor5-math/vitest.config.ts
@@ -22,6 +22,9 @@ export default defineConfig( {
include: [
'tests/**/*.[jt]s'
],
+ exclude: [
+ 'tests/setup.ts'
+ ],
globals: true,
watch: false,
coverage: {
diff --git a/packages/ckeditor5/src/i18n.ts b/packages/ckeditor5/src/i18n.ts
index a409fa437a..dd4e616d3d 100644
--- a/packages/ckeditor5/src/i18n.ts
+++ b/packages/ckeditor5/src/i18n.ts
@@ -50,6 +50,11 @@ const LOCALE_MAPPINGS: Record = {
coreTranslation: () => import("ckeditor5/translations/ja.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ja.js"),
},
+ pl: {
+ languageCode: "pl",
+ coreTranslation: () => import("ckeditor5/translations/pl.js"),
+ premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/pl.js"),
+ },
pt: {
languageCode: "pt",
coreTranslation: () => import("ckeditor5/translations/pt.js"),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a93b819206..bbd091fb62 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -104,8 +104,8 @@ importers:
specifier: 0.18.0
version: 0.18.0
rollup-plugin-webpack-stats:
- specifier: 2.1.8
- version: 2.1.8(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
+ specifier: 2.1.9
+ version: 2.1.9(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
tslib:
specifier: 2.8.1
version: 2.8.1
@@ -1257,6 +1257,9 @@ importers:
'@ckeditor/ckeditor5-icons':
specifier: 47.3.0
version: 47.3.0
+ mathlive:
+ specifier: 0.108.2
+ version: 0.108.2
devDependencies:
'@ckeditor/ckeditor5-dev-build-tools':
specifier: 54.2.3
@@ -2347,6 +2350,10 @@ packages:
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
+ '@cortex-js/compute-engine@0.30.2':
+ resolution: {integrity: sha512-Zx+iisk9WWdbxjm8EYsneIBszvjfUs7BHNwf1jBtSINIgfWGpHrTTq9vW0J59iGCFt6bOFxbmWyxNMRSmksHMA==}
+ engines: {node: '>=21.7.3', npm: '>=10.5.0'}
+
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@@ -7298,6 +7305,10 @@ packages:
compare-versions@6.1.1:
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
+ complex-esm@2.1.1-esm1:
+ resolution: {integrity: sha512-IShBEWHILB9s7MnfyevqNGxV0A1cfcSnewL/4uPFiSxkcQL4Mm3FxJ0pXMtCXuWLjYz3lRRyk6OfkeDZcjD6nw==}
+ engines: {node: '>=16.14.2', npm: '>=8.5.0'}
+
component-emitter@1.3.1:
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
@@ -10497,6 +10508,9 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
+ mathlive@0.108.2:
+ resolution: {integrity: sha512-GIZkfprGTxrbHckOvwo92ZmOOxdD018BHDzlrEwYUU+pzR5KabhqI1s43lxe/vqXdF5RLiQKgDcuk5jxEjhkYg==}
+
mathml-tag-names@2.1.3:
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
@@ -12613,8 +12627,8 @@ packages:
resolution: {integrity: sha512-EsoOi8moHN6CAYyTZipxDDVTJn0j2nBCWor4wRU45RQ8ER2qREDykXLr3Ulz6hBh6oBKCFTQIjo21i0FXNo/IA==}
hasBin: true
- rollup-plugin-stats@1.5.3:
- resolution: {integrity: sha512-0IYVGhsFTjcddpqcElzU7Mi4vmDLihCCTH5QgCCgWpNY1VKMXVoEpxmCmGjivtJKLzI6t5QIicsPBC93UWWN2g==}
+ rollup-plugin-stats@1.5.4:
+ resolution: {integrity: sha512-b1hYagYLTyr8mCVUb7e1x9fjxOXFyeWmV9hIr7vYqq/agN+WDaGNzz+KmM3GAx0KGGI2qllOL+zAUi/l39s/Sg==}
engines: {node: '>=18'}
peerDependencies:
rolldown: ^1.0.0-beta.0
@@ -12640,8 +12654,8 @@ packages:
peerDependencies:
rollup: ^3.0.0||^4.0.0
- rollup-plugin-webpack-stats@2.1.8:
- resolution: {integrity: sha512-agc1OE+QwG3sGeTSdruh16DkxPb6QkgR7I3gntPDFHMXsK1bR2ADHUVod1eoE+epAOqiv3idx/hcSqZAI3a1yg==}
+ rollup-plugin-webpack-stats@2.1.9:
+ resolution: {integrity: sha512-ft1vdp3xPjE+zw8A22yCToo5cpymoWCjNDefWNO1awywsDrSDoRJhkoZTENkhJwmfh6oe5ztpGu7PfnJOMXc2g==}
engines: {node: '>=18'}
peerDependencies:
rolldown: ^1.0.0-beta.0
@@ -15509,8 +15523,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-block-quote@47.3.0':
dependencies:
@@ -15521,8 +15533,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-bookmark@47.3.0':
dependencies:
@@ -15585,8 +15595,6 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-code-block@47.3.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
dependencies:
@@ -15758,6 +15766,8 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-easy-image@47.3.0':
dependencies:
@@ -15795,6 +15805,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-editor-inline@47.3.0':
dependencies:
@@ -15851,6 +15863,8 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.3.0
'@ckeditor/ckeditor5-engine': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-essentials@47.3.0':
dependencies:
@@ -15882,8 +15896,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-export-word@47.3.0':
dependencies:
@@ -15908,6 +15920,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-font@47.3.0':
dependencies:
@@ -15917,8 +15931,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-footnotes@47.3.0':
dependencies:
@@ -15949,8 +15961,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-heading@47.3.0':
dependencies:
@@ -15982,6 +15992,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-html-embed@47.3.0':
dependencies:
@@ -16041,8 +16053,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-indent@47.3.0':
dependencies:
@@ -16077,6 +16087,8 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-link@47.3.0':
dependencies:
@@ -16103,6 +16115,8 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-list@47.3.0':
dependencies:
@@ -16155,8 +16169,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-mention@47.3.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)':
dependencies:
@@ -16166,8 +16178,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-merge-fields@47.3.0':
dependencies:
@@ -16180,8 +16190,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-minimap@47.3.0':
dependencies:
@@ -16190,8 +16198,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-operations-compressor@47.3.0':
dependencies:
@@ -16432,8 +16438,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-template@47.3.0':
dependencies:
@@ -16715,6 +16719,11 @@ snapshots:
'@colors/colors@1.5.0': {}
+ '@cortex-js/compute-engine@0.30.2':
+ dependencies:
+ complex-esm: 2.1.1-esm1
+ decimal.js: 10.6.0
+
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
@@ -22523,6 +22532,8 @@ snapshots:
compare-versions@6.1.1: {}
+ complex-esm@2.1.1-esm1: {}
+
component-emitter@1.3.1: {}
compress-commons@6.0.2:
@@ -23209,8 +23220,7 @@ snapshots:
decimal.js@10.5.0: {}
- decimal.js@10.6.0:
- optional: true
+ decimal.js@10.6.0: {}
decko@1.2.0: {}
@@ -26560,6 +26570,10 @@ snapshots:
math-intrinsics@1.1.0: {}
+ mathlive@0.108.2:
+ dependencies:
+ '@cortex-js/compute-engine': 0.30.2
+
mathml-tag-names@2.1.3: {}
mdast-util-find-and-replace@3.0.2:
@@ -29024,7 +29038,7 @@ snapshots:
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.29
optional: true
- rollup-plugin-stats@1.5.3(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
+ rollup-plugin-stats@1.5.4(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
optionalDependencies:
rolldown: 1.0.0-beta.29
rollup: 4.52.0
@@ -29057,9 +29071,9 @@ snapshots:
'@rollup/pluginutils': 5.1.4(rollup@4.52.0)
rollup: 4.52.0
- rollup-plugin-webpack-stats@2.1.8(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
+ rollup-plugin-webpack-stats@2.1.9(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
dependencies:
- rollup-plugin-stats: 1.5.3(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
+ rollup-plugin-stats: 1.5.4(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
optionalDependencies:
rolldown: 1.0.0-beta.29
rollup: 4.52.0